diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8ae04a..3f3131a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,4 +31,16 @@ jobs: - name: πŸ–₯️ Build Electron app # Run Electron build script from root directory - run: npm run build \ No newline at end of file + + run: npm run build + + - name: 🚨 Send failure notification to Slack + if: failure() + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: general + SLACK_TITLE: "🚨 Build Failed" + SLACK_MESSAGE: "😭 Build failed for `${{ github.repository }}` repo on main branch." + SLACK_COLOR: 'danger' + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + diff --git a/README.md b/README.md index 8397aec..e166914 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,10 @@ We have a list of [help wanted](https://github.com/pickle-com/glass/issues?q=is% | Status | Issue | Description | |--------|--------------------------------|---------------------------------------------------| -| 🚧 WIP | Code Refactoring | Refactoring the entire codebase for better maintainability. | | 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows | | 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers | | 🚧 WIP | AEC Improvement | Transcription is not working occasionally | | 🚧 WIP | Firebase Data Storage Issue | Session & ask should be saved in firebase for signup users | -| 🚧 WIP | Login Issue | Currently breaking when switching between local and sign-in mode | | 🚧 WIP | Liquid Glass | Liquid Glass UI for MacOS 26 | ### Changelog diff --git a/electron-builder.yml b/electron-builder.yml index 35e6ed5..79b81fb 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -44,6 +44,8 @@ win: - target: portable arch: x64 requestedExecutionLevel: asInvoker + # Disable code signing to avoid symbolic link issues on Windows + signAndEditExecutable: false # NSIS installer configuration for Windows nsis: diff --git a/package-lock.json b/package-lock.json index edabde8..5ae800a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { + + "@anthropic-ai/sdk": "^0.56.0", + "@google/genai": "^1.8.0", "@google/generative-ai": "^0.24.1", "axios": "^1.10.0", @@ -51,6 +54,17 @@ "electron-liquid-glass": "^1.0.1" } }, + + "node_modules/@anthropic-ai/sdk": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.56.0.tgz", + "integrity": "sha512-SLCB8M8+VMg1cpCucnA1XWHGWqVSZtIWzmOdDOEu3eTFZMB+A0sGZ1ESO5MHDnqrNTXz3safMrWx9x4rMZSOqA==", + "license": "MIT", + "bin": { + "anthropic-ai-sdk": "bin/cli" + } + }, + "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -875,9 +889,11 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "license": "MIT", "optional": true, "dependencies": { @@ -885,9 +901,11 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ "ppc64" ], @@ -902,9 +920,11 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ "arm" ], @@ -919,9 +939,11 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ "arm64" ], @@ -936,9 +958,11 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ "x64" ], @@ -953,9 +977,11 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ "arm64" ], @@ -970,9 +996,11 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ "x64" ], @@ -987,9 +1015,11 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ "arm64" ], @@ -1004,9 +1034,11 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ "x64" ], @@ -1021,9 +1053,11 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ "arm" ], @@ -1038,9 +1072,11 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ "arm64" ], @@ -1055,9 +1091,11 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ "ia32" ], @@ -1072,9 +1110,11 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ "loong64" ], @@ -1089,9 +1129,11 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ "mips64el" ], @@ -1106,9 +1148,11 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ "ppc64" ], @@ -1123,9 +1167,11 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ "riscv64" ], @@ -1140,9 +1186,11 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ "s390x" ], @@ -1157,9 +1205,11 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ "x64" ], @@ -1174,9 +1224,11 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ "arm64" ], @@ -1191,9 +1243,11 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ "x64" ], @@ -1208,9 +1262,11 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ "arm64" ], @@ -1225,9 +1281,11 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ "x64" ], @@ -1241,10 +1299,29 @@ "node": ">=18" } }, + + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ "x64" ], @@ -1259,9 +1336,11 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ "arm64" ], @@ -1276,9 +1355,11 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ "ia32" ], @@ -1293,9 +1374,11 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ "x64" ], @@ -3172,9 +3255,11 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { "node": ">= 14" @@ -5970,9 +6055,11 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5983,31 +6070,34 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } }, "node_modules/escalade": { diff --git a/package.json b/package.json index d1f185b..36c681d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "name": "pickle-glass", "productName": "Glass", - "version": "0.2.1", + + "version": "0.2.2", + "description": "Cl*ely for Free", "main": "src/index.js", "scripts": { @@ -9,12 +11,14 @@ "start": "npm run build:renderer && electron-forge start", "package": "npm run build:renderer && electron-forge package", "make": "npm run build:renderer && electron-forge make", - "build": "npm run build:renderer && electron-builder --config electron-builder.yml --publish never", - "build:win": "npm run build:renderer && electron-builder --win --x64 --publish never", - "publish": "npm run build:renderer && electron-builder --config electron-builder.yml --publish always", + "build": "npm run build:all && electron-builder --config electron-builder.yml --publish never", + "build:win": "npm run build:all && electron-builder --win --x64 --publish never", + "publish": "npm run build:all && electron-builder --config electron-builder.yml --publish always", "lint": "eslint --ext .ts,.tsx,.js .", "postinstall": "electron-builder install-app-deps", "build:renderer": "node build.js", + "build:web": "cd pickleglass_web && npm run build && cd ..", + "build:all": "npm run build:renderer && npm run build:web", "watch:renderer": "node build.js --watch" }, "keywords": [ diff --git a/pickleglass_web/package-lock.json b/pickleglass_web/package-lock.json index c7d23a8..a1726d6 100644 --- a/pickleglass_web/package-lock.json +++ b/pickleglass_web/package-lock.json @@ -42,21 +42,27 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", + "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.2", + + "@emnapi/wasi-threads": "1.0.3", + "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "dev": true, "license": "MIT", "optional": true, @@ -65,9 +71,11 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", + "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "dev": true, "license": "MIT", "optional": true, @@ -2667,9 +2675,11 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.179", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", - "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", + + "version": "1.5.180", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.180.tgz", + "integrity": "sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==", + "license": "ISC" }, "node_modules/emoji-regex": { diff --git a/src/app/ApiKeyHeader.js b/src/app/ApiKeyHeader.js index 25f1571..92962c8 100644 --- a/src/app/ApiKeyHeader.js +++ b/src/app/ApiKeyHeader.js @@ -1,12 +1,17 @@ import { html, css, LitElement } from "../assets/lit-core-2.7.4.min.js" export class ApiKeyHeader extends LitElement { + //////// after_modelStateService //////// static properties = { - apiKey: { type: String }, + llmApiKey: { type: String }, + sttApiKey: { type: String }, + llmProvider: { type: String }, + sttProvider: { type: String }, isLoading: { type: Boolean }, errorMessage: { type: String }, - selectedProvider: { type: String }, + providers: { type: Object, state: true }, } + //////// after_modelStateService //////// static styles = css` :host { @@ -45,7 +50,7 @@ export class ApiKeyHeader extends LitElement { } .container { - width: 285px; + width: 350px; min-height: 260px; padding: 18px 20px; background: rgba(0, 0, 0, 0.3); @@ -153,28 +158,22 @@ export class ApiKeyHeader extends LitElement { outline: none; } - .provider-select { + .providers-container { display: flex; gap: 12px; width: 100%; } + .provider-column { flex: 1; display: flex; flex-direction: column; align-items: center; } + .provider-label { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 500; margin-bottom: 6px; } + .api-input, .provider-select { width: 100%; height: 34px; + text-align: center; background: rgba(255, 255, 255, 0.1); border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.2); padding: 0 10px; color: white; font-size: 12px; - font-weight: 400; margin-bottom: 6px; - text-align: center; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2714%27%20height%3D%278%27%20viewBox%3D%270%200%2014%208%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%3E%3Cpath%20d%3D%27M1%201l6%206%206-6%27%20stroke%3D%27%23ffffff%27%20stroke-width%3D%271.5%27%20fill%3D%27none%27%20fill-rule%3D%27evenodd%27/%3E%3C/svg%3E'); - background-repeat: no-repeat; - background-position: right 10px center; - background-size: 12px; - padding-right: 30px; } + .provider-select option { background: #1a1a1a; color: white; } .provider-select:hover { background-color: rgba(255, 255, 255, 0.15); @@ -187,11 +186,6 @@ export class ApiKeyHeader extends LitElement { border-color: rgba(255, 255, 255, 0.4); } - .provider-select option { - background: #1a1a1a; - color: white; - padding: 5px; - } .action-button { width: 100%; @@ -239,15 +233,7 @@ export class ApiKeyHeader extends LitElement { font-weight: 500; /* Medium */ margin: 10px 0; } - - .provider-label { - color: rgba(255, 255, 255, 0.7); - font-size: 11px; - font-weight: 400; - margin-bottom: 4px; - width: 100%; - text-align: left; - } + /* ────────────────[ GLASS BYPASS ]─────────────── */ :host-context(body.has-glass) .container, @@ -278,11 +264,16 @@ export class ApiKeyHeader extends LitElement { super() this.dragState = null this.wasJustDragged = false - this.apiKey = "" this.isLoading = false this.errorMessage = "" - this.validatedApiKey = null - this.selectedProvider = "openai" + //////// after_modelStateService //////// + this.llmApiKey = ""; + this.sttApiKey = ""; + this.llmProvider = "openai"; + this.sttProvider = "openai"; + this.providers = { llm: [], stt: [] }; // μ΄ˆκΈ°ν™” + this.loadProviderConfig(); + //////// after_modelStateService //////// this.handleMouseMove = this.handleMouseMove.bind(this) this.handleMouseUp = this.handleMouseUp.bind(this) @@ -303,6 +294,35 @@ export class ApiKeyHeader extends LitElement { this.requestUpdate() } + async loadProviderConfig() { + if (!window.require) return; + const { ipcRenderer } = window.require('electron'); + const config = await ipcRenderer.invoke('model:get-provider-config'); + + const llmProviders = []; + const sttProviders = []; + + for (const id in config) { + // 'openai-glass' 같은 가상 ProviderλŠ” UI에 ν‘œμ‹œν•˜μ§€ μ•ŠμŒ + if (id.includes('-glass')) continue; + + if (config[id].llmModels.length > 0) { + llmProviders.push({ id, name: config[id].name }); + } + if (config[id].sttModels.length > 0) { + sttProviders.push({ id, name: config[id].name }); + } + } + + this.providers = { llm: llmProviders, stt: sttProviders }; + + // κΈ°λ³Έ 선택 κ°’ μ„€μ • + if (llmProviders.length > 0) this.llmProvider = llmProviders[0].id; + if (sttProviders.length > 0) this.sttProvider = sttProviders[0].id; + + this.requestUpdate(); +} + async handleMouseDown(e) { if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") { return @@ -409,144 +429,45 @@ export class ApiKeyHeader extends LitElement { } } + //////// after_modelStateService //////// async handleSubmit() { - if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) { - console.log("Submit blocked:", { - wasJustDragged: this.wasJustDragged, - isLoading: this.isLoading, - hasApiKey: !!this.apiKey.trim(), - }) - return + console.log('[ApiKeyHeader] handleSubmit: Submitting API keys...'); + if (this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim()) { + this.errorMessage = "Please enter keys for both LLM and STT."; + return; } - console.log("Starting API key validation...") - this.isLoading = true - this.errorMessage = "" - this.requestUpdate() + this.isLoading = true; + this.errorMessage = ""; + this.requestUpdate(); - const apiKey = this.apiKey.trim() - const isValid = false - try { - const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider) + const { ipcRenderer } = window.require('electron'); - if (isValid) { - console.log("API key valid - starting slide out animation") - this.startSlideOutAnimation() - this.validatedApiKey = this.apiKey.trim() - this.validatedProvider = this.selectedProvider - } else { - this.errorMessage = "Invalid API key - please check and try again" - console.log("API key validation failed") - } - } catch (error) { - console.error("API key validation error:", error) - this.errorMessage = "Validation error - please try again" - } finally { - this.isLoading = false - this.requestUpdate() - } - } + console.log('[ApiKeyHeader] handleSubmit: Validating LLM key...'); + const llmValidation = ipcRenderer.invoke('model:validate-key', { provider: this.llmProvider, key: this.llmApiKey.trim() }); + const sttValidation = ipcRenderer.invoke('model:validate-key', { provider: this.sttProvider, key: this.sttApiKey.trim() }); - async validateApiKey(apiKey, provider = "openai") { - if (!apiKey || apiKey.length < 15) return false + const [llmResult, sttResult] = await Promise.all([llmValidation, sttValidation]); - if (provider === "openai") { - if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false - - try { - console.log("Validating OpenAI API key...") - - const response = await fetch("https://api.openai.com/v1/models", { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - }) - - if (response.ok) { - const data = await response.json() - - const hasGPTModels = data.data && data.data.some((m) => m.id.startsWith("gpt-")) - if (hasGPTModels) { - console.log("OpenAI API key validation successful") - return true - } else { - console.log("API key valid but no GPT models available") - return false - } - } else { - const errorData = await response.json().catch(() => ({})) - console.log("API key validation failed:", response.status, errorData.error?.message || "Unknown error") - return false - } - } catch (error) { - console.error("API key validation network error:", error) - return apiKey.length >= 20 // Fallback for network issues - } - } else if (provider === "gemini") { - // Gemini API keys typically start with 'AIza' - if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false - - try { - console.log("Validating Gemini API key...") - - // Test the API key with a simple models list request - const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`) - - if (response.ok) { - const data = await response.json() - if (data.models && data.models.length > 0) { - console.log("Gemini API key validation successful") - return true - } - } - - console.log("Gemini API key validation failed") - return false - } catch (error) { - console.error("Gemini API key validation network error:", error) - return apiKey.length >= 20 // Fallback - } - } else if (provider === "anthropic") { - // Anthropic API keys typically start with 'sk-ant-' - if (!apiKey.startsWith("sk-ant-") || !apiKey.match(/^[A-Za-z0-9_-]+$/)) return false - - try { - console.log("Validating Anthropic API key...") - - // Test the API key with a simple request - const response = await fetch("https://api.anthropic.com/v1/messages", { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": "2023-06-01", - }, - body: JSON.stringify({ - model: "claude-3-haiku-20240307", - max_tokens: 10, - messages: [{ role: "user", content: "Hi" }], - }), - }) - - if (response.ok || response.status === 400) { - // 400 is also acceptable as it means the API key is valid but request format might be wrong - console.log("Anthropic API key validation successful") - return true - } - - console.log("Anthropic API key validation failed:", response.status) - return false - } catch (error) { - console.error("Anthropic API key validation network error:", error) - return apiKey.length >= 20 // Fallback - } + if (llmResult.success && sttResult.success) { + console.log('[ApiKeyHeader] handleSubmit: Both LLM and STT keys are valid.'); + this.startSlideOutAnimation(); + } else { + console.log('[ApiKeyHeader] handleSubmit: Validation failed.'); + let errorParts = []; + if (!llmResult.success) errorParts.push(`LLM Key: ${llmResult.error || 'Invalid'}`); + if (!sttResult.success) errorParts.push(`STT Key: ${sttResult.error || 'Invalid'}`); + this.errorMessage = errorParts.join(' | '); } - return false - } + this.isLoading = false; + this.requestUpdate(); +} +//////// after_modelStateService //////// + startSlideOutAnimation() { + console.log('[ApiKeyHeader] startSlideOutAnimation: Starting slide out animation.'); this.classList.add("sliding-out") } @@ -567,25 +488,18 @@ export class ApiKeyHeader extends LitElement { } } + + //////// after_modelStateService //////// handleAnimationEnd(e) { - if (e.target !== this) return - - if (this.classList.contains("sliding-out")) { - this.classList.remove("sliding-out") - this.classList.add("hidden") - - if (this.validatedApiKey) { - if (window.require) { - window.require("electron").ipcRenderer.invoke("api-key-validated", { - apiKey: this.validatedApiKey, - provider: this.validatedProvider || "openai", - }) - } - this.validatedApiKey = null - this.validatedProvider = null - } - } + if (e.target !== this || !this.classList.contains('sliding-out')) return; + this.classList.remove("sliding-out"); + this.classList.add("hidden"); + window.require('electron').ipcRenderer.invoke('get-current-user').then(userState => { + console.log('[ApiKeyHeader] handleAnimationEnd: User state updated:', userState); + this.stateUpdateCallback?.(userState); + }); } +//////// after_modelStateService //////// connectedCallback() { super.connectedCallback() @@ -598,64 +512,40 @@ export class ApiKeyHeader extends LitElement { } render() { - const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim() - console.log("Rendering with provider:", this.selectedProvider) + const isButtonDisabled = this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim(); return html` -
- -

