From e77c1f305d9b592196edf5f0c6764755cf488ae7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 20:19:00 +0000 Subject: [PATCH 1/3] Refactor AI prompt system: deduplicate, shorten, and improve quality - Remove duplicated getKeyPropsForLayerType() from both system-prompt.ts and schemas.ts; replace with extractKeyProps() that auto-infers key properties from Zod schema shape order (first 4 fields) - Remove hardcoded getExampleProps() with manually maintained examples; tool descriptions now rely on schema defaults instead - Simplify system prompt from ~150 lines to ~50 lines by removing content already present in tool definitions (preset lists, layer tool lists, verbose JSON examples, repeated rules) - Add configure_project as explicit first workflow step to prevent AI from skipping project setup (background, dimensions) - Improve layer reference error messages in mutations.ts to include available layer names/IDs, helping AI self-correct on wrong references - Consolidate layer reference rules into one clear section instead of two separate explanations - Make tool descriptions concise and non-redundant https://claude.ai/code/session_01JxrfQtGiLm9wQKhf1UJN9L --- CLAUDE.md | 64 +++++++++----- src/lib/ai/mutations.ts | 37 ++++---- src/lib/ai/schemas.ts | 131 +++++++++------------------ src/lib/ai/system-prompt.ts | 170 ++++++++---------------------------- 4 files changed, 145 insertions(+), 257 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0381baf..6cc7dd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,26 +7,34 @@ Quick reference for AI assistants working with DevMotion, an animation editor bu ## Core Architecture Patterns ### 1. **Svelte 5 Runes** - Use exclusively, no legacy stores + ```typescript -let count = $state(0); // Reactive state -let doubled = $derived(count * 2); // Computed values -$effect(() => { /* side effects */ }); +let count = $state(0); // Reactive state +let doubled = $derived(count * 2); // Computed values +$effect(() => { + /* side effects */ +}); ``` ### 2. **Zod-First Types** - Single source of truth + ```typescript // Define schema first, infer types -export const LayerSchema = z.object({ /* ... */ }); +export const LayerSchema = z.object({ + /* ... */ +}); export type Layer = z.infer; ``` ### 3. **Shared Mutations** - Web app and MCP server share logic + ```typescript // src/lib/ai/mutations.ts - pure functions that modify Project objects -export function mutateCreateLayer(ctx: MutationContext, input) { } +export function mutateCreateLayer(ctx: MutationContext, input) {} ``` ### 4. **Layer Registry** - Dynamic layer type system + ```typescript // Register layer components with validation schemas registerLayer('my-layer', Component, PropsSchema); @@ -36,21 +44,22 @@ registerLayer('my-layer', Component, PropsSchema); ## Key File Locations -| What | Where | -|------|-------| -| **Global state** | `src/lib/stores/project.svelte.ts` (ProjectStore class) | -| **Type definitions** | `src/lib/schemas/animation.ts` (Zod schemas) | -| **Shared mutations** | `src/lib/ai/mutations.ts` (web + MCP) | -| **Animation engine** | `src/lib/engine/interpolation.ts` | -| **Layer components** | `src/lib/layers/components/` | -| **MCP server** | `src/routes/mcp/+server.ts` | -| **Database schemas** | `src/lib/server/db/schema/` | +| What | Where | +| -------------------- | ------------------------------------------------------- | +| **Global state** | `src/lib/stores/project.svelte.ts` (ProjectStore class) | +| **Type definitions** | `src/lib/schemas/animation.ts` (Zod schemas) | +| **Shared mutations** | `src/lib/ai/mutations.ts` (web + MCP) | +| **Animation engine** | `src/lib/engine/interpolation.ts` | +| **Layer components** | `src/lib/layers/components/` | +| **MCP server** | `src/routes/mcp/+server.ts` | +| **Database schemas** | `src/lib/server/db/schema/` | --- ## Essential Conventions ### Code Style + - **Package manager**: `pnpm` only (not npm/yarn) - **Naming**: `kebab-case.ts`, `PascalCase.svelte`, `camelCase` functions - **Imports**: External → SvelteKit → Internal → Relative @@ -58,12 +67,14 @@ registerLayer('my-layer', Component, PropsSchema); - **Prettier**: 2 spaces, single quotes, no trailing commas, 100 line width ### TypeScript + - Strict mode always on - Prefer type inference over explicit types - No `any` - use `unknown` or proper types - Prefix unused vars with `_` ### Project Structure + ``` src/lib/ ├── stores/ # Global state (Svelte 5 runes) @@ -94,6 +105,7 @@ src/lib/ ``` ### Animatable Properties + - Built-in: `position.x/y/z`, `scale.x/y/z`, `rotation.x/y/z`, `opacity`, `color` - Custom: `props.fontSize`, `props.fill`, etc. @@ -102,22 +114,25 @@ src/lib/ ## Common Tasks ### Adding a Layer Type + 1. Create component in `src/lib/layers/components/` 2. Export props schema: `export const MyLayerPropsSchema = z.object({ ... })` 3. Call `registerLayer('my-layer', MyComponent, MyLayerPropsSchema)` ### Modifying Project Store + ```typescript // src/lib/stores/project.svelte.ts class ProjectStore { myMethod() { - this.project = { ...this.project, /* changes */ }; + this.project = { ...this.project /* changes */ }; // Auto-saves to localStorage (debounced) } } ``` ### Database Changes + ```bash # 1. Define schema in src/lib/server/db/schema/ # 2. Export from schema/index.ts @@ -126,14 +141,19 @@ pnpm db:push # Apply to database ``` ### Adding MCP Tools + ```typescript // src/routes/mcp/+server.ts -server.registerTool('tool_name', { - description: '...', - inputSchema: z.object({ projectId: z.string(), /* ... */ }) -}, async ({ projectId, ...params }) => { - // Use shared mutations from src/lib/ai/mutations.ts -}); +server.registerTool( + 'tool_name', + { + description: '...', + inputSchema: z.object({ projectId: z.string() /* ... */ }) + }, + async ({ projectId, ...params }) => { + // Use shared mutations from src/lib/ai/mutations.ts + } +); ``` --- @@ -160,6 +180,7 @@ pnpm db:studio # Database GUI ## Critical Patterns to Follow ### ✅ DO + - Use Svelte 5 runes (`$state`, `$derived`, `$effect`) - Define Zod schemas first, infer TypeScript types - Use `projectStore` for global animation state @@ -168,6 +189,7 @@ pnpm db:studio # Database GUI - Use `pnpm` exclusively ### ❌ DON'T + - Use legacy Svelte stores (`writable`, `derived`) - Duplicate type definitions (Zod + TypeScript) - Mutate state directly without reactivity diff --git a/src/lib/ai/mutations.ts b/src/lib/ai/mutations.ts index 386a07f..2f8a490 100644 --- a/src/lib/ai/mutations.ts +++ b/src/lib/ai/mutations.ts @@ -87,6 +87,22 @@ function resolveLayerId( return null; } +/** + * Build an error string listing available layers so the AI can self-correct. + */ +function layerNotFoundError(project: ProjectData, ref: string): string { + if (project.layers.length === 0) { + return `Layer "${ref}" not found. The project has no layers — create one first.`; + } + const available = project.layers.map((l) => `"${l.name}" (id: ${l.id})`).join(', '); + return ( + `Layer "${ref}" not found. ` + + `Use layer_N for layers you just created in this conversation, ` + + `or reference existing layers by id/name. ` + + `Available layers: ${available}` + ); +} + // ============================================ // Mutations // ============================================ @@ -155,11 +171,8 @@ export function mutateAnimateLayer( const resolvedId = resolveLayerId(ctx.project, input.layerId, ctx.layerIdMap); if (!resolvedId) { - return { - success: false, - message: `Layer not found: ${input.layerId}`, - error: 'Use layer_0, layer_1, etc. for recently created layers, or an existing layer ID/name.' - }; + const errMsg = layerNotFoundError(ctx.project, input.layerId); + return { success: false, message: errMsg, error: errMsg }; } const layer = ctx.project.layers.find((l) => l.id === resolvedId); @@ -224,11 +237,8 @@ export function mutateAnimateLayer( export function mutateEditLayer(ctx: MutationContext, input: EditLayerInput): EditLayerOutput { const resolvedId = resolveLayerId(ctx.project, input.layerId, ctx.layerIdMap); if (!resolvedId) { - return { - success: false, - message: `Layer not found: ${input.layerId}`, - error: 'Invalid layer reference' - }; + const errMsg = layerNotFoundError(ctx.project, input.layerId); + return { success: false, message: errMsg, error: errMsg }; } const layerIndex = ctx.project.layers.findIndex((l) => l.id === resolvedId); @@ -289,11 +299,8 @@ export function mutateRemoveLayer( ): RemoveLayerOutput { const resolvedId = resolveLayerId(ctx.project, input.layerId, ctx.layerIdMap); if (!resolvedId) { - return { - success: false, - message: `Layer not found: ${input.layerId}`, - error: 'Invalid layer reference' - }; + const errMsg = layerNotFoundError(ctx.project, input.layerId); + return { success: false, message: errMsg, error: errMsg }; } const index = ctx.project.layers.findIndex((l) => l.id === resolvedId); diff --git a/src/lib/ai/schemas.ts b/src/lib/ai/schemas.ts index eeb0375..e88e2dd 100644 --- a/src/lib/ai/schemas.ts +++ b/src/lib/ai/schemas.ts @@ -10,7 +10,6 @@ import { getPresetIds } from '$lib/engine/presets'; import { tool, type InferUITools, type Tool } from 'ai'; import { layerRegistry, getAvailableLayerTypes, type LayerType } from '$lib/layers/registry'; import { extractDefaultValues } from '$lib/layers/base'; -import { BRAND_COLORS } from '$lib/constants/branding'; // ============================================ // Helper Functions @@ -32,81 +31,54 @@ export function getLayerTypeFromToolName(toolName: string): string | null { return match ? match[1] : null; } +// ============================================ +// Schema Introspection +// ============================================ + +/** + * Maximum number of key props to show in tool descriptions. + * Layer authors should place the most important fields first in their schema. + */ +const MAX_KEY_PROPS = 4; + +/** + * Extract key property names from a Zod object schema. + * Takes the first N fields from the schema shape, relying on layer authors + * ordering the most important properties first. + */ +function extractKeyProps(schema: z.ZodObject): string[] { + return Object.keys(schema.shape).slice(0, MAX_KEY_PROPS); +} + // ============================================ // Shared Schemas for Layer Creation // ============================================ const PositionSchema = z .object({ - x: z.number().default(0).describe('X position (0=center)'), - y: z.number().default(0).describe('Y position (0=center)') + x: z.number().default(0).describe('X position (0 = center)'), + y: z.number().default(0).describe('Y position (0 = center)') }) .optional() - .describe('Position on canvas - ALWAYS specify to avoid stacking at center'); + .describe('Position on canvas. IMPORTANT: always specify to avoid stacking at center'); const AnimationSchema = z .object({ - preset: z - .string() - .describe( - 'Animation preset ID (REQUIRED): fade-in, slide-in-left, scale-in, pop, bounce-in, zoom-in, etc.' - ), + preset: z.string().describe('Animation preset: ' + getPresetIds().join(', ')), startTime: z.number().min(0).default(0).describe('Start time in seconds'), duration: z.number().min(0.1).default(0.5).describe('Duration in seconds') }) .optional() - .describe('Animation to apply immediately when layer is created'); + .describe('Animation to apply on creation. Every layer should be animated'); // ============================================ // Layer Creation Tool Generator // ============================================ /** - * Get key props for each layer type for the description - * TODO: remove this and infer from the schema - */ -function getKeyPropsForLayerType(type: string): string[] { - const keyProps: Record = { - text: ['content', 'fontSize', 'color', 'fontFamily'], - icon: ['icon', 'size', 'color'], - shape: ['shapeType', 'background', 'width', 'height'], - code: ['code', 'language'], - image: ['src', 'width', 'height'], - button: ['text', 'backgroundColor', 'textColor'], - terminal: ['title', 'content'], - progress: ['progress', 'progressColor'], - mouse: ['pointerType', 'size'], - phone: ['url'], - browser: ['url'], - html: ['html', 'css'] - }; - return keyProps[type] || []; -} - -/** - * Get example props for each layer type - * TODO: remove this and infer from the schema - */ -function getExampleProps(type: string): string { - const examples: Record = { - text: '"content": "Hello World", "fontSize": 48, "color": "#ffffff"', - icon: '"icon": "star", "size": 64, "color": "#ffffff"', - shape: `"shapeType": "rectangle", "background": "${BRAND_COLORS.blue}", "width": 200, "height": 100"`, - code: '"code": "const x = 1;", "language": "typescript"', - image: '"src": "https://example.com/image.jpg", "width": 400', - button: `"text": "Click Me", "backgroundColor": "${BRAND_COLORS.blue}"`, - terminal: '"content": "$ npm install", "title": "Terminal"', - progress: '"progress": 75, "progressColor": "#22c55e"', - mouse: '"pointerType": "arrow", "size": 32', - phone: '"url": "https://example.com"', - browser: '"url": "https://example.com"', - html: '"html": "
Content
", "css": ".container { color: white; }"' - }; - return examples[type] || ''; -} - -/** - * Generate tool definitions for all layer types from the registry + * Generate tool definitions for all layer types from the registry. + * Key props and defaults are derived directly from each layer's Zod schema, + * so no manual mapping needs to be maintained. */ function generateLayerCreationTools(): Record { const tools: Record = {}; @@ -116,11 +88,10 @@ function generateLayerCreationTools(): Record { if (!definition) continue; const toolName = `create_${layerType}_layer`; - const keyProps = getKeyPropsForLayerType(layerType); - const exampleProps = getExampleProps(layerType); + const keyProps = extractKeyProps(definition.schema); const defaults = extractDefaultValues(definition.schema); - // Build key props description with defaults + // Build key props description with defaults from schema const keyPropsDescription = keyProps .map((prop) => { const defaultVal = defaults[prop]; @@ -128,7 +99,6 @@ function generateLayerCreationTools(): Record { }) .join(', '); - // Build the input schema with shared fields + layer-specific props const inputSchema = z.object({ name: z.string().optional().describe('Layer name for identification'), position: PositionSchema, @@ -136,17 +106,9 @@ function generateLayerCreationTools(): Record { animation: AnimationSchema }); - const description = `Create a ${definition.label} layer. ${definition.description} - -Key props: ${keyPropsDescription} - -Example: -{ - "name": "My ${definition.label}", - "position": { "x": 0, "y": 0 }, - "props": { ${exampleProps} }, - "animation": { "preset": "fade-in", "startTime": 0, "duration": 0.5 } -}`; + const description = + `Create a ${definition.label} layer. ${definition.description}\n` + + `Key props: ${keyPropsDescription}`; tools[toolName] = tool({ description, @@ -307,38 +269,29 @@ export const animationTools = { // Layer creation tools (dynamically generated from registry) ...layerCreationTools, - // Animation and editing tools animate_layer: tool({ - description: `Add animation to a layer. Use when you need to animate an existing layer or add complex keyframes. - -Presets: fade-in, slide-in-left, slide-in-right, slide-in-top, slide-in-bottom, scale-in, pop, bounce-in, zoom-in, rotate-in, pulse, float - -Example: { layerId: "layer_0", preset: { id: "pop", startTime: 0.3, duration: 0.6 } } - -Output: { success, keyframesAdded, message }`, + description: + 'Add animation to an existing layer via preset or custom keyframes. ' + + 'Use layer_N for layers you just created, or actual ID/name for existing layers.', inputSchema: AnimateLayerInputSchema }), edit_layer: tool({ - description: `Modify an existing layer's transform, style, or props. - -Output: { success, layerId, message } or { success: false, error } - -Layer reference: Use "layer_0" for layers you created, or the ID/name from PROJECT STATE.`, + description: + 'Modify an existing layer (position, scale, rotation, opacity, or props). ' + + 'Use layer_N for layers you just created, or actual ID/name for existing layers.', inputSchema: EditLayerInputSchema }), remove_layer: tool({ - description: `Delete a layer from the project. - -Output: { success, message } or { success: false, error }`, + description: 'Delete a layer from the project.', inputSchema: RemoveLayerInputSchema }), configure_project: tool({ - description: `Update project settings: dimensions, duration, background color. - -Output: { success, message }`, + description: + 'Set project dimensions, duration, and background color. ' + + 'Call this FIRST to match the target format (e.g. 1080x1920 for vertical video, dark background).', inputSchema: ConfigureProjectInputSchema }) }; diff --git a/src/lib/ai/system-prompt.ts b/src/lib/ai/system-prompt.ts index 01d493a..bce15c9 100644 --- a/src/lib/ai/system-prompt.ts +++ b/src/lib/ai/system-prompt.ts @@ -1,53 +1,13 @@ /** - * System prompt builder for progressive AI animation generation - * Uses static layer-specific tools with explicit schemas + * System prompt builder for progressive AI animation generation. + * + * Design goals: + * - Keep the prompt short and non-repetitive (LLMs degrade with long context). + * - Do NOT duplicate information already present in tool schemas/descriptions + * (preset lists, key props, etc. live in the tool definitions). + * - Derive everything possible from the registry/schemas at runtime. */ import type { Project } from '$lib/types/animation'; -import { layerRegistry, getAvailableLayerTypes, type LayerType } from '$lib/layers/registry'; -import { extractDefaultValues } from '$lib/layers/base'; - -/** - * Get key props for each layer type - */ -function getKeyPropsForLayerType(type: string): string[] { - const keyProps: Record = { - text: ['content', 'fontSize', 'color'], - icon: ['icon', 'size', 'color'], - shape: ['shapeType', 'background', 'width', 'height'], - code: ['code', 'language'], - image: ['src'], - button: ['text', 'backgroundColor'], - terminal: ['title', 'content'], - progress: ['progress', 'progressColor'], - mouse: ['pointerType'], - phone: ['url'], - browser: ['url'], - html: ['html', 'css'] - }; - return keyProps[type] || []; -} - -/** - * Build formatted list of available layer creation tools - */ -function buildLayerToolsList(): string { - return getAvailableLayerTypes() - .map((type) => { - const definition = layerRegistry[type as LayerType]; - const keyProps = getKeyPropsForLayerType(type); - const defaults = extractDefaultValues(definition.schema); - - const propsInfo = keyProps - .map((prop) => { - const defaultVal = defaults[prop]; - return defaultVal !== undefined ? `${prop}="${defaultVal}"` : prop; - }) - .join(', '); - - return `- **create_${type}_layer**: ${definition.description || definition.label} (${propsInfo})`; - }) - .join('\n'); -} /** * Build the system prompt for progressive tool-calling @@ -55,110 +15,57 @@ function buildLayerToolsList(): string { export function buildSystemPrompt(project: Project): string { const halfWidth = project.width / 2; const halfHeight = project.height / 2; + const thirdHeight = Math.round(project.height / 3); - return `# DevMotion AI - Motion Graphics Designer - -You create professional video animations by thinking through the design, then using tools step-by-step to build the animation progressively. - -## LAYER CREATION TOOLS + return `You are a motion-graphics designer. You create professional video animations by calling tools step-by-step. -Each layer type has its own creation tool with explicit properties: +## Workflow -${buildLayerToolsList()} +1. **configure_project** first if the user wants a specific format, background, or duration. +2. **Create layers** with create_*_layer tools. Each call returns a reference (layer_0, layer_1, ...) you can reuse. +3. **Animate every layer** — pass an \`animation\` object inline when creating, or call animate_layer after. No layer should be static. +4. **Refine** with edit_layer or remove_layer as needed. -## OTHER TOOLS +## Canvas -- **animate_layer**: Add animation to an existing layer -- **edit_layer**: Modify layer properties, position, or transform -- **remove_layer**: Delete a layer -- **configure_project**: Update project dimensions, duration, or background - -## YOUR WORKFLOW - -1. **Design**: Plan your composition and explain your approach -2. **Build**: Create layers using create_*_layer tools (returns layer_0, layer_1, etc.) -3. **Animate**: Add motion with inline animation or animate_layer -4. **Refine**: Edit and iterate based on tool results - -## CANVAS SPACE - -Canvas: ${project.width}x${project.height}px | Center: (0, 0) +${project.width}x${project.height}px, center at (0,0). X: -${halfWidth}..+${halfWidth}. Y: -${halfHeight}..+${halfHeight}. Duration: ${project.duration}s | FPS: ${project.fps} | Background: ${project.background} -Coordinate system: -- Center: (0, 0) -- Left edge: x = -${halfWidth} | Right edge: x = +${halfWidth} -- Top edge: y = -${halfHeight} | Bottom edge: y = +${halfHeight} - -## CURRENT PROJECT STATE -${buildCanvasState(project)} - -## MOTION DESIGN PRINCIPLES - -- **Timing**: Entrances 0.3-0.6s, exits 0.2-0.4s, stagger 0.1-0.2s -- **Easing**: ease-out (entrances), ease-in (exits), ease-in-out (movements) -- **Hierarchy**: Animate hero elements first, then supporting elements -- **Polish**: Subtle overshoot (scale: 1 → 1.05 → 1) adds polish - -## LAYER REFERENCES - -**For layers YOU create:** Use layer_0, layer_1, etc. (assigned in creation order) -**For EXISTING layers:** Use the exact ID or name shown in PROJECT STATE below - -Example: If PROJECT STATE shows '0. "Title" (id: "abc123", type: text)': -- Use "abc123" or "Title" to reference it -- layer_0 will NOT work (it only tracks layers you create) - -## ANIMATION IS REQUIRED - -**Every layer MUST have animation.** Static layers are not acceptable for motion graphics. - -Options to animate: -1. **Inline animation**: Pass \`animation: { preset: "fade-in", startTime: 0, duration: 0.5 }\` in create tool -2. **Separate call**: Use animate_layer after creating the layer - -Available presets: fade-in, fade-out, slide-in-left, slide-in-right, slide-in-top, slide-in-bottom, scale-in, pop, bounce-in, rotate-in, zoom-in, pulse, float +## Layout guidelines -Stagger timing: Start each layer's animation 0.1-0.3s after the previous one for professional flow. +Distribute layers across the canvas — never stack everything at (0,0). +- Title area: y ≈ -${thirdHeight} +- Main content: y ≈ 0 +- Footer / subtitle: y ≈ +${thirdHeight} -## PROPS ARE IMPORTANT +## Layer references -Every layer should have meaningful props that match the user's intent. +- **Layers you create**: use layer_0, layer_1, ... (assigned in creation order within this conversation). +- **Pre-existing layers**: use the exact \`id\` or \`name\` shown in PROJECT STATE. layer_N does NOT work for pre-existing layers. -**Position matters!** Don't stack everything at (0,0). Plan your composition: -- Title at top: y = -${project.height / 3} -- Main content: y = 0 -- Subtitle/footer: y = +${project.height / 3} -- Use x offset for side-by-side elements +## Animation tips -**GOOD example:** -\`\`\`json -create_text_layer({ - "name": "Hero Title", - "position": { "x": 0, "y": -150 }, - "props": { "content": "DevMotion", "fontSize": 96, "fontWeight": "bold", "color": "#ffffff" }, - "animation": { "preset": "slide-in-bottom", "startTime": 0.2, "duration": 0.6 } -}) -\`\`\` +- Stagger start times by 0.1–0.3 s between layers for a professional sequence. +- Entrances: 0.3–0.6 s with ease-out. Exits: 0.2–0.4 s with ease-in. +- Animate hero elements first, then supporting ones. -## KEY RULES +## Rules -1. Use the specific create tool for each layer type (create_text_layer, create_icon_layer, etc.) -2. ALWAYS add animation to every layer (inline or via animate_layer) -3. ALWAYS include meaningful props - the tool shows you exactly what's available -4. ALWAYS position layers intentionally - don't stack at center -5. Build progressively - create layers one by one -6. Use staggered timing for multi-layer animations +1. Always set meaningful props (content, colors, sizes) — do not rely on defaults for visible content. +2. Always position layers intentionally. +3. Always animate every layer. +4. Create layers one at a time; do not batch unrelated layers in a single call. -Now help the user create their animation.`; +## Project state +${buildCanvasState(project)}`; } /** - * Build visual canvas state + * Build a compact view of the current canvas state for the AI. */ function buildCanvasState(project: Project): string { if (project.layers.length === 0) { - return `Canvas is EMPTY. Start by creating layers.`; + return 'Empty canvas — no layers yet.'; } const halfHeight = project.height / 2; @@ -181,7 +88,6 @@ function buildCanvasState(project: Project): string { spatial += `BOTTOM: ${bottomLayers.map((l) => `[${l.name}]`).join(' ')}\n`; } - // Detailed layer list const layerList = project.layers .map((layer, index) => { const animatedProps = [...new Set(layer.keyframes.map((k) => k.property))]; @@ -198,6 +104,6 @@ function buildCanvasState(project: Project): string { .join('\n'); return `${spatial} -LAYERS (${project.layers.length} total): +${project.layers.length} layer(s): ${layerList}`; } From 48562e9ade4f44651cdb837c83c32703c668ae55 Mon Sep 17 00:00:00 2001 From: EmaDev Date: Thu, 5 Feb 2026 22:40:25 +0100 Subject: [PATCH 2/3] refactor: Reorganize tooltip components into a more modular structure with dedicated wrapper, provider, and button components. --- .../editor/timeline/timeline-keyframe.svelte | 6 +- src/lib/components/editor/toolbar.svelte | 216 +++++++++--------- src/lib/components/ui/tooltip/index.ts | 14 +- .../ui/tooltip/tooltip-button.svelte | 45 ++++ .../ui/tooltip/tooltip-content.svelte | 21 +- .../ui/tooltip/tooltip-portal.svelte | 7 + .../ui/tooltip/tooltip-provider.svelte | 7 + .../ui/tooltip/tooltip-wrapper.svelte | 41 ++++ src/lib/components/ui/tooltip/tooltip.svelte | 38 +-- src/lib/stores/ui.svelte.ts | 4 +- 10 files changed, 230 insertions(+), 169 deletions(-) create mode 100644 src/lib/components/ui/tooltip/tooltip-button.svelte create mode 100644 src/lib/components/ui/tooltip/tooltip-portal.svelte create mode 100644 src/lib/components/ui/tooltip/tooltip-provider.svelte create mode 100644 src/lib/components/ui/tooltip/tooltip-wrapper.svelte diff --git a/src/lib/components/editor/timeline/timeline-keyframe.svelte b/src/lib/components/editor/timeline/timeline-keyframe.svelte index 43125d7..fdc6b06 100644 --- a/src/lib/components/editor/timeline/timeline-keyframe.svelte +++ b/src/lib/components/editor/timeline/timeline-keyframe.svelte @@ -1,7 +1,7 @@ - + {#snippet content()}
@@ -160,4 +160,4 @@ aria-label="Keyframe at {firstKeyframe.time.toFixed(2)}s" > - + diff --git a/src/lib/components/editor/toolbar.svelte b/src/lib/components/editor/toolbar.svelte index 654bfed..6b3411c 100644 --- a/src/lib/components/editor/toolbar.svelte +++ b/src/lib/components/editor/toolbar.svelte @@ -18,7 +18,6 @@ import { projectStore } from '$lib/stores/project.svelte'; import ExportDialog from './export-dialog.svelte'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; - import Tooltip from '$lib/components/ui/tooltip'; import ProjectSettingsDialog from './project-settings-dialog.svelte'; import { uiStore } from '$lib/stores/ui.svelte'; import { getUser, signOut } from '$lib/functions/auth.remote'; @@ -61,6 +60,7 @@ CollapsibleTrigger } from '$lib/components/ui/collapsible'; import { Menu, X } from '@lucide/svelte'; + import TooltipButton from '../ui/tooltip/tooltip-button.svelte'; let headerOpen = $state(false); @@ -123,8 +123,8 @@ } } - function handleSaveToCloud() { - uiStore.requireLogin('save your project', doSaveToCloud); + async function handleSaveToCloud() { + await uiStore.requireLogin('save your project', doSaveToCloud); } async function handleToggleVisibility() { @@ -151,9 +151,7 @@ {#snippet leftContent()}
- - - + {#if isMcp}
- -
{#if projectId}
{#if isOwner && user} - -
{/if} @@ -248,75 +250,69 @@
{#if !isMobile} - - - - {#snippet child({ props })} - - + + Export Video + {/if} {#if user} - - - - {#snippet child({ props })} - - {/snippet} - - Logout - - - - - + + + {#snippet child({ props })} + + {/snippet} + + Logout + + + + {:else} {/if} diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts index ab3fb9f..36a2f3a 100644 --- a/src/lib/components/ui/tooltip/index.ts +++ b/src/lib/components/ui/tooltip/index.ts @@ -1,25 +1,19 @@ -import { Tooltip as TooltipPrimitive } from 'bits-ui'; +import Root from './tooltip.svelte'; import Trigger from './tooltip-trigger.svelte'; import Content from './tooltip-content.svelte'; -import Tooltip from './tooltip.svelte'; - -const Root = TooltipPrimitive.Root; -const Provider = TooltipPrimitive.Provider; -const Portal = TooltipPrimitive.Portal; +import Provider from './tooltip-provider.svelte'; +import Portal from './tooltip-portal.svelte'; export { - Tooltip, Root, Trigger, Content, Provider, Portal, // - Root as TooltipRoot, + Root as Tooltip, Content as TooltipContent, Trigger as TooltipTrigger, Provider as TooltipProvider, Portal as TooltipPortal }; - -export default Tooltip; diff --git a/src/lib/components/ui/tooltip/tooltip-button.svelte b/src/lib/components/ui/tooltip/tooltip-button.svelte new file mode 100644 index 0000000..83e4ceb --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-button.svelte @@ -0,0 +1,45 @@ + + + + + + {#snippet child({ props })} + + {/snippet} + + {#if content} + + {#if typeof content === 'string'} +

{content}

+ {:else} + {@render content()} + {/if} +
+ {/if} +
+
diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte index 3025f71..6cb98c6 100644 --- a/src/lib/components/ui/tooltip/tooltip-content.svelte +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -1,6 +1,9 @@ - + - + diff --git a/src/lib/components/ui/tooltip/tooltip-portal.svelte b/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..7b9e8f9 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip-provider.svelte b/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..5dbed32 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip-wrapper.svelte b/src/lib/components/ui/tooltip/tooltip-wrapper.svelte new file mode 100644 index 0000000..041a5ef --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-wrapper.svelte @@ -0,0 +1,41 @@ + + + + + + {@render children?.()} + + {#if content} + + {#if typeof content === 'string'} +

{content}

+ {:else} + {@render content()} + {/if} +
+ {/if} +
+
diff --git a/src/lib/components/ui/tooltip/tooltip.svelte b/src/lib/components/ui/tooltip/tooltip.svelte index 041a5ef..42a939b 100644 --- a/src/lib/components/ui/tooltip/tooltip.svelte +++ b/src/lib/components/ui/tooltip/tooltip.svelte @@ -1,41 +1,7 @@ - - - - {@render children?.()} - - {#if content} - - {#if typeof content === 'string'} -

{content}

- {:else} - {@render content()} - {/if} -
- {/if} -
-
+ diff --git a/src/lib/stores/ui.svelte.ts b/src/lib/stores/ui.svelte.ts index 935a3e3..e8cccdb 100644 --- a/src/lib/stores/ui.svelte.ts +++ b/src/lib/stores/ui.svelte.ts @@ -4,10 +4,10 @@ class UIStore { showLoginPrompt = $state(false); loginPromptAction = $state('this action'); - async requireLogin(action: string, fn: () => void) { + async requireLogin(action: string, fn: () => void | Promise) { const user = await getUser(); if (user) { - fn(); + await fn(); } else { this.loginPromptAction = action; this.showLoginPrompt = true; From 319bbc41f164b13c468e46abc67a52c822a3d439 Mon Sep 17 00:00:00 2001 From: EmaDev Date: Thu, 5 Feb 2026 23:00:07 +0100 Subject: [PATCH 3/3] ui --- Prompt demo.md | 28 +++++++ src/lib/components/ai/ai-chat.svelte | 70 +++++++---------- src/lib/components/ai/tool-part.svelte | 77 +++++++++++++++++++ .../components/editor/keyboard-handler.svelte | 4 +- src/lib/components/editor/toolbar.svelte | 8 +- 5 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 Prompt demo.md create mode 100644 src/lib/components/ai/tool-part.svelte diff --git a/Prompt demo.md b/Prompt demo.md new file mode 100644 index 0000000..72678a7 --- /dev/null +++ b/Prompt demo.md @@ -0,0 +1,28 @@ +Prompt demo: +generate a demo video for DevMotion. +Here some info + +## ✨ Features + +### Animation Studio + +- **Timeline Editor** – Full keyframe control with smooth interpolation and easing curves +- **Layer Management** – Text, shapes, and images with complete customization +- **Interactive Canvas** – Zoom, pan, grid controls, and real-time preview +- **Export** – High-quality MP4 videos with no watermarks or file limits + +### AI-Powered Workflow + +- **Intelligent Suggestions** – Get smart animation and layer recommendations +- **Auto-Generation** – Create motion sequences automatically +- **MCP Integration** – Use DevMotion tools directly in Claude via Model Context Protocol + +### Project Management + +- **Save & Load** – Store projects in JSON format for future editing +- **Database Storage** – Persistent project storage with PostgreSQL +- **Authentication** – Secure user accounts with Better Auth + +Logo info +Family name: Montserrat Thin +PostScript name: Montserrat-SemiBold diff --git a/src/lib/components/ai/ai-chat.svelte b/src/lib/components/ai/ai-chat.svelte index 2ca7aeb..7203edb 100644 --- a/src/lib/components/ai/ai-chat.svelte +++ b/src/lib/components/ai/ai-chat.svelte @@ -29,6 +29,8 @@ import { uiStore } from '$lib/stores/ui.svelte'; import { PersistedState } from 'runed'; + import ToolPart from './tool-part.svelte'; + let prompt = new PersistedState('prompt', ''); let showModelSelector = $state(false); let selectedModelId = $state(DEFAULT_MODEL_ID); @@ -192,54 +194,31 @@ {:else}
{#each chat.messages as message (message.id)} -
-
- {#if message.role === 'user'} - - {:else} - - {/if} -
- -
+
+
+
+ {#if message.role === 'user'} + + {:else} + + {/if} +

{message.role === 'user' ? 'You' : 'AI'}

+
+
{#each message.parts as part, partIndex (partIndex)} {#if part.type === 'text' && part.text.trim()}

{part.text}

{:else if isToolUIPart(part)} -
- - 🔧 {part.type.replace('tool-', '')} - ({part.state || 'pending'}) - -
- {#if part.input} -
- Input: - {JSON.stringify(part.input, null, 2)} -
- {/if} - {#if part.output} -
- Output: - {JSON.stringify(part.output, null, 2)} -
- {/if} -
-
- + {/if} {/each}
@@ -247,11 +226,14 @@ {/each} {#if chat.status === 'streaming' || chat.status === 'submitted'} -
-
- +
+
+
+ +
+

AI

-
+
Thinking...
diff --git a/src/lib/components/ai/tool-part.svelte b/src/lib/components/ai/tool-part.svelte new file mode 100644 index 0000000..a4ec9f5 --- /dev/null +++ b/src/lib/components/ai/tool-part.svelte @@ -0,0 +1,77 @@ + + +
+ +
+ + {#if success} + ✅ + {:else if hasOutput && output.success === false} + ❌ + {:else} + ⏳ + {/if} + +
+ {#if message} +

{message}

+ {:else} +

+ {tool.state === 'output-available' ? 'Completed' : 'Processing...'} +

+ {/if} +
+
+ + +
+ + 🔧 {tool.type.replace('tool-', '')} + {#if hasOutput && Object.keys(output).length > 2} + • {Object.keys(output).length - 2} more fields + {/if} + +
+ {#if tool.input} +
+
Input:
+
{JSON.stringify(
+              tool.input,
+              null,
+              2
+            )}
+
+ {/if} + {#if hasOutput} + {@const otherFields = Object.fromEntries( + Object.entries(output).filter(([key]) => key !== 'success' && key !== 'message') + )} + {#if Object.keys(otherFields).length > 0} +
+
Details:
+
{JSON.stringify(
+                otherFields,
+                null,
+                2
+              )}
+
+ {/if} + {/if} +
+
+
diff --git a/src/lib/components/editor/keyboard-handler.svelte b/src/lib/components/editor/keyboard-handler.svelte index e4ee3bb..76c3375 100644 --- a/src/lib/components/editor/keyboard-handler.svelte +++ b/src/lib/components/editor/keyboard-handler.svelte @@ -37,9 +37,7 @@ // Cmd/Ctrl + N - New project if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); - if (confirm('Create new project? Unsaved changes will be lost.')) { - projectStore.newProject(); - } + window.dispatchEvent(new CustomEvent('devmotion:new-project')); } // Cmd/Ctrl + G - Toggle grid diff --git a/src/lib/components/editor/toolbar.svelte b/src/lib/components/editor/toolbar.svelte index 6b3411c..7d10049 100644 --- a/src/lib/components/editor/toolbar.svelte +++ b/src/lib/components/editor/toolbar.svelte @@ -82,8 +82,13 @@ } const onSave = () => handleSaveToCloud(); + const onNewProject = () => newProject(); window.addEventListener('devmotion:save', onSave); - return () => window.removeEventListener('devmotion:save', onSave); + window.addEventListener('devmotion:new-project', onNewProject); + return () => { + window.removeEventListener('devmotion:save', onSave); + window.removeEventListener('devmotion:new-project', onNewProject); + }; }); const shortcuts = [ @@ -95,6 +100,7 @@ function newProject() { if (confirm('Create new project? Unsaved changes will be lost.')) { projectStore.newProject(); + goto(resolve('/')); } }