webui: implement modular demo system with TypeScript and shared fixtures
Replace hand-written HTML demo pages with TypeScript demo modules and
automated infrastructure to reduce maintenance overhead and improve
developer experience with type safety and shared code.
Problems Solved:
Demo Maintenance Overhead:
- Hand-written HTML demo pages contained extensive boilerplate duplication
- No type checking for demo setup code or component data
- Manual maintenance of demo/index.html with available demos
- Difficult to share common fake data between demo pages
- No hot module replacement for demo development
Code Quality and Consistency:
- Demo setup code written in plain JavaScript without type safety
- No validation that demo data matches component interfaces
- Inconsistent styling and structure across demo pages
- Duplicated fake data declarations in each demo file
Solution Architecture:
TypeScript Demo Module System:
- Created DemoModule interface for standardized demo structure
- Demo modules export title, description, imports, and setup functions
- Full TypeScript compilation with type checking for demo code
- Dynamic import system for on-demand demo loading with Vite integration
Shared Demo Infrastructure:
- demo-framework/ with types.ts and demo-runner.ts for core functionality
- DemoRunner class handles dynamic loading, cleanup, and error handling
- Single demo-runner.html page loads any demo module dynamically
- Supports URL hash routing for direct demo links
Centralized Fake Data:
- demo-fixtures/ directory with shared TypeScript data files
- sampleToolCalls, sampleTimelineMessages, and sampleContainerState
- Type-safe imports ensure demo data matches component interfaces
- demoUtils with helper functions for consistent demo UI creation
Auto-generated Index Page:
- generate-index.ts scans for *.demo.ts files and extracts metadata
- Creates index-generated.html with links to all available demos
- Automatically includes demo titles and descriptions
- Eliminates manual maintenance of demo listing
Implementation Details:
Demo Framework:
- DemoRunner.loadDemo() uses dynamic imports with Vite ignore comments
- Automatic component import based on demo module configuration
- Support for demo-specific CSS and cleanup functions
- Error handling with detailed error display for debugging
Demo Module Structure:
- sketch-chat-input.demo.ts: Interactive chat with message history
- sketch-container-status.demo.ts: Status variations with real-time updates
- sketch-tool-calls.demo.ts: Multiple tool call examples with progressive loading
- All use shared fixtures and utilities for consistent experience
Vite Integration:
- Hot Module Replacement works for demo modules and shared fixtures
- TypeScript compilation on-the-fly for immediate feedback
- Dynamic imports work seamlessly with Vite's module system
- @vite-ignore comments prevent import analysis warnings
Testing and Validation:
- Tested demo runner loads and displays available components
- Verified component discovery and dynamic import functionality
- Confirmed shared fixture imports work correctly
- Validated auto-generated index creation and content
Files Modified:
- demo-framework/types.ts: TypeScript interfaces for demo system
- demo-framework/demo-runner.ts: Core demo loading and execution logic
- demo-fixtures/: Shared fake data (tool-calls.ts, timeline-messages.ts, container-status.ts, index.ts)
- demo-runner.html: Interactive demo browser with sidebar navigation
- generate-index.ts: Auto-generation script for demo index
- sketch-chat-input.demo.ts: Converted chat input demo to TypeScript
- sketch-container-status.demo.ts: Container status demo with variations
- sketch-tool-calls.demo.ts: Tool calls demo with interactive examples
- readme.md: Comprehensive documentation for new demo system
Benefits:
- Developers get full TypeScript type checking for demo code
- Shared fake data ensures consistency and reduces duplication
- Hot module replacement provides instant feedback during development
- Auto-generated index eliminates manual maintenance
- Modular architecture makes it easy to add new demos
- Vite integration provides fast development iteration
The new system reduces demo maintenance overhead while providing
better developer experience through TypeScript, shared code, and
automated infrastructure.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s3d91894eb7c4a79fk
diff --git a/webui/src/web-components/demo/demo-fixtures/container-status.ts b/webui/src/web-components/demo/demo-fixtures/container-status.ts
new file mode 100644
index 0000000..14c4964
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/container-status.ts
@@ -0,0 +1,99 @@
+/**
+ * Shared fake container status data for demos
+ */
+
+import { State, CumulativeUsage } from "../../../types";
+
+export const sampleUsage: CumulativeUsage = {
+ start_time: "2024-01-15T10:00:00Z",
+ messages: 1337,
+ input_tokens: 25432,
+ output_tokens: 18765,
+ cache_read_input_tokens: 8234,
+ cache_creation_input_tokens: 12354,
+ total_cost_usd: 2.03,
+ tool_uses: {
+ bash: 45,
+ patch: 23,
+ think: 12,
+ "multiple-choice": 8,
+ keyword_search: 6,
+ },
+};
+
+export const sampleContainerState: State = {
+ state_version: 1,
+ message_count: 27,
+ total_usage: sampleUsage,
+ initial_commit: "decafbad42abc123",
+ slug: "file-upload-component",
+ branch_name: "sketch-wip",
+ branch_prefix: "sketch",
+ hostname: "example.hostname",
+ working_dir: "/app",
+ os: "linux",
+ git_origin: "https://github.com/user/repo.git",
+ outstanding_llm_calls: 0,
+ outstanding_tool_calls: null,
+ session_id: "session-abc123",
+ ssh_available: true,
+ in_container: true,
+ first_message_index: 0,
+ agent_state: "ready",
+ outside_hostname: "host.example.com",
+ inside_hostname: "container.local",
+ outside_os: "macOS",
+ inside_os: "linux",
+ outside_working_dir: "/Users/dev/project",
+ inside_working_dir: "/app",
+ todo_content:
+ "- Implement file upload component\n- Add drag and drop support\n- Write tests",
+ skaband_addr: "localhost:8080",
+ link_to_github: true,
+ ssh_connection_string: "ssh user@example.com",
+ diff_lines_added: 245,
+ diff_lines_removed: 67,
+};
+
+export const lightUsageState: State = {
+ ...sampleContainerState,
+ message_count: 5,
+ total_usage: {
+ ...sampleUsage,
+ messages: 5,
+ input_tokens: 1234,
+ output_tokens: 890,
+ total_cost_usd: 0.15,
+ tool_uses: {
+ bash: 2,
+ patch: 1,
+ },
+ },
+ diff_lines_added: 45,
+ diff_lines_removed: 12,
+};
+
+export const heavyUsageState: State = {
+ ...sampleContainerState,
+ message_count: 156,
+ total_usage: {
+ ...sampleUsage,
+ messages: 156,
+ input_tokens: 89234,
+ output_tokens: 67890,
+ cache_read_input_tokens: 23456,
+ cache_creation_input_tokens: 45678,
+ total_cost_usd: 12.45,
+ tool_uses: {
+ bash: 234,
+ patch: 89,
+ think: 67,
+ "multiple-choice": 23,
+ keyword_search: 45,
+ browser_navigate: 12,
+ codereview: 8,
+ },
+ },
+ diff_lines_added: 2847,
+ diff_lines_removed: 1456,
+};
diff --git a/webui/src/web-components/demo/demo-fixtures/index.ts b/webui/src/web-components/demo/demo-fixtures/index.ts
new file mode 100644
index 0000000..9a47f8e
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/index.ts
@@ -0,0 +1,104 @@
+/**
+ * Centralized exports for all demo fixtures
+ */
+
+// Tool calls
+export {
+ sampleToolCalls,
+ longBashCommand,
+ multipleToolCallGroups,
+} from "./tool-calls";
+
+// Timeline messages
+export {
+ sampleTimelineMessages,
+ longTimelineMessage,
+ mixedTimelineMessages,
+} from "./timeline-messages";
+
+// Container status
+export {
+ sampleUsage,
+ sampleContainerState,
+ lightUsageState,
+ heavyUsageState,
+} from "./container-status";
+
+// Common demo utilities
+export const demoStyles = {
+ container: `
+ max-width: 1200px;
+ margin: 20px auto;
+ padding: 20px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ `,
+
+ demoSection: `
+ margin: 20px 0;
+ padding: 15px;
+ border: 1px solid #e1e5e9;
+ border-radius: 8px;
+ background: #f8f9fa;
+ `,
+
+ demoHeader: `
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 10px;
+ color: #24292f;
+ `,
+};
+
+/**
+ * Common demo setup utilities
+ */
+export const demoUtils = {
+ /**
+ * Create a labeled demo section
+ */
+ createDemoSection(title: string, description?: string): HTMLElement {
+ const section = document.createElement("div");
+ section.style.cssText = demoStyles.demoSection;
+
+ const header = document.createElement("h3");
+ header.style.cssText = demoStyles.demoHeader;
+ header.textContent = title;
+ section.appendChild(header);
+
+ if (description) {
+ const desc = document.createElement("p");
+ desc.textContent = description;
+ desc.style.cssText = "color: #656d76; margin-bottom: 15px;";
+ section.appendChild(desc);
+ }
+
+ return section;
+ },
+
+ /**
+ * Wait for a specified number of milliseconds
+ */
+ delay(ms: number): Promise<void> {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ },
+
+ /**
+ * Create a simple button for demo interactions
+ */
+ createButton(text: string, onClick: () => void): HTMLButtonElement {
+ const button = document.createElement("button");
+ button.textContent = text;
+ button.style.cssText = `
+ padding: 8px 16px;
+ margin: 5px;
+ background: #0969da;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ `;
+ button.addEventListener("click", onClick);
+ return button;
+ },
+};
diff --git a/webui/src/web-components/demo/demo-fixtures/timeline-messages.ts b/webui/src/web-components/demo/demo-fixtures/timeline-messages.ts
new file mode 100644
index 0000000..a98c728
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/timeline-messages.ts
@@ -0,0 +1,113 @@
+/**
+ * Shared fake timeline message data for demos
+ */
+
+import { AgentMessage } from "../../../types";
+import { sampleToolCalls } from "./tool-calls";
+
+const baseTimestamp = new Date("2024-01-15T10:00:00Z");
+
+export const sampleTimelineMessages: AgentMessage[] = [
+ {
+ type: "user",
+ end_of_turn: true,
+ content:
+ "Can you help me implement a file upload component with drag and drop support?",
+ timestamp: new Date(baseTimestamp.getTime()).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 0,
+ },
+ {
+ type: "agent",
+ end_of_turn: false,
+ content:
+ "I'll help you create a file upload component with drag and drop support. Let me start by analyzing your current project structure and then implement the component.",
+ timestamp: new Date(baseTimestamp.getTime() + 1000).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 1,
+ },
+ {
+ type: "agent",
+ end_of_turn: false,
+ content: "First, let me check your current directory structure:",
+ tool_calls: [sampleToolCalls[2]], // bash command
+ timestamp: new Date(baseTimestamp.getTime() + 2000).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 2,
+ },
+ {
+ type: "tool",
+ end_of_turn: false,
+ content:
+ "src/\n├── components/\n│ ├── Button.tsx\n│ └── Input.tsx\n├── styles/\n│ └── globals.css\n└── utils/\n └── helpers.ts",
+ tool_name: "bash",
+ tool_call_id: "toolu_01bash123",
+ timestamp: new Date(baseTimestamp.getTime() + 3000).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 3,
+ },
+ {
+ type: "agent",
+ end_of_turn: true,
+ content:
+ "Perfect! I can see you have a components directory. Now I'll create a FileUpload component with drag and drop functionality. This will include:\n\n1. A drop zone area\n2. File selection via click\n3. Progress indicators\n4. File validation\n5. Preview of selected files",
+ timestamp: new Date(baseTimestamp.getTime() + 4000).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 4,
+ },
+];
+
+export const longTimelineMessage: AgentMessage = {
+ type: "agent",
+ end_of_turn: true,
+ content: `I've analyzed your codebase and here's a comprehensive plan for implementing the file upload component:
+
+## Implementation Plan
+
+### 1. Component Structure
+The FileUpload component will be built using React with TypeScript. It will consist of:
+- A main container with drop zone styling
+- File input element (hidden)
+- Visual feedback for drag states
+- File list display area
+- Progress indicators
+
+### 2. Key Features
+- **Drag & Drop**: Full drag and drop support with visual feedback
+- **Multiple Files**: Support for selecting multiple files at once
+- **File Validation**: Size limits, file type restrictions
+- **Progress Tracking**: Upload progress for each file
+- **Error Handling**: User-friendly error messages
+- **Accessibility**: Proper ARIA labels and keyboard navigation
+
+### 3. Technical Considerations
+- Use the HTML5 File API for file handling
+- Implement proper event handlers for drag events
+- Add debouncing for performance
+- Include comprehensive error boundaries
+- Ensure mobile responsiveness
+
+### 4. Styling Approach
+- CSS modules for component-scoped styles
+- Responsive design with mobile-first approach
+- Smooth animations and transitions
+- Consistent with your existing design system
+
+This implementation will provide a robust, user-friendly file upload experience that integrates seamlessly with your existing application.`,
+ timestamp: new Date(baseTimestamp.getTime() + 5000).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 5,
+};
+
+export const mixedTimelineMessages: AgentMessage[] = [
+ ...sampleTimelineMessages,
+ longTimelineMessage,
+ {
+ type: "user",
+ end_of_turn: true,
+ content: "That sounds great! Can you also add file type validation?",
+ timestamp: new Date(baseTimestamp.getTime() + 6000).toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 6,
+ },
+];
diff --git a/webui/src/web-components/demo/demo-fixtures/tool-calls.ts b/webui/src/web-components/demo/demo-fixtures/tool-calls.ts
new file mode 100644
index 0000000..eaa5009
--- /dev/null
+++ b/webui/src/web-components/demo/demo-fixtures/tool-calls.ts
@@ -0,0 +1,101 @@
+/**
+ * Shared fake tool call data for demos
+ */
+
+import { ToolCall } from "../../../types";
+
+export const sampleToolCalls: ToolCall[] = [
+ {
+ name: "multiple-choice",
+ input: JSON.stringify({
+ question: "What is your favorite programming language?",
+ choices: [
+ "JavaScript",
+ "TypeScript",
+ "Python",
+ "Go",
+ "Rust",
+ "Java",
+ "C#",
+ "C++",
+ ],
+ }),
+ tool_call_id: "toolu_01choice123",
+ result_message: {
+ type: "tool",
+ end_of_turn: false,
+ content: "Go",
+ tool_result: JSON.stringify({
+ selected: "Go",
+ }),
+ timestamp: new Date().toISOString(),
+ conversation_id: "demo-conversation",
+ idx: 1,
+ },
+ },
+ {
+ name: "multiple-choice",
+ input: JSON.stringify({
+ question: "Which feature would you like to implement next?",
+ choices: [
+ "Dark mode",
+ "User profiles",
+ "Social sharing",
+ "Analytics dashboard",
+ ],
+ }),
+ tool_call_id: "toolu_01choice456",
+ // No result yet, showing the choices without a selection
+ },
+ {
+ name: "bash",
+ input: JSON.stringify({
+ command:
+ "docker ps -a --format '{{.ID}} {{.Image }} {{.Names}}' | grep sketch | awk '{print $1 }' | xargs -I {} docker rm {} && docker image prune -af",
+ }),
+ tool_call_id: "toolu_01bash123",
+ result: "Removed containers and pruned images",
+ },
+ {
+ name: "patch",
+ input: JSON.stringify({
+ path: "/app/src/components/Button.tsx",
+ patches: [
+ {
+ operation: "replace",
+ oldText: "className='btn'",
+ newText: "className='btn btn-primary'",
+ },
+ ],
+ }),
+ tool_call_id: "toolu_01patch123",
+ result: "Applied patch successfully",
+ },
+ {
+ name: "think",
+ input: JSON.stringify({
+ thoughts:
+ "I need to analyze the user's requirements and break this down into smaller steps. The user wants to implement a file upload feature with drag-and-drop support.",
+ }),
+ tool_call_id: "toolu_01think123",
+ result: "Recorded thoughts for planning",
+ },
+];
+
+export const longBashCommand: ToolCall = {
+ name: "bash",
+ input: JSON.stringify({
+ command:
+ 'git commit --allow-empty -m "chore: create empty commit with very long message\n\nThis is an extremely long commit message to demonstrate how Git handles verbose commit messages.\nThis empty commit has no actual code changes, but contains a lengthy explanation.\n\nThe empty commit pattern can be useful in several scenarios:\n1. Triggering CI/CD pipelines without modifying code\n2. Marking significant project milestones or releases\n3. Creating annotated reference points in the commit history\n4. Documenting important project decisions"',
+ }),
+ tool_call_id: "toolu_01longbash",
+ result:
+ "[main abc1234] chore: create empty commit with very long message\n\ncommit created successfully",
+};
+
+export const multipleToolCallGroups = [
+ [sampleToolCalls[0], sampleToolCalls[1]], // Multiple choice examples
+ [sampleToolCalls[2]], // Single bash command
+ [sampleToolCalls[3], sampleToolCalls[4]], // Patch and think
+ [longBashCommand], // Long command example
+];