Merge branch 'main' into main
This commit is contained in:
commit
8da1ed5791
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@ -31,4 +31,16 @@ jobs:
|
||||
|
||||
- name: 🖥️ Build Electron app
|
||||
# Run Electron build script from root directory
|
||||
run: npm run build
|
||||
|
||||
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 }}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
308
package-lock.json
generated
308
package-lock.json
generated
@ -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": {
|
||||
|
12
package.json
12
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": [
|
||||
|
36
pickleglass_web/package-lock.json
generated
36
pickleglass_web/package-lock.json
generated
@ -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": {
|
||||
|
@ -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`
|
||||
<div class="container" @mousedown=${this.handleMouseDown}>
|
||||
<button class="close-button" @click=${this.handleClose} title="Close application">
|
||||
<svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
|
||||
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="title">Choose how to power your AI</h1>
|
||||
<div class="container" @mousedown=${this.handleMouseDown}>
|
||||
<h1 class="title">Enter Your API Keys</h1>
|
||||
|
||||
<div class="form-content">
|
||||
<div class="error-message">${this.errorMessage}</div>
|
||||
<div class="provider-label">Select AI Provider:</div>
|
||||
<select
|
||||
class="provider-select"
|
||||
.value=${this.selectedProvider || "openai"}
|
||||
@change=${this.handleProviderChange}
|
||||
?disabled=${this.isLoading}
|
||||
tabindex="0"
|
||||
>
|
||||
<option value="openai" ?selected=${this.selectedProvider === "openai"}>OpenAI</option>
|
||||
<option value="gemini" ?selected=${this.selectedProvider === "gemini"}>Google Gemini</option>
|
||||
<option value="anthropic" ?selected=${this.selectedProvider === "anthropic"}>Anthropic</option>
|
||||
<div class="providers-container">
|
||||
<div class="provider-column">
|
||||
<div class="provider-label"></div>
|
||||
<select class="provider-select" .value=${this.llmProvider} @change=${e => this.llmProvider = e.target.value} ?disabled=${this.isLoading}>
|
||||
${this.providers.llm.map(p => html`<option value=${p.id}>${p.name}</option>`)}
|
||||
</select>
|
||||
<input
|
||||
type="password"
|
||||
class="api-input"
|
||||
placeholder=${
|
||||
this.selectedProvider === "openai"
|
||||
? "Enter your OpenAI API key"
|
||||
: this.selectedProvider === "gemini"
|
||||
? "Enter your Gemini API key"
|
||||
: "Enter your Anthropic API key"
|
||||
}
|
||||
.value=${this.apiKey || ""}
|
||||
@input=${this.handleInput}
|
||||
@keypress=${this.handleKeyPress}
|
||||
@paste=${this.handlePaste}
|
||||
@focus=${() => (this.errorMessage = "")}
|
||||
?disabled=${this.isLoading}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
tabindex="0"
|
||||
/>
|
||||
<input type="password" class="api-input" placeholder="LLM Provider API Key" .value=${this.llmApiKey} @input=${e => this.llmApiKey = e.target.value} ?disabled=${this.isLoading}>
|
||||
</div>
|
||||
|
||||
<button class="action-button" @click=${this.handleSubmit} ?disabled=${isButtonDisabled} tabindex="0">
|
||||
${this.isLoading ? "Validating..." : "Confirm"}
|
||||
</button>
|
||||
|
||||
<div class="or-text">or</div>
|
||||
|
||||
<button class="action-button" @click=${this.handleUsePicklesKey}>Use Pickle's API Key</button>
|
||||
<div class="provider-column">
|
||||
<div class="provider-label"></div>
|
||||
<select class="provider-select" .value=${this.sttProvider} @change=${e => this.sttProvider = e.target.value} ?disabled=${this.isLoading}>
|
||||
${this.providers.stt.map(p => html`<option value=${p.id}>${p.name}</option>`)}
|
||||
</select>
|
||||
<input type="password" class="api-input" placeholder="STT Provider API Key" .value=${this.sttApiKey} @input=${e => this.sttApiKey = e.target.value} ?disabled=${this.isLoading}>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="error-message">${this.errorMessage}</div>
|
||||
|
||||
<button class="action-button" @click=${this.handleSubmit} ?disabled=${isButtonDisabled}>
|
||||
${this.isLoading ? "Validating..." : "Confirm"}
|
||||
</button>
|
||||
<div class="or-text">or</div>
|
||||
<button class="action-button" @click=${this.handleUsePicklesKey}>Use Pickle's Key (Login)</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("apikey-header", ApiKeyHeader)
|
||||
|
@ -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(() => {});
|
||||
}
|
||||
|
||||
|
@ -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<object>} 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.<string, Provider>}
|
||||
*/
|
||||
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,
|
||||
}
|
||||
};
|
@ -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 ////////
|
||||
};
|
||||
}
|
||||
}
|
||||
|
324
src/common/services/modelStateService.js
Normal file
324
src/common/services/modelStateService.js
Normal file
@ -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;
|
@ -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,
|
||||
};
|
@ -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);
|
||||
|
@ -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' };
|
||||
|
@ -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;
|
||||
|
@ -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(/<noise>/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(/<noise>/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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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`
|
||||
// <div class="settings-container">
|
||||
// <div class="loading-state">
|
||||
// <div class="loading-spinner"></div>
|
||||
// <span>Loading...</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// `;
|
||||
// }
|
||||
|
||||
// const loggedIn = !!this.firebaseUser;
|
||||
|
||||
// return html`
|
||||
// <div class="settings-container">
|
||||
// <div class="header-section">
|
||||
// <div>
|
||||
// <h1 class="app-title">Pickle Glass</h1>
|
||||
// <div class="account-info">
|
||||
// ${this.firebaseUser
|
||||
// ? html`Account: ${this.firebaseUser.email || 'Logged In'}`
|
||||
// : this.apiKey && this.apiKey.length > 10
|
||||
// ? html`API Key: ${this.apiKey.substring(0, 6)}...${this.apiKey.substring(this.apiKey.length - 6)}`
|
||||
// : `Account: Not Logged In`
|
||||
// }
|
||||
// </div>
|
||||
// </div>
|
||||
// <div class="invisibility-icon ${this.isContentProtectionOn ? 'visible' : ''}" title="Invisibility is On">
|
||||
// <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
// <path d="M9.785 7.41787C8.7 7.41787 7.79 8.19371 7.55667 9.22621C7.0025 8.98704 6.495 9.05121 6.11 9.22037C5.87083 8.18204 4.96083 7.41787 3.88167 7.41787C2.61583 7.41787 1.58333 8.46204 1.58333 9.75121C1.58333 11.0404 2.61583 12.0845 3.88167 12.0845C5.08333 12.0845 6.06333 11.1395 6.15667 9.93787C6.355 9.79787 6.87417 9.53537 7.51 9.94954C7.615 11.1454 8.58333 12.0845 9.785 12.0845C11.0508 12.0845 12.0833 11.0404 12.0833 9.75121C12.0833 8.46204 11.0508 7.41787 9.785 7.41787ZM3.88167 11.4195C2.97167 11.4195 2.2425 10.6729 2.2425 9.75121C2.2425 8.82954 2.9775 8.08287 3.88167 8.08287C4.79167 8.08287 5.52083 8.82954 5.52083 9.75121C5.52083 10.6729 4.79167 11.4195 3.88167 11.4195ZM9.785 11.4195C8.875 11.4195 8.14583 10.6729 8.14583 9.75121C8.14583 8.82954 8.875 8.08287 9.785 8.08287C10.695 8.08287 11.43 8.82954 11.43 9.75121C11.43 10.6729 10.6892 11.4195 9.785 11.4195ZM12.6667 5.95954H1V6.83454H12.6667V5.95954ZM8.8925 1.36871C8.76417 1.08287 8.4375 0.931207 8.12833 1.03037L6.83333 1.46204L5.5325 1.03037L5.50333 1.02454C5.19417 0.93704 4.8675 1.10037 4.75083 1.39787L3.33333 5.08454H10.3333L8.91 1.39787L8.8925 1.36871Z" fill="white"/>
|
||||
// </svg>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div class="api-key-section">
|
||||
// <input
|
||||
// type="password"
|
||||
// id="api-key-input"
|
||||
// placeholder="Enter API Key"
|
||||
// .value=${this.apiKey || ''}
|
||||
// ?disabled=${loggedIn}
|
||||
// >
|
||||
// <button class="settings-button full-width" @click=${this.handleSaveApiKey} ?disabled=${loggedIn}>
|
||||
// Save API Key
|
||||
// </button>
|
||||
// </div>
|
||||
|
||||
// <div class="shortcuts-section">
|
||||
// ${this.getMainShortcuts().map(shortcut => html`
|
||||
// <div class="shortcut-item">
|
||||
// <span class="shortcut-name">${shortcut.name}</span>
|
||||
// <div class="shortcut-keys">
|
||||
// <span class="cmd-key">⌘</span>
|
||||
// <span class="shortcut-key">${shortcut.key}</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// `)}
|
||||
// </div>
|
||||
|
||||
// <!-- Preset Management Section -->
|
||||
// <div class="preset-section">
|
||||
// <div class="preset-header">
|
||||
// <span class="preset-title">
|
||||
// My Presets
|
||||
// <span class="preset-count">(${this.presets.filter(p => p.is_default === 0).length})</span>
|
||||
// </span>
|
||||
// <span class="preset-toggle" @click=${this.togglePresets}>
|
||||
// ${this.showPresets ? '▼' : '▶'}
|
||||
// </span>
|
||||
// </div>
|
||||
|
||||
// <div class="preset-list ${this.showPresets ? '' : 'hidden'}">
|
||||
// ${this.presets.filter(p => p.is_default === 0).length === 0 ? html`
|
||||
// <div class="no-presets-message">
|
||||
// No custom presets yet.<br>
|
||||
// <span class="web-link" @click=${this.handlePersonalize}>
|
||||
// Create your first preset
|
||||
// </span>
|
||||
// </div>
|
||||
// ` : this.presets.filter(p => p.is_default === 0).map(preset => html`
|
||||
// <div class="preset-item ${this.selectedPreset?.id === preset.id ? 'selected' : ''}"
|
||||
// @click=${() => this.handlePresetSelect(preset)}>
|
||||
// <span class="preset-name">${preset.title}</span>
|
||||
// ${this.selectedPreset?.id === preset.id ? html`<span class="preset-status">Selected</span>` : ''}
|
||||
// </div>
|
||||
// `)}
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div class="buttons-section">
|
||||
// <button class="settings-button full-width" @click=${this.handlePersonalize}>
|
||||
// <span>Personalize / Meeting Notes</span>
|
||||
// </button>
|
||||
|
||||
// <div class="move-buttons">
|
||||
// <button class="settings-button half-width" @click=${this.handleMoveLeft}>
|
||||
// <span>← Move</span>
|
||||
// </button>
|
||||
// <button class="settings-button half-width" @click=${this.handleMoveRight}>
|
||||
// <span>Move →</span>
|
||||
// </button>
|
||||
// </div>
|
||||
|
||||
// <button class="settings-button full-width" @click=${this.handleToggleInvisibility}>
|
||||
// <span>${this.isContentProtectionOn ? 'Disable Invisibility' : 'Enable Invisibility'}</span>
|
||||
// </button>
|
||||
|
||||
// <div class="bottom-buttons">
|
||||
// ${this.firebaseUser
|
||||
// ? html`
|
||||
// <button class="settings-button half-width danger" @click=${this.handleFirebaseLogout}>
|
||||
// <span>Logout</span>
|
||||
// </button>
|
||||
// `
|
||||
// : html`
|
||||
// <button class="settings-button half-width danger" @click=${this.handleClearApiKey}>
|
||||
// <span>Clear API Key</span>
|
||||
// </button>
|
||||
// `
|
||||
// }
|
||||
// <button class="settings-button half-width danger" @click=${this.handleQuit}>
|
||||
// <span>Quit</span>
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// `;
|
||||
// }
|
||||
//////// 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`
|
||||
<div class="api-key-section">
|
||||
${Object.entries(this.providerConfig)
|
||||
.filter(([id, config]) => !id.includes('-glass'))
|
||||
.map(([id, config]) => html`
|
||||
<div class="provider-key-group">
|
||||
<label for="key-input-${id}">${config.name} API Key</label>
|
||||
<input type="password" id="key-input-${id}"
|
||||
placeholder=${loggedIn ? "Using Pickle's Key" : `Enter ${config.name} API Key`}
|
||||
.value=${this.apiKeys[id] || ''}
|
||||
|
||||
>
|
||||
<div class="key-buttons">
|
||||
<button class="settings-button" @click=${() => this.handleSaveKey(id)} >Save</button>
|
||||
<button class="settings-button danger" @click=${() => this.handleClearKey(id)} }>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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`
|
||||
<div class="model-selection-section">
|
||||
<div class="model-select-group">
|
||||
<label>LLM Model: <strong>${getModelName('llm', this.selectedLlm) || 'Not Set'}</strong></label>
|
||||
<button class="settings-button full-width" @click=${() => this.toggleModelList('llm')} ?disabled=${this.saving || this.availableLlmModels.length === 0}>
|
||||
Change LLM Model
|
||||
</button>
|
||||
${this.isLlmListVisible ? html`
|
||||
<div class="model-list">
|
||||
${this.availableLlmModels.map(model => html`
|
||||
<div class="model-item ${this.selectedLlm === model.id ? 'selected' : ''}" @click=${() => this.selectModel('llm', model.id)}>
|
||||
${model.name}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="model-select-group">
|
||||
<label>STT Model: <strong>${getModelName('stt', this.selectedStt) || 'Not Set'}</strong></label>
|
||||
<button class="settings-button full-width" @click=${() => this.toggleModelList('stt')} ?disabled=${this.saving || this.availableSttModels.length === 0}>
|
||||
Change STT Model
|
||||
</button>
|
||||
${this.isSttListVisible ? html`
|
||||
<div class="model-list">
|
||||
${this.availableSttModels.map(model => html`
|
||||
<div class="model-item ${this.selectedStt === model.id ? 'selected' : ''}" @click=${() => this.selectModel('stt', model.id)}>
|
||||
${model.name}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<div class="settings-container">
|
||||
<div class="header-section">
|
||||
@ -719,9 +1118,7 @@ export class SettingsView extends LitElement {
|
||||
<div class="account-info">
|
||||
${this.firebaseUser
|
||||
? html`Account: ${this.firebaseUser.email || 'Logged In'}`
|
||||
: this.apiKey && this.apiKey.length > 10
|
||||
? html`API Key: ${this.apiKey.substring(0, 6)}...${this.apiKey.substring(this.apiKey.length - 6)}`
|
||||
: `Account: Not Logged In`
|
||||
: `Account: Not Logged In`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -732,19 +1129,9 @@ export class SettingsView extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-key-section">
|
||||
<input
|
||||
type="password"
|
||||
id="api-key-input"
|
||||
placeholder="Enter API Key"
|
||||
.value=${this.apiKey || ''}
|
||||
?disabled=${loggedIn}
|
||||
>
|
||||
<button class="settings-button full-width" @click=${this.handleSaveApiKey} ?disabled=${loggedIn}>
|
||||
Save API Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${apiKeyManagementHTML}
|
||||
${modelSelectionHTML}
|
||||
|
||||
<div class="shortcuts-section">
|
||||
${this.getMainShortcuts().map(shortcut => html`
|
||||
<div class="shortcut-item">
|
||||
@ -757,7 +1144,6 @@ export class SettingsView extends LitElement {
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<!-- Preset Management Section -->
|
||||
<div class="preset-section">
|
||||
<div class="preset-header">
|
||||
<span class="preset-title">
|
||||
@ -813,8 +1199,8 @@ export class SettingsView extends LitElement {
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<button class="settings-button half-width danger" @click=${this.handleClearApiKey}>
|
||||
<span>Clear API Key</span>
|
||||
<button class="settings-button half-width" @click=${this.handleUsePicklesKey}>
|
||||
<span>Login</span>
|
||||
</button>
|
||||
`
|
||||
}
|
||||
@ -826,6 +1212,7 @@ export class SettingsView extends LitElement {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
//////// after_modelStateService ////////
|
||||
}
|
||||
|
||||
customElements.define('settings-view', SettingsView);
|
24
src/index.js
24
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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user