Merge branch 'main' into main
This commit is contained in:
commit
8da1ed5791
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -31,4 +31,16 @@ jobs:
|
|||||||
|
|
||||||
- name: 🖥️ Build Electron app
|
- name: 🖥️ Build Electron app
|
||||||
# Run Electron build script from root directory
|
# 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 |
|
| Status | Issue | Description |
|
||||||
|--------|--------------------------------|---------------------------------------------------|
|
|--------|--------------------------------|---------------------------------------------------|
|
||||||
| 🚧 WIP | Code Refactoring | Refactoring the entire codebase for better maintainability. |
|
|
||||||
| 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows |
|
| 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows |
|
||||||
| 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers |
|
| 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers |
|
||||||
| 🚧 WIP | AEC Improvement | Transcription is not working occasionally |
|
| 🚧 WIP | AEC Improvement | Transcription is not working occasionally |
|
||||||
| 🚧 WIP | Firebase Data Storage Issue | Session & ask should be saved in firebase for signup users |
|
| 🚧 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 |
|
| 🚧 WIP | Liquid Glass | Liquid Glass UI for MacOS 26 |
|
||||||
|
|
||||||
### Changelog
|
### Changelog
|
||||||
|
@ -44,6 +44,8 @@ win:
|
|||||||
- target: portable
|
- target: portable
|
||||||
arch: x64
|
arch: x64
|
||||||
requestedExecutionLevel: asInvoker
|
requestedExecutionLevel: asInvoker
|
||||||
|
# Disable code signing to avoid symbolic link issues on Windows
|
||||||
|
signAndEditExecutable: false
|
||||||
|
|
||||||
# NSIS installer configuration for Windows
|
# NSIS installer configuration for Windows
|
||||||
nsis:
|
nsis:
|
||||||
|
308
package-lock.json
generated
308
package-lock.json
generated
@ -10,6 +10,9 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk": "^0.56.0",
|
||||||
|
|
||||||
"@google/genai": "^1.8.0",
|
"@google/genai": "^1.8.0",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
@ -51,6 +54,17 @@
|
|||||||
"electron-liquid-glass": "^1.0.1"
|
"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": {
|
"node_modules/@develar/schema-utils": {
|
||||||
"version": "2.6.5",
|
"version": "2.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||||
@ -875,9 +889,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
"version": "1.4.4",
|
||||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==",
|
||||||
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -885,9 +901,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -902,9 +920,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -919,9 +939,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -936,9 +958,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -953,9 +977,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -970,9 +996,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -987,9 +1015,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1004,9 +1034,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1021,9 +1053,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -1038,9 +1072,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1055,9 +1091,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -1072,9 +1110,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@ -1089,9 +1129,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@ -1106,9 +1148,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -1123,9 +1167,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@ -1140,9 +1186,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@ -1157,9 +1205,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1174,9 +1224,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1191,9 +1243,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1208,9 +1262,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1225,9 +1281,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1241,10 +1299,29 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.25.5",
|
"version": "0.25.6",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
|
||||||
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
|
"integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -1259,9 +1336,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -1276,9 +1355,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -1293,9 +1374,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
|
||||||
|
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -3172,9 +3255,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
"version": "7.1.4",
|
||||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
@ -5970,9 +6055,11 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
"version": "0.25.6",
|
||||||
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||||
|
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
||||||
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -5983,31 +6070,34 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.25.5",
|
|
||||||
"@esbuild/android-arm": "0.25.5",
|
"@esbuild/aix-ppc64": "0.25.6",
|
||||||
"@esbuild/android-arm64": "0.25.5",
|
"@esbuild/android-arm": "0.25.6",
|
||||||
"@esbuild/android-x64": "0.25.5",
|
"@esbuild/android-arm64": "0.25.6",
|
||||||
"@esbuild/darwin-arm64": "0.25.5",
|
"@esbuild/android-x64": "0.25.6",
|
||||||
"@esbuild/darwin-x64": "0.25.5",
|
"@esbuild/darwin-arm64": "0.25.6",
|
||||||
"@esbuild/freebsd-arm64": "0.25.5",
|
"@esbuild/darwin-x64": "0.25.6",
|
||||||
"@esbuild/freebsd-x64": "0.25.5",
|
"@esbuild/freebsd-arm64": "0.25.6",
|
||||||
"@esbuild/linux-arm": "0.25.5",
|
"@esbuild/freebsd-x64": "0.25.6",
|
||||||
"@esbuild/linux-arm64": "0.25.5",
|
"@esbuild/linux-arm": "0.25.6",
|
||||||
"@esbuild/linux-ia32": "0.25.5",
|
"@esbuild/linux-arm64": "0.25.6",
|
||||||
"@esbuild/linux-loong64": "0.25.5",
|
"@esbuild/linux-ia32": "0.25.6",
|
||||||
"@esbuild/linux-mips64el": "0.25.5",
|
"@esbuild/linux-loong64": "0.25.6",
|
||||||
"@esbuild/linux-ppc64": "0.25.5",
|
"@esbuild/linux-mips64el": "0.25.6",
|
||||||
"@esbuild/linux-riscv64": "0.25.5",
|
"@esbuild/linux-ppc64": "0.25.6",
|
||||||
"@esbuild/linux-s390x": "0.25.5",
|
"@esbuild/linux-riscv64": "0.25.6",
|
||||||
"@esbuild/linux-x64": "0.25.5",
|
"@esbuild/linux-s390x": "0.25.6",
|
||||||
"@esbuild/netbsd-arm64": "0.25.5",
|
"@esbuild/linux-x64": "0.25.6",
|
||||||
"@esbuild/netbsd-x64": "0.25.5",
|
"@esbuild/netbsd-arm64": "0.25.6",
|
||||||
"@esbuild/openbsd-arm64": "0.25.5",
|
"@esbuild/netbsd-x64": "0.25.6",
|
||||||
"@esbuild/openbsd-x64": "0.25.5",
|
"@esbuild/openbsd-arm64": "0.25.6",
|
||||||
"@esbuild/sunos-x64": "0.25.5",
|
"@esbuild/openbsd-x64": "0.25.6",
|
||||||
"@esbuild/win32-arm64": "0.25.5",
|
"@esbuild/openharmony-arm64": "0.25.6",
|
||||||
"@esbuild/win32-ia32": "0.25.5",
|
"@esbuild/sunos-x64": "0.25.6",
|
||||||
"@esbuild/win32-x64": "0.25.5"
|
"@esbuild/win32-arm64": "0.25.6",
|
||||||
|
"@esbuild/win32-ia32": "0.25.6",
|
||||||
|
"@esbuild/win32-x64": "0.25.6"
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
|
12
package.json
12
package.json
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "pickle-glass",
|
"name": "pickle-glass",
|
||||||
"productName": "Glass",
|
"productName": "Glass",
|
||||||
"version": "0.2.1",
|
|
||||||
|
"version": "0.2.2",
|
||||||
|
|
||||||
"description": "Cl*ely for Free",
|
"description": "Cl*ely for Free",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -9,12 +11,14 @@
|
|||||||
"start": "npm run build:renderer && electron-forge start",
|
"start": "npm run build:renderer && electron-forge start",
|
||||||
"package": "npm run build:renderer && electron-forge package",
|
"package": "npm run build:renderer && electron-forge package",
|
||||||
"make": "npm run build:renderer && electron-forge make",
|
"make": "npm run build:renderer && electron-forge make",
|
||||||
"build": "npm run build:renderer && electron-builder --config electron-builder.yml --publish never",
|
"build": "npm run build:all && electron-builder --config electron-builder.yml --publish never",
|
||||||
"build:win": "npm run build:renderer && electron-builder --win --x64 --publish never",
|
"build:win": "npm run build:all && electron-builder --win --x64 --publish never",
|
||||||
"publish": "npm run build:renderer && electron-builder --config electron-builder.yml --publish always",
|
"publish": "npm run build:all && electron-builder --config electron-builder.yml --publish always",
|
||||||
"lint": "eslint --ext .ts,.tsx,.js .",
|
"lint": "eslint --ext .ts,.tsx,.js .",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:renderer": "node build.js",
|
"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"
|
"watch:renderer": "node build.js --watch"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
36
pickleglass_web/package-lock.json
generated
36
pickleglass_web/package-lock.json
generated
@ -42,21 +42,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
"version": "1.4.4",
|
||||||
"integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==",
|
||||||
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/wasi-threads": "1.0.2",
|
|
||||||
|
"@emnapi/wasi-threads": "1.0.3",
|
||||||
|
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
"version": "1.4.4",
|
||||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==",
|
||||||
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@ -65,9 +71,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
|
"version": "1.0.3",
|
||||||
"integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==",
|
||||||
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@ -2667,9 +2675,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.179",
|
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
|
"version": "1.5.180",
|
||||||
"integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.180.tgz",
|
||||||
|
"integrity": "sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==",
|
||||||
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { html, css, LitElement } from "../assets/lit-core-2.7.4.min.js"
|
import { html, css, LitElement } from "../assets/lit-core-2.7.4.min.js"
|
||||||
|
|
||||||
export class ApiKeyHeader extends LitElement {
|
export class ApiKeyHeader extends LitElement {
|
||||||
|
//////// after_modelStateService ////////
|
||||||
static properties = {
|
static properties = {
|
||||||
apiKey: { type: String },
|
llmApiKey: { type: String },
|
||||||
|
sttApiKey: { type: String },
|
||||||
|
llmProvider: { type: String },
|
||||||
|
sttProvider: { type: String },
|
||||||
isLoading: { type: Boolean },
|
isLoading: { type: Boolean },
|
||||||
errorMessage: { type: String },
|
errorMessage: { type: String },
|
||||||
selectedProvider: { type: String },
|
providers: { type: Object, state: true },
|
||||||
}
|
}
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
@ -45,7 +50,7 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 285px;
|
width: 350px;
|
||||||
min-height: 260px;
|
min-height: 260px;
|
||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
@ -153,28 +158,22 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
outline: none;
|
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%;
|
width: 100%;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
text-align: center;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
|
||||||
margin-bottom: 6px;
|
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 {
|
.provider-select:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.15);
|
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);
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-select option {
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: white;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -240,14 +234,6 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
margin: 10px 0;
|
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 ]─────────────── */
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
||||||
:host-context(body.has-glass) .container,
|
:host-context(body.has-glass) .container,
|
||||||
@ -278,11 +264,16 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
super()
|
super()
|
||||||
this.dragState = null
|
this.dragState = null
|
||||||
this.wasJustDragged = false
|
this.wasJustDragged = false
|
||||||
this.apiKey = ""
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.errorMessage = ""
|
this.errorMessage = ""
|
||||||
this.validatedApiKey = null
|
//////// after_modelStateService ////////
|
||||||
this.selectedProvider = "openai"
|
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.handleMouseMove = this.handleMouseMove.bind(this)
|
||||||
this.handleMouseUp = this.handleMouseUp.bind(this)
|
this.handleMouseUp = this.handleMouseUp.bind(this)
|
||||||
@ -303,6 +294,35 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
this.requestUpdate()
|
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) {
|
async handleMouseDown(e) {
|
||||||
if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") {
|
if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") {
|
||||||
return
|
return
|
||||||
@ -409,144 +429,45 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////// after_modelStateService ////////
|
||||||
async handleSubmit() {
|
async handleSubmit() {
|
||||||
if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) {
|
console.log('[ApiKeyHeader] handleSubmit: Submitting API keys...');
|
||||||
console.log("Submit blocked:", {
|
if (this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim()) {
|
||||||
wasJustDragged: this.wasJustDragged,
|
this.errorMessage = "Please enter keys for both LLM and STT.";
|
||||||
isLoading: this.isLoading,
|
return;
|
||||||
hasApiKey: !!this.apiKey.trim(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Starting API key validation...")
|
this.isLoading = true;
|
||||||
this.isLoading = true
|
this.errorMessage = "";
|
||||||
this.errorMessage = ""
|
this.requestUpdate();
|
||||||
this.requestUpdate()
|
|
||||||
|
|
||||||
const apiKey = this.apiKey.trim()
|
const { ipcRenderer } = window.require('electron');
|
||||||
const isValid = false
|
|
||||||
try {
|
|
||||||
const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider)
|
|
||||||
|
|
||||||
if (isValid) {
|
console.log('[ApiKeyHeader] handleSubmit: Validating LLM key...');
|
||||||
console.log("API key valid - starting slide out animation")
|
const llmValidation = ipcRenderer.invoke('model:validate-key', { provider: this.llmProvider, key: this.llmApiKey.trim() });
|
||||||
this.startSlideOutAnimation()
|
const sttValidation = ipcRenderer.invoke('model:validate-key', { provider: this.sttProvider, key: this.sttApiKey.trim() });
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateApiKey(apiKey, provider = "openai") {
|
const [llmResult, sttResult] = await Promise.all([llmValidation, sttValidation]);
|
||||||
if (!apiKey || apiKey.length < 15) return false
|
|
||||||
|
|
||||||
if (provider === "openai") {
|
if (llmResult.success && sttResult.success) {
|
||||||
if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false
|
console.log('[ApiKeyHeader] handleSubmit: Both LLM and STT keys are valid.');
|
||||||
|
this.startSlideOutAnimation();
|
||||||
try {
|
} else {
|
||||||
console.log("Validating OpenAI API key...")
|
console.log('[ApiKeyHeader] handleSubmit: Validation failed.');
|
||||||
|
let errorParts = [];
|
||||||
const response = await fetch("https://api.openai.com/v1/models", {
|
if (!llmResult.success) errorParts.push(`LLM Key: ${llmResult.error || 'Invalid'}`);
|
||||||
headers: {
|
if (!sttResult.success) errorParts.push(`STT Key: ${sttResult.error || 'Invalid'}`);
|
||||||
"Content-Type": "application/json",
|
this.errorMessage = errorParts.join(' | ');
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
this.isLoading = false;
|
||||||
}
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
|
|
||||||
startSlideOutAnimation() {
|
startSlideOutAnimation() {
|
||||||
|
console.log('[ApiKeyHeader] startSlideOutAnimation: Starting slide out animation.');
|
||||||
this.classList.add("sliding-out")
|
this.classList.add("sliding-out")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,25 +488,18 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////// after_modelStateService ////////
|
||||||
handleAnimationEnd(e) {
|
handleAnimationEnd(e) {
|
||||||
if (e.target !== this) return
|
if (e.target !== this || !this.classList.contains('sliding-out')) return;
|
||||||
|
this.classList.remove("sliding-out");
|
||||||
if (this.classList.contains("sliding-out")) {
|
this.classList.add("hidden");
|
||||||
this.classList.remove("sliding-out")
|
window.require('electron').ipcRenderer.invoke('get-current-user').then(userState => {
|
||||||
this.classList.add("hidden")
|
console.log('[ApiKeyHeader] handleAnimationEnd: User state updated:', userState);
|
||||||
|
this.stateUpdateCallback?.(userState);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback()
|
super.connectedCallback()
|
||||||
@ -598,64 +512,40 @@ export class ApiKeyHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim()
|
const isButtonDisabled = this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim();
|
||||||
console.log("Rendering with provider:", this.selectedProvider)
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="container" @mousedown=${this.handleMouseDown}>
|
<div class="container" @mousedown=${this.handleMouseDown}>
|
||||||
<button class="close-button" @click=${this.handleClose} title="Close application">
|
<h1 class="title">Enter Your API Keys</h1>
|
||||||
<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="form-content">
|
<div class="providers-container">
|
||||||
<div class="error-message">${this.errorMessage}</div>
|
<div class="provider-column">
|
||||||
<div class="provider-label">Select AI Provider:</div>
|
<div class="provider-label"></div>
|
||||||
<select
|
<select class="provider-select" .value=${this.llmProvider} @change=${e => this.llmProvider = e.target.value} ?disabled=${this.isLoading}>
|
||||||
class="provider-select"
|
${this.providers.llm.map(p => html`<option value=${p.id}>${p.name}</option>`)}
|
||||||
.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>
|
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input type="password" class="api-input" placeholder="LLM Provider API Key" .value=${this.llmApiKey} @input=${e => this.llmApiKey = e.target.value} ?disabled=${this.isLoading}>
|
||||||
type="password"
|
</div>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="action-button" @click=${this.handleSubmit} ?disabled=${isButtonDisabled} tabindex="0">
|
<div class="provider-column">
|
||||||
${this.isLoading ? "Validating..." : "Confirm"}
|
<div class="provider-label"></div>
|
||||||
</button>
|
<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>`)}
|
||||||
<div class="or-text">or</div>
|
</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}>
|
||||||
<button class="action-button" @click=${this.handleUsePicklesKey}>Use Pickle's API Key</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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)
|
customElements.define("apikey-header", ApiKeyHeader)
|
||||||
|
@ -15,7 +15,11 @@ class HeaderTransitionManager {
|
|||||||
* @param {'apikey'|'main'|'permission'} type
|
* @param {'apikey'|'main'|'permission'} type
|
||||||
*/
|
*/
|
||||||
this.ensureHeader = (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 = '';
|
this.headerContainer.innerHTML = '';
|
||||||
|
|
||||||
@ -26,6 +30,7 @@ class HeaderTransitionManager {
|
|||||||
// Create new header element
|
// Create new header element
|
||||||
if (type === 'apikey') {
|
if (type === 'apikey') {
|
||||||
this.apiKeyHeader = document.createElement('apikey-header');
|
this.apiKeyHeader = document.createElement('apikey-header');
|
||||||
|
this.apiKeyHeader.stateUpdateCallback = (userState) => this.handleStateUpdate(userState);
|
||||||
this.headerContainer.appendChild(this.apiKeyHeader);
|
this.headerContainer.appendChild(this.apiKeyHeader);
|
||||||
} else if (type === 'permission') {
|
} else if (type === 'permission') {
|
||||||
this.permissionHeader = document.createElement('permission-setup');
|
this.permissionHeader = document.createElement('permission-setup');
|
||||||
@ -60,6 +65,11 @@ class HeaderTransitionManager {
|
|||||||
this.apiKeyHeader.isLoading = false;
|
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) {
|
//////// after_modelStateService ////////
|
||||||
// Firebase user: Check permissions, then show Main or Permission header
|
async handleStateUpdate(userState) {
|
||||||
const permissionResult = await this.checkPermissions();
|
const { ipcRenderer } = window.require('electron');
|
||||||
if (permissionResult.success) {
|
const isConfigured = await ipcRenderer.invoke('model:are-providers-configured');
|
||||||
this.transitionToMainHeader();
|
|
||||||
|
if (isConfigured) {
|
||||||
|
const { isLoggedIn } = userState;
|
||||||
|
if (isLoggedIn) {
|
||||||
|
const permissionResult = await this.checkPermissions();
|
||||||
|
if (permissionResult.success) {
|
||||||
|
this.transitionToMainHeader();
|
||||||
|
} else {
|
||||||
|
this.transitionToPermissionHeader();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.transitionToPermissionHeader();
|
this.transitionToMainHeader();
|
||||||
}
|
}
|
||||||
} else if (hasApiKey) {
|
|
||||||
// API Key only user: Skip permission check, go directly to Main
|
|
||||||
this.transitionToMainHeader();
|
|
||||||
} else {
|
} else {
|
||||||
// No auth at all
|
|
||||||
await this._resizeForApiKey();
|
await this._resizeForApiKey();
|
||||||
this.ensureHeader('apikey');
|
this.ensureHeader('apikey');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
async transitionToPermissionHeader() {
|
async transitionToPermissionHeader() {
|
||||||
// Prevent duplicate transitions
|
// Prevent duplicate transitions
|
||||||
@ -159,7 +173,7 @@ class HeaderTransitionManager {
|
|||||||
if (!window.require) return;
|
if (!window.require) return;
|
||||||
return window
|
return window
|
||||||
.require('electron')
|
.require('electron')
|
||||||
.ipcRenderer.invoke('resize-header-window', { width: 285, height: 300 })
|
.ipcRenderer.invoke('resize-header-window', { width: 350, height: 300 })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,68 +1,121 @@
|
|||||||
const providers = {
|
// factory.js
|
||||||
openai: require("./providers/openai"),
|
|
||||||
gemini: require("./providers/gemini"),
|
|
||||||
anthropic: require("./providers/anthropic"),
|
|
||||||
// 추가 provider는 여기에 등록
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an STT session based on provider
|
* @typedef {object} ModelOption
|
||||||
* @param {string} provider - Provider name ('openai', 'gemini', etc.)
|
* @property {string} id
|
||||||
* @param {object} opts - Configuration options (apiKey, language, callbacks, etc.)
|
* @property {string} name
|
||||||
* @returns {Promise<object>} STT session object with sendRealtimeInput and close methods
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
function createSTT(provider, opts) {
|
||||||
if (!providers[provider]?.createSTT) {
|
if (provider === 'openai-glass') provider = 'openai';
|
||||||
throw new Error(`STT not supported for provider: ${provider}`)
|
|
||||||
|
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) {
|
function createLLM(provider, opts) {
|
||||||
if (!providers[provider]?.createLLM) {
|
if (provider === 'openai-glass') provider = 'openai';
|
||||||
throw new Error(`LLM not supported for provider: ${provider}`)
|
|
||||||
|
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) {
|
function createStreamingLLM(provider, opts) {
|
||||||
if (!providers[provider]?.createStreamingLLM) {
|
if (provider === 'openai-glass') provider = 'openai';
|
||||||
throw new Error(`Streaming LLM not supported for provider: ${provider}`)
|
|
||||||
|
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() {
|
function getAvailableProviders() {
|
||||||
const sttProviders = []
|
const stt = [];
|
||||||
const llmProviders = []
|
const llm = [];
|
||||||
|
for (const [id, provider] of Object.entries(PROVIDERS)) {
|
||||||
for (const [name, provider] of Object.entries(providers)) {
|
if (provider.sttModels.length > 0) stt.push(id);
|
||||||
if (provider.createSTT) sttProviders.push(name)
|
if (provider.llmModels.length > 0) llm.push(id);
|
||||||
if (provider.createLLM) llmProviders.push(name)
|
|
||||||
}
|
}
|
||||||
|
return { stt: [...new Set(stt)], llm: [...new Set(llm)] };
|
||||||
return { stt: sttProviders, llm: llmProviders }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
PROVIDERS,
|
||||||
createSTT,
|
createSTT,
|
||||||
createLLM,
|
createLLM,
|
||||||
createStreamingLLM,
|
createStreamingLLM,
|
||||||
getAvailableProviders,
|
getAvailableProviders,
|
||||||
}
|
};
|
@ -36,7 +36,6 @@ class AuthService {
|
|||||||
this.currentUserId = 'default_user';
|
this.currentUserId = 'default_user';
|
||||||
this.currentUserMode = 'local'; // 'local' or 'firebase'
|
this.currentUserMode = 'local'; // 'local' or 'firebase'
|
||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
this.hasApiKey = false; // Add a flag for API key status
|
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,20 +52,18 @@ class AuthService {
|
|||||||
this.currentUser = user;
|
this.currentUser = user;
|
||||||
this.currentUserId = user.uid;
|
this.currentUserId = user.uid;
|
||||||
this.currentUserMode = 'firebase';
|
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
|
// Start background task to fetch and save virtual key
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const idToken = await user.getIdToken(true);
|
const idToken = await user.getIdToken(true);
|
||||||
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
|
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.`);
|
if (global.modelStateService) {
|
||||||
// Now update the key status, which will trigger another broadcast
|
global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
||||||
await this.updateApiKeyStatus();
|
}
|
||||||
|
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
|
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
|
||||||
}
|
}
|
||||||
@ -74,23 +71,20 @@ class AuthService {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// User signed OUT
|
// User signed OUT
|
||||||
console.log(`[AuthService] Firebase user signed out.`);
|
console.log(`[AuthService] No Firebase user.`);
|
||||||
if (previousUser) {
|
if (previousUser) {
|
||||||
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
|
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.currentUser = null;
|
||||||
this.currentUserId = 'default_user';
|
this.currentUserId = 'default_user';
|
||||||
this.currentUserMode = 'local';
|
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;
|
this.isInitialized = true;
|
||||||
console.log('[AuthService] Initialized and attached to Firebase Auth state.');
|
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() {
|
getCurrentUserId() {
|
||||||
return this.currentUserId;
|
return this.currentUserId;
|
||||||
@ -161,7 +138,9 @@ class AuthService {
|
|||||||
displayName: this.currentUser.displayName,
|
displayName: this.currentUser.displayName,
|
||||||
mode: 'firebase',
|
mode: 'firebase',
|
||||||
isLoggedIn: true,
|
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 {
|
return {
|
||||||
@ -170,7 +149,9 @@ class AuthService {
|
|||||||
displayName: 'Default User',
|
displayName: 'Default User',
|
||||||
mode: 'local',
|
mode: 'local',
|
||||||
isLoggedIn: false,
|
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.');
|
console.log('[WindowManager] Received request to log out.');
|
||||||
|
|
||||||
await authService.signOut();
|
await authService.signOut();
|
||||||
await setApiKey(null);
|
//////// before_modelStateService ////////
|
||||||
|
// await setApiKey(null);
|
||||||
|
|
||||||
windowPool.forEach(win => {
|
// windowPool.forEach(win => {
|
||||||
if (win && !win.isDestroyed()) {
|
// if (win && !win.isDestroyed()) {
|
||||||
win.webContents.send('api-key-removed');
|
// win.webContents.send('api-key-removed');
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
//////// before_modelStateService ////////
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('check-system-permissions', async () => {
|
ipcMain.handle('check-system-permissions', async () => {
|
||||||
@ -1112,95 +1114,150 @@ function setupIpcHandlers(movementManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function setApiKey(apiKey, provider = 'openai') {
|
//////// before_modelStateService ////////
|
||||||
console.log('[WindowManager] Persisting API key and provider to DB');
|
// async function setApiKey(apiKey, provider = 'openai') {
|
||||||
|
// console.log('[WindowManager] Persisting API key and provider to DB');
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider);
|
// await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider);
|
||||||
console.log('[WindowManager] API key and provider saved to SQLite');
|
// console.log('[WindowManager] API key and provider saved to SQLite');
|
||||||
|
|
||||||
// Notify authService that the key status may have changed
|
// // Notify authService that the key status may have changed
|
||||||
await authService.updateApiKeyStatus();
|
// await authService.updateApiKeyStatus();
|
||||||
|
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
console.error('[WindowManager] Failed to save API key to SQLite:', err);
|
// console.error('[WindowManager] Failed to save API key to SQLite:', err);
|
||||||
}
|
// }
|
||||||
|
|
||||||
windowPool.forEach(win => {
|
// windowPool.forEach(win => {
|
||||||
if (win && !win.isDestroyed()) {
|
// if (win && !win.isDestroyed()) {
|
||||||
const js = apiKey ? `
|
// const js = apiKey ? `
|
||||||
localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)});
|
// localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)});
|
||||||
localStorage.setItem('ai_provider', ${JSON.stringify(provider)});
|
// localStorage.setItem('ai_provider', ${JSON.stringify(provider)});
|
||||||
` : `
|
// ` : `
|
||||||
localStorage.removeItem('openai_api_key');
|
// localStorage.removeItem('openai_api_key');
|
||||||
localStorage.removeItem('ai_provider');
|
// localStorage.removeItem('ai_provider');
|
||||||
`;
|
// `;
|
||||||
win.webContents.executeJavaScript(js).catch(() => {});
|
// 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() {
|
async function getStoredApiKey() {
|
||||||
const userId = authService.getCurrentUserId();
|
if (global.modelStateService) {
|
||||||
if (!userId) return null;
|
const provider = await getStoredProvider();
|
||||||
const user = await userRepository.getById(userId);
|
return global.modelStateService.getApiKey(provider);
|
||||||
return user?.api_key || null;
|
}
|
||||||
|
return null; // Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getStoredProvider() {
|
async function getStoredProvider() {
|
||||||
const userId = authService.getCurrentUserId();
|
if (global.modelStateService) {
|
||||||
if (!userId) return 'openai';
|
return global.modelStateService.getCurrentProvider('llm');
|
||||||
const user = await userRepository.getById(userId);
|
}
|
||||||
return user?.provider || 'openai';
|
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() {
|
function setupApiKeyIPC() {
|
||||||
const { ipcMain } = require('electron');
|
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-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) => {
|
ipcMain.handle('api-key-validated', async (event, data) => {
|
||||||
console.log('[WindowManager] API key validation completed, saving...');
|
console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'.");
|
||||||
|
|
||||||
// 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 };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('remove-api-key', async () => {
|
ipcMain.handle('remove-api-key', async () => {
|
||||||
console.log('[WindowManager] API key removal requested');
|
console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'.");
|
||||||
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 };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-ai-provider', getStoredProvider);
|
console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.');
|
||||||
|
|
||||||
console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)');
|
|
||||||
}
|
}
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
|
|
||||||
function getDefaultKeybinds() {
|
function getDefaultKeybinds() {
|
||||||
@ -1222,7 +1279,7 @@ function getDefaultKeybinds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) {
|
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) {
|
||||||
console.log('Updating global shortcuts with:', keybinds);
|
// console.log('Updating global shortcuts with:', keybinds);
|
||||||
|
|
||||||
// Unregister all existing shortcuts
|
// Unregister all existing shortcuts
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
@ -1276,7 +1333,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
|
|||||||
movementManager.moveStep(direction);
|
movementManager.moveStep(direction);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`Registered global shortcut: ${key} -> ${direction}`);
|
// console.log(`Registered global shortcut: ${key} -> ${direction}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register ${key}:`, 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);
|
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
||||||
});
|
});
|
||||||
console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
|
// console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, 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) {
|
} catch (error) {
|
||||||
console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, 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) {
|
} catch (error) {
|
||||||
console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, 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');
|
console.log('Previous response shortcut triggered');
|
||||||
sendToRenderer('navigate-previous-response');
|
sendToRenderer('navigate-previous-response');
|
||||||
});
|
});
|
||||||
console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
// console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, 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');
|
console.log('Next response shortcut triggered');
|
||||||
sendToRenderer('navigate-next-response');
|
sendToRenderer('navigate-next-response');
|
||||||
});
|
});
|
||||||
console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
// console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, 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');
|
console.log('Scroll up shortcut triggered');
|
||||||
sendToRenderer('scroll-response-up');
|
sendToRenderer('scroll-response-up');
|
||||||
});
|
});
|
||||||
console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
// console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, 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');
|
console.log('Scroll down shortcut triggered');
|
||||||
sendToRenderer('scroll-response-down');
|
sendToRenderer('scroll-response-down');
|
||||||
});
|
});
|
||||||
console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
// console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
|
console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
|
||||||
}
|
}
|
||||||
@ -1495,8 +1552,11 @@ module.exports = {
|
|||||||
createWindows,
|
createWindows,
|
||||||
windowPool,
|
windowPool,
|
||||||
fixedYPosition,
|
fixedYPosition,
|
||||||
setApiKey,
|
//////// before_modelStateService ////////
|
||||||
|
// setApiKey,
|
||||||
|
//////// before_modelStateService ////////
|
||||||
getStoredApiKey,
|
getStoredApiKey,
|
||||||
getStoredProvider,
|
getStoredProvider,
|
||||||
|
getCurrentModelInfo,
|
||||||
captureScreenshot,
|
captureScreenshot,
|
||||||
};
|
};
|
@ -1,6 +1,6 @@
|
|||||||
const { ipcMain, BrowserWindow } = require('electron');
|
const { ipcMain, BrowserWindow } = require('electron');
|
||||||
const { createStreamingLLM } = require('../../common/ai/factory');
|
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 authService = require('../../common/services/authService');
|
||||||
const sessionRepository = require('../../common/repositories/session');
|
const sessionRepository = require('../../common/repositories/session');
|
||||||
const askRepository = require('./repositories');
|
const askRepository = require('./repositories');
|
||||||
@ -31,6 +31,12 @@ async function sendMessage(userPrompt) {
|
|||||||
try {
|
try {
|
||||||
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
|
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 screenshotResult = await captureScreenshot({ quality: 'medium' });
|
||||||
const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null;
|
const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null;
|
||||||
|
|
||||||
@ -39,10 +45,6 @@ async function sendMessage(userPrompt) {
|
|||||||
|
|
||||||
const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false);
|
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 = [
|
const messages = [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
@ -61,36 +63,13 @@ async function sendMessage(userPrompt) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = await getStoredProvider();
|
const streamingLLM = createStreamingLLM(modelInfo.provider, {
|
||||||
const { isLoggedIn } = authService.getCurrentUser();
|
apiKey: modelInfo.apiKey,
|
||||||
|
model: modelInfo.model,
|
||||||
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,
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
maxTokens: 2048,
|
maxTokens: 2048,
|
||||||
usePortkey: provider === 'openai' && isLoggedIn,
|
usePortkey: modelInfo.provider === 'openai-glass',
|
||||||
portkeyVirtualKey: isLoggedIn ? API_KEY : undefined
|
portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await streamingLLM.streamChat(messages);
|
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 () => {
|
ipcMain.handle('start-macos-audio', async () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
return { success: false, error: 'macOS audio capture only available on macOS' };
|
return { success: false, error: 'macOS audio capture only available on macOS' };
|
||||||
|
@ -15,6 +15,8 @@ let micMediaStream = null;
|
|||||||
let screenshotInterval = null;
|
let screenshotInterval = null;
|
||||||
let audioContext = null;
|
let audioContext = null;
|
||||||
let audioProcessor = null;
|
let audioProcessor = null;
|
||||||
|
let systemAudioContext = null;
|
||||||
|
let systemAudioProcessor = null;
|
||||||
let currentImageQuality = 'medium';
|
let currentImageQuality = 'medium';
|
||||||
let lastScreenshotBase64 = null;
|
let lastScreenshotBase64 = null;
|
||||||
|
|
||||||
@ -345,6 +347,7 @@ function setupMicProcessing(micStream) {
|
|||||||
micProcessor.connect(micAudioContext.destination);
|
micProcessor.connect(micAudioContext.destination);
|
||||||
|
|
||||||
audioProcessor = micProcessor;
|
audioProcessor = micProcessor;
|
||||||
|
return { context: micAudioContext, processor: micProcessor };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLinuxMicProcessing(micStream) {
|
function setupLinuxMicProcessing(micStream) {
|
||||||
@ -380,34 +383,40 @@ function setupLinuxMicProcessing(micStream) {
|
|||||||
audioProcessor = micProcessor;
|
audioProcessor = micProcessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupWindowsLoopbackProcessing() {
|
function setupSystemAudioProcessing(systemStream) {
|
||||||
// Setup audio processing for Windows loopback audio only
|
const systemAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||||
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
const systemSource = systemAudioContext.createMediaStreamSource(systemStream);
|
||||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
const systemProcessor = systemAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
||||||
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
|
||||||
|
|
||||||
let audioBuffer = [];
|
let audioBuffer = [];
|
||||||
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
||||||
|
|
||||||
audioProcessor.onaudioprocess = async e => {
|
systemProcessor.onaudioprocess = async e => {
|
||||||
const inputData = e.inputBuffer.getChannelData(0);
|
const inputData = e.inputBuffer.getChannelData(0);
|
||||||
|
if (!inputData || inputData.length === 0) return;
|
||||||
|
|
||||||
audioBuffer.push(...inputData);
|
audioBuffer.push(...inputData);
|
||||||
|
|
||||||
// Process audio in chunks
|
|
||||||
while (audioBuffer.length >= samplesPerChunk) {
|
while (audioBuffer.length >= samplesPerChunk) {
|
||||||
const chunk = audioBuffer.splice(0, samplesPerChunk);
|
const chunk = audioBuffer.splice(0, samplesPerChunk);
|
||||||
const pcmData16 = convertFloat32ToInt16(chunk);
|
const pcmData16 = convertFloat32ToInt16(chunk);
|
||||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
||||||
|
|
||||||
await ipcRenderer.invoke('send-audio-content', {
|
try {
|
||||||
data: base64Data,
|
await ipcRenderer.invoke('send-system-audio-content', {
|
||||||
mimeType: 'audio/pcm;rate=24000',
|
data: base64Data,
|
||||||
});
|
mimeType: 'audio/pcm;rate=24000',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send system audio:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
source.connect(audioProcessor);
|
systemSource.connect(systemProcessor);
|
||||||
audioProcessor.connect(audioContext.destination);
|
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');
|
console.log('macOS microphone capture started');
|
||||||
setupMicProcessing(micMediaStream);
|
const { context, processor } = setupMicProcessing(micMediaStream);
|
||||||
|
audioContext = context;
|
||||||
|
audioProcessor = processor;
|
||||||
} catch (micErr) {
|
} catch (micErr) {
|
||||||
console.warn('Failed to get microphone on macOS:', 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');
|
console.log('Linux screen capture started');
|
||||||
} else {
|
} 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');
|
const screenResult = await ipcRenderer.invoke('start-screen-capture');
|
||||||
if (!screenResult.success) {
|
if (!screenResult.success) {
|
||||||
throw new Error('Failed to start screen capture: ' + screenResult.error);
|
throw new Error('Failed to start screen capture: ' + screenResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
// Ensure STT sessions are initialized before starting audio capture
|
||||||
video: false, // We don't need video in renderer
|
const sessionActive = await ipcRenderer.invoke('is-session-active');
|
||||||
audio: {
|
if (!sessionActive) {
|
||||||
sampleRate: SAMPLE_RATE,
|
throw new Error('STT sessions not initialized - please wait for initialization to complete');
|
||||||
channelCount: 1,
|
}
|
||||||
echoCancellation: true,
|
|
||||||
noiseSuppression: true,
|
|
||||||
autoGainControl: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
// 2. Get system audio using native Electron loopback
|
||||||
setupWindowsLoopbackProcessing();
|
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
|
// Start capturing screenshots - check if manual mode
|
||||||
@ -626,21 +672,31 @@ function stopCapture() {
|
|||||||
screenshotInterval = null;
|
screenshotInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up microphone resources
|
||||||
if (audioProcessor) {
|
if (audioProcessor) {
|
||||||
audioProcessor.disconnect();
|
audioProcessor.disconnect();
|
||||||
audioProcessor = null;
|
audioProcessor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioContext) {
|
if (audioContext) {
|
||||||
audioContext.close();
|
audioContext.close();
|
||||||
audioContext = null;
|
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) {
|
if (mediaStream) {
|
||||||
mediaStream.getTracks().forEach(track => track.stop());
|
mediaStream.getTracks().forEach(track => track.stop());
|
||||||
mediaStream = null;
|
mediaStream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (micMediaStream) {
|
if (micMediaStream) {
|
||||||
micMediaStream.getTracks().forEach(t => t.stop());
|
micMediaStream.getTracks().forEach(t => t.stop());
|
||||||
micMediaStream = null;
|
micMediaStream = null;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BrowserWindow } = require('electron');
|
const { BrowserWindow } = require('electron');
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const { createSTT } = require('../../../common/ai/factory');
|
const { createSTT } = require('../../../common/ai/factory');
|
||||||
const { getStoredApiKey, getStoredProvider } = require('../../../electron/windowManager');
|
const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager');
|
||||||
|
|
||||||
const COMPLETION_DEBOUNCE_MS = 2000;
|
const COMPLETION_DEBOUNCE_MS = 2000;
|
||||||
|
|
||||||
@ -29,6 +29,8 @@ class SttService {
|
|||||||
// Callbacks
|
// Callbacks
|
||||||
this.onTranscriptionComplete = null;
|
this.onTranscriptionComplete = null;
|
||||||
this.onStatusUpdate = null;
|
this.onStatusUpdate = null;
|
||||||
|
|
||||||
|
this.modelInfo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCallbacks({ onTranscriptionComplete, onStatusUpdate }) {
|
setCallbacks({ onTranscriptionComplete, onStatusUpdate }) {
|
||||||
@ -36,32 +38,32 @@ class SttService {
|
|||||||
this.onStatusUpdate = onStatusUpdate;
|
this.onStatusUpdate = onStatusUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApiKey() {
|
// async getApiKey() {
|
||||||
const storedKey = await getStoredApiKey();
|
// const storedKey = await getStoredApiKey();
|
||||||
if (storedKey) {
|
// if (storedKey) {
|
||||||
console.log('[SttService] Using stored API key');
|
// console.log('[SttService] Using stored API key');
|
||||||
return storedKey;
|
// return storedKey;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const envKey = process.env.OPENAI_API_KEY;
|
// const envKey = process.env.OPENAI_API_KEY;
|
||||||
if (envKey) {
|
// if (envKey) {
|
||||||
console.log('[SttService] Using environment API key');
|
// console.log('[SttService] Using environment API key');
|
||||||
return envKey;
|
// return envKey;
|
||||||
}
|
// }
|
||||||
|
|
||||||
console.error('[SttService] No API key found in storage or environment');
|
// console.error('[SttService] No API key found in storage or environment');
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
async getAiProvider() {
|
// async getAiProvider() {
|
||||||
try {
|
// try {
|
||||||
const { ipcRenderer } = require('electron');
|
// const { ipcRenderer } = require('electron');
|
||||||
const provider = await ipcRenderer.invoke('get-ai-provider');
|
// const provider = await ipcRenderer.invoke('get-ai-provider');
|
||||||
return provider || 'openai';
|
// return provider || 'openai';
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
return getStoredProvider ? getStoredProvider() : 'openai';
|
// return getStoredProvider ? getStoredProvider() : 'openai';
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
sendToRenderer(channel, data) {
|
sendToRenderer(channel, data) {
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
@ -72,7 +74,7 @@ class SttService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flushMyCompletion() {
|
flushMyCompletion() {
|
||||||
if (!this.myCompletionBuffer.trim()) return;
|
if (!this.modelInfo || !this.myCompletionBuffer.trim()) return;
|
||||||
|
|
||||||
const finalText = this.myCompletionBuffer.trim();
|
const finalText = this.myCompletionBuffer.trim();
|
||||||
|
|
||||||
@ -100,7 +102,7 @@ class SttService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flushTheirCompletion() {
|
flushTheirCompletion() {
|
||||||
if (!this.theirCompletionBuffer.trim()) return;
|
if (!this.modelInfo || !this.theirCompletionBuffer.trim()) return;
|
||||||
|
|
||||||
const finalText = this.theirCompletionBuffer.trim();
|
const finalText = this.theirCompletionBuffer.trim();
|
||||||
|
|
||||||
@ -156,17 +158,30 @@ class SttService {
|
|||||||
async initializeSttSessions(language = 'en') {
|
async initializeSttSessions(language = 'en') {
|
||||||
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
|
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
|
||||||
|
|
||||||
const API_KEY = await this.getApiKey();
|
// const API_KEY = await this.getApiKey();
|
||||||
if (!API_KEY) {
|
// if (!API_KEY) {
|
||||||
throw new Error('No API key available');
|
// throw new Error('No API key available');
|
||||||
}
|
// }
|
||||||
|
// const provider = await this.getAiProvider();
|
||||||
|
|
||||||
const provider = await this.getAiProvider();
|
const modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
|
||||||
const isGemini = provider === 'gemini';
|
if (!modelInfo || !modelInfo.apiKey) {
|
||||||
console.log(`[SttService] Initializing STT for provider: ${provider}`);
|
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 => {
|
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 || '';
|
const text = message.serverContent?.inputTranscription?.text || '';
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
const finalUtteranceText = text.trim().replace(/<noise>/g, '').trim();
|
const finalUtteranceText = text.trim().replace(/<noise>/g, '').trim();
|
||||||
@ -207,7 +222,12 @@ class SttService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTheirMessage = message => {
|
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 || '';
|
const text = message.serverContent?.inputTranscription?.text || '';
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
const finalUtteranceText = text.trim().replace(/<noise>/g, '').trim();
|
const finalUtteranceText = text.trim().replace(/<noise>/g, '').trim();
|
||||||
@ -265,20 +285,20 @@ class SttService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Determine auth options for providers that support it
|
// Determine auth options for providers that support it
|
||||||
const authService = require('../../../common/services/authService');
|
// const authService = require('../../../common/services/authService');
|
||||||
const userState = authService.getCurrentUser();
|
// const userState = authService.getCurrentUser();
|
||||||
const loggedIn = userState.isLoggedIn;
|
// const loggedIn = userState.isLoggedIn;
|
||||||
|
|
||||||
const sttOptions = {
|
const sttOptions = {
|
||||||
apiKey: API_KEY,
|
apiKey: this.modelInfo.apiKey,
|
||||||
language: effectiveLanguage,
|
language: effectiveLanguage,
|
||||||
usePortkey: !isGemini && loggedIn, // Only OpenAI supports Portkey
|
usePortkey: this.modelInfo.provider === 'openai-glass',
|
||||||
portkeyVirtualKey: loggedIn ? API_KEY : undefined
|
portkeyVirtualKey: this.modelInfo.provider === 'openai-glass' ? this.modelInfo.apiKey : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
[this.mySttSession, this.theirSttSession] = await Promise.all([
|
[this.mySttSession, this.theirSttSession] = await Promise.all([
|
||||||
createSTT(provider, { ...sttOptions, callbacks: mySttConfig.callbacks }),
|
createSTT(this.modelInfo.provider, { ...sttOptions, callbacks: mySttConfig.callbacks }),
|
||||||
createSTT(provider, { ...sttOptions, callbacks: theirSttConfig.callbacks }),
|
createSTT(this.modelInfo.provider, { ...sttOptions, callbacks: theirSttConfig.callbacks }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('✅ Both STT sessions initialized successfully.');
|
console.log('✅ Both STT sessions initialized successfully.');
|
||||||
@ -286,20 +306,50 @@ class SttService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendAudioContent(data, mimeType) {
|
async sendAudioContent(data, mimeType) {
|
||||||
const provider = await this.getAiProvider();
|
// const provider = await this.getAiProvider();
|
||||||
const isGemini = provider === 'gemini';
|
// const isGemini = provider === 'gemini';
|
||||||
|
|
||||||
if (!this.mySttSession) {
|
if (!this.mySttSession) {
|
||||||
throw new Error('User STT session not active');
|
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' } }
|
? { audio: { data, mimeType: mimeType || 'audio/pcm;rate=24000' } }
|
||||||
: data;
|
: data;
|
||||||
|
|
||||||
await this.mySttSession.sendRealtimeInput(payload);
|
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() {
|
killExistingSystemAudioDump() {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
console.log('Checking for existing SystemAudioDump processes...');
|
console.log('Checking for existing SystemAudioDump processes...');
|
||||||
@ -362,8 +412,17 @@ class SttService {
|
|||||||
|
|
||||||
let audioBuffer = Buffer.alloc(0);
|
let audioBuffer = Buffer.alloc(0);
|
||||||
|
|
||||||
const provider = await this.getAiProvider();
|
// const provider = await this.getAiProvider();
|
||||||
const isGemini = provider === 'gemini';
|
// 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 => {
|
this.systemAudioProc.stdout.on('data', async data => {
|
||||||
audioBuffer = Buffer.concat([audioBuffer, data]);
|
audioBuffer = Buffer.concat([audioBuffer, data]);
|
||||||
@ -379,7 +438,7 @@ class SttService {
|
|||||||
|
|
||||||
if (this.theirSttSession) {
|
if (this.theirSttSession) {
|
||||||
try {
|
try {
|
||||||
const payload = isGemini
|
const payload = modelInfo.provider === 'gemini'
|
||||||
? { audio: { data: base64Data, mimeType: 'audio/pcm;rate=24000' } }
|
? { audio: { data: base64Data, mimeType: 'audio/pcm;rate=24000' } }
|
||||||
: base64Data;
|
: base64Data;
|
||||||
await this.theirSttSession.sendRealtimeInput(payload);
|
await this.theirSttSession.sendRealtimeInput(payload);
|
||||||
@ -472,6 +531,7 @@ class SttService {
|
|||||||
this.theirLastPartialText = '';
|
this.theirLastPartialText = '';
|
||||||
this.myCompletionBuffer = '';
|
this.myCompletionBuffer = '';
|
||||||
this.theirCompletionBuffer = '';
|
this.theirCompletionBuffer = '';
|
||||||
|
this.modelInfo = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,30 @@
|
|||||||
const sqliteClient = require('../../../../common/services/sqliteClient');
|
const sqliteClient = require('../../../../common/services/sqliteClient');
|
||||||
|
|
||||||
function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
|
function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
|
||||||
const db = sqliteClient.getDb();
|
return new Promise((resolve, reject) => {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
try {
|
||||||
const query = `
|
const db = sqliteClient.getDb();
|
||||||
INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at)
|
const now = Math.floor(Date.now() / 1000);
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
const query = `
|
||||||
ON CONFLICT(session_id) DO UPDATE SET
|
INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at)
|
||||||
generated_at=excluded.generated_at,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
model=excluded.model,
|
ON CONFLICT(session_id) DO UPDATE SET
|
||||||
text=excluded.text,
|
generated_at=excluded.generated_at,
|
||||||
tldr=excluded.tldr,
|
model=excluded.model,
|
||||||
bullet_json=excluded.bullet_json,
|
text=excluded.text,
|
||||||
action_json=excluded.action_json,
|
tldr=excluded.tldr,
|
||||||
updated_at=excluded.updated_at
|
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);
|
||||||
const result = db.prepare(query).run(sessionId, now, model, text, tldr, bullet_json, action_json, now);
|
resolve({ changes: result.changes });
|
||||||
return { changes: result.changes };
|
} catch (err) {
|
||||||
} catch (err) {
|
console.error('Error saving summary:', err);
|
||||||
console.error('Error saving summary:', err);
|
reject(err);
|
||||||
throw err;
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSummaryBySessionId(sessionId) {
|
function getSummaryBySessionId(sessionId) {
|
||||||
|
@ -4,7 +4,7 @@ const { createLLM } = require('../../../common/ai/factory');
|
|||||||
const authService = require('../../../common/services/authService');
|
const authService = require('../../../common/services/authService');
|
||||||
const sessionRepository = require('../../../common/repositories/session');
|
const sessionRepository = require('../../../common/repositories/session');
|
||||||
const summaryRepository = require('./repositories');
|
const summaryRepository = require('./repositories');
|
||||||
const { getStoredApiKey, getStoredProvider } = require('../../../electron/windowManager');
|
const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager');
|
||||||
|
|
||||||
class SummaryService {
|
class SummaryService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -27,22 +27,22 @@ class SummaryService {
|
|||||||
this.currentSessionId = sessionId;
|
this.currentSessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApiKey() {
|
// async getApiKey() {
|
||||||
const storedKey = await getStoredApiKey();
|
// const storedKey = await getStoredApiKey();
|
||||||
if (storedKey) {
|
// if (storedKey) {
|
||||||
console.log('[SummaryService] Using stored API key');
|
// console.log('[SummaryService] Using stored API key');
|
||||||
return storedKey;
|
// return storedKey;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const envKey = process.env.OPENAI_API_KEY;
|
// const envKey = process.env.OPENAI_API_KEY;
|
||||||
if (envKey) {
|
// if (envKey) {
|
||||||
console.log('[SummaryService] Using environment API key');
|
// console.log('[SummaryService] Using environment API key');
|
||||||
return envKey;
|
// return envKey;
|
||||||
}
|
// }
|
||||||
|
|
||||||
console.error('[SummaryService] No API key found in storage or environment');
|
// console.error('[SummaryService] No API key found in storage or environment');
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
sendToRenderer(channel, data) {
|
sendToRenderer(channel, data) {
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
@ -115,6 +115,12 @@ Please build upon this context while analyzing the new conversation segments.
|
|||||||
await sessionRepository.touch(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 = [
|
const messages = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
@ -148,23 +154,13 @@ Keep all points concise and build upon previous analysis if provided.`,
|
|||||||
|
|
||||||
console.log('🤖 Sending analysis request to AI...');
|
console.log('🤖 Sending analysis request to AI...');
|
||||||
|
|
||||||
const API_KEY = await this.getApiKey();
|
const llm = createLLM(modelInfo.provider, {
|
||||||
if (!API_KEY) {
|
apiKey: modelInfo.apiKey,
|
||||||
throw new Error('No API key available');
|
model: modelInfo.model,
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
maxTokens: 1024,
|
maxTokens: 1024,
|
||||||
usePortkey: provider === 'openai' && loggedIn,
|
usePortkey: modelInfo.provider === 'openai-glass',
|
||||||
portkeyVirtualKey: loggedIn ? API_KEY : undefined
|
portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const completion = await llm.chat(messages);
|
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);
|
const structuredData = this.parseResponseText(responseText, this.previousAnalysisResult);
|
||||||
|
|
||||||
if (this.currentSessionId) {
|
if (this.currentSessionId) {
|
||||||
summaryRepository.saveSummary({
|
try {
|
||||||
sessionId: this.currentSessionId,
|
summaryRepository.saveSummary({
|
||||||
text: responseText,
|
sessionId: this.currentSessionId,
|
||||||
tldr: structuredData.summary.join('\n'),
|
text: responseText,
|
||||||
bullet_json: JSON.stringify(structuredData.topic.bullets),
|
tldr: structuredData.summary.join('\n'),
|
||||||
action_json: JSON.stringify(structuredData.actions),
|
bullet_json: JSON.stringify(structuredData.topic.bullets),
|
||||||
model: 'gpt-4.1'
|
action_json: JSON.stringify(structuredData.actions),
|
||||||
}).catch(err => console.error('[DB] Failed to save summary:', err));
|
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,
|
conversationLength: conversationTexts.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 히스토리 크기 제한 (최근 10개만 유지)
|
|
||||||
if (this.analysisHistory.length > 10) {
|
if (this.analysisHistory.length > 10) {
|
||||||
this.analysisHistory.shift();
|
this.analysisHistory.shift();
|
||||||
}
|
}
|
||||||
|
@ -375,6 +375,43 @@ export class SettingsView extends LitElement {
|
|||||||
display: none;
|
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 ]─────────────── */
|
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
||||||
:host-context(body.has-glass) {
|
:host-context(body.has-glass) {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
@ -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 = {
|
static properties = {
|
||||||
firebaseUser: { type: Object, state: true },
|
firebaseUser: { type: Object, state: true },
|
||||||
apiKey: { type: String, state: true },
|
|
||||||
isLoading: { type: Boolean, state: true },
|
isLoading: { type: Boolean, state: true },
|
||||||
isContentProtectionOn: { 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 },
|
presets: { type: Array, state: true },
|
||||||
selectedPreset: { type: Object, state: true },
|
selectedPreset: { type: Object, state: true },
|
||||||
showPresets: { type: Boolean, state: true },
|
showPresets: { type: Boolean, state: true },
|
||||||
saving: { type: Boolean, state: true },
|
|
||||||
};
|
};
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
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.firebaseUser = null;
|
||||||
this.apiKey = null;
|
this.apiKeys = { openai: '', gemini: '', anthropic: '' };
|
||||||
this.isLoading = false;
|
this.providerConfig = {};
|
||||||
|
this.isLoading = true;
|
||||||
this.isContentProtectionOn = 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.presets = [];
|
||||||
this.selectedPreset = null;
|
this.selectedPreset = null;
|
||||||
this.showPresets = false;
|
this.showPresets = false;
|
||||||
this.saving = false;
|
this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this)
|
||||||
this.loadInitialData();
|
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() {
|
async loadInitialData() {
|
||||||
if (!window.require) return;
|
if (!window.require) return;
|
||||||
|
this.isLoading = true;
|
||||||
|
const { ipcRenderer } = window.require('electron');
|
||||||
try {
|
try {
|
||||||
this.isLoading = true;
|
const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection] = await Promise.all([
|
||||||
const { ipcRenderer } = window.require('electron');
|
ipcRenderer.invoke('get-current-user'),
|
||||||
|
ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드
|
||||||
// Load all data in parallel
|
ipcRenderer.invoke('model:get-all-keys'),
|
||||||
const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([
|
ipcRenderer.invoke('model:get-available-models', { type: 'llm' }),
|
||||||
ipcRenderer.invoke('settings:getSettings'),
|
ipcRenderer.invoke('model:get-available-models', { type: 'stt' }),
|
||||||
|
ipcRenderer.invoke('model:get-selected-models'),
|
||||||
ipcRenderer.invoke('settings:getPresets'),
|
ipcRenderer.invoke('settings:getPresets'),
|
||||||
ipcRenderer.invoke('get-stored-api-key'),
|
ipcRenderer.invoke('get-content-protection-status')
|
||||||
ipcRenderer.invoke('get-content-protection-status'),
|
|
||||||
ipcRenderer.invoke('get-current-user')
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
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.presets = presets || [];
|
||||||
this.apiKey = apiKey;
|
|
||||||
this.isContentProtectionOn = contentProtection;
|
this.isContentProtectionOn = contentProtection;
|
||||||
|
|
||||||
// Set first user preset as selected
|
|
||||||
if (this.presets.length > 0) {
|
if (this.presets.length > 0) {
|
||||||
const firstUserPreset = this.presets.find(p => p.is_default === 0);
|
const firstUserPreset = this.presets.find(p => p.is_default === 0);
|
||||||
if (firstUserPreset) {
|
if (firstUserPreset) this.selectedPreset = firstUserPreset;
|
||||||
this.selectedPreset = firstUserPreset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userState && userState.isLoggedIn) {
|
|
||||||
this.firebaseUser = userState.user;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading initial data:', error);
|
console.error('Error loading initial settings data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
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() {
|
connectedCallback() {
|
||||||
super.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() {
|
render() {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return html`
|
return html`
|
||||||
@ -711,6 +1048,68 @@ export class SettingsView extends LitElement {
|
|||||||
|
|
||||||
const loggedIn = !!this.firebaseUser;
|
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`
|
return html`
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
@ -719,9 +1118,7 @@ export class SettingsView extends LitElement {
|
|||||||
<div class="account-info">
|
<div class="account-info">
|
||||||
${this.firebaseUser
|
${this.firebaseUser
|
||||||
? html`Account: ${this.firebaseUser.email || 'Logged In'}`
|
? html`Account: ${this.firebaseUser.email || 'Logged In'}`
|
||||||
: this.apiKey && this.apiKey.length > 10
|
: `Account: Not Logged In`
|
||||||
? html`API Key: ${this.apiKey.substring(0, 6)}...${this.apiKey.substring(this.apiKey.length - 6)}`
|
|
||||||
: `Account: Not Logged In`
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -732,18 +1129,8 @@ export class SettingsView extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="api-key-section">
|
${apiKeyManagementHTML}
|
||||||
<input
|
${modelSelectionHTML}
|
||||||
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">
|
<div class="shortcuts-section">
|
||||||
${this.getMainShortcuts().map(shortcut => html`
|
${this.getMainShortcuts().map(shortcut => html`
|
||||||
@ -757,7 +1144,6 @@ export class SettingsView extends LitElement {
|
|||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Preset Management Section -->
|
|
||||||
<div class="preset-section">
|
<div class="preset-section">
|
||||||
<div class="preset-header">
|
<div class="preset-header">
|
||||||
<span class="preset-title">
|
<span class="preset-title">
|
||||||
@ -813,8 +1199,8 @@ export class SettingsView extends LitElement {
|
|||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<button class="settings-button half-width danger" @click=${this.handleClearApiKey}>
|
<button class="settings-button half-width" @click=${this.handleUsePicklesKey}>
|
||||||
<span>Clear API Key</span>
|
<span>Login</span>
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -826,6 +1212,7 @@ export class SettingsView extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
//////// after_modelStateService ////////
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('settings-view', SettingsView);
|
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);
|
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 { createWindows } = require('./electron/windowManager.js');
|
||||||
const ListenService = require('./features/listen/listenService');
|
const ListenService = require('./features/listen/listenService');
|
||||||
const { initializeFirebase } = require('./common/services/firebaseClient');
|
const { initializeFirebase } = require('./common/services/firebaseClient');
|
||||||
@ -25,6 +25,7 @@ const { EventEmitter } = require('events');
|
|||||||
const askService = require('./features/ask/askService');
|
const askService = require('./features/ask/askService');
|
||||||
const settingsService = require('./features/settings/settingsService');
|
const settingsService = require('./features/settings/settingsService');
|
||||||
const sessionRepository = require('./common/repositories/session');
|
const sessionRepository = require('./common/repositories/session');
|
||||||
|
const ModelStateService = require('./common/services/modelStateService');
|
||||||
|
|
||||||
const eventBridge = new EventEmitter();
|
const eventBridge = new EventEmitter();
|
||||||
let WEB_PORT = 3000;
|
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
|
// Make listenService globally accessible so other modules (e.g., windowManager, askService) can reuse the same instance
|
||||||
global.listenService = listenService;
|
global.listenService = listenService;
|
||||||
|
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
const modelStateService = new ModelStateService(authService);
|
||||||
|
global.modelStateService = modelStateService;
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
// Native deep link handling - cross-platform compatible
|
// Native deep link handling - cross-platform compatible
|
||||||
let pendingDeepLinkUrl = null;
|
let pendingDeepLinkUrl = null;
|
||||||
|
|
||||||
@ -162,6 +168,17 @@ setupProtocolHandling();
|
|||||||
|
|
||||||
app.whenReady().then(async () => {
|
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
|
// Initialize core services
|
||||||
initializeFirebase();
|
initializeFirebase();
|
||||||
|
|
||||||
@ -173,6 +190,11 @@ app.whenReady().then(async () => {
|
|||||||
sessionRepository.endAllActiveSessions();
|
sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
authService.initialize();
|
authService.initialize();
|
||||||
|
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
modelStateService.initialize();
|
||||||
|
//////// after_modelStateService ////////
|
||||||
|
|
||||||
listenService.setupIpcHandlers();
|
listenService.setupIpcHandlers();
|
||||||
askService.initialize();
|
askService.initialize();
|
||||||
settingsService.initialize();
|
settingsService.initialize();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user