diff --git a/.env.example b/.env.example index 2308ae0..f11f398 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,6 @@ PRIVATE_BETTER_AUTH_SECRET=mysecretpassword # Google OAuth GOOGLE_CLIENT_ID=your_google_client_id_here GOOGLE_CLIENT_SECRET=your_google_client_secret_here + +# AI Generation (OpenRouter) +OPENROUTER_API_KEY=your_openrouter_api_key_here diff --git a/package.json b/package.json index 077a3ca..bb571b2 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,10 @@ "vitest-browser-svelte": "^1.1.0" }, "dependencies": { + "@ai-sdk/openai": "^3.0.18", + "@openrouter/ai-sdk-provider": "^2.0.2", "@sveltejs/adapter-static": "^3.0.10", + "ai": "^6.0.49", "better-auth": "^1.3.27", "bezier-easing": "^2.1.0", "lucide-svelte": "^0.563.0", @@ -72,6 +75,7 @@ "postgres": "^3.4.7", "runed": "^0.34.0", "schema-dts": "^1.1.5", + "svelte-sonner": "^1.0.7", "zod": "^4.3.6" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1db05f9..a11f665 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@ai-sdk/openai': + specifier: ^3.0.18 + version: 3.0.18(zod@4.3.6) + '@openrouter/ai-sdk-provider': + specifier: ^2.0.2 + version: 2.0.2(ai@6.0.49(zod@4.3.6))(zod@4.3.6) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) + version: 3.0.10(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) + ai: + specifier: ^6.0.49 + version: 6.0.49(zod@4.3.6) better-auth: specifier: ^1.3.27 - version: 1.3.27(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + version: 1.3.27(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) bezier-easing: specifier: ^2.1.0 version: 2.1.0 @@ -31,10 +40,13 @@ importers: version: 3.4.7 runed: specifier: ^0.34.0 - version: 0.34.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + version: 0.34.0(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) schema-dts: specifier: ^1.1.5 version: 1.1.5 + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.48.2) zod: specifier: ^4.3.6 version: 4.3.6 @@ -56,10 +68,10 @@ importers: version: 0.563.1(svelte@5.48.2) '@sveltejs/adapter-node': specifier: ^5.5.2 - version: 5.5.2(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) + version: 5.5.2(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))) '@sveltejs/kit': specifier: ^2.50.1 - version: 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + version: 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.4 version: 6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) @@ -77,7 +89,7 @@ importers: version: 3.2.4(playwright@1.58.0)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4) bits-ui: specifier: ^2.15.4 - version: 2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + version: 2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -86,7 +98,7 @@ importers: version: 0.31.8 drizzle-orm: specifier: ^0.45.1 - version: 0.45.1(kysely@0.27.6)(postgres@3.4.7) + version: 0.45.1(@opentelemetry/api@1.9.0)(kysely@0.27.6)(postgres@3.4.7) eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -156,6 +168,28 @@ importers: packages: + '@ai-sdk/gateway@3.0.22': + resolution: {integrity: sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.18': + resolution: {integrity: sha512-uYscTyoaWij9FoPpKRNK8YgtDEuPpQlqREYylJCA8o5YQVQXghV0Dwgk1ehPVpg6USIO4L0C8GqQJ4AMm/Xb1g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.9': + resolution: {integrity: sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + engines: {node: '>=18'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -618,6 +652,20 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@openrouter/ai-sdk-provider@2.0.2': + resolution: {integrity: sha512-G+8Z7Q4R61anj/nk/hmb1JAAjYpP/LEFA4mjQnitMGDLscoeThPpOl6xFKalzfkd2WSweeKRYAID5HxzEMCbPQ==} + engines: {node: '>=18'} + peerDependencies: + ai: ^6.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@openrouter/sdk@0.1.27': + resolution: {integrity: sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ==} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@peculiar/asn1-android@2.5.0': resolution: {integrity: sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==} @@ -820,6 +868,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.6': resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} peerDependencies: @@ -1069,6 +1120,10 @@ packages: resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@vitest/browser@3.2.4': resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} peerDependencies: @@ -1123,6 +1178,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ai@6.0.49: + resolution: {integrity: sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1532,6 +1593,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -1685,6 +1750,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2088,6 +2156,11 @@ packages: peerDependencies: svelte: ^5.7.0 + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + runed@0.29.2: resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} peerDependencies: @@ -2198,6 +2271,11 @@ packages: svelte: optional: true + svelte-sonner@1.0.7: + resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} + peerDependencies: + svelte: ^5.0.0 + svelte-toolbelt@0.10.6: resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -2494,6 +2572,30 @@ packages: snapshots: + '@ai-sdk/gateway@3.0.22(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 + + '@ai-sdk/openai@3.0.18(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@4.3.6) + zod: 4.3.6 + + '@ai-sdk/provider-utils@4.0.9(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 + + '@ai-sdk/provider@3.0.5': + dependencies: + json-schema: 0.4.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -2829,6 +2931,18 @@ snapshots: '@noble/hashes@2.0.1': {} + '@openrouter/ai-sdk-provider@2.0.2(ai@6.0.49(zod@4.3.6))(zod@4.3.6)': + dependencies: + '@openrouter/sdk': 0.1.27 + ai: 6.0.49(zod@4.3.6) + zod: 4.3.6 + + '@openrouter/sdk@0.1.27': + dependencies: + zod: 4.3.6 + + '@opentelemetry/api@1.9.0': {} + '@peculiar/asn1-android@2.5.0': dependencies: '@peculiar/asn1-schema': 2.5.0 @@ -3048,23 +3162,25 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-node@5.5.2(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': + '@sveltejs/adapter-node@5.5.2(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': dependencies: '@rollup/plugin-commonjs': 28.0.6(rollup@4.52.4) '@rollup/plugin-json': 6.1.0(rollup@4.52.4) '@rollup/plugin-node-resolve': 16.0.2(rollup@4.52.4) - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) rollup: 4.52.4 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))': dependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) - '@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))': + '@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) @@ -3083,6 +3199,7 @@ snapshots: svelte: 5.48.2 vite: 7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1) optionalDependencies: + '@opentelemetry/api': 1.9.0 typescript: 5.9.3 '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))': @@ -3324,6 +3441,8 @@ snapshots: '@typescript-eslint/types': 8.53.1 eslint-visitor-keys: 4.2.1 + '@vercel/oidc@3.1.0': {} + '@vitest/browser@3.2.4(playwright@1.58.0)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 @@ -3391,6 +3510,14 @@ snapshots: acorn@8.15.0: {} + ai@6.0.49(zod@4.3.6): + dependencies: + '@ai-sdk/gateway': 3.0.22(zod@4.3.6) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@4.3.6) + '@opentelemetry/api': 1.9.0 + zod: 4.3.6 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -3428,7 +3555,7 @@ snapshots: balanced-match@1.0.2: {} - better-auth@1.3.27(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + better-auth@1.3.27(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: '@better-auth/core': 1.3.27 '@better-auth/utils': 0.3.0 @@ -3444,7 +3571,7 @@ snapshots: nanostores: 1.0.1 zod: 4.3.6 optionalDependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) svelte: 5.48.2 better-call@1.0.19: @@ -3457,15 +3584,15 @@ snapshots: bezier-easing@2.1.0: {} - bits-ui@2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + bits-ui@2.15.4(@internationalized/date@3.10.1)(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.1 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + runed: 0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) svelte: 5.48.2 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) tabbable: 6.2.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -3571,8 +3698,9 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(kysely@0.27.6)(postgres@3.4.7): + drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(kysely@0.27.6)(postgres@3.4.7): optionalDependencies: + '@opentelemetry/api': 1.9.0 kysely: 0.27.6 postgres: 3.4.7 @@ -3750,6 +3878,8 @@ snapshots: esutils@2.0.3: {} + eventsource-parser@3.0.6: {} + expect-type@1.2.2: {} fast-deep-equal@3.1.3: {} @@ -3863,6 +3993,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -4166,28 +4298,33 @@ snapshots: esm-env: 1.2.2 svelte: 5.48.2 + runed@0.28.0(svelte@5.48.2): + dependencies: + esm-env: 1.2.2 + svelte: 5.48.2 + runed@0.29.2(svelte@5.48.2): dependencies: esm-env: 1.2.2 svelte: 5.48.2 - runed@0.34.0(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + runed@0.34.0(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.48.2 optionalDependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) - runed@0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + runed@0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 svelte: 5.48.2 optionalDependencies: - '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) + '@sveltejs/kit': 2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)) sade@1.8.1: dependencies: @@ -4270,10 +4407,15 @@ snapshots: optionalDependencies: svelte: 5.48.2 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): + svelte-sonner@1.0.7(svelte@5.48.2): + dependencies: + runed: 0.28.0(svelte@5.48.2) + svelte: 5.48.2 + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) + runed: 0.35.1(@sveltejs/kit@2.50.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.2)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2)(typescript@5.9.3)(vite@7.1.9(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.48.2) style-to-object: 1.0.11 svelte: 5.48.2 transitivePeerDependencies: diff --git a/src/lib/ai/schemas.ts b/src/lib/ai/schemas.ts new file mode 100644 index 0000000..3c3c6f9 --- /dev/null +++ b/src/lib/ai/schemas.ts @@ -0,0 +1,204 @@ +/** + * Zod schemas for AI tool calls + * Defines the structured operations the AI can perform + */ +import { z } from 'zod'; +import { LayerTypeSchema, EasingSchema, AnchorPointSchema } from '$lib/schemas/animation'; + +// ============================================ +// Layer Operations +// ============================================ + +/** + * Add a new layer - props should ALWAYS be specified with meaningful content + */ +export const AddLayerToolSchema = z.object({ + action: z.literal('add_layer'), + type: LayerTypeSchema.describe('The type of layer to create'), + name: z.string().optional().describe('Layer name (auto-generated if not provided)'), + position: z + .object({ + x: z.number().default(0), + y: z.number().default(0) + }) + .optional() + .describe('Initial position on canvas (0,0 is center)'), + props: z + .record(z.string(), z.unknown()) + .describe( + 'Layer-specific properties - MUST include meaningful content (e.g., text content, colors, dimensions)' + ) +}); + +/** + * Edit an existing layer + */ +export const EditLayerToolSchema = z.object({ + action: z.literal('edit_layer'), + layerId: z.string().describe('ID of the layer to edit (use actual ID from project state)'), + updates: z.object({ + name: z.string().optional(), + visible: z.boolean().optional(), + locked: z.boolean().optional(), + transform: z + .object({ + x: z.number().optional(), + y: z.number().optional(), + z: z.number().optional(), + rotationX: z.number().optional(), + rotationY: z.number().optional(), + rotationZ: z.number().optional(), + scaleX: z.number().optional(), + scaleY: z.number().optional(), + scaleZ: z.number().optional(), + anchor: AnchorPointSchema.optional() + }) + .optional(), + style: z + .object({ + opacity: z.number().min(0).max(1).optional() + }) + .optional(), + props: z.record(z.string(), z.unknown()).optional() + }) +}); + +/** + * Remove a layer + */ +export const RemoveLayerToolSchema = z.object({ + action: z.literal('remove_layer'), + layerId: z.string().describe('ID of the layer to remove') +}); + +// ============================================ +// Keyframe Operations +// ============================================ + +/** + * Add a keyframe to a layer + */ +export const AddKeyframeToolSchema = z.object({ + action: z.literal('add_keyframe'), + layerId: z + .string() + .describe( + 'ID of the layer - use "layer_0", "layer_1", etc. for newly created layers, or actual ID for existing layers' + ), + keyframe: z.object({ + time: z.number().min(0).describe('Time in seconds'), + property: z + .string() + .describe( + 'Property path: position.x, position.y, position.z, scale.x, scale.y, scale.z, rotation.x, rotation.y, rotation.z, opacity, or props.' + ), + value: z.union([z.number(), z.string(), z.boolean()]).describe('Value at this keyframe'), + easing: EasingSchema.optional().describe('Easing function (defaults to ease-in-out)') + }) +}); + +/** + * Edit an existing keyframe + */ +export const EditKeyframeToolSchema = z.object({ + action: z.literal('edit_keyframe'), + layerId: z.string().describe('ID of the layer'), + keyframeId: z.string().describe('ID of the keyframe to edit'), + updates: z.object({ + time: z.number().min(0).optional(), + value: z.union([z.number(), z.string(), z.boolean()]).optional(), + easing: EasingSchema.optional() + }) +}); + +/** + * Remove a keyframe + */ +export const RemoveKeyframeToolSchema = z.object({ + action: z.literal('remove_keyframe'), + layerId: z.string().describe('ID of the layer'), + keyframeId: z.string().describe('ID of the keyframe to remove') +}); + +// ============================================ +// Animation Preset Operations +// ============================================ + +/** + * Apply an animation preset to a layer + */ +export const ApplyPresetToolSchema = z.object({ + action: z.literal('apply_preset'), + layerId: z + .string() + .describe( + 'ID of the layer - use "layer_0", "layer_1", etc. for newly created layers, or actual ID for existing layers' + ), + presetId: z + .string() + .describe( + 'ID of the preset to apply: fade-in, fade-out, slide-in-left, slide-in-right, slide-in-top, slide-in-bottom, scale-in, scale-out, bounce, rotate-in, pop, typewriter, pulse, shake, float' + ), + startTime: z.number().min(0).default(0).describe('Time to start the animation (seconds)'), + duration: z.number().min(0.1).default(1).describe('Duration of the animation (seconds)') +}); + +/** + * Add multiple keyframes at once (batch operation) + */ +export const BatchKeyframesToolSchema = z.object({ + action: z.literal('batch_keyframes'), + layerId: z.string().describe('ID of the layer'), + keyframes: z.array( + z.object({ + time: z.number().min(0), + property: z.string(), + value: z.union([z.number(), z.string(), z.boolean()]), + easing: EasingSchema.optional() + }) + ) +}); + +// ============================================ +// Combined Schema +// ============================================ + +/** + * All possible AI tool calls + */ +export const AIToolCallSchema = z.discriminatedUnion('action', [ + AddLayerToolSchema, + EditLayerToolSchema, + RemoveLayerToolSchema, + AddKeyframeToolSchema, + EditKeyframeToolSchema, + RemoveKeyframeToolSchema, + ApplyPresetToolSchema, + BatchKeyframesToolSchema +]); + +export type AIToolCall = z.infer; +export type AddLayerTool = z.infer; +export type EditLayerTool = z.infer; +export type RemoveLayerTool = z.infer; +export type AddKeyframeTool = z.infer; +export type EditKeyframeTool = z.infer; +export type RemoveKeyframeTool = z.infer; +export type ApplyPresetTool = z.infer; +export type BatchKeyframesTool = z.infer; + +/** + * AI Response schema - what the model returns + */ +export const AIResponseSchema = z.object({ + message: z + .string() + .max(100) + .describe('A brief, friendly message to show the user (for toast notification, max 100 chars)'), + operations: z + .array(AIToolCallSchema) + .min(1) + .describe('List of operations to perform on the project - must not be empty') +}); + +export type AIResponse = z.infer; diff --git a/src/lib/ai/system-prompt.ts b/src/lib/ai/system-prompt.ts new file mode 100644 index 0000000..1f70d22 --- /dev/null +++ b/src/lib/ai/system-prompt.ts @@ -0,0 +1,615 @@ +/** + * System prompt builder for AI animation generation + * Generates context-aware prompts with layer metadata and project state + */ +import { layerRegistry, type LayerType } from '$lib/layers/registry'; +import { extractPropertyMetadata, extractDefaultValues } from '$lib/layers/base'; +import type { Project, Layer } from '$lib/types/animation'; +import { animationPresets } from '$lib/engine/presets'; + +/** + * Build the system prompt for the AI model + */ +export function buildSystemPrompt(project: Project): string { + return `You are DevMotion AI, a professional motion graphics designer that creates high-quality video animations. You translate natural language descriptions into precise, visually stunning animation sequences. + +## YOUR MISSION +Create professional-quality motion graphics that would impress clients at a top creative agency. Every animation should be: +- Visually polished with thoughtful timing +- Well-composed with clear visual hierarchy +- Engaging with smooth, purposeful motion +- Complete with all necessary content (text, colors, positions) + +## CRITICAL RULES + +### 1. Layer ID References +When adding keyframes to layers you just created, use INDEX-BASED references: +- First add_layer operation creates layer_0 +- Second add_layer operation creates layer_1 +- Third add_layer operation creates layer_2 +- And so on... + +**CORRECT:** +\`\`\`json +{"action": "add_layer", "type": "text", "props": {"content": "Hello"}}, +{"action": "add_keyframe", "layerId": "layer_0", "keyframe": {...}} +\`\`\` + +**WRONG:** +\`\`\`json +{"action": "add_layer", "type": "text", "name": "Title"}, +{"action": "add_keyframe", "layerId": "Title", "keyframe": {...}} +\`\`\` + +For existing layers, use their actual ID from the project state. + +### 2. ALWAYS Specify Props +NEVER leave props empty. Every layer MUST have meaningful content: +- Text layers: ALWAYS set "content" with actual text +- Shape layers: ALWAYS set "fill" color, "width", "height" +- Button layers: ALWAYS set "text", "backgroundColor" +- All layers: Use colors that match a cohesive visual theme + +**CORRECT:** +\`\`\`json +{"action": "add_layer", "type": "text", "props": {"content": "Welcome", "fontSize": 72, "color": "#ffffff", "fontWeight": "bold"}} +\`\`\` + +**WRONG:** +\`\`\`json +{"action": "add_layer", "type": "text", "props": {}} +\`\`\` + +### 3. Complete Animations +Every animated layer needs: +- Initial state keyframes (usually at time=0) +- End state keyframes +- Proper easing for smooth motion + +## COORDINATE SYSTEM +- Canvas: ${project.width}x${project.height} pixels +- Origin (0, 0): CENTER of canvas +- X: negative=left, positive=right (range: -${project.width / 2} to ${project.width / 2}) +- Y: negative=top, positive=bottom (range: -${project.height / 2} to ${project.height / 2}) +- Off-screen positions: x < -${project.width / 2} (left), x > ${project.width / 2} (right), y < -${project.height / 2} (top), y > ${project.height / 2} (bottom) + +## PROJECT SETTINGS +- Duration: ${project.duration} seconds +- FPS: ${project.fps} +- Background: ${project.backgroundColor} + +## LAYER TYPES & REQUIRED PROPS + +${buildLayerTypesReference()} + +## CURRENT PROJECT STATE +${buildProjectStateReference(project)} + +## ANIMATABLE PROPERTIES + +### Transform (use directly): +- position.x, position.y, position.z (pixels) +- rotation.x, rotation.y, rotation.z (radians - Math.PI = 180°) +- scale.x, scale.y, scale.z (1 = 100%, 0.5 = 50%, 2 = 200%) + +### Style: +- opacity (0 = invisible, 1 = fully visible) + +### Layer Props (prefix with "props."): +- props.content (text content) +- props.fontSize (text size) +- props.color (text/fill color - animates as color interpolation) +- props.fill (shape fill color) +- props.width, props.height (dimensions) +- Any other layer-specific property + +## ANIMATION PRESETS +You can apply these presets using the apply_preset action: +${animationPresets.map((p) => `- "${p.id}": ${p.name}`).join('\n')} + +## PROFESSIONAL ANIMATION PRINCIPLES + +### Timing & Spacing +- Entrance animations: 0.3-0.6s (quick, snappy) +- Exit animations: 0.2-0.4s (slightly faster than entrances) +- Text reveals: 0.4-0.8s +- Stagger delay between elements: 0.1-0.2s +- Hold/pause before transitions: 0.5-1.0s + +### Visual Hierarchy +1. Hero elements (titles): Largest, enter first, center or top-center +2. Supporting text: Smaller, enter after hero, below hero +3. CTAs/buttons: Enter last, bottom portion of screen +4. Background elements: Scale/fade subtly, don't compete + +### Motion Design Best Practices +- Use ease-out for entrances (fast start, slow end = "arriving") +- Use ease-in for exits (slow start, fast end = "leaving") +- Use ease-in-out for position changes within the frame +- Add slight overshoot (scale to 1.05 then 1.0) for "pop" effect +- Stagger element entrances for rhythm + +### Color Guidance +- Dark backgrounds: Use bright, high-contrast text (#ffffff, #f0f0f0) +- Light backgrounds: Use dark text (#1a1a1a, #333333) +- Accent colors: Use sparingly for CTAs and highlights +- Maintain 3-4 color palette maximum + +## SCENE STRUCTURE FOR MULTI-ELEMENT VIDEOS + +For a ${project.duration}s video, structure like this: +${generateSceneStructure(project.duration)} + +## RESPONSE FORMAT + +Return a JSON object with: +\`\`\`json +{ + "message": "Brief description (max 100 chars)", + "operations": [/* array of operations */] +} +\`\`\` + +### Operation Types: + +**add_layer** - Create a new layer +\`\`\`json +{ + "action": "add_layer", + "type": "text|shape|button|terminal|phone|browser|mouse", + "name": "Optional name", + "position": {"x": 0, "y": -200}, + "props": {/* REQUIRED: layer-specific props */} +} +\`\`\` + +**add_keyframe** - Add animation keyframe +\`\`\`json +{ + "action": "add_keyframe", + "layerId": "layer_0 or existing-id", + "keyframe": { + "time": 0.5, + "property": "position.x|opacity|scale.x|props.content|etc", + "value": 100, + "easing": {"type": "ease-out"} + } +} +\`\`\` + +**apply_preset** - Apply animation preset +\`\`\`json +{ + "action": "apply_preset", + "layerId": "layer_0", + "presetId": "fade-in|scale-in|slide-in-left|etc", + "startTime": 0.5, + "duration": 0.6 +} +\`\`\` + +**edit_layer** - Modify existing layer +\`\`\`json +{ + "action": "edit_layer", + "layerId": "existing-layer-id", + "updates": { + "props": {"content": "New text"}, + "transform": {"x": 100} + } +} +\`\`\` + +**remove_layer** / **remove_keyframe** / **edit_keyframe** - Other operations + +## COMPLETE EXAMPLE + +User: "Create a modern promo video for a tech product called 'CloudSync'" + +Response: +\`\`\`json +{ + "message": "Created CloudSync promo with animated title, tagline, and CTA", + "operations": [ + { + "action": "add_layer", + "type": "text", + "name": "Title", + "position": {"x": 0, "y": -100}, + "props": { + "content": "CloudSync", + "fontSize": 96, + "fontFamily": "Poppins", + "fontWeight": "bold", + "color": "#ffffff" + } + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0, "property": "opacity", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0, "property": "scale.x", "value": 0.8} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0, "property": "scale.y", "value": 0.8} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0.5, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0.5, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_0", + "keyframe": {"time": 0.5, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_layer", + "type": "text", + "name": "Tagline", + "position": {"x": 0, "y": 50}, + "props": { + "content": "Sync Everything. Everywhere.", + "fontSize": 32, + "fontFamily": "Inter", + "fontWeight": "normal", + "color": "#a0a0a0" + } + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0, "property": "opacity", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0, "property": "position.y", "value": 80} + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0.8, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_1", + "keyframe": {"time": 0.8, "property": "position.y", "value": 50, "easing": {"type": "ease-out"}} + }, + { + "action": "add_layer", + "type": "button", + "name": "CTA", + "position": {"x": 0, "y": 200}, + "props": { + "text": "Get Started Free", + "backgroundColor": "#3b82f6", + "textColor": "#ffffff", + "fontSize": 18, + "fontWeight": "bold", + "borderRadius": 12, + "width": 200, + "height": 56 + } + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 0, "property": "opacity", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 0, "property": "scale.x", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 0, "property": "scale.y", "value": 0} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.2, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.2, "property": "scale.x", "value": 1.05, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.2, "property": "scale.y", "value": 1.05, "easing": {"type": "ease-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.4, "property": "scale.x", "value": 1, "easing": {"type": "ease-in-out"}} + }, + { + "action": "add_keyframe", + "layerId": "layer_2", + "keyframe": {"time": 1.4, "property": "scale.y", "value": 1, "easing": {"type": "ease-in-out"}} + } + ] +} +\`\`\` + +## VIDEO TYPE TEMPLATES + +### Tech Demo / Developer Tutorial +- Use code layer for snippets +- Use terminal for commands +- Use icon layers (code, terminal, rocket) for visual interest +- Dark background (#0f0f0f, #1a1a2e) +- Accent colors: blues (#3b82f6), greens (#22c55e), purples (#8b5cf6) + +### Feature Showcase +- Icon layer for feature icon +- Text for feature name +- Smaller text for description +- Progress bar for stats +- Staggered entrance animations + +### Social Media Ad +- Vertical format (720x1280) +- Large, punchy text +- Bold colors for engagement +- Quick animations (0.3-0.5s) +- CTA prominent at end + +## EXAMPLE: Tech Demo + +User: "Show a terminal with a npm install command" + +\`\`\`json +{ + "message": "Created terminal with npm install animation", + "operations": [ + { + "action": "add_layer", + "type": "terminal", + "name": "Terminal", + "position": {"x": 0, "y": 0}, + "props": { + "title": "Terminal", + "content": "", + "width": 600, + "height": 300, + "backgroundColor": "#1e1e1e", + "textColor": "#22c55e", + "fontSize": 16 + } + }, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.5, "property": "props.content", "value": ""}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 2, "property": "props.content", "value": "$ npm install devmotion\\n\\nadded 127 packages in 2.3s"}} + ] +} +\`\`\` + +## EXAMPLE: Feature Highlight with Icons + +User: "Show 3 features with icons" + +\`\`\`json +{ + "message": "Created 3 feature highlights with staggered animations", + "operations": [ + {"action": "add_layer", "type": "icon", "name": "Icon1", "position": {"x": -200, "y": -100}, "props": {"icon": "zap", "size": 48, "color": "#fbbf24", "backgroundColor": "#fbbf2420", "backgroundRadius": 12, "backgroundPadding": 16}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "scale.x", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0, "property": "scale.y", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_0", "keyframe": {"time": 0.3, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "text", "name": "Feature1", "position": {"x": -200, "y": 0}, "props": {"content": "Lightning Fast", "fontSize": 24, "fontWeight": "bold", "color": "#ffffff"}}, + {"action": "add_keyframe", "layerId": "layer_1", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_1", "keyframe": {"time": 0.5, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "icon", "name": "Icon2", "position": {"x": 0, "y": -100}, "props": {"icon": "shield", "size": 48, "color": "#22c55e", "backgroundColor": "#22c55e20", "backgroundRadius": 12, "backgroundPadding": 16}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0, "property": "scale.x", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0, "property": "scale.y", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0.5, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0.5, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_2", "keyframe": {"time": 0.5, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "text", "name": "Feature2", "position": {"x": 0, "y": 0}, "props": {"content": "Secure", "fontSize": 24, "fontWeight": "bold", "color": "#ffffff"}}, + {"action": "add_keyframe", "layerId": "layer_3", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_3", "keyframe": {"time": 0.7, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "icon", "name": "Icon3", "position": {"x": 200, "y": -100}, "props": {"icon": "sparkles", "size": 48, "color": "#a855f7", "backgroundColor": "#a855f720", "backgroundRadius": 12, "backgroundPadding": 16}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0, "property": "scale.x", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0, "property": "scale.y", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0.7, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0.7, "property": "scale.x", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_keyframe", "layerId": "layer_4", "keyframe": {"time": 0.7, "property": "scale.y", "value": 1, "easing": {"type": "ease-out"}}}, + {"action": "add_layer", "type": "text", "name": "Feature3", "position": {"x": 200, "y": 0}, "props": {"content": "AI-Powered", "fontSize": 24, "fontWeight": "bold", "color": "#ffffff"}}, + {"action": "add_keyframe", "layerId": "layer_5", "keyframe": {"time": 0, "property": "opacity", "value": 0}}, + {"action": "add_keyframe", "layerId": "layer_5", "keyframe": {"time": 0.9, "property": "opacity", "value": 1, "easing": {"type": "ease-out"}}} + ] +} +\`\`\` + +Now respond to the user's request with professional-quality animations.`; +} + +/** + * Generate scene structure guidance based on duration + */ +function generateSceneStructure(duration: number): string { + if (duration <= 3) { + return `- 0-0.5s: Main element entrance +- 0.5-${duration - 0.5}s: Display/hold +- ${duration - 0.5}-${duration}s: Optional exit or hold`; + } + + if (duration <= 5) { + return `- 0-0.8s: Hero entrance (title, main visual) +- 0.6-1.2s: Supporting elements (tagline, subtitle) +- 1.0-1.5s: CTA/action elements +- 1.5-${duration - 1}s: Hold/display period +- ${duration - 1}-${duration}s: Optional exits or final emphasis`; + } + + if (duration <= 10) { + return `- 0-1s: Scene 1 - Hero entrance +- 1-2s: Scene 1 - Supporting content +- 2-3s: Transition/hold +- 3-5s: Scene 2 - Feature highlights +- 5-7s: Scene 3 - Benefits/details +- 7-${duration - 1}s: Scene 4 - CTA/conclusion +- ${duration - 1}-${duration}s: Final hold`; + } + + // Longer videos + const sceneCount = Math.floor(duration / 3); + const lines = [`- Structure into ${sceneCount} scenes of ~3s each`]; + for (let i = 0; i < sceneCount; i++) { + const start = i * 3; + const end = Math.min((i + 1) * 3, duration); + lines.push(`- ${start}-${end}s: Scene ${i + 1}`); + } + return lines.join('\n'); +} + +/** + * Build reference documentation for all layer types + */ +function buildLayerTypesReference(): string { + const layerTypes = Object.keys(layerRegistry) as LayerType[]; + + return layerTypes + .map((type) => { + const definition = layerRegistry[type]; + const metadata = extractPropertyMetadata(definition.schema); + const defaults = extractDefaultValues(definition.schema); + + // Identify required/important props + const requiredProps = getRequiredPropsForType(type); + + const propsDesc = metadata + .map((m) => { + const defaultVal = defaults[m.name]; + let typeDesc: string = m.type; + if (m.options) { + typeDesc = `enum: ${m.options.map((o) => `"${o.value}"`).join(' | ')}`; + } + if (m.min !== undefined || m.max !== undefined) { + typeDesc += ` (${m.min ?? ''}..${m.max ?? ''})`; + } + const defaultStr = + defaultVal !== undefined ? ` [default: ${JSON.stringify(defaultVal)}]` : ''; + const requiredMarker = requiredProps.includes(m.name) ? ' ⚠️ REQUIRED' : ''; + return ` - ${m.name}: ${typeDesc}${defaultStr}${requiredMarker}`; + }) + .join('\n'); + + return `### ${definition.label} (type: "${type}") +${getLayerUsageHint(type)} +Props: +${propsDesc}`; + }) + .join('\n\n'); +} + +/** + * Get required props for each layer type + */ +function getRequiredPropsForType(type: LayerType): string[] { + const required: Record = { + text: ['content', 'fontSize', 'color'], + shape: ['shapeType', 'width', 'height', 'fill'], + terminal: ['content', 'width', 'height'], + button: ['text', 'backgroundColor', 'width', 'height'], + phone: ['width', 'height'], + browser: ['width', 'height'], + mouse: ['pointerType'], + icon: ['icon', 'size', 'color'], + progress: ['progress', 'width', 'progressColor'], + code: ['code', 'language', 'width'] + }; + return required[type] || []; +} + +/** + * Get usage hints for each layer type + */ +function getLayerUsageHint(type: LayerType): string { + const hints: Record = { + text: 'Perfect for titles, taglines, labels. Always set content, fontSize, and color.', + shape: + 'Use for backgrounds, decorations, visual accents. Great for creating geometric patterns.', + terminal: 'Code/command display. Animate props.content for typing effect.', + button: 'Call-to-action elements. Set text, backgroundColor for brand colors.', + phone: 'Mobile device mockup. Great for app demos.', + browser: 'Desktop browser mockup. Perfect for website showcases.', + mouse: 'Cursor animation. Use with position animations to show interactions.', + icon: 'Lucide icons for visual elements. Perfect for feature icons, social media, UI elements.', + progress: 'Progress bars and loading states. Animate props.progress for loading animations.', + code: 'Code snippets with syntax highlighting. Perfect for tech/dev tutorials.' + }; + return hints[type] || ''; +} + +/** + * Build reference of current project state + */ +function buildProjectStateReference(project: Project): string { + if (project.layers.length === 0) { + return 'No layers currently in the project. You will create new layers.'; + } + + const layerList = project.layers + .map((layer, index) => { + const keyframesSummary = + layer.keyframes.length > 0 + ? `\n Keyframes: ${summarizeKeyframes(layer)}` + : '\n No keyframes (static)'; + + const propsStr = Object.entries(layer.props) + .filter(([, v]) => v !== undefined && v !== null) + .slice(0, 5) // Limit to 5 props to keep it concise + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .join(', '); + + return `${index}. "${layer.name}" (id: "${layer.id}", type: ${layer.type}) + Position: (${layer.transform.x}, ${layer.transform.y}) + Scale: (${layer.transform.scaleX}, ${layer.transform.scaleY}) + Opacity: ${layer.style.opacity} + Props: { ${propsStr} }${keyframesSummary}`; + }) + .join('\n\n'); + + return `Existing layers (use their IDs for edits): +${layerList}`; +} + +/** + * Summarize keyframes for a layer + */ +function summarizeKeyframes(layer: Layer): string { + const grouped = new Map(); + + for (const kf of layer.keyframes) { + const times = grouped.get(kf.property) || []; + times.push(kf.time); + grouped.set(kf.property, times); + } + + return Array.from(grouped.entries()) + .map(([prop, times]) => `${prop} @ ${times.sort((a, b) => a - b).join('s, ')}s`) + .join('; '); +} diff --git a/src/lib/components/ai/ai-chat.svelte b/src/lib/components/ai/ai-chat.svelte new file mode 100644 index 0000000..698a876 --- /dev/null +++ b/src/lib/components/ai/ai-chat.svelte @@ -0,0 +1,408 @@ + + +
+ + + +
diff --git a/src/lib/components/editor/keyboard-handler.svelte b/src/lib/components/editor/keyboard-handler.svelte index 1266e92..11c9b69 100644 --- a/src/lib/components/editor/keyboard-handler.svelte +++ b/src/lib/components/editor/keyboard-handler.svelte @@ -1,7 +1,7 @@ + + + +
+ {#if showHeader} +
+
+
+
+
+
+ {fileName} +
+ {/if} + +
+ + + {#each lines as line, i (i)} + + {#if showLineNumbers} + + {/if} + + + {/each} + +
+ {i + 1} + + + {@html highlightLine(line, language)} +
+
+
diff --git a/src/lib/layers/components/IconLayer.svelte b/src/lib/layers/components/IconLayer.svelte new file mode 100644 index 0000000..09e922b --- /dev/null +++ b/src/lib/layers/components/IconLayer.svelte @@ -0,0 +1,264 @@ + + + + +
+ +
diff --git a/src/lib/layers/ImageLayer.svelte b/src/lib/layers/components/ImageLayer.svelte similarity index 73% rename from src/lib/layers/ImageLayer.svelte rename to src/lib/layers/components/ImageLayer.svelte index 1f14327..aed85e4 100644 --- a/src/lib/layers/ImageLayer.svelte +++ b/src/lib/layers/components/ImageLayer.svelte @@ -1,9 +1,12 @@ + + + +
+ {#if showLabel && labelPosition === 'top'} + + {Math.round(clampedProgress)}% + + {/if} + +
+
+ {#if showLabel && labelPosition === 'inside' && height >= 16} + + {Math.round(clampedProgress)}% + + {/if} +
+ + {#if animated} +
+ {/if} +
+ + {#if showLabel && labelPosition === 'right'} + + {Math.round(clampedProgress)}% + + {/if} +
+ + diff --git a/src/lib/layers/ShapeLayer.svelte b/src/lib/layers/components/ShapeLayer.svelte similarity index 91% rename from src/lib/layers/ShapeLayer.svelte rename to src/lib/layers/components/ShapeLayer.svelte index d346460..04b7987 100644 --- a/src/lib/layers/ShapeLayer.svelte +++ b/src/lib/layers/components/ShapeLayer.svelte @@ -1,10 +1,12 @@