From f0b0ef949821465117aca54f59d49b8b2fdb7888 Mon Sep 17 00:00:00 2001 From: 808StaN Date: Tue, 8 Sep 2026 16:30:18 +0200 Subject: [PATCH 1/3] refactor(ai): migrate AI agent to TypeScript --- README.md | 1 + eslint.config.js | 7 +- package-lock.json | 369 +++++++++++++++++- package.json | 4 + ...ions.test.js => aiAgentOperations.test.ts} | 0 ...gentOperations.js => aiAgentOperations.ts} | 6 + src/ai/{aiAgentPrompt.js => aiAgentPrompt.ts} | 8 +- src/ai/{aiClient.test.js => aiClient.test.ts} | 0 src/ai/{aiClient.js => aiClient.ts} | 11 +- ...mmary.test.js => aiProjectSummary.test.ts} | 0 ...iProjectSummary.js => aiProjectSummary.ts} | 19 +- src/ai/{aiProviders.js => aiProviders.ts} | 12 +- ...pleIndex.test.js => aiSampleIndex.test.ts} | 0 src/ai/{aiSampleIndex.js => aiSampleIndex.ts} | 48 ++- ...iniClient.test.js => geminiClient.test.ts} | 0 src/ai/{geminiClient.js => geminiClient.ts} | 54 ++- ...nAiClient.test.js => openAiClient.test.ts} | 0 src/ai/{openAiClient.js => openAiClient.ts} | 49 ++- .../{AiAgentWindow.jsx => AiAgentWindow.tsx} | 18 +- ...onList.jsx => AiAgentConversationList.tsx} | 15 +- ...iAgentMessages.jsx => AiAgentMessages.tsx} | 11 +- ...gentPlanPanel.jsx => AiAgentPlanPanel.tsx} | 26 +- ...tController.js => useAiAgentController.ts} | 96 +++-- src/types/ai.ts | 130 ++++++ src/types/daw.ts | 80 ++++ src/vite-env.d.ts | 6 + tsconfig.json | 23 ++ 27 files changed, 874 insertions(+), 119 deletions(-) rename src/ai/{aiAgentOperations.test.js => aiAgentOperations.test.ts} (100%) rename src/ai/{aiAgentOperations.js => aiAgentOperations.ts} (98%) rename src/ai/{aiAgentPrompt.js => aiAgentPrompt.ts} (95%) rename src/ai/{aiClient.test.js => aiClient.test.ts} (100%) rename src/ai/{aiClient.js => aiClient.ts} (66%) rename src/ai/{aiProjectSummary.test.js => aiProjectSummary.test.ts} (100%) rename src/ai/{aiProjectSummary.js => aiProjectSummary.ts} (86%) rename src/ai/{aiProviders.js => aiProviders.ts} (77%) rename src/ai/{aiSampleIndex.test.js => aiSampleIndex.test.ts} (100%) rename src/ai/{aiSampleIndex.js => aiSampleIndex.ts} (72%) rename src/ai/{geminiClient.test.js => geminiClient.test.ts} (100%) rename src/ai/{geminiClient.js => geminiClient.ts} (72%) rename src/ai/{openAiClient.test.js => openAiClient.test.ts} (100%) rename src/ai/{openAiClient.js => openAiClient.ts} (74%) rename src/components/{AiAgentWindow.jsx => AiAgentWindow.tsx} (90%) rename src/components/ai-agent/{AiAgentConversationList.jsx => AiAgentConversationList.tsx} (87%) rename src/components/ai-agent/{AiAgentMessages.jsx => AiAgentMessages.tsx} (78%) rename src/components/ai-agent/{AiAgentPlanPanel.jsx => AiAgentPlanPanel.tsx} (79%) rename src/components/ai-agent/{useAiAgentController.js => useAiAgentController.ts} (84%) create mode 100644 src/types/ai.ts create mode 100644 src/types/daw.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json diff --git a/README.md b/README.md index 903973c..5a06d42 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ Installer artifacts are generated in `release/`. - `npm run desktop:start` - build and launch the unpacked desktop app - `npm run desktop:installer` - build the Windows installer - `npm run lint` - run ESLint +- `npm run typecheck` - run TypeScript validation without emitting files ## Project Structure diff --git a/eslint.config.js b/eslint.config.js index 76e092d..2514e50 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,14 +2,16 @@ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ globalIgnores(['dist']), { - files: ['**/*.{js,jsx}'], + files: ['**/*.{js,jsx,ts,tsx}'], extends: [ js.configs.recommended, + ...tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], @@ -23,7 +25,8 @@ export default defineConfig([ }, }, rules: { - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], }, }, { diff --git a/package-lock.json b/package-lock.json index bc53f28..89ef6d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.13.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -35,6 +36,8 @@ "globals": "^17.4.0", "jsdom": "^29.1.1", "rcedit": "^4.0.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.70.0", "vite": "^8.0.1", "vitest": "^4.1.5" } @@ -2059,12 +2062,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/plist": { @@ -2143,6 +2146,301 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -3489,6 +3787,20 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/config-file-ts/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -8307,6 +8619,19 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8341,9 +8666,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8354,6 +8679,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -8365,9 +8714,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unique-filename": { diff --git a/package.json b/package.json index 4f320f3..f2f000f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "desktop:start": "npm run desktop:pack && start \"\" \".\\release\\win-unpacked\\OpenStudio.exe\"", "desktop:installer": "npm run desktop:pack && npm run installer:assets && electron-builder --win nsis --prepackaged release/win-unpacked --config.win.signAndEditExecutable=false", "lint": "eslint .", + "typecheck": "tsc --noEmit", "preview": "vite preview", "test": "vitest", "test:run": "vitest run" @@ -40,6 +41,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.13.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", @@ -51,6 +53,8 @@ "globals": "^17.4.0", "jsdom": "^29.1.1", "rcedit": "^4.0.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.70.0", "vite": "^8.0.1", "vitest": "^4.1.5" }, diff --git a/src/ai/aiAgentOperations.test.js b/src/ai/aiAgentOperations.test.ts similarity index 100% rename from src/ai/aiAgentOperations.test.js rename to src/ai/aiAgentOperations.test.ts diff --git a/src/ai/aiAgentOperations.js b/src/ai/aiAgentOperations.ts similarity index 98% rename from src/ai/aiAgentOperations.js rename to src/ai/aiAgentOperations.ts index ad92c2d..5220a03 100644 --- a/src/ai/aiAgentOperations.js +++ b/src/ai/aiAgentOperations.ts @@ -1,3 +1,9 @@ +// This module is moved before the Redux reducers because it dispatches their +// untyped JS actions directly. Its public contracts are typed in src/types/ai. +// Internal strict typing follows with the store migration. +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + import { addChannel, addMixerTrack, diff --git a/src/ai/aiAgentPrompt.js b/src/ai/aiAgentPrompt.ts similarity index 95% rename from src/ai/aiAgentPrompt.js rename to src/ai/aiAgentPrompt.ts index 474815f..fc83d70 100644 --- a/src/ai/aiAgentPrompt.js +++ b/src/ai/aiAgentPrompt.ts @@ -1,4 +1,6 @@ -export const AI_AGENT_DEFAULT_MODEL = "gpt-5.5"; +import type { AiOperationType } from "../types/ai" + +export const AI_AGENT_DEFAULT_MODEL = "gpt-5.5" export const AI_AGENT_OPERATION_TYPES = [ "set_bpm", @@ -30,9 +32,9 @@ export const AI_AGENT_OPERATION_TYPES = [ "set_fx_reverb_param", "set_fx_maximizer_param", "set_fx_graphic_eq_band_gain", -]; +] as const satisfies readonly AiOperationType[] -export function getAiAgentSystemPrompt() { +export function getAiAgentSystemPrompt(): string { return [ "You are OpenStudio AI Agent, a DAW assistant inside a React/Web Audio music app.", "You must return JSON only. Do not include Markdown.", diff --git a/src/ai/aiClient.test.js b/src/ai/aiClient.test.ts similarity index 100% rename from src/ai/aiClient.test.js rename to src/ai/aiClient.test.ts diff --git a/src/ai/aiClient.js b/src/ai/aiClient.ts similarity index 66% rename from src/ai/aiClient.js rename to src/ai/aiClient.ts index d0552aa..76c19bd 100644 --- a/src/ai/aiClient.js +++ b/src/ai/aiClient.ts @@ -8,8 +8,12 @@ import { AI_AGENT_PROVIDER_OPENAI, getAiProviderConfig, } from "./aiProviders"; +import type { AiAgentPlan, AiPlanRequest, AiProviderId } from "../types/ai" -export async function requestAiAgentPlan({ provider, ...params }) { +export async function requestAiAgentPlan({ + provider, + ...params +}: AiPlanRequest & { provider?: AiProviderId }): Promise { const config = getAiProviderConfig(provider); if (config.id === AI_AGENT_PROVIDER_GEMINI) { return requestGeminiAgentPlan(params); @@ -17,7 +21,10 @@ export async function requestAiAgentPlan({ provider, ...params }) { return requestOpenAiAgentPlan(params); } -export async function testAiConnection({ provider, ...params }) { +export async function testAiConnection({ + provider, + ...params +}: Pick & { provider?: AiProviderId }): Promise<{ model: string }> { const config = getAiProviderConfig(provider); if (config.id === AI_AGENT_PROVIDER_GEMINI) { return testGeminiConnection(params); diff --git a/src/ai/aiProjectSummary.test.js b/src/ai/aiProjectSummary.test.ts similarity index 100% rename from src/ai/aiProjectSummary.test.js rename to src/ai/aiProjectSummary.test.ts diff --git a/src/ai/aiProjectSummary.js b/src/ai/aiProjectSummary.ts similarity index 86% rename from src/ai/aiProjectSummary.js rename to src/ai/aiProjectSummary.ts index 5442929..0bafa1b 100644 --- a/src/ai/aiProjectSummary.js +++ b/src/ai/aiProjectSummary.ts @@ -1,14 +1,16 @@ import { PLUGIN_EFFECTS } from "../data/pluginEffects"; import { PLUGIN_INSTRUMENTS } from "../data/pluginInstruments"; +import type { AiProjectSummary, AiSample } from "../types/ai" +import type { DawFxSlot, DawMixerInsert, DawPattern, DawState, PianoNote } from "../types/daw" // Returns { channelId: [{ start, length, pitch, velocity }] } without note ids. // Only used for the active pattern so the payload stays compact. -function summarizePianoPreview(pianoPreview) { +function summarizePianoPreview(pianoPreview: DawPattern["pianoPreview"]): Record { if (!pianoPreview || typeof pianoPreview !== "object") { return {}; } - const result = {}; + const result: Record = {}; Object.keys(pianoPreview).forEach(function (channelId) { const notes = Array.isArray(pianoPreview[channelId]) ? pianoPreview[channelId] @@ -29,12 +31,12 @@ function summarizePianoPreview(pianoPreview) { } // Returns { channelId: [bool, bool, ...] } for the active pattern only. -function summarizeStepGrid(stepGrid) { +function summarizeStepGrid(stepGrid: DawPattern["stepGrid"]): Record { if (!stepGrid || typeof stepGrid !== "object") { return {}; } - const result = {}; + const result: Record = {}; Object.keys(stepGrid).forEach(function (channelId) { const row = Array.isArray(stepGrid[channelId]) ? stepGrid[channelId] : []; if (row.length === 0) { @@ -47,7 +49,7 @@ function summarizeStepGrid(stepGrid) { // Lightweight count so the agent knows a non-active pattern is not empty // without receiving every note (keeps the token budget reasonable). -function countPianoNotes(pianoPreview) { +function countPianoNotes(pianoPreview: DawPattern["pianoPreview"]): number { if (!pianoPreview || typeof pianoPreview !== "object") { return 0; } @@ -58,7 +60,7 @@ function countPianoNotes(pianoPreview) { }, 0); } -function summarizeFxSlots(insert) { +function summarizeFxSlots(insert: DawMixerInsert): Array> { return (Array.isArray(insert?.fxSlots) ? insert.fxSlots : []) .filter(function (slot) { return slot?.effectType && slot.effectType !== "none"; @@ -73,7 +75,10 @@ function summarizeFxSlots(insert) { }); } -export function buildAiProjectSummary(dawState, availableSamples = []) { +export function buildAiProjectSummary( + dawState: DawState, + availableSamples: AiSample[] = [], +): AiProjectSummary { const project = dawState?.project || {}; const mixer = dawState?.mixer || {}; const transport = dawState?.transport || {}; diff --git a/src/ai/aiProviders.js b/src/ai/aiProviders.ts similarity index 77% rename from src/ai/aiProviders.js rename to src/ai/aiProviders.ts index 64e1693..71fc918 100644 --- a/src/ai/aiProviders.js +++ b/src/ai/aiProviders.ts @@ -1,8 +1,10 @@ -export const AI_AGENT_PROVIDER_OPENAI = "openai"; -export const AI_AGENT_PROVIDER_GEMINI = "gemini"; +import type { AiProviderConfig, AiProviderId } from "../types/ai" + +export const AI_AGENT_PROVIDER_OPENAI: AiProviderId = "openai" +export const AI_AGENT_PROVIDER_GEMINI: AiProviderId = "gemini" export const AI_AGENT_DEFAULT_PROVIDER = AI_AGENT_PROVIDER_OPENAI; -export const AI_AGENT_PROVIDERS = [ +export const AI_AGENT_PROVIDERS: readonly AiProviderConfig[] = [ { id: AI_AGENT_PROVIDER_OPENAI, label: "OpenAI", @@ -32,14 +34,14 @@ export const AI_AGENT_PROVIDERS = [ }, ]; -export function getAiProviderConfig(providerId) { +export function getAiProviderConfig(providerId: unknown): AiProviderConfig { const safeProviderId = String(providerId || "").trim(); return AI_AGENT_PROVIDERS.find(function (provider) { return provider.id === safeProviderId; }) || AI_AGENT_PROVIDERS[0]; } -export function getAiProviderModel(providerId, model) { +export function getAiProviderModel(providerId: unknown, model: unknown): string { const provider = getAiProviderConfig(providerId); const safeModel = String(model || "").trim(); const hasModel = provider.models.some(function (item) { diff --git a/src/ai/aiSampleIndex.test.js b/src/ai/aiSampleIndex.test.ts similarity index 100% rename from src/ai/aiSampleIndex.test.js rename to src/ai/aiSampleIndex.test.ts diff --git a/src/ai/aiSampleIndex.js b/src/ai/aiSampleIndex.ts similarity index 72% rename from src/ai/aiSampleIndex.js rename to src/ai/aiSampleIndex.ts index 74483e7..0302ccd 100644 --- a/src/ai/aiSampleIndex.js +++ b/src/ai/aiSampleIndex.ts @@ -1,4 +1,20 @@ -function makePacksPath(relativePath) { +import type { AiSample } from "../types/ai" + +interface PackManifestItem { + name?: unknown + path?: unknown +} + +interface PackManifestFolder { + folder?: unknown + items?: unknown +} + +interface PackManifest { + folders?: unknown +} + +function makePacksPath(relativePath: unknown): string { const cleanRelative = String(relativePath || "") .replace(/^\/+/, "") .trim(); @@ -22,7 +38,7 @@ function makePacksPath(relativePath) { return "/" + cleanRelative; } -function normalizePackItemPath(rawPath) { +function normalizePackItemPath(rawPath: unknown): string { const input = String(rawPath || "").trim(); if (!input) { return ""; @@ -40,7 +56,7 @@ function normalizePackItemPath(rawPath) { return makePacksPath("packs/" + noLeadingSlash); } -export async function loadAiSampleIndex(limit = 240) { +export async function loadAiSampleIndex(limit = 240): Promise { try { const response = await fetch( makePacksPath("packs/manifest.json") + "?ts=" + Date.now(), @@ -51,17 +67,21 @@ export async function loadAiSampleIndex(limit = 240) { return []; } - const manifest = await response.json(); - const folders = Array.isArray(manifest?.folders) ? manifest.folders : []; - const samples = []; + const manifest = await response.json() as PackManifest; + const folders: PackManifestFolder[] = Array.isArray(manifest?.folders) + ? manifest.folders + : []; + const samples: AiSample[] = []; folders.forEach(function (folder) { const folderName = String(folder?.folder || "Packs"); - const items = Array.isArray(folder?.items) ? folder.items : []; + const items: Array = Array.isArray(folder?.items) + ? folder.items + : []; items.forEach(function (item) { const name = typeof item === "string" ? item.split("/").pop() || item - : String(item?.name || item?.path || "Sample").split("/").pop(); + : String(item?.name || item?.path || "Sample").split("/").pop() || "Sample"; const path = typeof item === "string" ? normalizePackItemPath(item) : normalizePackItemPath(item?.path); @@ -84,7 +104,7 @@ export async function loadAiSampleIndex(limit = 240) { } } -function tokenizeSampleQuery(query) { +function tokenizeSampleQuery(query: unknown): string[] { return String(query || "") .toLowerCase() .split(/[^a-z0-9]+/i) @@ -96,7 +116,7 @@ function tokenizeSampleQuery(query) { }); } -function scoreSample(sample, tokens) { +function scoreSample(sample: AiSample, tokens: string[]): number { const haystack = [sample?.name, sample?.folder, sample?.path] .join(" ") .toLowerCase(); @@ -111,8 +131,12 @@ function scoreSample(sample, tokens) { }, 0); } -export function searchAiSamples(samples, query, limit = 80) { - const source = Array.isArray(samples) ? samples : []; +export function searchAiSamples( + samples: AiSample[] | unknown, + query: unknown, + limit = 80, +): AiSample[] { + const source: AiSample[] = Array.isArray(samples) ? samples : []; const tokens = tokenizeSampleQuery(query); if (tokens.length === 0) { diff --git a/src/ai/geminiClient.test.js b/src/ai/geminiClient.test.ts similarity index 100% rename from src/ai/geminiClient.test.js rename to src/ai/geminiClient.test.ts diff --git a/src/ai/geminiClient.js b/src/ai/geminiClient.ts similarity index 72% rename from src/ai/geminiClient.js rename to src/ai/geminiClient.ts index 7026c27..745492b 100644 --- a/src/ai/geminiClient.js +++ b/src/ai/geminiClient.ts @@ -1,27 +1,50 @@ import { getAiAgentSystemPrompt } from "./aiAgentPrompt"; import { getAiProviderConfig, AI_AGENT_PROVIDER_GEMINI } from "./aiProviders"; +import type { AiAgentPlan, AiPlanRequest } from "../types/ai" const GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models"; -function parseAiJsonContent(content) { +interface GeminiErrorResponse { + error?: { message?: string } +} + +interface GeminiGenerateResponse extends GeminiErrorResponse { + candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> +} + +interface ParsedAiPlan { + message?: unknown + operations?: unknown +} + +interface GeminiContent { + role: "user" | "model" + parts: Array<{ text: string }> +} + +function parseAiJsonContent(content: unknown): ParsedAiPlan { const raw = String(content || "").trim(); if (!raw) { throw new Error("AI returned an empty response."); } try { - return JSON.parse(raw); + return JSON.parse(raw) as ParsedAiPlan; } catch { const jsonMatch = raw.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error("AI response was not valid JSON."); } - return JSON.parse(jsonMatch[0]); + return JSON.parse(jsonMatch[0]) as ParsedAiPlan; } } -function buildGeminiContents({ userMessage, projectSummary, conversationHistory }) { - const historyContents = (Array.isArray(conversationHistory) +function buildGeminiContents({ + userMessage, + projectSummary, + conversationHistory, +}: Pick): GeminiContent[] { + const historyContents: GeminiContent[] = (Array.isArray(conversationHistory) ? conversationHistory : [] ) @@ -48,7 +71,7 @@ function buildGeminiContents({ userMessage, projectSummary, conversationHistory }); } -function makeGeminiGenerateUrl(model, apiKey) { +function makeGeminiGenerateUrl(model: string, apiKey: string): string { return ( GEMINI_API_BASE_URL + "/" + @@ -64,7 +87,7 @@ export async function requestGeminiAgentPlan({ userMessage, projectSummary, conversationHistory = [], -}) { +}: AiPlanRequest): Promise { const provider = getAiProviderConfig(AI_AGENT_PROVIDER_GEMINI); const safeApiKey = String(apiKey || "").trim(); const safeMessage = String(userMessage || "").trim(); @@ -98,9 +121,9 @@ export async function requestGeminiAgentPlan({ }), }); - const result = await response.json().catch(function () { - return null; - }); + const result = await response.json().catch(function (): GeminiGenerateResponse | null { + return null + }) as GeminiGenerateResponse | null if (!response.ok) { throw new Error( @@ -118,7 +141,10 @@ export async function requestGeminiAgentPlan({ }; } -export async function testGeminiConnection({ apiKey, model }) { +export async function testGeminiConnection({ + apiKey, + model, +}: Pick): Promise<{ model: string }> { const provider = getAiProviderConfig(AI_AGENT_PROVIDER_GEMINI); const safeApiKey = String(apiKey || "").trim(); const safeModel = String(model || provider.defaultModel).trim() || provider.defaultModel; @@ -145,9 +171,9 @@ export async function testGeminiConnection({ apiKey, model }) { }), }); - const result = await response.json().catch(function () { - return null; - }); + const result = await response.json().catch(function (): GeminiErrorResponse | null { + return null + }) as GeminiErrorResponse | null if (!response.ok) { throw new Error( diff --git a/src/ai/openAiClient.test.js b/src/ai/openAiClient.test.ts similarity index 100% rename from src/ai/openAiClient.test.js rename to src/ai/openAiClient.test.ts diff --git a/src/ai/openAiClient.js b/src/ai/openAiClient.ts similarity index 74% rename from src/ai/openAiClient.js rename to src/ai/openAiClient.ts index 8747535..4a158e1 100644 --- a/src/ai/openAiClient.js +++ b/src/ai/openAiClient.ts @@ -1,22 +1,40 @@ import { AI_AGENT_DEFAULT_MODEL, getAiAgentSystemPrompt } from "./aiAgentPrompt"; +import type { AiAgentPlan, AiChatMessage, AiPlanRequest } from "../types/ai" const OPENAI_CHAT_COMPLETIONS_URL = "https://api.openai.com/v1/chat/completions"; const OPENAI_MODELS_URL = "https://api.openai.com/v1/models"; -function parseAiJsonContent(content) { +interface OpenAiErrorResponse { + error?: { message?: string } +} + +interface OpenAiCompletionResponse extends OpenAiErrorResponse { + choices?: Array<{ message?: { content?: string } }> +} + +interface OpenAiModelResponse extends OpenAiErrorResponse { + id?: string +} + +interface ParsedAiPlan { + message?: unknown + operations?: unknown +} + +function parseAiJsonContent(content: unknown): ParsedAiPlan { const raw = String(content || "").trim(); if (!raw) { throw new Error("AI returned an empty response."); } try { - return JSON.parse(raw); + return JSON.parse(raw) as ParsedAiPlan; } catch { const jsonMatch = raw.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error("AI response was not valid JSON."); } - return JSON.parse(jsonMatch[0]); + return JSON.parse(jsonMatch[0]) as ParsedAiPlan; } } @@ -26,7 +44,7 @@ export async function requestAiAgentPlan({ userMessage, projectSummary, conversationHistory = [], -}) { +}: AiPlanRequest): Promise { const safeApiKey = String(apiKey || "").trim(); const safeMessage = String(userMessage || "").trim(); const safeModel = String(model || AI_AGENT_DEFAULT_MODEL).trim() || AI_AGENT_DEFAULT_MODEL; @@ -47,7 +65,7 @@ export async function requestAiAgentPlan({ // then the current user request with the live project summary. This gives // the model context from earlier in the chat (loaded from Supabase or // built up during the current session). - const historyMessages = (Array.isArray(conversationHistory) + const historyMessages: AiChatMessage[] = (Array.isArray(conversationHistory) ? conversationHistory : [] ) @@ -58,7 +76,12 @@ export async function requestAiAgentPlan({ return { role: msg.role, content: msg.content }; }); - const requestBody = { + const requestBody: { + model: string + response_format: { type: "json_object" } + messages: Array<{ role: "system" | "user" | "assistant"; content: string }> + temperature?: number + } = { model: safeModel, response_format: { type: "json_object" }, messages: [ @@ -90,9 +113,9 @@ export async function requestAiAgentPlan({ body: JSON.stringify(requestBody), }); - const result = await response.json().catch(function () { - return null; - }); + const result = await response.json().catch(function (): OpenAiCompletionResponse | null { + return null + }) as OpenAiCompletionResponse | null if (!response.ok) { const message = @@ -113,7 +136,7 @@ export async function requestAiAgentPlan({ export async function testOpenAiConnection({ apiKey, model = AI_AGENT_DEFAULT_MODEL, -}) { +}: Pick): Promise<{ model: string }> { const safeApiKey = String(apiKey || "").trim(); const safeModel = String(model || AI_AGENT_DEFAULT_MODEL).trim() || AI_AGENT_DEFAULT_MODEL; @@ -131,9 +154,9 @@ export async function testOpenAiConnection({ }, ); - const result = await response.json().catch(function () { - return null; - }); + const result = await response.json().catch(function (): OpenAiModelResponse | null { + return null + }) as OpenAiModelResponse | null if (!response.ok) { throw new Error( diff --git a/src/components/AiAgentWindow.jsx b/src/components/AiAgentWindow.tsx similarity index 90% rename from src/components/AiAgentWindow.jsx rename to src/components/AiAgentWindow.tsx index 7167822..0694176 100644 --- a/src/components/AiAgentWindow.jsx +++ b/src/components/AiAgentWindow.tsx @@ -1,11 +1,19 @@ import { useState } from "react"; +import type { ChangeEvent, KeyboardEvent } from "react" import { Bot } from "lucide-react"; import { AiAgentConversationList } from "./ai-agent/AiAgentConversationList"; import { AiAgentMessages } from "./ai-agent/AiAgentMessages"; import { AiAgentPlanPanel } from "./ai-agent/AiAgentPlanPanel"; import { useAiAgentController } from "./ai-agent/useAiAgentController"; +import type { AiProviderModel } from "../types/ai" -function AiAgentSelect({ value, options, onChange }) { +interface AiAgentSelectProps { + value: string + options: readonly AiProviderModel[] + onChange: (value: string) => void +} + +function AiAgentSelect({ value, options, onChange }: AiAgentSelectProps) { const [isOpen, setIsOpen] = useState(false); const activeOption = options.find(function (option) { return option.value === value; @@ -80,7 +88,7 @@ export function AiAgentWindow() { value={agent.apiKey} placeholder={agent.apiKeyPlaceholder} autoComplete="off" - onChange={function (event) { + onChange={function (event: ChangeEvent) { agent.setApiKey(event.target.value); }} /> @@ -99,7 +107,7 @@ export function AiAgentWindow() { ) { agent.setRememberKey(event.target.checked); }} /> @@ -142,10 +150,10 @@ export function AiAgentWindow() { value={agent.input} placeholder="Example: create a dark trap pattern with kick, snare and hats" disabled={agent.isSending} - onChange={function (event) { + onChange={function (event: ChangeEvent) { agent.setInput(event.target.value); }} - onKeyDown={function (event) { + onKeyDown={function (event: KeyboardEvent) { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); agent.sendMessage(event); diff --git a/src/components/ai-agent/AiAgentConversationList.jsx b/src/components/ai-agent/AiAgentConversationList.tsx similarity index 87% rename from src/components/ai-agent/AiAgentConversationList.jsx rename to src/components/ai-agent/AiAgentConversationList.tsx index b198ba4..683866c 100644 --- a/src/components/ai-agent/AiAgentConversationList.jsx +++ b/src/components/ai-agent/AiAgentConversationList.tsx @@ -1,4 +1,5 @@ import { MessageSquarePlus, Trash2 } from "lucide-react"; +import type { AiConversationSummary } from "../../types/ai" /** * @fileoverview AiAgentConversationList — Sidebar showing saved AI chat @@ -6,7 +7,7 @@ import { MessageSquarePlus, Trash2 } from "lucide-react"; * sign-in prompt instead of the list. */ -function formatRelativeTime(isoString) { +function formatRelativeTime(isoString: string | null | undefined): string { if (!isoString) { return ""; } @@ -37,6 +38,16 @@ function formatRelativeTime(isoString) { return new Date(then).toLocaleDateString(); } +interface AiAgentConversationListProps { + conversations: AiConversationSummary[] + activeConversationId: string | null + onSelect: (conversationId: string) => void + onNew: () => void + onDelete: (conversationId: string) => void + isLoading: boolean + isAuthenticated: boolean +} + export function AiAgentConversationList({ conversations, activeConversationId, @@ -45,7 +56,7 @@ export function AiAgentConversationList({ onDelete, isLoading, isAuthenticated, -}) { +}: AiAgentConversationListProps) { if (!isAuthenticated) { return (