add modelStateService for provider, model, apikey selection
This commit is contained in:
parent
516b1ae61e
commit
649c3b5c31
@ -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);
|
||||||
|
@ -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 => {
|
||||||
@ -156,17 +158,25 @@ 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.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 +217,7 @@ class SttService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTheirMessage = message => {
|
const handleTheirMessage = message => {
|
||||||
if (isGemini) {
|
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 +275,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,14 +296,23 @@ 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;
|
||||||
|
|
||||||
@ -362,8 +381,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 +407,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 +500,7 @@ class SttService {
|
|||||||
this.theirLastPartialText = '';
|
this.theirLastPartialText = '';
|
||||||
this.myCompletionBuffer = '';
|
this.myCompletionBuffer = '';
|
||||||
this.theirCompletionBuffer = '';
|
this.theirCompletionBuffer = '';
|
||||||
|
this.modelInfo = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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('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);
|
||||||
@ -180,7 +176,7 @@ Keep all points concise and build upon previous analysis if provided.`,
|
|||||||
tldr: structuredData.summary.join('\n'),
|
tldr: structuredData.summary.join('\n'),
|
||||||
bullet_json: JSON.stringify(structuredData.topic.bullets),
|
bullet_json: JSON.stringify(structuredData.topic.bullets),
|
||||||
action_json: JSON.stringify(structuredData.actions),
|
action_json: JSON.stringify(structuredData.actions),
|
||||||
model: 'gpt-4.1'
|
model: modelInfo.model
|
||||||
}).catch(err => console.error('[DB] Failed to save summary:', err));
|
}).catch(err => console.error('[DB] Failed to save summary:', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +188,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);
|
@ -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;
|
||||||
|
|
||||||
@ -173,6 +179,9 @@ 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