Choose how to power your AI

+
+

Enter Your API Keys

-
-
${this.errorMessage}
-
Select AI Provider:
- this.llmProvider = e.target.value} ?disabled=${this.isLoading}> + ${this.providers.llm.map(p => html``)} - (this.errorMessage = "")} - ?disabled=${this.isLoading} - autocomplete="off" - spellcheck="false" - tabindex="0" - /> + this.llmApiKey = e.target.value} ?disabled=${this.isLoading}> +
- - -
or
- - +
+
+ + this.sttApiKey = e.target.value} ?disabled=${this.isLoading}>
- ` - } + +
${this.errorMessage}
+ + +
or
+ +
+ `; +} } customElements.define("apikey-header", ApiKeyHeader) diff --git a/src/app/HeaderController.js b/src/app/HeaderController.js index 26313f1..e2c5fe8 100644 --- a/src/app/HeaderController.js +++ b/src/app/HeaderController.js @@ -15,7 +15,11 @@ class HeaderTransitionManager { * @param {'apikey'|'main'|'permission'} type */ this.ensureHeader = (type) => { - if (this.currentHeaderType === type) return; + console.log('[HeaderController] ensureHeader: Ensuring header of type:', type); + if (this.currentHeaderType === type) { + console.log('[HeaderController] ensureHeader: Header of type:', type, 'already exists.'); + return; + } this.headerContainer.innerHTML = ''; @@ -26,6 +30,7 @@ class HeaderTransitionManager { // Create new header element if (type === 'apikey') { this.apiKeyHeader = document.createElement('apikey-header'); + this.apiKeyHeader.stateUpdateCallback = (userState) => this.handleStateUpdate(userState); this.headerContainer.appendChild(this.apiKeyHeader); } else if (type === 'permission') { this.permissionHeader = document.createElement('permission-setup'); @@ -60,6 +65,11 @@ class HeaderTransitionManager { this.apiKeyHeader.isLoading = false; } }); + ipcRenderer.on('force-show-apikey-header', async () => { + console.log('[HeaderController] Received broadcast to show apikey header. Switching now.'); + await this._resizeForApiKey(); + this.ensureHeader('apikey'); + }); } } @@ -83,26 +93,30 @@ class HeaderTransitionManager { } } - async handleStateUpdate(userState) { - const { isLoggedIn, hasApiKey } = userState; - if (isLoggedIn) { - // Firebase user: Check permissions, then show Main or Permission header - const permissionResult = await this.checkPermissions(); - if (permissionResult.success) { - this.transitionToMainHeader(); + //////// after_modelStateService //////// + async handleStateUpdate(userState) { + const { ipcRenderer } = window.require('electron'); + const isConfigured = await ipcRenderer.invoke('model:are-providers-configured'); + + if (isConfigured) { + const { isLoggedIn } = userState; + if (isLoggedIn) { + const permissionResult = await this.checkPermissions(); + if (permissionResult.success) { + this.transitionToMainHeader(); + } else { + this.transitionToPermissionHeader(); + } } else { - this.transitionToPermissionHeader(); + this.transitionToMainHeader(); } - } else if (hasApiKey) { - // API Key only user: Skip permission check, go directly to Main - this.transitionToMainHeader(); } else { - // No auth at all await this._resizeForApiKey(); this.ensureHeader('apikey'); } } + //////// after_modelStateService //////// async transitionToPermissionHeader() { // Prevent duplicate transitions @@ -159,7 +173,7 @@ class HeaderTransitionManager { if (!window.require) return; return window .require('electron') - .ipcRenderer.invoke('resize-header-window', { width: 285, height: 300 }) + .ipcRenderer.invoke('resize-header-window', { width: 350, height: 300 }) .catch(() => {}); } diff --git a/src/common/ai/factory.js b/src/common/ai/factory.js index 8d0d6b5..ea86ff0 100644 --- a/src/common/ai/factory.js +++ b/src/common/ai/factory.js @@ -1,68 +1,121 @@ -const providers = { - openai: require("./providers/openai"), - gemini: require("./providers/gemini"), - anthropic: require("./providers/anthropic"), - // μΆ”κ°€ providerλŠ” 여기에 등둝 -} +// factory.js /** - * Creates an STT session based on provider - * @param {string} provider - Provider name ('openai', 'gemini', etc.) - * @param {object} opts - Configuration options (apiKey, language, callbacks, etc.) - * @returns {Promise} STT session object with sendRealtimeInput and close methods + * @typedef {object} ModelOption + * @property {string} id + * @property {string} name */ + +/** + * @typedef {object} Provider + * @property {string} name + * @property {() => any} handler + * @property {ModelOption[]} llmModels + * @property {ModelOption[]} sttModels + */ + +/** + * @type {Object.} + */ +const PROVIDERS = { + 'openai': { + name: 'OpenAI', + handler: () => require("./providers/openai"), + llmModels: [ + { id: 'gpt-4.1', name: 'GPT-4.1' }, + ], + sttModels: [ + { id: 'gpt-4o-mini-transcribe', name: 'GPT-4o Mini Transcribe' } + ], + }, + + 'openai-glass': { + name: 'OpenAI (Glass)', + handler: () => require("./providers/openai"), + llmModels: [ + { id: 'gpt-4.1-glass', name: 'GPT-4.1 (glass)' }, + ], + sttModels: [ + { id: 'gpt-4o-mini-transcribe-glass', name: 'GPT-4o Mini Transcribe (glass)' } + ], + }, + 'gemini': { + name: 'Gemini', + handler: () => require("./providers/gemini"), + llmModels: [ + { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' }, + ], + sttModels: [ + { id: 'gemini-live-2.5-flash-preview', name: 'Gemini Live 2.5 Flash' } + ], + }, + 'anthropic': { + name: 'Anthropic', + handler: () => require("./providers/anthropic"), + llmModels: [ + { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' }, + ], + sttModels: [], + }, +}; + +function sanitizeModelId(model) { + return (typeof model === 'string') ? model.replace(/-glass$/, '') : model; +} + function createSTT(provider, opts) { - if (!providers[provider]?.createSTT) { - throw new Error(`STT not supported for provider: ${provider}`) + if (provider === 'openai-glass') provider = 'openai'; + + const handler = PROVIDERS[provider]?.handler(); + if (!handler?.createSTT) { + throw new Error(`STT not supported for provider: ${provider}`); } - return providers[provider].createSTT(opts) + if (opts && opts.model) { + opts = { ...opts, model: sanitizeModelId(opts.model) }; + } + return handler.createSTT(opts); } -/** - * Creates an LLM instance based on provider - * @param {string} provider - Provider name ('openai', 'gemini', etc.) - * @param {object} opts - Configuration options (apiKey, model, temperature, etc.) - * @returns {object} LLM instance with generateContent method - */ function createLLM(provider, opts) { - if (!providers[provider]?.createLLM) { - throw new Error(`LLM not supported for provider: ${provider}`) + if (provider === 'openai-glass') provider = 'openai'; + + const handler = PROVIDERS[provider]?.handler(); + if (!handler?.createLLM) { + throw new Error(`LLM not supported for provider: ${provider}`); } - return providers[provider].createLLM(opts) + if (opts && opts.model) { + opts = { ...opts, model: sanitizeModelId(opts.model) }; + } + return handler.createLLM(opts); } -/** - * Creates a streaming LLM instance based on provider - * @param {string} provider - Provider name ('openai', 'gemini', etc.) - * @param {object} opts - Configuration options (apiKey, model, temperature, etc.) - * @returns {object} Streaming LLM instance - */ function createStreamingLLM(provider, opts) { - if (!providers[provider]?.createStreamingLLM) { - throw new Error(`Streaming LLM not supported for provider: ${provider}`) + if (provider === 'openai-glass') provider = 'openai'; + + const handler = PROVIDERS[provider]?.handler(); + if (!handler?.createStreamingLLM) { + throw new Error(`Streaming LLM not supported for provider: ${provider}`); } - return providers[provider].createStreamingLLM(opts) + if (opts && opts.model) { + opts = { ...opts, model: sanitizeModelId(opts.model) }; + } + return handler.createStreamingLLM(opts); } -/** - * Gets list of available providers - * @returns {object} Object with stt and llm arrays - */ function getAvailableProviders() { - const sttProviders = [] - const llmProviders = [] - - for (const [name, provider] of Object.entries(providers)) { - if (provider.createSTT) sttProviders.push(name) - if (provider.createLLM) llmProviders.push(name) + const stt = []; + const llm = []; + for (const [id, provider] of Object.entries(PROVIDERS)) { + if (provider.sttModels.length > 0) stt.push(id); + if (provider.llmModels.length > 0) llm.push(id); } - - return { stt: sttProviders, llm: llmProviders } + return { stt: [...new Set(stt)], llm: [...new Set(llm)] }; } module.exports = { + PROVIDERS, createSTT, createLLM, createStreamingLLM, getAvailableProviders, -} +}; \ No newline at end of file diff --git a/src/common/services/authService.js b/src/common/services/authService.js index f41f87e..a664ba1 100644 --- a/src/common/services/authService.js +++ b/src/common/services/authService.js @@ -36,7 +36,6 @@ class AuthService { this.currentUserId = 'default_user'; this.currentUserMode = 'local'; // 'local' or 'firebase' this.currentUser = null; - this.hasApiKey = false; // Add a flag for API key status this.isInitialized = false; } @@ -53,20 +52,18 @@ class AuthService { this.currentUser = user; this.currentUserId = user.uid; this.currentUserMode = 'firebase'; - this.hasApiKey = false; // Optimistically assume no key yet - - // Broadcast immediately to make UI feel responsive - this.broadcastUserState(); // Start background task to fetch and save virtual key (async () => { try { const idToken = await user.getIdToken(true); const virtualKey = await getVirtualKeyByEmail(user.email, idToken); - await userRepository.saveApiKey(virtualKey, user.uid, 'openai'); - console.log(`[AuthService] BG: Virtual key for ${user.email} has been saved.`); - // Now update the key status, which will trigger another broadcast - await this.updateApiKeyStatus(); + + if (global.modelStateService) { + global.modelStateService.setFirebaseVirtualKey(virtualKey); + } + console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`); + } catch (error) { console.error('[AuthService] BG: Failed to fetch or save virtual key:', error); } @@ -74,23 +71,20 @@ class AuthService { } else { // User signed OUT - console.log(`[AuthService] Firebase user signed out.`); + console.log(`[AuthService] No Firebase user.`); if (previousUser) { console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`); - await userRepository.saveApiKey(null, previousUser.uid); + if (global.modelStateService) { + global.modelStateService.setFirebaseVirtualKey(null); + } } this.currentUser = null; this.currentUserId = 'default_user'; this.currentUserMode = 'local'; - // Update API key status (e.g., if a local key for default_user exists) - // This will also broadcast the final logged-out state. - await this.updateApiKeyStatus(); } + this.broadcastUserState(); }); - // Check for initial API key state - this.updateApiKeyStatus(); - this.isInitialized = true; console.log('[AuthService] Initialized and attached to Firebase Auth state.'); } @@ -129,23 +123,6 @@ class AuthService { }); } - /** - * Updates the internal API key status from the repository and broadcasts if changed. - */ - async updateApiKeyStatus() { - try { - const user = await userRepository.getById(this.currentUserId); - const newStatus = !!(user && user.api_key); - if (this.hasApiKey !== newStatus) { - console.log(`[AuthService] API key status changed to: ${newStatus}`); - this.hasApiKey = newStatus; - this.broadcastUserState(); - } - } catch (error) { - console.error('[AuthService] Error checking API key status:', error); - this.hasApiKey = false; - } - } getCurrentUserId() { return this.currentUserId; @@ -161,7 +138,9 @@ class AuthService { displayName: this.currentUser.displayName, mode: 'firebase', isLoggedIn: true, - hasApiKey: this.hasApiKey // Always true for firebase users, but good practice + //////// before_modelStateService //////// + // hasApiKey: this.hasApiKey // Always true for firebase users, but good practice + //////// before_modelStateService //////// }; } return { @@ -170,7 +149,9 @@ class AuthService { displayName: 'Default User', mode: 'local', isLoggedIn: false, - hasApiKey: this.hasApiKey + //////// before_modelStateService //////// + // hasApiKey: this.hasApiKey + //////// before_modelStateService //////// }; } } diff --git a/src/common/services/modelStateService.js b/src/common/services/modelStateService.js new file mode 100644 index 0000000..33de127 --- /dev/null +++ b/src/common/services/modelStateService.js @@ -0,0 +1,324 @@ +const Store = require('electron-store'); +const fetch = require('node-fetch'); +const { ipcMain, webContents } = require('electron'); +const { PROVIDERS } = require('../ai/factory'); + +class ModelStateService { + constructor(authService) { + this.authService = authService; + this.store = new Store({ name: 'pickle-glass-model-state' }); + this.state = {}; + } + + initialize() { + this._loadStateForCurrentUser(); + + this.setupIpcHandlers(); + console.log('[ModelStateService] Initialized.'); + } + + _logCurrentSelection() { + const llmModel = this.state.selectedModels.llm; + const sttModel = this.state.selectedModels.stt; + const llmProvider = this.getProviderForModel('llm', llmModel) || 'None'; + const sttProvider = this.getProviderForModel('stt', sttModel) || 'None'; + + console.log(`[ModelStateService] 🌟 Current Selection -> LLM: ${llmModel || 'None'} (Provider: ${llmProvider}), STT: ${sttModel || 'None'} (Provider: ${sttProvider})`); + } + + _autoSelectAvailableModels() { + console.log('[ModelStateService] Running auto-selection for models...'); + const types = ['llm', 'stt']; + + types.forEach(type => { + const currentModelId = this.state.selectedModels[type]; + let isCurrentModelValid = false; + + if (currentModelId) { + const provider = this.getProviderForModel(type, currentModelId); + if (provider && this.getApiKey(provider)) { + isCurrentModelValid = true; + } + } + + if (!isCurrentModelValid) { + console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected. Finding an alternative...`); + const availableModels = this.getAvailableModels(type); + if (availableModels.length > 0) { + this.state.selectedModels[type] = availableModels[0].id; + console.log(`[ModelStateService] Auto-selected ${type.toUpperCase()} model: ${availableModels[0].id}`); + } else { + this.state.selectedModels[type] = null; + } + } + }); + } + + _loadStateForCurrentUser() { + const userId = this.authService.getCurrentUserId(); + const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => { + acc[key] = null; + return acc; + }, {}); + + const defaultState = { + apiKeys: initialApiKeys, + selectedModels: { llm: null, stt: null }, + }; + this.state = this.store.get(`users.${userId}`, defaultState); + console.log(`[ModelStateService] State loaded for user: ${userId}`); + this._autoSelectAvailableModels(); + this._saveState(); + this._logCurrentSelection(); + } + + + _saveState() { + const userId = this.authService.getCurrentUserId(); + this.store.set(`users.${userId}`, this.state); + console.log(`[ModelStateService] State saved for user: ${userId}`); + this._logCurrentSelection(); + } + + async validateApiKey(provider, key) { + if (!key || key.trim() === '') { + return { success: false, error: 'API key cannot be empty.' }; + } + + let validationUrl, headers; + const body = undefined; + + switch (provider) { + case 'openai': + validationUrl = 'https://api.openai.com/v1/models'; + headers = { 'Authorization': `Bearer ${key}` }; + break; + case 'gemini': + validationUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`; + headers = {}; + break; + case 'anthropic': { + if (!key.startsWith('sk-ant-')) { + throw new Error('Invalid Anthropic key format.'); + } + const response = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": key, + "anthropic-version": "2023-06-01", + }, + body: JSON.stringify({ + model: "claude-3-haiku-20240307", + max_tokens: 1, + messages: [{ role: "user", content: "Hi" }], + }), + }); + + if (!response.ok && response.status !== 400) { + const errorData = await response.json().catch(() => ({})); + return { success: false, error: errorData.error?.message || `Validation failed with status: ${response.status}` }; + } + + console.log(`[ModelStateService] API key for ${provider} is valid.`); + this.setApiKey(provider, key); + return { success: true }; + } + default: + return { success: false, error: 'Unknown provider.' }; + } + + try { + const response = await fetch(validationUrl, { headers, body }); + if (response.ok) { + console.log(`[ModelStateService] API key for ${provider} is valid.`); + this.setApiKey(provider, key); + return { success: true }; + } else { + const errorData = await response.json().catch(() => ({})); + const message = errorData.error?.message || `Validation failed with status: ${response.status}`; + console.log(`[ModelStateService] API key for ${provider} is invalid: ${message}`); + return { success: false, error: message }; + } + } catch (error) { + console.error(`[ModelStateService] Network error during ${provider} key validation:`, error); + return { success: false, error: 'A network error occurred during validation.' }; + } + } + + setFirebaseVirtualKey(virtualKey) { + console.log(`[ModelStateService] Setting Firebase virtual key (for openai-glass).`); + this.state.apiKeys['openai-glass'] = virtualKey; + + const llmModels = PROVIDERS['openai-glass']?.llmModels; + const sttModels = PROVIDERS['openai-glass']?.sttModels; + + if (!this.state.selectedModels.llm && llmModels?.length > 0) { + this.state.selectedModels.llm = llmModels[0].id; + } + if (!this.state.selectedModels.stt && sttModels?.length > 0) { + this.state.selectedModels.stt = sttModels[0].id; + } + this._autoSelectAvailableModels(); + this._saveState(); + this._logCurrentSelection(); + } + + setApiKey(provider, key) { + if (provider in this.state.apiKeys) { + this.state.apiKeys[provider] = key; + + const llmModels = PROVIDERS[provider]?.llmModels; + const sttModels = PROVIDERS[provider]?.sttModels; + + if (!this.state.selectedModels.llm && llmModels?.length > 0) { + this.state.selectedModels.llm = llmModels[0].id; + } + if (!this.state.selectedModels.stt && sttModels?.length > 0) { + this.state.selectedModels.stt = sttModels[0].id; + } + this._saveState(); + this._logCurrentSelection(); + return true; + } + return false; + } + + getApiKey(provider) { + return this.state.apiKeys[provider] || null; + } + + getAllApiKeys() { + const { 'openai-glass': _, ...displayKeys } = this.state.apiKeys; + return displayKeys; + } + + removeApiKey(provider) { + if (provider in this.state.apiKeys) { + this.state.apiKeys[provider] = null; + const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm); + if (llmProvider === provider) this.state.selectedModels.llm = null; + + const sttProvider = this.getProviderForModel('stt', this.state.selectedModels.stt); + if (sttProvider === provider) this.state.selectedModels.stt = null; + + this._autoSelectAvailableModels(); + this._saveState(); + this._logCurrentSelection(); + return true; + } + return false; + } + + getProviderForModel(type, modelId) { + if (!modelId) return null; + for (const providerId in PROVIDERS) { + const models = type === 'llm' ? PROVIDERS[providerId].llmModels : PROVIDERS[providerId].sttModels; + if (models.some(m => m.id === modelId)) { + return providerId; + } + } + return null; + } + + getCurrentProvider(type) { + const selectedModel = this.state.selectedModels[type]; + return this.getProviderForModel(type, selectedModel); + } + + isLoggedInWithFirebase() { + return this.authService.getCurrentUser().isLoggedIn; + } + + areProvidersConfigured() { + if (this.isLoggedInWithFirebase()) return true; + + // LLMκ³Ό STT λͺ¨λΈμ„ μ œκ³΅ν•˜λŠ” Provider 쀑 ν•˜λ‚˜λΌλ„ API ν‚€κ°€ μ„€μ •λ˜μ—ˆλŠ”μ§€ 확인 + const hasLlmKey = Object.entries(this.state.apiKeys).some(([provider, key]) => key && PROVIDERS[provider]?.llmModels.length > 0); + const hasSttKey = Object.entries(this.state.apiKeys).some(([provider, key]) => key && PROVIDERS[provider]?.sttModels.length > 0); + + return hasLlmKey && hasSttKey; + } + + + getAvailableModels(type) { + const available = []; + const modelList = type === 'llm' ? 'llmModels' : 'sttModels'; + + Object.entries(this.state.apiKeys).forEach(([providerId, key]) => { + if (key && PROVIDERS[providerId]?.[modelList]) { + available.push(...PROVIDERS[providerId][modelList]); + } + }); + return [...new Map(available.map(item => [item.id, item])).values()]; + } + + getSelectedModels() { + return this.state.selectedModels; + } + + setSelectedModel(type, modelId) { + const provider = this.getProviderForModel(type, modelId); + if (provider && this.state.apiKeys[provider]) { + this.state.selectedModels[type] = modelId; + this._saveState(); + return true; + } + return false; + } + + /** + * + * @param {('llm' | 'stt')} type + * @returns {{provider: string, model: string, apiKey: string} | null} + */ + getCurrentModelInfo(type) { + this._logCurrentSelection(); + const model = this.state.selectedModels[type]; + if (!model) { + return null; + } + + const provider = this.getProviderForModel(type, model); + if (!provider) { + return null; + } + + const apiKey = this.getApiKey(provider); + return { provider, model, apiKey }; + } + + setupIpcHandlers() { + ipcMain.handle('model:validate-key', (e, { provider, key }) => this.validateApiKey(provider, key)); + ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys()); + ipcMain.handle('model:set-api-key', (e, { provider, key }) => this.setApiKey(provider, key)); + ipcMain.handle('model:remove-api-key', (e, { provider }) => { + const success = this.removeApiKey(provider); + if (success) { + const selectedModels = this.getSelectedModels(); + if (!selectedModels.llm || !selectedModels.stt) { + webContents.getAllWebContents().forEach(wc => { + wc.send('force-show-apikey-header'); + }); + } + } + return success; + }); + ipcMain.handle('model:get-selected-models', () => this.getSelectedModels()); + ipcMain.handle('model:set-selected-model', (e, { type, modelId }) => this.setSelectedModel(type, modelId)); + ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type)); + ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured()); + ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type)); + + ipcMain.handle('model:get-provider-config', () => { + const serializableProviders = {}; + for (const key in PROVIDERS) { + const { handler, ...rest } = PROVIDERS[key]; + serializableProviders[key] = rest; + } + return serializableProviders; + }); + } +} + +module.exports = ModelStateService; \ No newline at end of file diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index 4b540f6..32441b7 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -971,13 +971,15 @@ function setupIpcHandlers(movementManager) { console.log('[WindowManager] Received request to log out.'); await authService.signOut(); - await setApiKey(null); + //////// before_modelStateService //////// + // await setApiKey(null); - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-removed'); - } - }); + // windowPool.forEach(win => { + // if (win && !win.isDestroyed()) { + // win.webContents.send('api-key-removed'); + // } + // }); + //////// before_modelStateService //////// }); ipcMain.handle('check-system-permissions', async () => { @@ -1112,95 +1114,150 @@ function setupIpcHandlers(movementManager) { } -async function setApiKey(apiKey, provider = 'openai') { - console.log('[WindowManager] Persisting API key and provider to DB'); +//////// before_modelStateService //////// +// async function setApiKey(apiKey, provider = 'openai') { +// console.log('[WindowManager] Persisting API key and provider to DB'); - try { - await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider); - console.log('[WindowManager] API key and provider saved to SQLite'); +// try { +// await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider); +// console.log('[WindowManager] API key and provider saved to SQLite'); - // Notify authService that the key status may have changed - await authService.updateApiKeyStatus(); +// // Notify authService that the key status may have changed +// await authService.updateApiKeyStatus(); - } catch (err) { - console.error('[WindowManager] Failed to save API key to SQLite:', err); - } +// } catch (err) { +// console.error('[WindowManager] Failed to save API key to SQLite:', err); +// } - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - const js = apiKey ? ` - localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)}); - localStorage.setItem('ai_provider', ${JSON.stringify(provider)}); - ` : ` - localStorage.removeItem('openai_api_key'); - localStorage.removeItem('ai_provider'); - `; - win.webContents.executeJavaScript(js).catch(() => {}); - } - }); -} +// windowPool.forEach(win => { +// if (win && !win.isDestroyed()) { +// const js = apiKey ? ` +// localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)}); +// localStorage.setItem('ai_provider', ${JSON.stringify(provider)}); +// ` : ` +// localStorage.removeItem('openai_api_key'); +// localStorage.removeItem('ai_provider'); +// `; +// win.webContents.executeJavaScript(js).catch(() => {}); +// } +// }); +// } + +// async function getStoredApiKey() { +// const userId = authService.getCurrentUserId(); +// if (!userId) return null; +// const user = await userRepository.getById(userId); +// return user?.api_key || null; +// } + +// async function getStoredProvider() { +// const userId = authService.getCurrentUserId(); +// if (!userId) return 'openai'; +// const user = await userRepository.getById(userId); +// return user?.provider || 'openai'; +// } + +// function setupApiKeyIPC() { +// const { ipcMain } = require('electron'); + +// // Both handlers now do the same thing: fetch the key from the source of truth. +// ipcMain.handle('get-stored-api-key', getStoredApiKey); + +// ipcMain.handle('api-key-validated', async (event, data) => { +// console.log('[WindowManager] API key validation completed, saving...'); + +// // Support both old format (string) and new format (object) +// const apiKey = typeof data === 'string' ? data : data.apiKey; +// const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai'); + +// await setApiKey(apiKey, provider); + +// windowPool.forEach((win, name) => { +// if (win && !win.isDestroyed()) { +// win.webContents.send('api-key-validated', { apiKey, provider }); +// } +// }); + +// return { success: true }; +// }); + +// ipcMain.handle('remove-api-key', async () => { +// console.log('[WindowManager] API key removal requested'); +// await setApiKey(null); + +// windowPool.forEach((win, name) => { +// if (win && !win.isDestroyed()) { +// win.webContents.send('api-key-removed'); +// } +// }); + +// const settingsWindow = windowPool.get('settings'); +// if (settingsWindow && settingsWindow.isVisible()) { +// settingsWindow.hide(); +// console.log('[WindowManager] Settings window hidden after clearing API key.'); +// } + +// return { success: true }; +// }); + +// ipcMain.handle('get-ai-provider', getStoredProvider); + +// console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); +// } +//////// before_modelStateService //////// + + + + +//////// after_modelStateService //////// async function getStoredApiKey() { - const userId = authService.getCurrentUserId(); - if (!userId) return null; - const user = await userRepository.getById(userId); - return user?.api_key || null; + if (global.modelStateService) { + const provider = await getStoredProvider(); + return global.modelStateService.getApiKey(provider); + } + return null; // Fallback } async function getStoredProvider() { - const userId = authService.getCurrentUserId(); - if (!userId) return 'openai'; - const user = await userRepository.getById(userId); - return user?.provider || 'openai'; + if (global.modelStateService) { + return global.modelStateService.getCurrentProvider('llm'); + } + return 'openai'; // Fallback +} + +/** + * λ Œλ”λŸ¬μ—μ„œ μš”μ²­ν•œ νƒ€μž…('llm' λ˜λŠ” 'stt')에 λŒ€ν•œ λͺ¨λΈ 정보λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. + * @param {IpcMainInvokeEvent} event - μΌλ ‰νŠΈλ‘  IPC 이벀트 객체 + * @param {{type: 'llm' | 'stt'}} { type } - μš”μ²­ν•  λͺ¨λΈ νƒ€μž… + */ +async function getCurrentModelInfo(event, { type }) { + if (global.modelStateService && (type === 'llm' || type === 'stt')) { + return global.modelStateService.getCurrentModelInfo(type); + } + return null; // μ„œλΉ„μŠ€κ°€ μ—†κ±°λ‚˜ μœ νš¨ν•˜μ§€ μ•Šμ€ νƒ€μž…μΌ 경우 null λ°˜ν™˜ } function setupApiKeyIPC() { const { ipcMain } = require('electron'); - // Both handlers now do the same thing: fetch the key from the source of truth. ipcMain.handle('get-stored-api-key', getStoredApiKey); + ipcMain.handle('get-ai-provider', getStoredProvider); + ipcMain.handle('get-current-model-info', getCurrentModelInfo); ipcMain.handle('api-key-validated', async (event, data) => { - console.log('[WindowManager] API key validation completed, saving...'); - - // Support both old format (string) and new format (object) - const apiKey = typeof data === 'string' ? data : data.apiKey; - const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai'); - - await setApiKey(apiKey, provider); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-validated', { apiKey, provider }); - } - }); - + console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'."); return { success: true }; }); ipcMain.handle('remove-api-key', async () => { - console.log('[WindowManager] API key removal requested'); - await setApiKey(null); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-removed'); - } - }); - - const settingsWindow = windowPool.get('settings'); - if (settingsWindow && settingsWindow.isVisible()) { - settingsWindow.hide(); - console.log('[WindowManager] Settings window hidden after clearing API key.'); - } - + console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'."); return { success: true }; }); - ipcMain.handle('get-ai-provider', getStoredProvider); - - console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); + console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.'); } +//////// after_modelStateService //////// function getDefaultKeybinds() { @@ -1222,7 +1279,7 @@ function getDefaultKeybinds() { } function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) { - console.log('Updating global shortcuts with:', keybinds); + // console.log('Updating global shortcuts with:', keybinds); // Unregister all existing shortcuts globalShortcut.unregisterAll(); @@ -1276,7 +1333,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan movementManager.moveStep(direction); } }); - console.log(`Registered global shortcut: ${key} -> ${direction}`); + // console.log(`Registered global shortcut: ${key} -> ${direction}`); } catch (error) { console.error(`Failed to register ${key}:`, error); } @@ -1316,7 +1373,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan } mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored); }); - console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`); + // console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`); } catch (error) { console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error); } @@ -1352,7 +1409,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan } } }); - console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`); + // console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`); } catch (error) { console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, error); } @@ -1370,7 +1427,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan } `); }); - console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`); + // console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`); } catch (error) { console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, error); } @@ -1382,7 +1439,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Previous response shortcut triggered'); sendToRenderer('navigate-previous-response'); }); - console.log(`Registered previousResponse: ${keybinds.previousResponse}`); + // console.log(`Registered previousResponse: ${keybinds.previousResponse}`); } catch (error) { console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error); } @@ -1394,7 +1451,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Next response shortcut triggered'); sendToRenderer('navigate-next-response'); }); - console.log(`Registered nextResponse: ${keybinds.nextResponse}`); + // console.log(`Registered nextResponse: ${keybinds.nextResponse}`); } catch (error) { console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error); } @@ -1406,7 +1463,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Scroll up shortcut triggered'); sendToRenderer('scroll-response-up'); }); - console.log(`Registered scrollUp: ${keybinds.scrollUp}`); + // console.log(`Registered scrollUp: ${keybinds.scrollUp}`); } catch (error) { console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error); } @@ -1418,7 +1475,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Scroll down shortcut triggered'); sendToRenderer('scroll-response-down'); }); - console.log(`Registered scrollDown: ${keybinds.scrollDown}`); + // console.log(`Registered scrollDown: ${keybinds.scrollDown}`); } catch (error) { console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error); } @@ -1495,8 +1552,11 @@ module.exports = { createWindows, windowPool, fixedYPosition, - setApiKey, + //////// before_modelStateService //////// + // setApiKey, + //////// before_modelStateService //////// getStoredApiKey, getStoredProvider, + getCurrentModelInfo, captureScreenshot, }; \ No newline at end of file diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 33428be..b59b65d 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -1,6 +1,6 @@ const { ipcMain, BrowserWindow } = require('electron'); const { createStreamingLLM } = require('../../common/ai/factory'); -const { getStoredApiKey, getStoredProvider, windowPool, captureScreenshot } = require('../../electron/windowManager'); +const { getStoredApiKey, getStoredProvider, getCurrentModelInfo, windowPool, captureScreenshot } = require('../../electron/windowManager'); const authService = require('../../common/services/authService'); const sessionRepository = require('../../common/repositories/session'); const askRepository = require('./repositories'); @@ -31,6 +31,12 @@ async function sendMessage(userPrompt) { try { console.log(`[AskService] πŸ€– Processing message: ${userPrompt.substring(0, 50)}...`); + const modelInfo = await getCurrentModelInfo(null, { type: 'llm' }); + if (!modelInfo || !modelInfo.apiKey) { + throw new Error('AI model or API key not configured.'); + } + console.log(`[AskService] Using model: ${modelInfo.model} for provider: ${modelInfo.provider}`); + const screenshotResult = await captureScreenshot({ quality: 'medium' }); const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null; @@ -39,10 +45,6 @@ async function sendMessage(userPrompt) { const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false); - const API_KEY = await getStoredApiKey(); - if (!API_KEY) { - throw new Error('No API key found'); - } const messages = [ { role: 'system', content: systemPrompt }, @@ -61,36 +63,13 @@ async function sendMessage(userPrompt) { }); } - const provider = await getStoredProvider(); - const { isLoggedIn } = authService.getCurrentUser(); - - console.log(`[AskService] πŸš€ Sending request to ${provider} AI...`); - - // FIX: Proper model selection for each provider - let model; - switch (provider) { - case 'openai': - model = 'gpt-4o'; // Use a valid OpenAI model - break; - case 'gemini': - model = 'gemini-2.0-flash-exp'; // Use a valid Gemini model - break; - case 'anthropic': - model = 'claude-3-5-sonnet-20241022'; // Use a valid Claude model - break; - default: - model = 'gpt-4o'; // Default fallback - } - - console.log(`[AskService] Using model: ${model} for provider: ${provider}`); - - const streamingLLM = createStreamingLLM(provider, { - apiKey: API_KEY, - model: model, + const streamingLLM = createStreamingLLM(modelInfo.provider, { + apiKey: modelInfo.apiKey, + model: modelInfo.model, temperature: 0.7, maxTokens: 2048, - usePortkey: provider === 'openai' && isLoggedIn, - portkeyVirtualKey: isLoggedIn ? API_KEY : undefined + usePortkey: modelInfo.provider === 'openai-glass', + portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined, }); const response = await streamingLLM.streamChat(messages); diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index a0dcde8..3b89aa2 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -219,6 +219,20 @@ class ListenService { } }); + ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => { + try { + await this.sttService.sendSystemAudioContent(data, mimeType); + + // Send system audio data back to renderer for AEC reference (like macOS does) + this.sendToRenderer('system-audio-data', { data }); + + return { success: true }; + } catch (error) { + console.error('Error sending system audio:', error); + return { success: false, error: error.message }; + } + }); + ipcMain.handle('start-macos-audio', async () => { if (process.platform !== 'darwin') { return { success: false, error: 'macOS audio capture only available on macOS' }; diff --git a/src/features/listen/renderer/listenCapture.js b/src/features/listen/renderer/listenCapture.js index a4eef43..1c27f75 100644 --- a/src/features/listen/renderer/listenCapture.js +++ b/src/features/listen/renderer/listenCapture.js @@ -15,6 +15,8 @@ let micMediaStream = null; let screenshotInterval = null; let audioContext = null; let audioProcessor = null; +let systemAudioContext = null; +let systemAudioProcessor = null; let currentImageQuality = 'medium'; let lastScreenshotBase64 = null; @@ -345,6 +347,7 @@ function setupMicProcessing(micStream) { micProcessor.connect(micAudioContext.destination); audioProcessor = micProcessor; + return { context: micAudioContext, processor: micProcessor }; } function setupLinuxMicProcessing(micStream) { @@ -380,34 +383,40 @@ function setupLinuxMicProcessing(micStream) { audioProcessor = micProcessor; } -function setupWindowsLoopbackProcessing() { - // Setup audio processing for Windows loopback audio only - audioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); - const source = audioContext.createMediaStreamSource(mediaStream); - audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1); +function setupSystemAudioProcessing(systemStream) { + const systemAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); + const systemSource = systemAudioContext.createMediaStreamSource(systemStream); + const systemProcessor = systemAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1); let audioBuffer = []; const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION; - audioProcessor.onaudioprocess = async e => { + systemProcessor.onaudioprocess = async e => { const inputData = e.inputBuffer.getChannelData(0); + if (!inputData || inputData.length === 0) return; + audioBuffer.push(...inputData); - // Process audio in chunks while (audioBuffer.length >= samplesPerChunk) { const chunk = audioBuffer.splice(0, samplesPerChunk); const pcmData16 = convertFloat32ToInt16(chunk); const base64Data = arrayBufferToBase64(pcmData16.buffer); - await ipcRenderer.invoke('send-audio-content', { - data: base64Data, - mimeType: 'audio/pcm;rate=24000', - }); + try { + await ipcRenderer.invoke('send-system-audio-content', { + data: base64Data, + mimeType: 'audio/pcm;rate=24000', + }); + } catch (error) { + console.error('Failed to send system audio:', error); + } } }; - source.connect(audioProcessor); - audioProcessor.connect(audioContext.destination); + systemSource.connect(systemProcessor); + systemProcessor.connect(systemAudioContext.destination); + + return { context: systemAudioContext, processor: systemProcessor }; } // --------------------------- @@ -534,7 +543,9 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu }); console.log('macOS microphone capture started'); - setupMicProcessing(micMediaStream); + const { context, processor } = setupMicProcessing(micMediaStream); + audioContext = context; + audioProcessor = processor; } catch (micErr) { console.warn('Failed to get microphone on macOS:', micErr); } @@ -577,27 +588,62 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu console.log('Linux screen capture started'); } else { - // Windows - use display media for audio, main process for screenshots + // Windows - capture mic and system audio separately using native loopback + console.log('Starting Windows capture with native loopback audio...'); + + // Start screen capture in main process for screenshots const screenResult = await ipcRenderer.invoke('start-screen-capture'); if (!screenResult.success) { throw new Error('Failed to start screen capture: ' + screenResult.error); } - mediaStream = await navigator.mediaDevices.getDisplayMedia({ - video: false, // We don't need video in renderer - audio: { - sampleRate: SAMPLE_RATE, - channelCount: 1, - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - }, - }); + // Ensure STT sessions are initialized before starting audio capture + const sessionActive = await ipcRenderer.invoke('is-session-active'); + if (!sessionActive) { + throw new Error('STT sessions not initialized - please wait for initialization to complete'); + } - console.log('Windows capture started with loopback audio'); + // 1. Get user's microphone + try { + micMediaStream = await navigator.mediaDevices.getUserMedia({ + audio: { + sampleRate: SAMPLE_RATE, + channelCount: 1, + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + video: false, + }); + console.log('Windows microphone capture started'); + const { context, processor } = setupMicProcessing(micMediaStream); + audioContext = context; + audioProcessor = processor; + } catch (micErr) { + console.warn('Could not get microphone access on Windows:', micErr); + } - // Setup audio processing for Windows loopback audio only - setupWindowsLoopbackProcessing(); + // 2. Get system audio using native Electron loopback + try { + mediaStream = await navigator.mediaDevices.getDisplayMedia({ + video: true, + audio: true // This will now use native loopback from our handler + }); + + // Verify we got audio tracks + const audioTracks = mediaStream.getAudioTracks(); + if (audioTracks.length === 0) { + throw new Error('No audio track in native loopback stream'); + } + + console.log('Windows native loopback audio capture started'); + const { context, processor } = setupSystemAudioProcessing(mediaStream); + systemAudioContext = context; + systemAudioProcessor = processor; + } catch (sysAudioErr) { + console.error('Failed to start Windows native loopback audio:', sysAudioErr); + // Continue without system audio + } } // Start capturing screenshots - check if manual mode @@ -626,21 +672,31 @@ function stopCapture() { screenshotInterval = null; } + // Clean up microphone resources if (audioProcessor) { audioProcessor.disconnect(); audioProcessor = null; } - if (audioContext) { audioContext.close(); audioContext = null; } + // Clean up system audio resources + if (systemAudioProcessor) { + systemAudioProcessor.disconnect(); + systemAudioProcessor = null; + } + if (systemAudioContext) { + systemAudioContext.close(); + systemAudioContext = null; + } + + // Stop and release media stream tracks if (mediaStream) { mediaStream.getTracks().forEach(track => track.stop()); mediaStream = null; } - if (micMediaStream) { micMediaStream.getTracks().forEach(t => t.stop()); micMediaStream = null; diff --git a/src/features/listen/stt/sttService.js b/src/features/listen/stt/sttService.js index 294bd74..e11d579 100644 --- a/src/features/listen/stt/sttService.js +++ b/src/features/listen/stt/sttService.js @@ -1,7 +1,7 @@ const { BrowserWindow } = require('electron'); const { spawn } = require('child_process'); const { createSTT } = require('../../../common/ai/factory'); -const { getStoredApiKey, getStoredProvider } = require('../../../electron/windowManager'); +const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager'); const COMPLETION_DEBOUNCE_MS = 2000; @@ -29,6 +29,8 @@ class SttService { // Callbacks this.onTranscriptionComplete = null; this.onStatusUpdate = null; + + this.modelInfo = null; } setCallbacks({ onTranscriptionComplete, onStatusUpdate }) { @@ -36,32 +38,32 @@ class SttService { this.onStatusUpdate = onStatusUpdate; } - async getApiKey() { - const storedKey = await getStoredApiKey(); - if (storedKey) { - console.log('[SttService] Using stored API key'); - return storedKey; - } + // async getApiKey() { + // const storedKey = await getStoredApiKey(); + // if (storedKey) { + // console.log('[SttService] Using stored API key'); + // return storedKey; + // } - const envKey = process.env.OPENAI_API_KEY; - if (envKey) { - console.log('[SttService] Using environment API key'); - return envKey; - } + // const envKey = process.env.OPENAI_API_KEY; + // if (envKey) { + // console.log('[SttService] Using environment API key'); + // return envKey; + // } - console.error('[SttService] No API key found in storage or environment'); - return null; - } + // console.error('[SttService] No API key found in storage or environment'); + // return null; + // } - async getAiProvider() { - try { - const { ipcRenderer } = require('electron'); - const provider = await ipcRenderer.invoke('get-ai-provider'); - return provider || 'openai'; - } catch (error) { - return getStoredProvider ? getStoredProvider() : 'openai'; - } - } + // async getAiProvider() { + // try { + // const { ipcRenderer } = require('electron'); + // const provider = await ipcRenderer.invoke('get-ai-provider'); + // return provider || 'openai'; + // } catch (error) { + // return getStoredProvider ? getStoredProvider() : 'openai'; + // } + // } sendToRenderer(channel, data) { BrowserWindow.getAllWindows().forEach(win => { @@ -72,7 +74,7 @@ class SttService { } flushMyCompletion() { - if (!this.myCompletionBuffer.trim()) return; + if (!this.modelInfo || !this.myCompletionBuffer.trim()) return; const finalText = this.myCompletionBuffer.trim(); @@ -100,7 +102,7 @@ class SttService { } flushTheirCompletion() { - if (!this.theirCompletionBuffer.trim()) return; + if (!this.modelInfo || !this.theirCompletionBuffer.trim()) return; const finalText = this.theirCompletionBuffer.trim(); @@ -156,17 +158,30 @@ class SttService { async initializeSttSessions(language = 'en') { const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en'; - const API_KEY = await this.getApiKey(); - if (!API_KEY) { - throw new Error('No API key available'); - } + // const API_KEY = await this.getApiKey(); + // if (!API_KEY) { + // throw new Error('No API key available'); + // } + // const provider = await this.getAiProvider(); - const provider = await this.getAiProvider(); - const isGemini = provider === 'gemini'; - console.log(`[SttService] Initializing STT for provider: ${provider}`); + const modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + if (!modelInfo || !modelInfo.apiKey) { + throw new Error('AI model or API key is not configured.'); + } + this.modelInfo = modelInfo; + console.log(`[SttService] Initializing STT for ${modelInfo.provider} using model ${modelInfo.model}`); + + + // const isGemini = modelInfo.provider === 'gemini'; + // console.log(`[SttService] Initializing STT for provider: ${modelInfo.provider}`); const handleMyMessage = message => { - if (isGemini) { + if (!this.modelInfo) { + console.log('[SttService] Ignoring message - session already closed'); + return; + } + + if (this.modelInfo.provider === 'gemini') { const text = message.serverContent?.inputTranscription?.text || ''; if (text && text.trim()) { const finalUtteranceText = text.trim().replace(//g, '').trim(); @@ -207,7 +222,12 @@ class SttService { }; const handleTheirMessage = message => { - if (isGemini) { + if (!this.modelInfo) { + console.log('[SttService] Ignoring message - session already closed'); + return; + } + + if (this.modelInfo.provider === 'gemini') { const text = message.serverContent?.inputTranscription?.text || ''; if (text && text.trim()) { const finalUtteranceText = text.trim().replace(//g, '').trim(); @@ -265,20 +285,20 @@ class SttService { }; // Determine auth options for providers that support it - const authService = require('../../../common/services/authService'); - const userState = authService.getCurrentUser(); - const loggedIn = userState.isLoggedIn; + // const authService = require('../../../common/services/authService'); + // const userState = authService.getCurrentUser(); + // const loggedIn = userState.isLoggedIn; const sttOptions = { - apiKey: API_KEY, + apiKey: this.modelInfo.apiKey, language: effectiveLanguage, - usePortkey: !isGemini && loggedIn, // Only OpenAI supports Portkey - portkeyVirtualKey: loggedIn ? API_KEY : undefined + usePortkey: this.modelInfo.provider === 'openai-glass', + portkeyVirtualKey: this.modelInfo.provider === 'openai-glass' ? this.modelInfo.apiKey : undefined, }; [this.mySttSession, this.theirSttSession] = await Promise.all([ - createSTT(provider, { ...sttOptions, callbacks: mySttConfig.callbacks }), - createSTT(provider, { ...sttOptions, callbacks: theirSttConfig.callbacks }), + createSTT(this.modelInfo.provider, { ...sttOptions, callbacks: mySttConfig.callbacks }), + createSTT(this.modelInfo.provider, { ...sttOptions, callbacks: theirSttConfig.callbacks }), ]); console.log('βœ… Both STT sessions initialized successfully.'); @@ -286,20 +306,50 @@ class SttService { } async sendAudioContent(data, mimeType) { - const provider = await this.getAiProvider(); - const isGemini = provider === 'gemini'; + // const provider = await this.getAiProvider(); + // const isGemini = provider === 'gemini'; if (!this.mySttSession) { throw new Error('User STT session not active'); } - const payload = isGemini + let modelInfo = this.modelInfo; + if (!modelInfo) { + console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); + modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + } + if (!modelInfo) { + throw new Error('STT model info could not be retrieved.'); + } + + const payload = modelInfo.provider === 'gemini' ? { audio: { data, mimeType: mimeType || 'audio/pcm;rate=24000' } } : data; await this.mySttSession.sendRealtimeInput(payload); } + async sendSystemAudioContent(data, mimeType) { + if (!this.theirSttSession) { + throw new Error('Their STT session not active'); + } + + let modelInfo = this.modelInfo; + if (!modelInfo) { + console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); + modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + } + if (!modelInfo) { + throw new Error('STT model info could not be retrieved.'); + } + + const payload = modelInfo.provider === 'gemini' + ? { audio: { data, mimeType: mimeType || 'audio/pcm;rate=24000' } } + : data; + + await this.theirSttSession.sendRealtimeInput(payload); + } + killExistingSystemAudioDump() { return new Promise(resolve => { console.log('Checking for existing SystemAudioDump processes...'); @@ -362,8 +412,17 @@ class SttService { let audioBuffer = Buffer.alloc(0); - const provider = await this.getAiProvider(); - const isGemini = provider === 'gemini'; + // const provider = await this.getAiProvider(); + // const isGemini = provider === 'gemini'; + + let modelInfo = this.modelInfo; + if (!modelInfo) { + console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); + modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + } + if (!modelInfo) { + throw new Error('STT model info could not be retrieved.'); + } this.systemAudioProc.stdout.on('data', async data => { audioBuffer = Buffer.concat([audioBuffer, data]); @@ -379,7 +438,7 @@ class SttService { if (this.theirSttSession) { try { - const payload = isGemini + const payload = modelInfo.provider === 'gemini' ? { audio: { data: base64Data, mimeType: 'audio/pcm;rate=24000' } } : base64Data; await this.theirSttSession.sendRealtimeInput(payload); @@ -472,6 +531,7 @@ class SttService { this.theirLastPartialText = ''; this.myCompletionBuffer = ''; this.theirCompletionBuffer = ''; + this.modelInfo = null; } } diff --git a/src/features/listen/summary/repositories/sqlite.repository.js b/src/features/listen/summary/repositories/sqlite.repository.js index b365090..008aa21 100644 --- a/src/features/listen/summary/repositories/sqlite.repository.js +++ b/src/features/listen/summary/repositories/sqlite.repository.js @@ -1,28 +1,30 @@ const sqliteClient = require('../../../../common/services/sqliteClient'); function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) { - const db = sqliteClient.getDb(); - const now = Math.floor(Date.now() / 1000); - const query = ` - INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(session_id) DO UPDATE SET - generated_at=excluded.generated_at, - model=excluded.model, - text=excluded.text, - tldr=excluded.tldr, - bullet_json=excluded.bullet_json, - action_json=excluded.action_json, - updated_at=excluded.updated_at - `; - - try { - const result = db.prepare(query).run(sessionId, now, model, text, tldr, bullet_json, action_json, now); - return { changes: result.changes }; - } catch (err) { - console.error('Error saving summary:', err); - throw err; - } + return new Promise((resolve, reject) => { + try { + const db = sqliteClient.getDb(); + const now = Math.floor(Date.now() / 1000); + const query = ` + INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(session_id) DO UPDATE SET + generated_at=excluded.generated_at, + model=excluded.model, + text=excluded.text, + tldr=excluded.tldr, + bullet_json=excluded.bullet_json, + action_json=excluded.action_json, + updated_at=excluded.updated_at + `; + + const result = db.prepare(query).run(sessionId, now, model, text, tldr, bullet_json, action_json, now); + resolve({ changes: result.changes }); + } catch (err) { + console.error('Error saving summary:', err); + reject(err); + } + }); } function getSummaryBySessionId(sessionId) { diff --git a/src/features/listen/summary/summaryService.js b/src/features/listen/summary/summaryService.js index 860fa35..53d3b79 100644 --- a/src/features/listen/summary/summaryService.js +++ b/src/features/listen/summary/summaryService.js @@ -4,7 +4,7 @@ const { createLLM } = require('../../../common/ai/factory'); const authService = require('../../../common/services/authService'); const sessionRepository = require('../../../common/repositories/session'); const summaryRepository = require('./repositories'); -const { getStoredApiKey, getStoredProvider } = require('../../../electron/windowManager'); +const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager'); class SummaryService { constructor() { @@ -27,22 +27,22 @@ class SummaryService { this.currentSessionId = sessionId; } - async getApiKey() { - const storedKey = await getStoredApiKey(); - if (storedKey) { - console.log('[SummaryService] Using stored API key'); - return storedKey; - } + // async getApiKey() { + // const storedKey = await getStoredApiKey(); + // if (storedKey) { + // console.log('[SummaryService] Using stored API key'); + // return storedKey; + // } - const envKey = process.env.OPENAI_API_KEY; - if (envKey) { - console.log('[SummaryService] Using environment API key'); - return envKey; - } + // const envKey = process.env.OPENAI_API_KEY; + // if (envKey) { + // console.log('[SummaryService] Using environment API key'); + // return envKey; + // } - console.error('[SummaryService] No API key found in storage or environment'); - return null; - } + // console.error('[SummaryService] No API key found in storage or environment'); + // return null; + // } sendToRenderer(channel, data) { BrowserWindow.getAllWindows().forEach(win => { @@ -114,6 +114,12 @@ Please build upon this context while analyzing the new conversation segments. if (this.currentSessionId) { await sessionRepository.touch(this.currentSessionId); } + + const modelInfo = await getCurrentModelInfo(null, { type: 'llm' }); + if (!modelInfo || !modelInfo.apiKey) { + throw new Error('AI model or API key is not configured.'); + } + console.log(`πŸ€– Sending analysis request to ${modelInfo.provider} using model ${modelInfo.model}`); const messages = [ { @@ -148,23 +154,13 @@ Keep all points concise and build upon previous analysis if provided.`, console.log('πŸ€– Sending analysis request to AI...'); - const API_KEY = await this.getApiKey(); - if (!API_KEY) { - throw new Error('No API key available'); - } - - const provider = getStoredProvider ? await getStoredProvider() : 'openai'; - const loggedIn = authService.getCurrentUser().isLoggedIn; - - console.log(`[SummaryService] provider: ${provider}, loggedIn: ${loggedIn}`); - - const llm = createLLM(provider, { - apiKey: API_KEY, - model: provider === 'openai' ? 'gpt-4.1' : 'gemini-2.5-flash', + const llm = createLLM(modelInfo.provider, { + apiKey: modelInfo.apiKey, + model: modelInfo.model, temperature: 0.7, maxTokens: 1024, - usePortkey: provider === 'openai' && loggedIn, - portkeyVirtualKey: loggedIn ? API_KEY : undefined + usePortkey: modelInfo.provider === 'openai-glass', + portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined, }); const completion = await llm.chat(messages); @@ -174,14 +170,18 @@ Keep all points concise and build upon previous analysis if provided.`, const structuredData = this.parseResponseText(responseText, this.previousAnalysisResult); if (this.currentSessionId) { - summaryRepository.saveSummary({ - sessionId: this.currentSessionId, - text: responseText, - tldr: structuredData.summary.join('\n'), - bullet_json: JSON.stringify(structuredData.topic.bullets), - action_json: JSON.stringify(structuredData.actions), - model: 'gpt-4.1' - }).catch(err => console.error('[DB] Failed to save summary:', err)); + try { + summaryRepository.saveSummary({ + sessionId: this.currentSessionId, + text: responseText, + tldr: structuredData.summary.join('\n'), + bullet_json: JSON.stringify(structuredData.topic.bullets), + action_json: JSON.stringify(structuredData.actions), + model: modelInfo.model + }); + } catch (err) { + console.error('[DB] Failed to save summary:', err); + } } // 뢄석 κ²°κ³Ό μ €μž₯ @@ -192,7 +192,6 @@ Keep all points concise and build upon previous analysis if provided.`, conversationLength: conversationTexts.length, }); - // νžˆμŠ€ν† λ¦¬ 크기 μ œν•œ (졜근 10개만 μœ μ§€) if (this.analysisHistory.length > 10) { this.analysisHistory.shift(); } diff --git a/src/features/settings/SettingsView.js b/src/features/settings/SettingsView.js index 49a0854..d78703c 100644 --- a/src/features/settings/SettingsView.js +++ b/src/features/settings/SettingsView.js @@ -374,6 +374,43 @@ export class SettingsView extends LitElement { .hidden { display: none; } + + .api-key-section, .model-selection-section { + padding: 8px 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + flex-direction: column; + gap: 10px; + } + .provider-key-group, .model-select-group { + display: flex; + flex-direction: column; + gap: 4px; + } + label { + font-size: 11px; + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + margin-left: 2px; + } + label > strong { + color: white; + font-weight: 600; + } + .provider-key-group input { + width: 100%; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.2); + color: white; border-radius: 4px; padding: 5px 8px; font-size: 11px; box-sizing: border-box; + } + .key-buttons { display: flex; gap: 4px; } + .key-buttons .settings-button { flex: 1; padding: 4px; } + .model-list { + display: flex; flex-direction: column; gap: 2px; max-height: 120px; + overflow-y: auto; background: rgba(0,0,0,0.3); border-radius: 4px; + padding: 4px; margin-top: 4px; + } + .model-item { padding: 5px 8px; font-size: 11px; border-radius: 3px; cursor: pointer; transition: background-color 0.15s; } + .model-item:hover { background-color: rgba(255,255,255,0.1); } + .model-item.selected { background-color: rgba(0, 122, 255, 0.4); font-weight: 500; } /* ────────────────[ GLASS BYPASS ]─────────────── */ :host-context(body.has-glass) { @@ -400,71 +437,237 @@ export class SettingsView extends LitElement { } `; + //////// before_modelStateService //////// + // static properties = { + // firebaseUser: { type: Object, state: true }, + // apiKey: { type: String, state: true }, + // isLoading: { type: Boolean, state: true }, + // isContentProtectionOn: { type: Boolean, state: true }, + // settings: { type: Object, state: true }, + // presets: { type: Array, state: true }, + // selectedPreset: { type: Object, state: true }, + // showPresets: { type: Boolean, state: true }, + // saving: { type: Boolean, state: true }, + // }; + //////// before_modelStateService //////// + + //////// after_modelStateService //////// static properties = { firebaseUser: { type: Object, state: true }, - apiKey: { type: String, state: true }, isLoading: { type: Boolean, state: true }, isContentProtectionOn: { type: Boolean, state: true }, - settings: { type: Object, state: true }, + saving: { type: Boolean, state: true }, + providerConfig: { type: Object, state: true }, + apiKeys: { type: Object, state: true }, + availableLlmModels: { type: Array, state: true }, + availableSttModels: { type: Array, state: true }, + selectedLlm: { type: String, state: true }, + selectedStt: { type: String, state: true }, + isLlmListVisible: { type: Boolean }, + isSttListVisible: { type: Boolean }, presets: { type: Array, state: true }, selectedPreset: { type: Object, state: true }, showPresets: { type: Boolean, state: true }, - saving: { type: Boolean, state: true }, }; + //////// after_modelStateService //////// constructor() { super(); + //////// before_modelStateService //////// + // this.firebaseUser = null; + // this.apiKey = null; + // this.isLoading = false; + // this.isContentProtectionOn = true; + // this.settings = null; + // this.presets = []; + // this.selectedPreset = null; + // this.showPresets = false; + // this.saving = false; + // this.loadInitialData(); + //////// before_modelStateService //////// + + //////// after_modelStateService //////// this.firebaseUser = null; - this.apiKey = null; - this.isLoading = false; + this.apiKeys = { openai: '', gemini: '', anthropic: '' }; + this.providerConfig = {}; + this.isLoading = true; this.isContentProtectionOn = true; - this.settings = null; + this.saving = false; + this.availableLlmModels = []; + this.availableSttModels = []; + this.selectedLlm = null; + this.selectedStt = null; + this.isLlmListVisible = false; + this.isSttListVisible = false; this.presets = []; this.selectedPreset = null; this.showPresets = false; - this.saving = false; + this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this) this.loadInitialData(); + //////// after_modelStateService //////// } + + //////// before_modelStateService //////// + // async loadInitialData() { + // if (!window.require) return; + + // try { + // this.isLoading = true; + // const { ipcRenderer } = window.require('electron'); + + // // Load all data in parallel + // const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([ + // ipcRenderer.invoke('settings:getSettings'), + // ipcRenderer.invoke('settings:getPresets'), + // ipcRenderer.invoke('get-stored-api-key'), + // ipcRenderer.invoke('get-content-protection-status'), + // ipcRenderer.invoke('get-current-user') + // ]); + + // this.settings = settings; + // this.presets = presets || []; + // this.apiKey = apiKey; + // this.isContentProtectionOn = contentProtection; + + // // Set first user preset as selected + // if (this.presets.length > 0) { + // const firstUserPreset = this.presets.find(p => p.is_default === 0); + // if (firstUserPreset) { + // this.selectedPreset = firstUserPreset; + // } + // } + + // if (userState && userState.isLoggedIn) { + // this.firebaseUser = userState.user; + // } + // } catch (error) { + // console.error('Error loading initial data:', error); + // } finally { + // this.isLoading = false; + // } + // } + //////// before_modelStateService //////// + + //////// after_modelStateService //////// async loadInitialData() { if (!window.require) return; - + this.isLoading = true; + const { ipcRenderer } = window.require('electron'); try { - this.isLoading = true; - const { ipcRenderer } = window.require('electron'); - - // Load all data in parallel - const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([ - ipcRenderer.invoke('settings:getSettings'), + const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection] = await Promise.all([ + ipcRenderer.invoke('get-current-user'), + ipcRenderer.invoke('model:get-provider-config'), // Provider μ„€μ • λ‘œλ“œ + ipcRenderer.invoke('model:get-all-keys'), + ipcRenderer.invoke('model:get-available-models', { type: 'llm' }), + ipcRenderer.invoke('model:get-available-models', { type: 'stt' }), + ipcRenderer.invoke('model:get-selected-models'), ipcRenderer.invoke('settings:getPresets'), - ipcRenderer.invoke('get-stored-api-key'), - ipcRenderer.invoke('get-content-protection-status'), - ipcRenderer.invoke('get-current-user') + ipcRenderer.invoke('get-content-protection-status') ]); - this.settings = settings; + if (userState && userState.isLoggedIn) this.firebaseUser = userState; + this.providerConfig = config; + this.apiKeys = storedKeys; + this.availableLlmModels = availableLlm; + this.availableSttModels = availableStt; + this.selectedLlm = selectedModels.llm; + this.selectedStt = selectedModels.stt; this.presets = presets || []; - this.apiKey = apiKey; this.isContentProtectionOn = contentProtection; - - // Set first user preset as selected if (this.presets.length > 0) { const firstUserPreset = this.presets.find(p => p.is_default === 0); - if (firstUserPreset) { - this.selectedPreset = firstUserPreset; - } - } - - if (userState && userState.isLoggedIn) { - this.firebaseUser = userState.user; + if (firstUserPreset) this.selectedPreset = firstUserPreset; } } catch (error) { - console.error('Error loading initial data:', error); + console.error('Error loading initial settings data:', error); } finally { this.isLoading = false; } } + async handleSaveKey(provider) { + const input = this.shadowRoot.querySelector(`#key-input-${provider}`); + if (!input) return; + const key = input.value; + this.saving = true; + + const { ipcRenderer } = window.require('electron'); + const result = await ipcRenderer.invoke('model:validate-key', { provider, key }); + + if (result.success) { + this.apiKeys = { ...this.apiKeys, [provider]: key }; + await this.refreshModelData(); + } else { + alert(`Failed to save ${provider} key: ${result.error}`); + input.value = this.apiKeys[provider] || ''; + } + this.saving = false; + } + + async handleClearKey(provider) { + this.saving = true; + const { ipcRenderer } = window.require('electron'); + await ipcRenderer.invoke('model:remove-api-key', { provider }); + this.apiKeys = { ...this.apiKeys, [provider]: '' }; + await this.refreshModelData(); + this.saving = false; + } + + async refreshModelData() { + const { ipcRenderer } = window.require('electron'); + const [availableLlm, availableStt, selected] = await Promise.all([ + ipcRenderer.invoke('model:get-available-models', { type: 'llm' }), + ipcRenderer.invoke('model:get-available-models', { type: 'stt' }), + ipcRenderer.invoke('model:get-selected-models') + ]); + this.availableLlmModels = availableLlm; + this.availableSttModels = availableStt; + this.selectedLlm = selected.llm; + this.selectedStt = selected.stt; + this.requestUpdate(); + } + + async toggleModelList(type) { + const visibilityProp = type === 'llm' ? 'isLlmListVisible' : 'isSttListVisible'; + + if (!this[visibilityProp]) { + this.saving = true; + this.requestUpdate(); + + await this.refreshModelData(); + + this.saving = false; + } + + // 데이터 μƒˆλ‘œκ³ μΉ¨ ν›„, λͺ©λ‘μ˜ ν‘œμ‹œ μƒνƒœλ₯Ό ν† κΈ€ν•©λ‹ˆλ‹€. + this[visibilityProp] = !this[visibilityProp]; + this.requestUpdate(); + } + + async selectModel(type, modelId) { + this.saving = true; + const { ipcRenderer } = window.require('electron'); + await ipcRenderer.invoke('model:set-selected-model', { type, modelId }); + if (type === 'llm') this.selectedLlm = modelId; + if (type === 'stt') this.selectedStt = modelId; + this.isLlmListVisible = false; + this.isSttListVisible = false; + this.saving = false; + this.requestUpdate(); + } + + handleUsePicklesKey(e) { + e.preventDefault() + if (this.wasJustDragged) return + + console.log("Requesting Firebase authentication from main process...") + if (window.require) { + window.require("electron").ipcRenderer.invoke("start-firebase-auth") + } + } + //////// after_modelStateService //////// + connectedCallback() { super.connectedCallback(); @@ -697,6 +900,140 @@ export class SettingsView extends LitElement { } } + + //////// before_modelStateService //////// + // render() { + // if (this.isLoading) { + // return html` + //
+ //
+ //
+ // Loading... + //
+ //
+ // `; + // } + + // const loggedIn = !!this.firebaseUser; + + // return html` + //
+ //
+ //
+ //

Pickle Glass

+ // + //
+ //
+ // + // + // + //
+ //
+ + //
+ // + // + //
+ + //
+ // ${this.getMainShortcuts().map(shortcut => html` + //
+ // ${shortcut.name} + //
+ // ⌘ + // ${shortcut.key} + //
+ //
+ // `)} + //
+ + // + //
+ //
+ // + // My Presets + // (${this.presets.filter(p => p.is_default === 0).length}) + // + // + // ${this.showPresets ? 'β–Ό' : 'β–Ά'} + // + //
+ + //
+ // ${this.presets.filter(p => p.is_default === 0).length === 0 ? html` + //
+ // No custom presets yet.
+ // + // Create your first preset + // + //
+ // ` : this.presets.filter(p => p.is_default === 0).map(preset => html` + //
this.handlePresetSelect(preset)}> + // ${preset.title} + // ${this.selectedPreset?.id === preset.id ? html`Selected` : ''} + //
+ // `)} + //
+ //
+ + //
+ // + + //
+ // + // + //
+ + // + + //
+ // ${this.firebaseUser + // ? html` + // + // ` + // : html` + // + // ` + // } + // + //
+ //
+ //
+ // `; + // } + //////// before_modelStateService //////// + + //////// after_modelStateService //////// render() { if (this.isLoading) { return html` @@ -711,6 +1048,68 @@ export class SettingsView extends LitElement { const loggedIn = !!this.firebaseUser; + const apiKeyManagementHTML = html` +
+ ${Object.entries(this.providerConfig) + .filter(([id, config]) => !id.includes('-glass')) + .map(([id, config]) => html` +
+ + +
+ + +
+
+ `)} +
+ `; + + const getModelName = (type, id) => { + const models = type === 'llm' ? this.availableLlmModels : this.availableSttModels; + const model = models.find(m => m.id === id); + return model ? model.name : id; + } + + const modelSelectionHTML = html` +
+
+ + + ${this.isLlmListVisible ? html` +
+ ${this.availableLlmModels.map(model => html` +
this.selectModel('llm', model.id)}> + ${model.name} +
+ `)} +
+ ` : ''} +
+
+ + + ${this.isSttListVisible ? html` +
+ ${this.availableSttModels.map(model => html` +
this.selectModel('stt', model.id)}> + ${model.name} +
+ `)} +
+ ` : ''} +
+
+ `; + return html`
@@ -719,9 +1118,7 @@ export class SettingsView extends LitElement {
@@ -732,19 +1129,9 @@ export class SettingsView extends LitElement {
-
- - -
- + ${apiKeyManagementHTML} + ${modelSelectionHTML} +
${this.getMainShortcuts().map(shortcut => html`
@@ -757,7 +1144,6 @@ export class SettingsView extends LitElement { `)}
-
@@ -813,8 +1199,8 @@ export class SettingsView extends LitElement { ` : html` - ` } @@ -826,6 +1212,7 @@ export class SettingsView extends LitElement {
`; } + //////// after_modelStateService //////// } customElements.define('settings-view', SettingsView); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 52c057f..31ae822 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ if (require('electron-squirrel-startup')) { process.exit(0); } -const { app, BrowserWindow, shell, ipcMain, dialog } = require('electron'); +const { app, BrowserWindow, shell, ipcMain, dialog, desktopCapturer, session } = require('electron'); const { createWindows } = require('./electron/windowManager.js'); const ListenService = require('./features/listen/listenService'); const { initializeFirebase } = require('./common/services/firebaseClient'); @@ -25,6 +25,7 @@ const { EventEmitter } = require('events'); const askService = require('./features/ask/askService'); const settingsService = require('./features/settings/settingsService'); const sessionRepository = require('./common/repositories/session'); +const ModelStateService = require('./common/services/modelStateService'); const eventBridge = new EventEmitter(); let WEB_PORT = 3000; @@ -33,6 +34,11 @@ const listenService = new ListenService(); // Make listenService globally accessible so other modules (e.g., windowManager, askService) can reuse the same instance global.listenService = listenService; +//////// after_modelStateService //////// +const modelStateService = new ModelStateService(authService); +global.modelStateService = modelStateService; +//////// after_modelStateService //////// + // Native deep link handling - cross-platform compatible let pendingDeepLinkUrl = null; @@ -162,6 +168,17 @@ setupProtocolHandling(); app.whenReady().then(async () => { + // Setup native loopback audio capture for Windows + session.defaultSession.setDisplayMediaRequestHandler((request, callback) => { + desktopCapturer.getSources({ types: ['screen'] }).then((sources) => { + // Grant access to the first screen found with loopback audio + callback({ video: sources[0], audio: 'loopback' }); + }).catch((error) => { + console.error('Failed to get desktop capturer sources:', error); + callback({}); + }); + }); + // Initialize core services initializeFirebase(); @@ -173,6 +190,11 @@ app.whenReady().then(async () => { sessionRepository.endAllActiveSessions(); authService.initialize(); + + //////// after_modelStateService //////// + modelStateService.initialize(); + //////// after_modelStateService //////// + listenService.setupIpcHandlers(); askService.initialize(); settingsService.initialize();