From 17b10b1ad0bc5d3bff34dcc48b4ccc84cdae49bd Mon Sep 17 00:00:00 2001 From: Surya Date: Mon, 7 Jul 2025 16:23:30 +0530 Subject: [PATCH] Feature:Anthropic AI Integration --- package.json | 1 + src/app/ApiKeyHeader.js | 609 ++++++++++++++------------- src/common/ai/factory.js | 37 +- src/common/ai/providers/anthropic.js | 280 ++++++++++++ src/electron/windowManager.js | 47 ++- 5 files changed, 656 insertions(+), 318 deletions(-) create mode 100644 src/common/ai/providers/anthropic.js diff --git a/package.json b/package.json index 3a6f218..2bdaca8 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "license": "GPL-3.0", "dependencies": { + "@anthropic-ai/sdk": "^0.56.0", "@google/genai": "^1.8.0", "@google/generative-ai": "^0.24.1", "axios": "^1.10.0", diff --git a/src/app/ApiKeyHeader.js b/src/app/ApiKeyHeader.js index 39598bd..25f1571 100644 --- a/src/app/ApiKeyHeader.js +++ b/src/app/ApiKeyHeader.js @@ -1,14 +1,14 @@ -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 { - static properties = { - apiKey: { type: String }, - isLoading: { type: Boolean }, - errorMessage: { type: String }, - selectedProvider: { type: String }, - }; + static properties = { + apiKey: { type: String }, + isLoading: { type: Boolean }, + errorMessage: { type: String }, + selectedProvider: { type: String }, + } - static styles = css` + static styles = css` :host { display: block; transform: translate3d(0, 0, 0); @@ -272,304 +272,336 @@ export class ApiKeyHeader extends LitElement { :host-context(body.has-glass) .close-button:hover { background: transparent !important; } - `; + ` - constructor() { - super(); - this.dragState = null; - this.wasJustDragged = false; - this.apiKey = ''; - this.isLoading = false; - this.errorMessage = ''; - this.validatedApiKey = null; - this.selectedProvider = 'openai'; + constructor() { + super() + this.dragState = null + this.wasJustDragged = false + this.apiKey = "" + this.isLoading = false + this.errorMessage = "" + this.validatedApiKey = null + this.selectedProvider = "openai" - this.handleMouseMove = this.handleMouseMove.bind(this); - this.handleMouseUp = this.handleMouseUp.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleInput = this.handleInput.bind(this); - this.handleAnimationEnd = this.handleAnimationEnd.bind(this); - this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this); - this.handleProviderChange = this.handleProviderChange.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this) + this.handleMouseUp = this.handleMouseUp.bind(this) + this.handleKeyPress = this.handleKeyPress.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + this.handleInput = this.handleInput.bind(this) + this.handleAnimationEnd = this.handleAnimationEnd.bind(this) + this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this) + this.handleProviderChange = this.handleProviderChange.bind(this) + } + + reset() { + this.apiKey = "" + this.isLoading = false + this.errorMessage = "" + this.validatedApiKey = null + this.selectedProvider = "openai" + this.requestUpdate() + } + + async handleMouseDown(e) { + if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") { + return } - reset() { - this.apiKey = ''; - this.isLoading = false; - this.errorMessage = ''; - this.validatedApiKey = null; - this.selectedProvider = 'openai'; - this.requestUpdate(); + e.preventDefault() + + const { ipcRenderer } = window.require("electron") + const initialPosition = await ipcRenderer.invoke("get-header-position") + + this.dragState = { + initialMouseX: e.screenX, + initialMouseY: e.screenY, + initialWindowX: initialPosition.x, + initialWindowY: initialPosition.y, + moved: false, } - async handleMouseDown(e) { - if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') { - return; + window.addEventListener("mousemove", this.handleMouseMove) + window.addEventListener("mouseup", this.handleMouseUp, { once: true }) + } + + handleMouseMove(e) { + if (!this.dragState) return + + const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX) + const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY) + + if (deltaX > 3 || deltaY > 3) { + this.dragState.moved = true + } + + const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX) + const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY) + + const { ipcRenderer } = window.require("electron") + ipcRenderer.invoke("move-header-to", newWindowX, newWindowY) + } + + handleMouseUp(e) { + if (!this.dragState) return + + const wasDragged = this.dragState.moved + + window.removeEventListener("mousemove", this.handleMouseMove) + this.dragState = null + + if (wasDragged) { + this.wasJustDragged = true + setTimeout(() => { + this.wasJustDragged = false + }, 200) + } + } + + handleInput(e) { + this.apiKey = e.target.value + this.errorMessage = "" + console.log("Input changed:", this.apiKey?.length || 0, "chars") + + this.requestUpdate() + this.updateComplete.then(() => { + const inputField = this.shadowRoot?.querySelector(".apikey-input") + if (inputField && this.isInputFocused) { + inputField.focus() + } + }) + } + + handleProviderChange(e) { + this.selectedProvider = e.target.value + this.errorMessage = "" + console.log("Provider changed to:", this.selectedProvider) + this.requestUpdate() + } + + handlePaste(e) { + e.preventDefault() + this.errorMessage = "" + const clipboardText = (e.clipboardData || window.clipboardData).getData("text") + console.log("Paste event detected:", clipboardText?.substring(0, 10) + "...") + + if (clipboardText) { + this.apiKey = clipboardText.trim() + + const inputElement = e.target + inputElement.value = this.apiKey + } + + this.requestUpdate() + this.updateComplete.then(() => { + const inputField = this.shadowRoot?.querySelector(".apikey-input") + if (inputField) { + inputField.focus() + inputField.setSelectionRange(inputField.value.length, inputField.value.length) + } + }) + } + + handleKeyPress(e) { + if (e.key === "Enter") { + e.preventDefault() + this.handleSubmit() + } + } + + async handleSubmit() { + if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) { + console.log("Submit blocked:", { + wasJustDragged: this.wasJustDragged, + isLoading: this.isLoading, + hasApiKey: !!this.apiKey.trim(), + }) + return + } + + console.log("Starting API key validation...") + this.isLoading = true + this.errorMessage = "" + this.requestUpdate() + + const apiKey = this.apiKey.trim() + const isValid = false + try { + const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider) + + if (isValid) { + console.log("API key valid - starting slide out animation") + this.startSlideOutAnimation() + this.validatedApiKey = this.apiKey.trim() + this.validatedProvider = this.selectedProvider + } else { + this.errorMessage = "Invalid API key - please check and try again" + console.log("API key validation failed") + } + } catch (error) { + console.error("API key validation error:", error) + this.errorMessage = "Validation error - please try again" + } finally { + this.isLoading = false + this.requestUpdate() + } + } + + async validateApiKey(apiKey, provider = "openai") { + if (!apiKey || apiKey.length < 15) return false + + if (provider === "openai") { + if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false + + try { + console.log("Validating OpenAI API key...") + + const response = await fetch("https://api.openai.com/v1/models", { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + }) + + if (response.ok) { + const data = await response.json() + + const hasGPTModels = data.data && data.data.some((m) => m.id.startsWith("gpt-")) + if (hasGPTModels) { + console.log("OpenAI API key validation successful") + return true + } else { + console.log("API key valid but no GPT models available") + return false + } + } else { + const errorData = await response.json().catch(() => ({})) + console.log("API key validation failed:", response.status, errorData.error?.message || "Unknown error") + return false + } + } catch (error) { + console.error("API key validation network error:", error) + return apiKey.length >= 20 // Fallback for network issues + } + } else if (provider === "gemini") { + // Gemini API keys typically start with 'AIza' + if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false + + try { + console.log("Validating Gemini API key...") + + // Test the API key with a simple models list request + const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`) + + if (response.ok) { + const data = await response.json() + if (data.models && data.models.length > 0) { + console.log("Gemini API key validation successful") + return true + } } - e.preventDefault(); + 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 - const { ipcRenderer } = window.require('electron'); - const initialPosition = await ipcRenderer.invoke('get-header-position'); + try { + console.log("Validating Anthropic API key...") - this.dragState = { - initialMouseX: e.screenX, - initialMouseY: e.screenY, - initialWindowX: initialPosition.x, - initialWindowY: initialPosition.y, - moved: false, - }; + // 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" }], + }), + }) - window.addEventListener('mousemove', this.handleMouseMove); - window.addEventListener('mouseup', this.handleMouseUp, { once: true }); - } - - handleMouseMove(e) { - if (!this.dragState) return; - - const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX); - const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY); - - if (deltaX > 3 || deltaY > 3) { - this.dragState.moved = true; + 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 } - const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX); - const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY); - - const { ipcRenderer } = window.require('electron'); - ipcRenderer.invoke('move-header-to', newWindowX, newWindowY); + 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 + } } - handleMouseUp(e) { - if (!this.dragState) return; + return false + } - const wasDragged = this.dragState.moved; + startSlideOutAnimation() { + this.classList.add("sliding-out") + } - window.removeEventListener('mousemove', this.handleMouseMove); - this.dragState = null; + handleUsePicklesKey(e) { + e.preventDefault() + if (this.wasJustDragged) return - if (wasDragged) { - this.wasJustDragged = true; - setTimeout(() => { - this.wasJustDragged = false; - }, 200); - } + console.log("Requesting Firebase authentication from main process...") + if (window.require) { + window.require("electron").ipcRenderer.invoke("start-firebase-auth") } + } - handleInput(e) { - this.apiKey = e.target.value; - this.errorMessage = ''; - console.log('Input changed:', this.apiKey?.length || 0, 'chars'); - - this.requestUpdate(); - this.updateComplete.then(() => { - const inputField = this.shadowRoot?.querySelector('.apikey-input'); - if (inputField && this.isInputFocused) { - inputField.focus(); - } - }); + handleClose() { + console.log("Close button clicked") + if (window.require) { + window.require("electron").ipcRenderer.invoke("quit-application") } + } - handleProviderChange(e) { - this.selectedProvider = e.target.value; - this.errorMessage = ''; - console.log('Provider changed to:', this.selectedProvider); - this.requestUpdate(); - } + handleAnimationEnd(e) { + if (e.target !== this) return - handlePaste(e) { - e.preventDefault(); - this.errorMessage = ''; - const clipboardText = (e.clipboardData || window.clipboardData).getData('text'); - console.log('Paste event detected:', clipboardText?.substring(0, 10) + '...'); + if (this.classList.contains("sliding-out")) { + this.classList.remove("sliding-out") + this.classList.add("hidden") - if (clipboardText) { - this.apiKey = clipboardText.trim(); - - const inputElement = e.target; - inputElement.value = this.apiKey; - } - - this.requestUpdate(); - this.updateComplete.then(() => { - const inputField = this.shadowRoot?.querySelector('.apikey-input'); - if (inputField) { - inputField.focus(); - inputField.setSelectionRange(inputField.value.length, inputField.value.length); - } - }); - } - - handleKeyPress(e) { - if (e.key === 'Enter') { - e.preventDefault(); - this.handleSubmit(); - } - } - - async handleSubmit() { - if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) { - console.log('Submit blocked:', { - wasJustDragged: this.wasJustDragged, - isLoading: this.isLoading, - hasApiKey: !!this.apiKey.trim(), - }); - return; - } - - console.log('Starting API key validation...'); - this.isLoading = true; - this.errorMessage = ''; - this.requestUpdate(); - - const apiKey = this.apiKey.trim(); - let isValid = false; - try { - const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider); - - if (isValid) { - console.log('API key valid - starting slide out animation'); - this.startSlideOutAnimation(); - this.validatedApiKey = this.apiKey.trim(); - this.validatedProvider = this.selectedProvider; - } else { - this.errorMessage = 'Invalid API key - please check and try again'; - console.log('API key validation failed'); - } - } catch (error) { - console.error('API key validation error:', error); - this.errorMessage = 'Validation error - please try again'; - } finally { - this.isLoading = false; - this.requestUpdate(); - } - } - - async validateApiKey(apiKey, provider = 'openai') { - if (!apiKey || apiKey.length < 15) return false; - - if (provider === 'openai') { - if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false; - - try { - console.log('Validating OpenAI API key...'); - - const response = await fetch('https://api.openai.com/v1/models', { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - - const hasGPTModels = data.data && data.data.some(m => m.id.startsWith('gpt-')); - if (hasGPTModels) { - console.log('OpenAI API key validation successful'); - return true; - } else { - console.log('API key valid but no GPT models available'); - return false; - } - } else { - const errorData = await response.json().catch(() => ({})); - console.log('API key validation failed:', response.status, errorData.error?.message || 'Unknown error'); - return false; - } - } catch (error) { - console.error('API key validation network error:', error); - return apiKey.length >= 20; // Fallback for network issues - } - } else if (provider === 'gemini') { - // Gemini API keys typically start with 'AIza' - if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false; - - try { - console.log('Validating Gemini API key...'); - - // Test the API key with a simple models list request - const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`); - - if (response.ok) { - const data = await response.json(); - if (data.models && data.models.length > 0) { - console.log('Gemini API key validation successful'); - return true; - } - } - - console.log('Gemini API key validation failed'); - return false; - } catch (error) { - console.error('Gemini API key validation network error:', error); - return apiKey.length >= 20; // Fallback - } - } - - return false; - } - - startSlideOutAnimation() { - this.classList.add('sliding-out'); - } - - handleUsePicklesKey(e) { - e.preventDefault(); - if (this.wasJustDragged) return; - - console.log('Requesting Firebase authentication from main process...'); + if (this.validatedApiKey) { if (window.require) { - window.require('electron').ipcRenderer.invoke('start-firebase-auth'); + window.require("electron").ipcRenderer.invoke("api-key-validated", { + apiKey: this.validatedApiKey, + provider: this.validatedProvider || "openai", + }) } + this.validatedApiKey = null + this.validatedProvider = null + } } + } - handleClose() { - console.log('Close button clicked'); - if (window.require) { - window.require('electron').ipcRenderer.invoke('quit-application'); - } - } + connectedCallback() { + super.connectedCallback() + this.addEventListener("animationend", this.handleAnimationEnd) + } - handleAnimationEnd(e) { - if (e.target !== this) return; + disconnectedCallback() { + super.disconnectedCallback() + this.removeEventListener("animationend", this.handleAnimationEnd) + } - if (this.classList.contains('sliding-out')) { - this.classList.remove('sliding-out'); - this.classList.add('hidden'); + render() { + const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim() + console.log("Rendering with provider:", this.selectedProvider) - 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; - } - } - } - - connectedCallback() { - super.connectedCallback(); - this.addEventListener('animationend', this.handleAnimationEnd); - - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.removeEventListener('animationend', this.handleAnimationEnd); - - } - - render() { - const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim(); - console.log('Rendering with provider:', this.selectedProvider); - - return html` + return html`
or
@@ -615,8 +654,8 @@ export class ApiKeyHeader extends LitElement {
- `; - } + ` + } } -customElements.define('apikey-header', ApiKeyHeader); +customElements.define("apikey-header", ApiKeyHeader) diff --git a/src/common/ai/factory.js b/src/common/ai/factory.js index 05a6780..8d0d6b5 100644 --- a/src/common/ai/factory.js +++ b/src/common/ai/factory.js @@ -1,8 +1,9 @@ const providers = { - openai: require('./providers/openai'), - gemini: require('./providers/gemini'), + openai: require("./providers/openai"), + gemini: require("./providers/gemini"), + anthropic: require("./providers/anthropic"), // 추가 provider는 여기에 등록 -}; +} /** * Creates an STT session based on provider @@ -12,9 +13,9 @@ const providers = { */ function createSTT(provider, opts) { if (!providers[provider]?.createSTT) { - throw new Error(`STT not supported for provider: ${provider}`); + throw new Error(`STT not supported for provider: ${provider}`) } - return providers[provider].createSTT(opts); + return providers[provider].createSTT(opts) } /** @@ -25,9 +26,9 @@ function createSTT(provider, opts) { */ function createLLM(provider, opts) { if (!providers[provider]?.createLLM) { - throw new Error(`LLM not supported for provider: ${provider}`); + throw new Error(`LLM not supported for provider: ${provider}`) } - return providers[provider].createLLM(opts); + return providers[provider].createLLM(opts) } /** @@ -38,9 +39,9 @@ function createLLM(provider, opts) { */ function createStreamingLLM(provider, opts) { if (!providers[provider]?.createStreamingLLM) { - throw new Error(`Streaming LLM not supported for provider: ${provider}`); + throw new Error(`Streaming LLM not supported for provider: ${provider}`) } - return providers[provider].createStreamingLLM(opts); + return providers[provider].createStreamingLLM(opts) } /** @@ -48,20 +49,20 @@ function createStreamingLLM(provider, opts) { * @returns {object} Object with stt and llm arrays */ function getAvailableProviders() { - const sttProviders = []; - const llmProviders = []; - + const sttProviders = [] + const llmProviders = [] + for (const [name, provider] of Object.entries(providers)) { - if (provider.createSTT) sttProviders.push(name); - if (provider.createLLM) llmProviders.push(name); + if (provider.createSTT) sttProviders.push(name) + if (provider.createLLM) llmProviders.push(name) } - - return { stt: sttProviders, llm: llmProviders }; + + return { stt: sttProviders, llm: llmProviders } } module.exports = { createSTT, createLLM, createStreamingLLM, - getAvailableProviders -}; \ No newline at end of file + getAvailableProviders, +} diff --git a/src/common/ai/providers/anthropic.js b/src/common/ai/providers/anthropic.js new file mode 100644 index 0000000..415fa95 --- /dev/null +++ b/src/common/ai/providers/anthropic.js @@ -0,0 +1,280 @@ +const Anthropic = require("@anthropic-ai/sdk") + +/** + * Creates an Anthropic STT session + * Note: Anthropic doesn't have native real-time STT, so this is a placeholder + * You might want to use a different STT service or implement a workaround + * @param {object} opts - Configuration options + * @param {string} opts.apiKey - Anthropic API key + * @param {string} [opts.language='en'] - Language code + * @param {object} [opts.callbacks] - Event callbacks + * @returns {Promise} STT session placeholder + */ +async function createSTT({ apiKey, language = "en", callbacks = {}, ...config }) { + console.warn("[Anthropic] STT not natively supported. Consider using OpenAI or Gemini for STT.") + + // Return a mock STT session that doesn't actually do anything + // You might want to fallback to another provider for STT + return { + sendRealtimeInput: async (audioData) => { + console.warn("[Anthropic] STT sendRealtimeInput called but not implemented") + }, + close: async () => { + console.log("[Anthropic] STT session closed") + }, + } +} + +/** + * Creates an Anthropic LLM instance + * @param {object} opts - Configuration options + * @param {string} opts.apiKey - Anthropic API key + * @param {string} [opts.model='claude-3-5-sonnet-20241022'] - Model name + * @param {number} [opts.temperature=0.7] - Temperature + * @param {number} [opts.maxTokens=4096] - Max tokens + * @returns {object} LLM instance + */ +function createLLM({ apiKey, model = "claude-3-5-sonnet-20241022", temperature = 0.7, maxTokens = 4096, ...config }) { + const client = new Anthropic({ apiKey }) + + return { + generateContent: async (parts) => { + const messages = [] + let systemPrompt = "" + const userContent = [] + + for (const part of parts) { + if (typeof part === "string") { + if (systemPrompt === "" && part.includes("You are")) { + systemPrompt = part + } else { + userContent.push({ type: "text", text: part }) + } + } else if (part.inlineData) { + userContent.push({ + type: "image", + source: { + type: "base64", + media_type: part.inlineData.mimeType, + data: part.inlineData.data, + }, + }) + } + } + + if (userContent.length > 0) { + messages.push({ role: "user", content: userContent }) + } + + try { + const response = await client.messages.create({ + model: model, + max_tokens: maxTokens, + temperature: temperature, + system: systemPrompt || undefined, + messages: messages, + }) + + return { + response: { + text: () => response.content[0].text, + }, + raw: response, + } + } catch (error) { + console.error("Anthropic API error:", error) + throw error + } + }, + + // For compatibility with chat-style interfaces + chat: async (messages) => { + let systemPrompt = "" + const anthropicMessages = [] + + for (const msg of messages) { + if (msg.role === "system") { + systemPrompt = msg.content + } else { + // Handle multimodal content + let content + if (Array.isArray(msg.content)) { + content = [] + for (const part of msg.content) { + if (typeof part === "string") { + content.push({ type: "text", text: part }) + } else if (part.type === "text") { + content.push({ type: "text", text: part.text }) + } else if (part.type === "image_url" && part.image_url) { + // Convert base64 image to Anthropic format + const base64Data = part.image_url.url.split(",")[1] + content.push({ + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: base64Data, + }, + }) + } + } + } else { + content = [{ type: "text", text: msg.content }] + } + + anthropicMessages.push({ + role: msg.role === "user" ? "user" : "assistant", + content: content, + }) + } + } + + const response = await client.messages.create({ + model: model, + max_tokens: maxTokens, + temperature: temperature, + system: systemPrompt || undefined, + messages: anthropicMessages, + }) + + return { + content: response.content[0].text, + raw: response, + } + }, + } +} + +/** + * Creates an Anthropic streaming LLM instance + * @param {object} opts - Configuration options + * @param {string} opts.apiKey - Anthropic API key + * @param {string} [opts.model='claude-3-5-sonnet-20241022'] - Model name + * @param {number} [opts.temperature=0.7] - Temperature + * @param {number} [opts.maxTokens=4096] - Max tokens + * @returns {object} Streaming LLM instance + */ +function createStreamingLLM({ + apiKey, + model = "claude-3-5-sonnet-20241022", + temperature = 0.7, + maxTokens = 4096, + ...config +}) { + const client = new Anthropic({ apiKey }) + + return { + streamChat: async (messages) => { + console.log("[Anthropic Provider] Starting streaming request") + + let systemPrompt = "" + const anthropicMessages = [] + + for (const msg of messages) { + if (msg.role === "system") { + systemPrompt = msg.content + } else { + // Handle multimodal content + let content + if (Array.isArray(msg.content)) { + content = [] + for (const part of msg.content) { + if (typeof part === "string") { + content.push({ type: "text", text: part }) + } else if (part.type === "text") { + content.push({ type: "text", text: part.text }) + } else if (part.type === "image_url" && part.image_url) { + // Convert base64 image to Anthropic format + const base64Data = part.image_url.url.split(",")[1] + content.push({ + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: base64Data, + }, + }) + } + } + } else { + content = [{ type: "text", text: msg.content }] + } + + anthropicMessages.push({ + role: msg.role === "user" ? "user" : "assistant", + content: content, + }) + } + } + + // Create a ReadableStream to handle Anthropic's streaming + const stream = new ReadableStream({ + async start(controller) { + try { + console.log("[Anthropic Provider] Processing messages:", anthropicMessages.length, "messages") + + let chunkCount = 0 + let totalContent = "" + + // Stream the response + const stream = await client.messages.create({ + model: model, + max_tokens: maxTokens, + temperature: temperature, + system: systemPrompt || undefined, + messages: anthropicMessages, + stream: true, + }) + + for await (const chunk of stream) { + if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") { + chunkCount++ + const chunkText = chunk.delta.text || "" + totalContent += chunkText + + // Format as SSE data + const data = JSON.stringify({ + choices: [ + { + delta: { + content: chunkText, + }, + }, + ], + }) + controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`)) + } + } + + console.log( + `[Anthropic Provider] Streamed ${chunkCount} chunks, total length: ${totalContent.length} chars`, + ) + + // Send the final done message + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")) + controller.close() + console.log("[Anthropic Provider] Streaming completed successfully") + } catch (error) { + console.error("[Anthropic Provider] Streaming error:", error) + controller.error(error) + } + }, + }) + + // Create a Response object with the stream + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + }, + } +} + +module.exports = { + createSTT, + createLLM, + createStreamingLLM, +} diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index 9d74334..4b540f6 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -1,7 +1,6 @@ const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron'); const WindowLayoutManager = require('./windowLayoutManager'); const SmoothMovementManager = require('./smoothMovementManager'); -const liquidGlass = require('electron-liquid-glass'); const path = require('node:path'); const fs = require('node:fs'); const os = require('os'); @@ -15,6 +14,7 @@ const fetch = require('node-fetch'); /* ────────────────[ GLASS BYPASS ]─────────────── */ +let liquidGlass; const isLiquidGlassSupported = () => { if (process.platform !== 'darwin') { return false; @@ -23,7 +23,15 @@ const isLiquidGlassSupported = () => { // return majorVersion >= 25; // macOS 26+ (Darwin 25+) return majorVersion >= 26; // See you soon! }; -const shouldUseLiquidGlass = isLiquidGlassSupported(); +let shouldUseLiquidGlass = isLiquidGlassSupported(); +if (shouldUseLiquidGlass) { + try { + liquidGlass = require('electron-liquid-glass'); + } catch (e) { + console.warn('Could not load optional dependency "electron-liquid-glass". The feature will be disabled.'); + shouldUseLiquidGlass = false; + } +} /* ────────────────[ GLASS BYPASS ]─────────────── */ let isContentProtectionOn = true; @@ -83,7 +91,9 @@ function createFeatureWindows(header) { }); listen.setContentProtection(isContentProtectionOn); listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - listen.setWindowButtonVisibility(false); + if (process.platform === 'darwin') { + listen.setWindowButtonVisibility(false); + } const listenLoadOptions = { query: { view: 'listen' } }; if (!shouldUseLiquidGlass) { listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions); @@ -112,7 +122,9 @@ function createFeatureWindows(header) { const ask = new BrowserWindow({ ...commonChildOptions, width:600 }); ask.setContentProtection(isContentProtectionOn); ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - ask.setWindowButtonVisibility(false); + if (process.platform === 'darwin') { + ask.setWindowButtonVisibility(false); + } const askLoadOptions = { query: { view: 'ask' } }; if (!shouldUseLiquidGlass) { ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions); @@ -146,7 +158,9 @@ function createFeatureWindows(header) { const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined }); settings.setContentProtection(isContentProtectionOn); settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true}); - settings.setWindowButtonVisibility(false); + if (process.platform === 'darwin') { + settings.setWindowButtonVisibility(false); + } const settingsLoadOptions = { query: { view: 'settings' } }; if (!shouldUseLiquidGlass) { settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions) @@ -236,12 +250,13 @@ function toggleAllWindowsVisibility(movementManager) { if (win.isVisible()) { lastVisibleWindows.add(name); if (name !== 'header') { - win.webContents.send('window-hide-animation'); - setTimeout(() => { - if (!win.isDestroyed()) { - win.hide(); - } - }, 200); + // win.webContents.send('window-hide-animation'); + // setTimeout(() => { + // if (!win.isDestroyed()) { + // win.hide(); + // } + // }, 200); + win.hide(); } } }); @@ -251,7 +266,7 @@ function toggleAllWindowsVisibility(movementManager) { movementManager.hideToEdge(nearestEdge, () => { header.hide(); console.log('[Visibility] Smart hide completed'); - }); + }, { instant: true }); } else { console.log('[Visibility] Smart showing from hidden position'); console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows)); @@ -306,7 +321,9 @@ function createWindows() { webSecurity: false, }, }); - header.setWindowButtonVisibility(false); + if (process.platform === 'darwin') { + header.setWindowButtonVisibility(false); + } const headerLoadOptions = {}; if (!shouldUseLiquidGlass) { header.loadFile(path.join(__dirname, '../app/header.html'), headerLoadOptions); @@ -372,7 +389,7 @@ function createWindows() { // loadAndRegisterShortcuts(); // }); - ipcMain.handle('toggle-all-windows-visibility', toggleAllWindowsVisibility); + ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager)); ipcMain.handle('toggle-feature', async (event, featureName) => { if (!windowPool.get(featureName) && currentHeaderState === 'main') { @@ -1482,4 +1499,4 @@ module.exports = { getStoredApiKey, getStoredProvider, captureScreenshot, -}; +}; \ No newline at end of file