diff --git a/src/app/MainHeader.js b/src/app/MainHeader.js index 3f78f678..fa45b89c 100644 --- a/src/app/MainHeader.js +++ b/src/app/MainHeader.js @@ -549,6 +549,7 @@ export class MainHeader extends LitElement { } async _handleListenClick() { + if (this.wasJustDragged) return; if (this.isTogglingSession) { return; diff --git a/src/app/PickleGlassApp.js b/src/app/PickleGlassApp.js index b5aa9d03..5f5b7961 100644 --- a/src/app/PickleGlassApp.js +++ b/src/app/PickleGlassApp.js @@ -69,6 +69,23 @@ export class PickleGlassApp extends LitElement { this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || 'medium'; this._isClickThrough = false; + // Initialize structured data for Live insights + this.structuredData = { + summary: [], + topic: { header: '', bullets: [] }, + actions: [], + followUps: [], + }; + + // Set up structured data callback for Live insights + if (window.pickleGlass) { + window.pickleGlass.setStructuredData = data => { + this.updateStructuredData(data); + }; + } + + // Load language from settings on startup + this.loadLanguageFromSettings(); } connectedCallback() { @@ -80,6 +97,14 @@ export class PickleGlassApp extends LitElement { ipcRenderer.on('click-through-toggled', (_, isEnabled) => { this._isClickThrough = isEnabled; }); + + // Language change listener for multi-language support + ipcRenderer.on('language-changed', (_, languageCode) => { + console.log('Language changed to:', languageCode); + this.selectedLanguage = languageCode; + localStorage.setItem('selectedLanguage', languageCode); + }); + // ipcRenderer.on('start-listening-session', () => { // console.log('Received start-listening-session command, calling handleListenClick.'); // this.handleListenClick(); @@ -92,6 +117,7 @@ export class PickleGlassApp extends LitElement { if (window.require) { const { ipcRenderer } = window.require('electron'); ipcRenderer.removeAllListeners('click-through-toggled'); + ipcRenderer.removeAllListeners('language-changed'); // ipcRenderer.removeAllListeners('start-listening-session'); } } @@ -159,6 +185,34 @@ export class PickleGlassApp extends LitElement { // this.isMainViewVisible = true; // } + async loadLanguageFromSettings() { + if (!window.require) return; + + try { + const { ipcRenderer } = window.require('electron'); + const currentLanguage = await ipcRenderer.invoke('settings:get-current-language'); + if (currentLanguage && currentLanguage !== this.selectedLanguage) { + this.selectedLanguage = currentLanguage; + localStorage.setItem('selectedLanguage', currentLanguage); + console.log(`[PickleGlassApp] Language loaded from settings: ${currentLanguage}`); + } + } catch (error) { + console.warn('[PickleGlassApp] Failed to load language from settings:', error); + } + } + + updateStructuredData(data) { + console.log('📝 PickleGlassApp updateStructuredData:', data); + this.structuredData = data; + this.requestUpdate(); + + const assistantView = this.shadowRoot?.querySelector('assistant-view'); + if (assistantView) { + assistantView.structuredData = data; + console.log('✅ Structured data passed to AssistantView'); + } + } + async handleClose() { if (window.require) { const { ipcRenderer } = window.require('electron'); @@ -169,6 +223,7 @@ export class PickleGlassApp extends LitElement { + render() { switch (this.currentView) { case 'listen': diff --git a/src/common/ai/providers/gemini.js b/src/common/ai/providers/gemini.js index c09065ff..3a24cb68 100644 --- a/src/common/ai/providers/gemini.js +++ b/src/common/ai/providers/gemini.js @@ -1,5 +1,6 @@ const { GoogleGenerativeAI } = require("@google/generative-ai") const { GoogleGenAI } = require("@google/genai") +const { getLanguageForProvider } = require('../../config/languages') class GeminiProvider { static async validateApiKey(key) { @@ -34,11 +35,12 @@ class GeminiProvider { * @param {object} [opts.callbacks] - Event callbacks * @returns {Promise} STT session */ -async function createSTT({ apiKey, language = "en-US", callbacks = {}, ...config }) { +async function createSTT({ apiKey, language = "en", callbacks = {}, ...config }) { const liveClient = new GoogleGenAI({ vertexai: false, apiKey }) - // Language code BCP-47 conversion - const lang = language.includes("-") ? language : `${language}-US` + // Get the proper language code for Gemini (BCP-47 format) + const geminiLanguage = getLanguageForProvider(language, 'gemini'); + console.log(`[Gemini STT] Using language: ${geminiLanguage} (from ${language})`); const session = await liveClient.live.connect({ @@ -54,7 +56,7 @@ async function createSTT({ apiKey, language = "en-US", callbacks = {}, ...config config: { inputAudioTranscription: {}, - speechConfig: { languageCode: lang }, + speechConfig: { languageCode: geminiLanguage }, }, }) diff --git a/src/common/ai/providers/openai.js b/src/common/ai/providers/openai.js index fe37c611..89685f85 100644 --- a/src/common/ai/providers/openai.js +++ b/src/common/ai/providers/openai.js @@ -1,5 +1,6 @@ const OpenAI = require('openai'); const WebSocket = require('ws'); +const { getLanguageForProvider } = require('../../config/languages'); const { Portkey } = require('portkey-ai'); const { Readable } = require('stream'); const { getProviderForModel } = require('../factory.js'); @@ -30,7 +31,6 @@ class OpenAIProvider { } } - /** * Creates an OpenAI STT session * @param {object} opts - Configuration options @@ -45,6 +45,10 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey = const keyType = usePortkey ? 'vKey' : 'apiKey'; const key = usePortkey ? (portkeyVirtualKey || apiKey) : apiKey; + // Get the proper language code for OpenAI + const openaiLanguage = getLanguageForProvider(language, 'openai'); + console.log(`[OpenAI STT] Using language: ${openaiLanguage} (from ${language})`); + const wsUrl = keyType === 'apiKey' ? 'wss://api.openai.com/v1/realtime?intent=transcription' : 'wss://api.portkey.ai/v1/realtime?intent=transcription'; @@ -64,7 +68,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey = return new Promise((resolve, reject) => { ws.onopen = () => { - console.log("WebSocket session opened."); + console.log(`[OpenAI STT] WebSocket session opened for language: ${openaiLanguage}`); const sessionConfig = { type: 'transcription_session.update', @@ -73,7 +77,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey = input_audio_transcription: { model: 'gpt-4o-mini-transcribe', prompt: config.prompt || '', - language: language || 'en' + language: openaiLanguage }, turn_detection: { type: 'server_vad', diff --git a/src/common/ai/providers/whisper.js b/src/common/ai/providers/whisper.js index 11909776..1cacefea 100644 --- a/src/common/ai/providers/whisper.js +++ b/src/common/ai/providers/whisper.js @@ -1,9 +1,10 @@ -let spawn, path, EventEmitter; +let spawn, path, EventEmitter, getLanguageForProvider; if (typeof window === 'undefined') { spawn = require('child_process').spawn; path = require('path'); EventEmitter = require('events').EventEmitter; + getLanguageForProvider = require('../../config/languages').getLanguageForProvider; } else { class DummyEventEmitter { on() {} @@ -11,14 +12,16 @@ if (typeof window === 'undefined') { removeAllListeners() {} } EventEmitter = DummyEventEmitter; + getLanguageForProvider = () => 'en'; } class WhisperSTTSession extends EventEmitter { - constructor(model, whisperService, sessionId) { + constructor(model, whisperService, sessionId, language = 'en') { super(); this.model = model; this.whisperService = whisperService; this.sessionId = sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + this.language = language; this.process = null; this.isRunning = false; this.audioBuffer = Buffer.alloc(0); @@ -71,13 +74,17 @@ class WhisperSTTSession extends EventEmitter { return; } + // Get the proper language code for Whisper + const whisperLanguage = getLanguageForProvider(this.language, 'whisper'); + console.log(`[WhisperSTT-${this.sessionId}] Using language: ${whisperLanguage} (from ${this.language})`); + this.process = spawn(whisperPath, [ '-m', modelPath, '-f', tempFile, '--no-timestamps', '--output-txt', '--output-json', - '--language', 'auto', + '--language', whisperLanguage, '--threads', '4', '--print-progress', 'false' ]); @@ -195,11 +202,12 @@ class WhisperProvider { const model = config.model || 'whisper-tiny'; const sessionType = config.sessionType || 'unknown'; - console.log(`[WhisperProvider] Creating ${sessionType} STT session with model: ${model}`); + const language = config.language || 'en'; + console.log(`[WhisperProvider] Creating ${sessionType} STT session with model: ${model}, language: ${language}`); // Create unique session ID based on type const sessionId = `${sessionType}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; - const session = new WhisperSTTSession(model, this.whisperService, sessionId); + const session = new WhisperSTTSession(model, this.whisperService, sessionId, language); // Log session creation console.log(`[WhisperProvider] Created session: ${sessionId}`); diff --git a/src/common/config/languages.js b/src/common/config/languages.js new file mode 100644 index 00000000..937fe88c --- /dev/null +++ b/src/common/config/languages.js @@ -0,0 +1,445 @@ +/** + * Language configuration for multi-language support + * Includes mappings for different STT and LLM providers + */ + +// Common language codes and their display names +const LANGUAGES = { + 'en': { + name: 'English', + nativeName: 'English', + // STT provider mappings + openai: 'en', + gemini: 'en-US', + whisper: 'en', + // LLM context + llmContext: 'Please respond in English.', + rtl: false + }, + 'es': { + name: 'Spanish', + nativeName: 'Español', + openai: 'es', + gemini: 'es-ES', + whisper: 'es', + llmContext: 'Please respond in Spanish.', + rtl: false + }, + 'fr': { + name: 'French', + nativeName: 'Français', + openai: 'fr', + gemini: 'fr-FR', + whisper: 'fr', + llmContext: 'Please respond in French.', + rtl: false + }, + 'de': { + name: 'German', + nativeName: 'Deutsch', + openai: 'de', + gemini: 'de-DE', + whisper: 'de', + llmContext: 'Please respond in German.', + rtl: false + }, + 'it': { + name: 'Italian', + nativeName: 'Italiano', + openai: 'it', + gemini: 'it-IT', + whisper: 'it', + llmContext: 'Please respond in Italian.', + rtl: false + }, + 'pt': { + name: 'Portuguese', + nativeName: 'Português', + openai: 'pt', + gemini: 'pt-BR', + whisper: 'pt', + llmContext: 'Please respond in Portuguese.', + rtl: false + }, + 'ru': { + name: 'Russian', + nativeName: 'Русский', + openai: 'ru', + gemini: 'ru-RU', + whisper: 'ru', + llmContext: 'Please respond in Russian.', + rtl: false + }, + 'ja': { + name: 'Japanese', + nativeName: '日本語', + openai: 'ja', + gemini: 'ja-JP', + whisper: 'ja', + llmContext: 'Please respond in Japanese.', + rtl: false + }, + 'ko': { + name: 'Korean', + nativeName: '한국어', + openai: 'ko', + gemini: 'ko-KR', + whisper: 'ko', + llmContext: 'Please respond in Korean.', + rtl: false + }, + 'zh': { + name: 'Chinese (Simplified)', + nativeName: '中文(简体)', + openai: 'zh', + gemini: 'zh-CN', + whisper: 'zh', + llmContext: 'Please respond in Chinese (Simplified).', + rtl: false + }, + 'zh-TW': { + name: 'Chinese (Traditional)', + nativeName: '中文(繁體)', + openai: 'zh', + gemini: 'zh-TW', + whisper: 'zh', + llmContext: 'Please respond in Chinese (Traditional).', + rtl: false + }, + 'ar': { + name: 'Arabic', + nativeName: 'العربية', + openai: 'ar', + gemini: 'ar-SA', + whisper: 'ar', + llmContext: 'Please respond in Arabic.', + rtl: true + }, + 'hi': { + name: 'Hindi', + nativeName: 'हिन्दी', + openai: 'hi', + gemini: 'hi-IN', + whisper: 'hi', + llmContext: 'Please respond in Hindi.', + rtl: false + }, + 'tr': { + name: 'Turkish', + nativeName: 'Türkçe', + openai: 'tr', + gemini: 'tr-TR', + whisper: 'tr', + llmContext: 'Please respond in Turkish.', + rtl: false + }, + 'nl': { + name: 'Dutch', + nativeName: 'Nederlands', + openai: 'nl', + gemini: 'nl-NL', + whisper: 'nl', + llmContext: 'Please respond in Dutch.', + rtl: false + }, + 'pl': { + name: 'Polish', + nativeName: 'Polski', + openai: 'pl', + gemini: 'pl-PL', + whisper: 'pl', + llmContext: 'Please respond in Polish.', + rtl: false + }, + 'sv': { + name: 'Swedish', + nativeName: 'Svenska', + openai: 'sv', + gemini: 'sv-SE', + whisper: 'sv', + llmContext: 'Please respond in Swedish.', + rtl: false + }, + 'da': { + name: 'Danish', + nativeName: 'Dansk', + openai: 'da', + gemini: 'da-DK', + whisper: 'da', + llmContext: 'Please respond in Danish.', + rtl: false + }, + 'no': { + name: 'Norwegian', + nativeName: 'Norsk', + openai: 'no', + gemini: 'no-NO', + whisper: 'no', + llmContext: 'Please respond in Norwegian.', + rtl: false + }, + 'fi': { + name: 'Finnish', + nativeName: 'Suomi', + openai: 'fi', + gemini: 'fi-FI', + whisper: 'fi', + llmContext: 'Please respond in Finnish.', + rtl: false + }, + 'he': { + name: 'Hebrew', + nativeName: 'עברית', + openai: 'he', + gemini: 'he-IL', + whisper: 'he', + llmContext: 'Please respond in Hebrew.', + rtl: true + }, + 'th': { + name: 'Thai', + nativeName: 'ไทย', + openai: 'th', + gemini: 'th-TH', + whisper: 'th', + llmContext: 'Please respond in Thai.', + rtl: false + }, + 'vi': { + name: 'Vietnamese', + nativeName: 'Tiếng Việt', + openai: 'vi', + gemini: 'vi-VN', + whisper: 'vi', + llmContext: 'Please respond in Vietnamese.', + rtl: false + }, + 'uk': { + name: 'Ukrainian', + nativeName: 'Українська', + openai: 'uk', + gemini: 'uk-UA', + whisper: 'uk', + llmContext: 'Please respond in Ukrainian.', + rtl: false + }, + 'cs': { + name: 'Czech', + nativeName: 'Čeština', + openai: 'cs', + gemini: 'cs-CZ', + whisper: 'cs', + llmContext: 'Please respond in Czech.', + rtl: false + }, + 'hu': { + name: 'Hungarian', + nativeName: 'Magyar', + openai: 'hu', + gemini: 'hu-HU', + whisper: 'hu', + llmContext: 'Please respond in Hungarian.', + rtl: false + }, + 'ro': { + name: 'Romanian', + nativeName: 'Română', + openai: 'ro', + gemini: 'ro-RO', + whisper: 'ro', + llmContext: 'Please respond in Romanian.', + rtl: false + }, + 'bg': { + name: 'Bulgarian', + nativeName: 'Български', + openai: 'bg', + gemini: 'bg-BG', + whisper: 'bg', + llmContext: 'Please respond in Bulgarian.', + rtl: false + }, + 'hr': { + name: 'Croatian', + nativeName: 'Hrvatski', + openai: 'hr', + gemini: 'hr-HR', + whisper: 'hr', + llmContext: 'Please respond in Croatian.', + rtl: false + }, + 'sk': { + name: 'Slovak', + nativeName: 'Slovenčina', + openai: 'sk', + gemini: 'sk-SK', + whisper: 'sk', + llmContext: 'Please respond in Slovak.', + rtl: false + }, + 'sl': { + name: 'Slovenian', + nativeName: 'Slovenščina', + openai: 'sl', + gemini: 'sl-SI', + whisper: 'sl', + llmContext: 'Please respond in Slovenian.', + rtl: false + }, + 'et': { + name: 'Estonian', + nativeName: 'Eesti', + openai: 'et', + gemini: 'et-EE', + whisper: 'et', + llmContext: 'Please respond in Estonian.', + rtl: false + }, + 'lv': { + name: 'Latvian', + nativeName: 'Latviešu', + openai: 'lv', + gemini: 'lv-LV', + whisper: 'lv', + llmContext: 'Please respond in Latvian.', + rtl: false + }, + 'lt': { + name: 'Lithuanian', + nativeName: 'Lietuvių', + openai: 'lt', + gemini: 'lt-LT', + whisper: 'lt', + llmContext: 'Please respond in Lithuanian.', + rtl: false + }, + 'id': { + name: 'Indonesian', + nativeName: 'Bahasa Indonesia', + openai: 'id', + gemini: 'id-ID', + whisper: 'id', + llmContext: 'Please respond in Indonesian.', + rtl: false + }, + 'ms': { + name: 'Malay', + nativeName: 'Bahasa Melayu', + openai: 'ms', + gemini: 'ms-MY', + whisper: 'ms', + llmContext: 'Please respond in Malay.', + rtl: false + }, + 'tl': { + name: 'Filipino', + nativeName: 'Filipino', + openai: 'tl', + gemini: 'tl-PH', + whisper: 'tl', + llmContext: 'Please respond in Filipino.', + rtl: false + } +}; + +// Default language +const DEFAULT_LANGUAGE = 'en'; + +// Get language configuration for a specific provider +function getLanguageForProvider(languageCode, provider) { + const language = LANGUAGES[languageCode]; + if (!language) { + console.warn(`[Languages] Unknown language code: ${languageCode}, falling back to English`); + return LANGUAGES[DEFAULT_LANGUAGE][provider] || DEFAULT_LANGUAGE; + } + + return language[provider] || language.openai || languageCode; +} + +// Get display name for a language +function getLanguageDisplayName(languageCode) { + const language = LANGUAGES[languageCode]; + return language ? language.name : languageCode; +} + +// Get native name for a language +function getLanguageNativeName(languageCode) { + const language = LANGUAGES[languageCode]; + return language ? language.nativeName : languageCode; +} + +// Get LLM context instruction for a language +function getLanguageLLMContext(languageCode) { + const language = LANGUAGES[languageCode]; + return language ? language.llmContext : LANGUAGES[DEFAULT_LANGUAGE].llmContext; +} + +// Check if language is RTL +function isRTL(languageCode) { + const language = LANGUAGES[languageCode]; + return language ? language.rtl : false; +} + +// Get all available languages as array +function getAvailableLanguages() { + return Object.keys(LANGUAGES).map(code => ({ + code, + name: LANGUAGES[code].name, + nativeName: LANGUAGES[code].nativeName, + rtl: LANGUAGES[code].rtl + })); +} + +// Get popular languages (most commonly used) +function getPopularLanguages() { + const popularCodes = ['en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh', 'ar', 'hi']; + return popularCodes.map(code => ({ + code, + name: LANGUAGES[code].name, + nativeName: LANGUAGES[code].nativeName, + rtl: LANGUAGES[code].rtl + })); +} + +// Validate language code +function isValidLanguageCode(languageCode) { + return languageCode in LANGUAGES; +} + +// Get language code from various formats (e.g., 'en-US' -> 'en') +function normalizeLanguageCode(languageCode) { + if (!languageCode) return DEFAULT_LANGUAGE; + + // Handle exact matches first + if (languageCode in LANGUAGES) { + return languageCode; + } + + // Handle language-region codes (e.g., 'en-US' -> 'en') + const baseCode = languageCode.split('-')[0]; + if (baseCode in LANGUAGES) { + return baseCode; + } + + // Handle special cases + if (languageCode.startsWith('zh-TW') || languageCode.startsWith('zh-Hant')) { + return 'zh-TW'; + } + + console.warn(`[Languages] Could not normalize language code: ${languageCode}, falling back to ${DEFAULT_LANGUAGE}`); + return DEFAULT_LANGUAGE; +} + +module.exports = { + LANGUAGES, + DEFAULT_LANGUAGE, + getLanguageForProvider, + getLanguageDisplayName, + getLanguageNativeName, + getLanguageLLMContext, + isRTL, + getAvailableLanguages, + getPopularLanguages, + isValidLanguageCode, + normalizeLanguageCode +}; \ No newline at end of file diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index c4bbd81d..763c34ec 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -20,6 +20,7 @@ try { const authService = require('../common/services/authService'); const systemSettingsRepository = require('../common/repositories/systemSettings'); const userRepository = require('../common/repositories/user'); +const { getSettings } = require('../features/settings/settingsService'); const fetch = require('node-fetch'); const Store = require('electron-store'); const shortCutStore = new Store({ @@ -103,7 +104,19 @@ async function toggleFeature(featureName) { listenWindow.show(); updateLayout(); listenWindow.webContents.send('window-show-animation'); - await listenService.initializeSession(); + + // Get current language setting and pass it to initializeSession + try { + const settings = await getSettings(); + const language = settings.language || 'en'; + console.log(`[WindowManager] Initializing session with language: ${language}`); + await listenService.initializeSession(language); + } catch (error) { + console.error('[WindowManager] Failed to get language setting, using default:', error); + await listenService.initializeSession('en'); + } + + listenWindow.webContents.send('session-state-changed', { isActive: true }); header.webContents.send('session-state-text', 'Stop'); } diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 33d661e5..f686e328 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -5,6 +5,8 @@ const authService = require('../../common/services/authService'); const sessionRepository = require('../../common/repositories/session'); const askRepository = require('./repositories'); const { getSystemPrompt } = require('../../common/prompts/promptBuilder'); +const { getSettings } = require('../settings/settingsService'); +const { getLanguageLLMContext } = require('../../common/config/languages'); function formatConversationForPrompt(conversationTexts) { if (!conversationTexts || conversationTexts.length === 0) return 'No conversation history available.'; @@ -52,11 +54,21 @@ async function sendMessage(userPrompt) { const conversationHistoryRaw = getConversationHistory(); const conversationHistory = formatConversationForPrompt(conversationHistoryRaw); - const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false); + // Get language context + let languageContext = ''; + try { + const settings = await getSettings(); + languageContext = getLanguageLLMContext(settings.language || 'en'); + } catch (error) { + console.warn('[AskService] Failed to get language context:', error); + languageContext = getLanguageLLMContext('en'); + } + const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false); + const systemPromptWithLanguage = `${systemPrompt}\n\n${languageContext}`; const messages = [ - { role: 'system', content: systemPrompt }, + { role: 'system', content: systemPromptWithLanguage }, { role: 'user', content: [ diff --git a/src/features/listen/AssistantView.js b/src/features/listen/AssistantView.js index c6d78611..418841e8 100644 --- a/src/features/listen/AssistantView.js +++ b/src/features/listen/AssistantView.js @@ -48,25 +48,28 @@ export class AssistantView extends LitElement { margin: 8px 0 !important; overflow-x: auto !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; + max-width: 100% !important; } .insights-container code { font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important; font-size: 11px !important; background: transparent !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; + max-width: 100% !important; } .insights-container pre code { - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; display: block !important; + max-width: 100% !important; } .insights-container p code { diff --git a/src/features/listen/stt/sttService.js b/src/features/listen/stt/sttService.js index 39f7aa8f..44c04720 100644 --- a/src/features/listen/stt/sttService.js +++ b/src/features/listen/stt/sttService.js @@ -2,6 +2,7 @@ const { BrowserWindow } = require('electron'); const { spawn } = require('child_process'); const { createSTT } = require('../../../common/ai/factory'); const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager'); +const { getSettings } = require('../../settings/settingsService'); const COMPLETION_DEBOUNCE_MS = 2000; @@ -117,8 +118,22 @@ class SttService { this.theirCompletionTimer = setTimeout(() => this.flushTheirCompletion(), COMPLETION_DEBOUNCE_MS); } - async initializeSttSessions(language = 'en') { - const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en'; + async initializeSttSessions(language = null) { + // Get language from settings if not provided + let effectiveLanguage = language; + if (!effectiveLanguage) { + try { + const settings = await getSettings(); + effectiveLanguage = settings.language || 'en'; + } catch (error) { + console.warn('[SttService] Failed to get language from settings, using default:', error); + effectiveLanguage = 'en'; + } + } + + // Allow environment override + effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || effectiveLanguage; + console.log(`[SttService] Using language: ${effectiveLanguage}`); const modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); if (!modelInfo || !modelInfo.apiKey) { diff --git a/src/features/listen/summary/SummaryView.js b/src/features/listen/summary/SummaryView.js index b5006896..40d80663 100644 --- a/src/features/listen/summary/SummaryView.js +++ b/src/features/listen/summary/SummaryView.js @@ -17,25 +17,28 @@ export class SummaryView extends LitElement { margin: 8px 0 !important; overflow-x: auto !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; + max-width: 100% !important; } .insights-container code { font-family: 'Monaco', 'Menlo', 'Consolas', monospace !important; font-size: 11px !important; background: transparent !important; - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; + max-width: 100% !important; } .insights-container pre code { - white-space: pre !important; - word-wrap: normal !important; - word-break: normal !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-word !important; display: block !important; + max-width: 100% !important; } .insights-container p code { @@ -78,12 +81,16 @@ export class SummaryView extends LitElement { .insights-container { overflow-y: auto; + overflow-x: hidden; padding: 12px 16px 16px 16px; position: relative; z-index: 1; min-height: 150px; max-height: 600px; flex: 1; + width: 100%; + max-width: 100%; + box-sizing: border-box; } /* Visibility handled by parent component */ @@ -142,6 +149,10 @@ export class SummaryView extends LitElement { transition: background-color 0.15s ease; cursor: pointer; word-wrap: break-word; + word-break: break-word; + overflow-wrap: break-word; + max-width: 100%; + box-sizing: border-box; } .outline-item:hover { @@ -158,7 +169,11 @@ export class SummaryView extends LitElement { background: transparent; cursor: default; word-wrap: break-word; + word-break: break-word; + overflow-wrap: break-word; transition: background-color 0.15s ease; + max-width: 100%; + box-sizing: border-box; } .request-item.clickable { @@ -181,7 +196,11 @@ export class SummaryView extends LitElement { background: transparent; cursor: pointer; word-wrap: break-word; + word-break: break-word; + overflow-wrap: break-word; transition: all 0.15s ease; + max-width: 100%; + box-sizing: border-box; } .markdown-content:hover { diff --git a/src/features/listen/summary/summaryService.js b/src/features/listen/summary/summaryService.js index a3dc0658..c4d7e404 100644 --- a/src/features/listen/summary/summaryService.js +++ b/src/features/listen/summary/summaryService.js @@ -5,6 +5,8 @@ const authService = require('../../../common/services/authService'); const sessionRepository = require('../../../common/repositories/session'); const summaryRepository = require('./repositories'); const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager'); +const { getLanguageLLMContext } = require('../../../common/config/languages'); +const { getSettings } = require('../../settings/settingsService'); class SummaryService { constructor() { @@ -93,6 +95,18 @@ Please build upon this context while analyzing the new conversation segments. const basePrompt = getSystemPrompt('pickle_glass_analysis', '', false); const systemPrompt = basePrompt.replace('{{CONVERSATION_HISTORY}}', recentConversation); + // Get language context + let languageContext = ''; + try { + const settings = await getSettings(); + languageContext = getLanguageLLMContext(settings.language || 'en'); + } catch (error) { + console.warn('[SummaryService] Failed to get language context:', error); + languageContext = getLanguageLLMContext('en'); + } + + const systemPromptWithLanguage = `${systemPrompt}\n\n${languageContext}`; + try { if (this.currentSessionId) { await sessionRepository.touch(this.currentSessionId); @@ -107,7 +121,7 @@ Please build upon this context while analyzing the new conversation segments. const messages = [ { role: 'system', - content: systemPrompt, + content: systemPromptWithLanguage, }, { role: 'user', diff --git a/src/features/settings/SettingsView.js b/src/features/settings/SettingsView.js index c0b40ea2..d9b1510d 100644 --- a/src/features/settings/SettingsView.js +++ b/src/features/settings/SettingsView.js @@ -505,6 +505,9 @@ export class SettingsView extends LitElement { installingModels: { type: Object, state: true }, // Whisper related properties whisperModels: { type: Array, state: true }, + // Language properties + availableLanguages: { type: Array, state: true }, + currentLanguage: { type: String, state: true }, }; //////// after_modelStateService //////// @@ -538,6 +541,9 @@ export class SettingsView extends LitElement { this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this) this.autoUpdateEnabled = true; this.autoUpdateLoading = true; + // Language related + this.availableLanguages = []; + this.currentLanguage = 'en'; this.loadInitialData(); //////// after_modelStateService //////// } @@ -578,13 +584,30 @@ export class SettingsView extends LitElement { this.requestUpdate(); } + async handleLanguageChange(languageCode) { + if (!window.require) return; + + const { ipcRenderer } = window.require('electron'); + try { + const result = await ipcRenderer.invoke('settings:set-language', languageCode); + if (result.success) { + this.currentLanguage = languageCode; + console.log(`[SettingsView] Language changed to: ${languageCode}`); + } else { + console.error('[SettingsView] Failed to save language:', result.error); + } + } catch (error) { + console.error('[SettingsView] Failed to change language:', error); + } + } + //////// after_modelStateService //////// async loadInitialData() { if (!window.require) return; this.isLoading = true; const { ipcRenderer } = window.require('electron'); try { - const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection, shortcuts, ollamaStatus, whisperModelsResult] = await Promise.all([ + const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection, shortcuts, ollamaStatus, whisperModelsResult, availableLanguages, currentLanguage] = await Promise.all([ ipcRenderer.invoke('get-current-user'), ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드 ipcRenderer.invoke('model:get-all-keys'), @@ -595,7 +618,9 @@ export class SettingsView extends LitElement { ipcRenderer.invoke('get-content-protection-status'), ipcRenderer.invoke('get-current-shortcuts'), ipcRenderer.invoke('ollama:get-status'), - ipcRenderer.invoke('whisper:get-installed-models') + ipcRenderer.invoke('whisper:get-installed-models'), + ipcRenderer.invoke('settings:get-available-languages'), + ipcRenderer.invoke('settings:get-current-language') ]); if (userState && userState.isLoggedIn) this.firebaseUser = userState; @@ -608,6 +633,8 @@ export class SettingsView extends LitElement { this.presets = presets || []; this.isContentProtectionOn = contentProtection; this.shortcuts = shortcuts || {}; + this.availableLanguages = availableLanguages || []; + this.currentLanguage = currentLanguage || 'en'; if (this.presets.length > 0) { const firstUserPreset = this.presets.find(p => p.is_default === 0); if (firstUserPreset) this.selectedPreset = firstUserPreset; @@ -1563,6 +1590,21 @@ export class SettingsView extends LitElement { ${apiKeyManagementHTML} ${modelSelectionHTML} + +
+
+ + +
+
+