* 2. Include this script: * 3. Create charts with minimal configuration - colors are auto-applied! */ (function() { 'use strict'; // ========================================================================== // READ COLORS FROM CSS CUSTOM PROPERTIES // This ensures chart colors stay in sync with the theme // ========================================================================== /** * Get a CSS custom property value from :root */ function getCSSVar(name, fallback = '') { if (typeof getComputedStyle === 'undefined') return fallback; const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return value || fallback; } /** * Build palette from CSS custom properties (with fallbacks) */ function buildPaletteFromCSS() { return { // Primary brand colors dartmouthGreen: getCSSVar('--dartmouth-green', '#00693e'), textPrimary: getCSSVar('--text-primary', '#0a2518'), textSecondary: getCSSVar('--text-secondary', '#0a3d23'), // Chart colors (from CSS --chart-color-N variables) chartColors: [ getCSSVar('--chart-color-1', '#00693e'), getCSSVar('--chart-color-2', '#267aba'), getCSSVar('--chart-color-3', '#ffa00f'), getCSSVar('--chart-color-4', '#9d162e'), getCSSVar('--chart-color-5', '#8a6996'), getCSSVar('--chart-color-6', '#a5d75f'), getCSSVar('--chart-color-7', '#003c73'), getCSSVar('--chart-color-8', '#d94415'), getCSSVar('--chart-color-9', '#643c20'), getCSSVar('--chart-color-10', '#c4dd88'), getCSSVar('--chart-color-11', '#f5dc69'), getCSSVar('--chart-color-12', '#424141'), ], // Background colors (semi-transparent versions) chartBgColors: [ getCSSVar('--chart-bg-1', 'rgba(0, 105, 62, 0.5)'), getCSSVar('--chart-bg-2', 'rgba(38, 122, 186, 0.5)'), getCSSVar('--chart-bg-3', 'rgba(255, 160, 15, 0.5)'), getCSSVar('--chart-bg-4', 'rgba(157, 22, 46, 0.5)'), getCSSVar('--chart-bg-5', 'rgba(138, 105, 150, 0.5)'), getCSSVar('--chart-bg-6', 'rgba(165, 215, 95, 0.5)'), ], // Semantic colors positive: getCSSVar('--chart-positive', '#00693e'), negative: getCSSVar('--chart-negative', '#9d162e'), neutral: getCSSVar('--chart-neutral', '#424141'), highlight: getCSSVar('--chart-highlight', '#ffa00f'), // Grid and axis colors gridLight: getCSSVar('--chart-grid-light', 'rgba(0, 105, 62, 0.1)'), gridMedium: getCSSVar('--chart-grid-medium', 'rgba(0, 105, 62, 0.15)'), gridDark: getCSSVar('--chart-grid-dark', 'rgba(0, 105, 62, 0.2)'), axisColor: getCSSVar('--chart-axis-color', '#0a2518'), // Font fontFamily: getCSSVar('--chart-font-family', "'Avenir LT Std', 'Avenir', 'Avenir Next', -apple-system, BlinkMacSystemFont, sans-serif"), }; } // Initialize palette (will be populated when DOM is ready) let CDL_PALETTE = null; // For convenience, expose primary chart colors array let CHART_COLORS = null; // ========================================================================== // FONT CONFIGURATION // Responsive font sizes based on typical Marp slide dimensions (1280x720) // ========================================================================== const FONT_CONFIG = { sizes: { title: 22, // Chart title subtitle: 18, // Subtitle legend: 16, // Legend labels axisTitle: 18, // Axis titles axisTicks: 16, // Axis tick labels tooltip: 14, // Tooltip text dataLabels: 14, // Data labels on charts }, weight: { normal: 400, medium: 500, bold: 600, }, }; // ========================================================================== // HELPER FUNCTIONS // ========================================================================== /** * Ensure palette is initialized */ function ensurePalette() { if (!CDL_PALETTE) { CDL_PALETTE = buildPaletteFromCSS(); CHART_COLORS = CDL_PALETTE.chartColors; } return CDL_PALETTE; } /** * Get color for a dataset at given index * Cycles through palette if more datasets than colors */ function getColor(index) { ensurePalette(); return CHART_COLORS[index % CHART_COLORS.length]; } /** * Get color with alpha transparency */ function getColorWithAlpha(color, alpha) { // Handle hex colors if (color.startsWith('#')) { const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } // Handle rgba colors if (color.startsWith('rgba')) { return color.replace(/[\d.]+\)$/, `${alpha})`); } return color; } /** * Generate colors for all datasets in chart data * Automatically assigns colors if not specified */ function autoAssignColors(data, chartType) { if (!data || !data.datasets) return data; data.datasets.forEach((dataset, index) => { const baseColor = getColor(index); // Only assign colors if not already specified switch (chartType) { case 'bar': case 'horizontalBar': if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } break; case 'line': if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (!dataset.backgroundColor) { dataset.backgroundColor = getColorWithAlpha(baseColor, 0.1); } if (dataset.borderWidth === undefined) { dataset.borderWidth = 3; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 6; } if (!dataset.pointBackgroundColor) { dataset.pointBackgroundColor = baseColor; } if (dataset.tension === undefined) { dataset.tension = 0.3; } break; case 'scatter': case 'bubble': if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 15; } if (dataset.pointHoverRadius === undefined) { dataset.pointHoverRadius = 18; } break; case 'pie': case 'doughnut': case 'polarArea': // For pie charts, we need multiple colors for one dataset if (!dataset.backgroundColor) { const numItems = dataset.data ? dataset.data.length : 6; dataset.backgroundColor = []; for (let i = 0; i < numItems; i++) { dataset.backgroundColor.push(getColor(i)); } } if (!dataset.borderColor) { dataset.borderColor = '#d8d8d8'; // Slide background } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } break; case 'radar': if (!dataset.borderColor) { dataset.borderColor = baseColor; } if (!dataset.backgroundColor) { dataset.backgroundColor = getColorWithAlpha(baseColor, 0.2); } if (dataset.borderWidth === undefined) { dataset.borderWidth = 2; } if (dataset.pointRadius === undefined) { dataset.pointRadius = 4; } if (!dataset.pointBackgroundColor) { dataset.pointBackgroundColor = baseColor; } break; default: // Generic color assignment if (!dataset.backgroundColor) { dataset.backgroundColor = baseColor; } if (!dataset.borderColor) { dataset.borderColor = baseColor; } } }); return data; } // ========================================================================== // CHART.JS GLOBAL DEFAULTS // ========================================================================== function applyGlobalDefaults() { if (typeof Chart === 'undefined') { console.warn('Chart.js not loaded. chart-defaults.js requires Chart.js to be loaded first.'); return false; } // Ensure palette is loaded from CSS const palette = ensurePalette(); // Font defaults Chart.defaults.font.family = palette.fontFamily; Chart.defaults.font.size = FONT_CONFIG.sizes.axisTicks; Chart.defaults.color = palette.textPrimary; // Responsive defaults Chart.defaults.responsive = true; Chart.defaults.maintainAspectRatio = false; // Animation (subtle) Chart.defaults.animation.duration = 400; // Plugin defaults // Legend Chart.defaults.plugins.legend.labels.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.legend, weight: FONT_CONFIG.weight.normal, }; Chart.defaults.plugins.legend.labels.color = palette.textPrimary; Chart.defaults.plugins.legend.labels.usePointStyle = true; Chart.defaults.plugins.legend.labels.padding = 20; // Title Chart.defaults.plugins.title.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.title, weight: FONT_CONFIG.weight.medium, }; Chart.defaults.plugins.title.color = palette.textPrimary; // Tooltip Chart.defaults.plugins.tooltip.backgroundColor = palette.textPrimary; Chart.defaults.plugins.tooltip.titleFont = { family: palette.fontFamily, size: FONT_CONFIG.sizes.tooltip, weight: FONT_CONFIG.weight.medium, }; Chart.defaults.plugins.tooltip.bodyFont = { family: palette.fontFamily, size: FONT_CONFIG.sizes.tooltip, }; Chart.defaults.plugins.tooltip.cornerRadius = 4; Chart.defaults.plugins.tooltip.padding = 10; // Scale defaults (for cartesian charts) // These need to be applied per-scale type const scaleDefaults = { grid: { color: palette.gridLight, lineWidth: 1, }, border: { color: palette.gridDark, width: 1, }, ticks: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTicks, }, color: palette.textPrimary, }, title: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTitle, weight: FONT_CONFIG.weight.normal, }, color: palette.textPrimary, }, }; // Apply scale defaults to linear scale if (Chart.defaults.scales && Chart.defaults.scales.linear) { if (Chart.defaults.scales.linear.grid) Object.assign(Chart.defaults.scales.linear.grid, scaleDefaults.grid); if (Chart.defaults.scales.linear.border) Object.assign(Chart.defaults.scales.linear.border, scaleDefaults.border); if (Chart.defaults.scales.linear.ticks) Object.assign(Chart.defaults.scales.linear.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.linear.title) Object.assign(Chart.defaults.scales.linear.title, scaleDefaults.title); } // Apply scale defaults to category scale if (Chart.defaults.scales && Chart.defaults.scales.category) { if (Chart.defaults.scales.category.grid) Object.assign(Chart.defaults.scales.category.grid, scaleDefaults.grid); if (Chart.defaults.scales.category.border) Object.assign(Chart.defaults.scales.category.border, scaleDefaults.border); if (Chart.defaults.scales.category.ticks) Object.assign(Chart.defaults.scales.category.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.category.title) Object.assign(Chart.defaults.scales.category.title, scaleDefaults.title); } // Apply scale defaults to logarithmic scale if (Chart.defaults.scales && Chart.defaults.scales.logarithmic) { if (Chart.defaults.scales.logarithmic.grid) Object.assign(Chart.defaults.scales.logarithmic.grid, scaleDefaults.grid); if (Chart.defaults.scales.logarithmic.border) Object.assign(Chart.defaults.scales.logarithmic.border, scaleDefaults.border); if (Chart.defaults.scales.logarithmic.ticks) Object.assign(Chart.defaults.scales.logarithmic.ticks, scaleDefaults.ticks); if (Chart.defaults.scales.logarithmic.title) Object.assign(Chart.defaults.scales.logarithmic.title, scaleDefaults.title); } // Apply scale defaults to radial scale (for radar charts) if (Chart.defaults.scales && Chart.defaults.scales.radialLinear) { if (Chart.defaults.scales.radialLinear.grid) Chart.defaults.scales.radialLinear.grid.color = palette.gridLight; if (Chart.defaults.scales.radialLinear.angleLines) Chart.defaults.scales.radialLinear.angleLines.color = palette.gridMedium; if (Chart.defaults.scales.radialLinear.pointLabels) { Chart.defaults.scales.radialLinear.pointLabels.font = { family: palette.fontFamily, size: FONT_CONFIG.sizes.axisTicks, }; Chart.defaults.scales.radialLinear.pointLabels.color = palette.textPrimary; } } return true; } // ========================================================================== // CHART WRAPPER FOR AUTO-STYLING // ========================================================================== /** * Wrap the Chart constructor to automatically apply CDL styling */ function wrapChartConstructor() { if (typeof Chart === 'undefined') return; const OriginalChart = Chart; // Create a wrapper that auto-applies colors window.Chart = function(ctx, config) { // Auto-assign colors if not specified if (config && config.data) { config.data = autoAssignColors(config.data, config.type); } // Merge default options for specific chart types if (config && config.options) { config.options = applyChartTypeDefaults(config.type, config.options); } // Call original constructor return new OriginalChart(ctx, config); }; // Copy static properties and methods Object.setPrototypeOf(window.Chart, OriginalChart); Object.assign(window.Chart, OriginalChart); // Preserve the prototype chain window.Chart.prototype = OriginalChart.prototype; } /** * Apply chart-type specific defaults */ function applyChartTypeDefaults(chartType, userOptions) { const options = { ...userOptions }; switch (chartType) { case 'bar': case 'horizontalBar': // Bar chart defaults if (!options.scales) options.scales = {}; if (!options.scales.x) options.scales.x = {}; if (!options.scales.y) options.scales.y = {}; // Hide x-axis grid for cleaner look if (options.scales.x.grid === undefined) { options.scales.x.grid = { display: false }; } break; case 'line': // Line chart defaults if (!options.interaction) { options.interaction = { intersect: false, mode: 'index' }; } break; case 'pie': case 'doughnut': // Pie/doughnut defaults if (!options.plugins) options.plugins = {}; if (options.plugins.legend === undefined) { const palette = ensurePalette(); options.plugins.legend = { position: 'right', labels: { font: { family: palette.fontFamily, size: FONT_CONFIG.sizes.legend, }, color: palette.textPrimary, padding: 15, }, }; } break; case 'radar': // Radar chart defaults - keep as-is, scale defaults applied globally break; case 'scatter': case 'bubble': // Scatter/bubble defaults if (!options.scales) options.scales = {}; if (!options.scales.x) options.scales.x = {}; if (!options.scales.y) options.scales.y = {}; break; } return options; } // ========================================================================== // CONVENIENCE FUNCTIONS FOR USERS // Exposed on window.CDLChart for easy access // ========================================================================== window.CDLChart = { // Color palette access (getters to ensure lazy initialization) get colors() { return ensurePalette().chartColors; }, get palette() { return ensurePalette(); }, // Get specific color by index getColor: getColor, // Get color with transparency getColorWithAlpha: getColorWithAlpha, // Get array of colors for a specific count getColors: function(count) { ensurePalette(); const result = []; for (let i = 0; i < count; i++) { result.push(getColor(i)); } return result; }, // Font configuration fonts: FONT_CONFIG, // Quick chart creation helpers // These create minimal config that auto-applies all styling /** * Create a simple bar chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - X-axis labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ bar: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'bar', data: { labels: labels, datasets: [{ data: data }], }, options: { plugins: { legend: { display: false } }, ...options, }, }); }, /** * Create a simple line chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - X-axis labels * @param {Array} datasets - Array of {label, data} objects * @param {object} options - Optional overrides */ line: function(canvasId, labels, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'line', data: { labels: labels, datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, fill: ds.fill !== undefined ? ds.fill : true, })), }, options: options, }); }, /** * Create a simple pie chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Slice labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ pie: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'pie', data: { labels: labels, datasets: [{ data: data }], }, options: options, }); }, /** * Create a simple scatter chart * @param {string} canvasId - Canvas element ID * @param {Array} datasets - Array of {label, data: [{x, y}]} objects * @param {object} options - Optional overrides */ scatter: function(canvasId, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'scatter', data: { datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, })), }, options: options, }); }, /** * Create a doughnut chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Slice labels * @param {number[]} data - Data values * @param {object} options - Optional overrides */ doughnut: function(canvasId, labels, data, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'doughnut', data: { labels: labels, datasets: [{ data: data }], }, options: options, }); }, /** * Create a radar chart * @param {string} canvasId - Canvas element ID * @param {string[]} labels - Axis labels * @param {Array} datasets - Array of {label, data} objects * @param {object} options - Optional overrides */ radar: function(canvasId, labels, datasets, options = {}) { return new Chart(document.getElementById(canvasId), { type: 'radar', data: { labels: labels, datasets: datasets.map(ds => ({ label: ds.label, data: ds.data, })), }, options: options, }); }, }; // ========================================================================== // INITIALIZATION // ========================================================================== function initialize() { // Wait for Chart.js to be available if (typeof Chart !== 'undefined') { applyGlobalDefaults(); wrapChartConstructor(); console.log('CDL Chart defaults applied successfully.'); return true; } else { // Chart.js not yet loaded - wait and retry let retries = 0; const maxRetries = 50; // 5 seconds max wait const checkInterval = setInterval(function() { retries++; if (typeof Chart !== 'undefined') { clearInterval(checkInterval); applyGlobalDefaults(); wrapChartConstructor(); console.log('CDL Chart defaults applied successfully (after waiting for Chart.js).'); } else if (retries >= maxRetries) { clearInterval(checkInterval); console.warn('Chart.js not found after waiting. CDL Chart defaults not applied.'); } }, 100); return false; } } // Initialize IMMEDIATELY - this must run BEFORE any chart creation scripts // Chart.js CDN should be loaded before this script initialize(); })();

Lecture 9: Vibe coding tips and tricks

PSYC 51.17: Models of language and communication

Jeremy R. Manning
Dartmouth College
Winter 2026

Today's agenda

Vibe coding means using "AI" coding agents to rapidly prototype and implement software by describing what you want in natural language, then iterating on the output.

  1. Free AI coding tools for students: GitHub Copilot, Google Gemini
  2. Setting up your environment: VS Code, OpenCode, oh-my-opencode
  3. The spec-kit workflow: from design docs to implementation
  4. Live demo: build something together!

Install the tools (and try using them) as we go— and ask questions as they arise!

Free coding models: use them!

  • GitHub Copilot: great at code completion, chat assistance, moderate coding tasks
  • Google Gemini: very long context windows, powerful models that are good at reasoning-heavy tasks
  • Dartmouth GenAI: free access to many models (Claude, ChatGPT, Mistral, etc.)
  • Ollama and LM Studio: run LLMs directly on your laptop (some good ones: llama3.2, deepseek-r1, gemma3, qwen3, gpt-oss)
  • Hugging Face hosts many open models you can try in your browser or download to run locally. Not great for vibe coding, but useful for integrating models into your projects.

My favorite paid options

  • Anthropic Claude: fantastic coding model (this is what I use most of the time!).
  • OpenAI ChatGPT: powerful, different behavior and feel from Claude; sometimes when one model struggles, the other can help.

Most paid LLM services offer student discounts and/or free tiers.

Setting up your environment: two(ish) options

  1. Integrated Development Environment (IDE): full-featured environment with syntax highlighting, debugging, Git integration, extensions (e.g., VS Code, PyCharm)
  2. Terminal-based coding agent: lightweight, fast, scriptable (e.g., Claude Code, ChatGPT Codex CLI, OpenCode)
  • OpenCode, Claude, and OpenAI all have native desktop apps that combine terminal-based coding agents with IDE-like features (file browsing, syntax highlighting, etc.)
  • Some IDEs are explicitly designed for AI coding (e.g., Antigravity, Cursor)
  • Google Colab now builds in AI coding assistance directly into notebooks (similar to VS Code's Copilot extension); no installation required!

Setting up VS Code

Setting up VS Code

  • Download and install from code.visualstudio.com
  • Install essential extensions:
    • GitHub Copilot
    • Jupyter
    • Python
    • OpenCode
  • Activate Copilot with your GitHub account (click the Accounts icon in bottom left)

Setting up OpenCode (in Terminal)


1curl -fsSL https://opencode.ai/install | bash

Launch OpenCode

Launch OpenCode

OpenCode configuration

  • Use the /models command to connect opencode with your Copilot and Gemini accounts (and OpenAI/Anthropic accounts if you have them)
  • Then install oh-my-opencode by entering the following command into opencode:

1Install and configure oh-my-opencode by following the instructions here:
2https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md

Oh-my-opencode highlights

  • The main thing oh-my-opencode does is intelligently (and automatically!) orchestrate multiple AI agents to work together on complex tasks. You don't have to do anything special to make this happen (just install the plugin).
  • If you include the keyword "ultrawork" in any prompt, the LLM will turn on much stricter execution policies to ensure it completes tasks fully and correctly.
  • The /ralph-loop command lets you set up a self-referential development loop that continues working on a task until it is fully complete (or you hit a max iteration limit).
  • The OpenCode extension for VS Code lets you run OpenCode commands directly inside the IDE (super convenient!)

Initializing OpenCode projects

First clone (download) your project's GitHub repository to your computer:


1git clone <your-repo-url>
2cd <your-repo-name>
  • Then launch opencode (run the opencode command inside the project folder):
  • Then initialize the project using the /init-deep command (in opencode)
    • This launches a deep analysis of the codebase to help coding agents understand the project structure and dependencies
    • The process leaves behind a set of AGENTS.md files that help future agents understand the project without having to re-analyze everything from scratch

The classic four-step vibe coding workflow (for serious projects that you can't just one-shot prompt)

Describe Design Plan Implement
  • What are the inputs/outputs?
  • What are the edge cases?
  • Include examples!
  • Have AI draft the architecture
  • Iterate until you're happy
  • Include skeleton code
  • Break into small tasks
  • Include verification steps
  • Don't exceed context limits
  • Let AI write the code
  • Test each piece
  • Stress test the result!

spec-kit: GitHub's toolkit for Spec-Driven Development (SDD)

Instead of writing a technical design doc and implementation plan from scratch, you write a specification first. The spec becomes the executable source of truth. spec-kit guides you through the process.

  1. /speckit.constitution: define architectural rules
  2. /speckit.specify: create the spec (what + why, no how)
  3. /speckit.clarify: AI asks clarifying questions
  4. /speckit.plan: generate technical plan
  5. /speckit.tasks: break into actionable tasks
  6. /speckit.implement: execute with verification

Writing a good spec

Focus on WHAT and WHY, not HOW. No languages, databases, or APIs in the spec!


1### User Story 1 - Add task to board (Priority: P1)
2User can create a new task with a title and optional description.
3Task appears in the "To Do" column by default.
4
5**Acceptance Scenarios:**
61. **Given** an empty board, **When** user clicks "Add Task"
7   **Then** a form appears with title and description fields
82. **Given** a filled form, **When** user clicks "Save"
9   **Then** task appears in "To Do" column

Demo: AI-powered search engine

A single HTML file that creates an AI-powered search engine:

  1. User asks a question
  2. Browser-based LLM (SmolLM2-360M) generates search terms
  3. Fetches and parses Google results
  4. LLM summarizes each result, then synthesizes a final answer
  5. Returns answer with annotated references

Let's create a minimalist UI with smooth animations and real-time progress indicators showing which pipeline step is running and estimated time remaining

The spec-kit workflow for our demo

Constitution Specify Clarify Plan Tasks Implement

Each step produces a document that becomes the source of truth. The AI can't drift from the spec!

LLM-based coding agents can easily get confused, distracted, or go off-track. This happens especially often with complex tasks that require multiple steps (multiple agents workng together, large codebases, and so on). The spec-kit workflow keeps everything grounded in a clear, unambiguous specification that every agent can refer back to as needed.

Constitution: define the architectural DNA— rules that govern ALL code in the project


1Create a constitution for this project with these principles:
2- Single HTML file (no build tools, no npm)
3- Use Transformers.js for browser-based LLM inference
4- Model: SmolLM2-360M-Instruct (small enough for browser)
5- Modern CSS (flexbox, grid, CSS variables)
6- Vanilla JavaScript only (no frameworks)
7- Graceful degradation if WebGPU unavailable

A constitution.md file with inviolable rules.

Specify: describe WHAT we want (not HOW)


1Build a web-based AI search assistant. Single HTML file.
2
3User Journey:
41. User sees "Ask me a question!" prompt
52. User types question and submits
63. System shows real-time progress with current step and time estimate
74. System displays final answer with numbered references
85. ... (add more details!)

A spec.md file with detailed user stories, acceptance criteria, and examples.

Clarify (/speckit.clarify): use the coding agent to identify ambiguities and ask questions

  1. How should search results be fetched?
  2. What if the LLM generates poor search terms?
  3. How many results to fetch? (You said 10)
  4. What's the fallback if WebGPU is unavailable?
  5. Should users be able to ask follow-up questions?

Answer these questions (interactively) and/or provide additional details and clarifications. You can run /speckit.clarify multiple times until you're satisfied, or you can manually edit the spec.md file to add, subtract, or modify any details.

An updated spec.md file with clarifications incorporated.

Plan: specify the technical approach


1Create a technical plan using:
2- Transformers.js with SmolLM2-360M-Instruct
3- Google search via allorigins.win CORS proxy
4- Single HTML file with embedded CSS/JS
5- CSS animations for progress states
6- LocalStorage for model caching
7
8Include: architecture diagram, data flow, component breakdown.

plan.md with architecture, data models, component specs.

Tasks: break the plan into implementable chunks


1Break the plan into tasks. Each task should:
2- Be completable in <30 minutes
3- Have clear acceptance criteria
4- Be independently testable
  1. HTML skeleton + CSS variables + loading animation
  2. Transformers.js setup + model loading with progress
  3. Search term generation prompt + LLM call
  4. Google search fetch + result parsing
  5. Result summarization loop with progress updates
  6. Final synthesis + reference formatting
  7. Error handling + offline support

Implement: execute each task (and check against spec)


1For each task:
21. Write the code to implement the task.
32. Write tests to verify correctness.
43. Use ultrawork policies to ensure completeness.
54. Verify appearance using the playwright tool.
65. Run tests and fix any issues as you go.

Multiple commits implementing each task with verification steps...and a draft of the final product!

Implementation can take a while to run. You can do other stuff in the meantime!

Testing and verification

Even if you were careful in planning out your spec and tasks, things can still go wrong during implementation. Always test each piece as you complete it, and stress-test the final product to ensure it meets all acceptance criteria. You will likely need to iterate a few times to get everything working perfectly!

When you find something broken, it's tempting to vibe code a quick fix without updating the spec or plan. But this is a very bad idea: you (and your coding agent helpers!) will quickly find yourselves lost and confused about what the code is supposed to do. Always go back and update the spec, plan, and tasks to reflect any changes you make during implementation.

You don't necessarily need to be the one to update your spec/plan/tasks. You can have your coding agents do it for you! Just be sure to review and verify the changes they make.

Guiding principles

Simplicity is the art of maximizing the amount of work not done.

  1. Spend time at the start of your project carefully figuring out what you really want to build.
  2. Ambiguities in your initial idea(s) are OK at first, but then you should work with your coding agents to clarify and refine your vision before you start implementing.
  3. Always be on the lookout for opportunities to simplify your design, plan, and implementation.
  4. Use the "single source of truth" principle whereby each function or module is coded once and resued as needed.
  5. Make sure your project remains clean: continually remove unused files, functions, and dependencies as you go.

Questions?

📧 Email me
💬 Join our Discord
💁 Come to office hours

Lecture 10 (tomorrow): classic embeddings (LSA, LDA)