document.addEventListener('DOMContentLoaded', function() { const CONTEXT_PROFILES_STORAGE_KEY = 'contextProfiles'; const ACTIVE_CONTEXT_PROFILE_STORAGE_KEY = 'activeContextProfileId'; const CONTEXTS_BY_PROFILE_STORAGE_KEY = 'contextsByProfile'; const ACTIVE_AUTOMATION_STORAGE_KEY = 'activeAutomationId'; const DEFAULT_CONTEXT_PROFILE_ID = 'interview_software'; const DEFAULT_CONTEXT_PROFILES = [ { id: 'interview_software', name: 'Interview (Software Development)', mode: 'interview', systemPrompt: 'You are an interview assistant for software development. Keep responses concise, technically correct, and structured.' }, { id: 'meeting_standup', name: 'Meeting (Daily Standup)', mode: 'standup', systemPrompt: 'You are a standup meeting assistant. Focus on updates, blockers, owners, and next steps.' }, { id: 'meeting_sales', name: 'Meeting (Sales Call)', mode: 'meeting', systemPrompt: 'You are a sales call assistant. Focus on customer needs, objections, commitments, and clear follow-up actions.' } ]; const toggleButton = document.getElementById('toggleListening'); const pauseButton = document.getElementById('pauseListening'); const sessionContextProfile = document.getElementById('sessionContextProfile'); const sessionContextHint = document.getElementById('sessionContextHint'); const sessionAutomationSelect = document.getElementById('sessionAutomationSelect'); const runSelectedAutomationNowBtn = document.getElementById('runSelectedAutomationNow'); const sessionAutomationHint = document.getElementById('sessionAutomationHint'); const storeSessionToggle = document.getElementById('storeSessionToggle'); const forgetSessionBtn = document.getElementById('forgetSession'); const transcriptDiv = document.getElementById('transcript'); const aiResponseDiv = document.getElementById('aiResponse'); const apiKeyInput = document.getElementById('apiKeyInput'); const saveApiKeyButton = document.getElementById('saveApiKey'); const aiProviderSelect = document.getElementById('aiProvider'); const modelSelect = document.getElementById('modelSelect'); const apiKeyStatus = document.getElementById('apiKeyStatus'); const sttProviderSelect = document.getElementById('sttProvider'); const sttModelSelect = document.getElementById('sttModel'); const sttLanguageModeSelect = document.getElementById('sttLanguageMode'); const sttForcedLanguageInput = document.getElementById('sttForcedLanguage'); const sttTaskSelect = document.getElementById('sttTask'); const sttVadFilterToggle = document.getElementById('sttVadFilter'); const sttBeamSizeInput = document.getElementById('sttBeamSize'); const sttEndpointInput = document.getElementById('sttEndpointInput'); const sttApiKeyInput = document.getElementById('sttApiKeyInput'); const saveSttApiKeyButton = document.getElementById('saveSttApiKey'); const testSttConnectionButton = document.getElementById('testSttConnection'); const sttStatus = document.getElementById('sttStatus'); const showOverlayBtn = document.getElementById('showOverlay'); const overlayStatus = document.getElementById('overlayStatus'); const requestMicPermissionBtn = document.getElementById('requestMicPermission'); const micPermissionStatus = document.getElementById('micPermissionStatus'); const grantTabAccessBtn = document.getElementById('grantTabAccess'); const tabAccessStatus = document.getElementById('tabAccessStatus'); const speedModeToggle = document.getElementById('speedModeToggle'); const captureModeSelect = document.getElementById('captureModeSelect'); const autoOpenAssistantWindowToggle = document.getElementById('autoOpenAssistantWindow'); const autoStartToggle = document.getElementById('autoStartToggle'); const inputDeviceSelect = document.getElementById('inputDeviceSelect'); const inputDeviceStatus = document.getElementById('inputDeviceStatus'); const micLevelBar = document.getElementById('micLevelBar'); const startMicMonitorBtn = document.getElementById('startMicMonitor'); // Context management elements const contextFileInput = document.getElementById('contextFileInput'); const uploadContextBtn = document.getElementById('uploadContextBtn'); const contextTextInput = document.getElementById('contextTextInput'); const contextTypeSelect = document.getElementById('contextTypeSelect'); const contextTitleInput = document.getElementById('contextTitleInput'); const addContextBtn = document.getElementById('addContextBtn'); const contextList = document.getElementById('contextList'); const clearAllContextBtn = document.getElementById('clearAllContextBtn'); const contextProfileSelect = document.getElementById('contextProfileSelect'); const contextProfileHint = document.getElementById('contextProfileHint'); const profileNameInput = document.getElementById('profileNameInput'); const profileModeSelect = document.getElementById('profileModeSelect'); const profilePromptInput = document.getElementById('profilePromptInput'); const newProfileBtn = document.getElementById('newProfileBtn'); const saveProfileBtn = document.getElementById('saveProfileBtn'); const deleteProfileBtn = document.getElementById('deleteProfileBtn'); const profileManagerStatus = document.getElementById('profileManagerStatus'); // Multi-device elements const enableRemoteListening = document.getElementById('enableRemoteListening'); const remoteStatus = document.getElementById('remoteStatus'); const deviceInfo = document.getElementById('deviceInfo'); const accessUrl = document.getElementById('accessUrl'); const copyUrlBtn = document.getElementById('copyUrlBtn'); const qrCode = document.getElementById('qrCode'); const openSettingsBtn = document.getElementById('openSettings'); let isListening = false; let isPaused = false; let remoteServerActive = false; let contextProfiles = [...DEFAULT_CONTEXT_PROFILES]; let activeContextProfileId = DEFAULT_CONTEXT_PROFILE_ID; let activeAutomationId = ''; let availableAutomations = []; let draftProfileId = null; let micMonitorStream = null; let micMonitorCtx = null; let micMonitorSource = null; let micMonitorAnalyser = null; let micMonitorRaf = null; let editingContextId = null; // AI Provider configurations const aiProviders = { openai: { name: 'OpenAI', models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'], defaultModel: 'gpt-4o-mini', apiKeyPlaceholder: 'Enter your OpenAI API Key', requiresKey: true }, anthropic: { name: 'Anthropic', models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'], defaultModel: 'claude-3-5-sonnet-20241022', apiKeyPlaceholder: 'Enter your Anthropic API Key', requiresKey: true }, google: { name: 'Google', models: ['gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-pro'], defaultModel: 'gemini-1.5-flash', apiKeyPlaceholder: 'Enter your Google AI API Key', requiresKey: true }, deepseek: { name: 'DeepSeek', models: ['deepseek-chat', 'deepseek-reasoner'], defaultModel: 'deepseek-chat', apiKeyPlaceholder: 'Enter your DeepSeek API Key', requiresKey: true }, ollama: { name: 'Ollama', models: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'phi3'], defaultModel: 'llama3.2', apiKeyPlaceholder: 'No API key required (Local)', requiresKey: false } }; const sttProviders = { openai: { name: 'OpenAI Whisper', models: ['whisper-1'], defaultModel: 'whisper-1', requiresKey: true, keyPlaceholder: 'Enter your OpenAI API key for transcription' }, local: { name: 'Local faster-whisper bridge', models: ['small', 'medium', 'large-v3'], defaultModel: 'small', requiresKey: false, keyPlaceholder: 'Optional local STT API key', requiresEndpoint: true, defaultEndpoint: 'http://localhost:8790/transcribe' }, browser: { name: 'Browser SpeechRecognition', models: ['browser-default'], defaultModel: 'browser-default', requiresKey: false, keyPlaceholder: 'No API key required' } }; const modelCache = {}; const modelFetchState = {}; // Load saved settings chrome.storage.sync.get(['aiProvider', 'selectedModel', 'apiKeys', 'speedMode', 'captureMode', 'autoOpenAssistantWindow', 'inputDeviceId', 'autoStartEnabled', ACTIVE_AUTOMATION_STORAGE_KEY, 'sttProvider', 'sttModel', 'sttApiKeys', 'sttEndpoint', 'sttLanguageMode', 'sttForcedLanguage', 'sttTask', 'sttVadFilter', 'sttBeamSize'], (result) => { const savedProvider = result.aiProvider || 'openai'; const savedModel = result.selectedModel || aiProviders[savedProvider].defaultModel; const savedApiKeys = result.apiKeys || {}; const savedSttProvider = result.sttProvider || 'openai'; const savedSttModel = result.sttModel || sttProviders[savedSttProvider].defaultModel; const savedSttApiKeys = result.sttApiKeys || {}; const savedSttEndpoint = result.sttEndpoint || sttProviders.local.defaultEndpoint; const savedSttLanguageMode = result.sttLanguageMode || 'auto'; const savedSttForcedLanguage = result.sttForcedLanguage || ''; const savedSttTask = result.sttTask || 'transcribe'; const savedSttVadFilter = result.sttVadFilter !== false; const savedSttBeamSize = Number(result.sttBeamSize) || 5; const speedMode = Boolean(result.speedMode); const captureMode = result.captureMode || 'tab'; const autoOpenAssistantWindow = Boolean(result.autoOpenAssistantWindow); const savedInputDeviceId = result.inputDeviceId || ''; const autoStartEnabled = Boolean(result.autoStartEnabled); activeAutomationId = result[ACTIVE_AUTOMATION_STORAGE_KEY] || ''; aiProviderSelect.value = savedProvider; if (sttProviderSelect) sttProviderSelect.value = savedSttProvider; if (sttModelSelect) sttModelSelect.value = savedSttModel; if (sttLanguageModeSelect) sttLanguageModeSelect.value = savedSttLanguageMode; if (sttForcedLanguageInput) sttForcedLanguageInput.value = savedSttForcedLanguage; if (sttTaskSelect) sttTaskSelect.value = savedSttTask; if (sttVadFilterToggle) sttVadFilterToggle.checked = savedSttVadFilter; if (sttBeamSizeInput) sttBeamSizeInput.value = String(savedSttBeamSize); if (captureModeSelect) captureModeSelect.value = captureMode; if (speedModeToggle) speedModeToggle.checked = speedMode; if (autoOpenAssistantWindowToggle) autoOpenAssistantWindowToggle.checked = autoOpenAssistantWindow; if (autoStartToggle) autoStartToggle.checked = autoStartEnabled; refreshModelOptions(savedProvider, savedModel, savedApiKeys[savedProvider]); updateApiKeyInput(savedProvider); updateSttSettingsUI(savedSttProvider, savedSttModel, savedSttApiKeys[savedSttProvider], savedSttEndpoint); if (savedApiKeys[savedProvider] && aiProviders[savedProvider].requiresKey) { apiKeyInput.value = savedApiKeys[savedProvider]; updateApiKeyStatus('API Key Saved', 'success'); saveApiKeyButton.textContent = 'API Key Saved'; saveApiKeyButton.disabled = true; } if (inputDeviceSelect) { loadInputDevices(savedInputDeviceId); } loadAutomations(); }); function getModeLabel(mode) { const labels = { interview: 'Interview mode', meeting: 'Meeting mode', standup: 'Standup mode', custom: 'Custom mode' }; return labels[mode] || 'Custom mode'; } function getActiveProfile() { return contextProfiles.find((profile) => profile.id === activeContextProfileId) || contextProfiles[0]; } function createProfileId() { return `profile_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; } function updateProfileManagerStatus(message, type = '') { if (!profileManagerStatus) return; profileManagerStatus.textContent = message || ''; profileManagerStatus.className = `status-message ${type}`.trim(); } function renderAutomationSelector() { if (!sessionAutomationSelect) return; sessionAutomationSelect.innerHTML = ''; const autoOption = document.createElement('option'); autoOption.value = ''; autoOption.textContent = 'Automatic (all matching automations)'; sessionAutomationSelect.appendChild(autoOption); availableAutomations.forEach((automation) => { const option = document.createElement('option'); option.value = automation.id; option.textContent = automation.name || 'Untitled automation'; sessionAutomationSelect.appendChild(option); }); const exists = availableAutomations.some((item) => item.id === activeAutomationId); sessionAutomationSelect.value = exists ? activeAutomationId : ''; if (!exists) activeAutomationId = ''; updateAutomationHint(); } function updateAutomationHint() { if (!sessionAutomationHint) return; if (!activeAutomationId) { sessionAutomationHint.textContent = 'Session start/end uses all enabled automations that match trigger.'; return; } const selected = availableAutomations.find((item) => item.id === activeAutomationId); if (!selected) { sessionAutomationHint.textContent = 'Selected automation not found. Using automatic mode.'; return; } sessionAutomationHint.textContent = `Selected automation: ${selected.name || selected.id}`; } function loadAutomations() { chrome.runtime.sendMessage({ action: 'automation:list' }, (response) => { if (chrome.runtime.lastError) return; const list = response && Array.isArray(response.automations) ? response.automations : []; availableAutomations = list.filter((automation) => Boolean(automation.enabled)); renderAutomationSelector(); }); } function handleAutomationSettingsChanged(changes, areaName) { if (areaName !== 'sync') return; if (!changes.advancedSettings && !changes[ACTIVE_AUTOMATION_STORAGE_KEY]) return; if (changes[ACTIVE_AUTOMATION_STORAGE_KEY]) { activeAutomationId = changes[ACTIVE_AUTOMATION_STORAGE_KEY].newValue || ''; } loadAutomations(); } function loadProfileEditor(profileId) { if (!profileNameInput || !profileModeSelect || !profilePromptInput) return; const profile = contextProfiles.find((item) => item.id === profileId); if (!profile) return; draftProfileId = profile.id; profileNameInput.value = profile.name || ''; profileModeSelect.value = profile.mode || 'custom'; profilePromptInput.value = profile.systemPrompt || ''; } function updateProfileHints() { const profile = getActiveProfile(); if (!profile) return; const modeText = getModeLabel(profile.mode); const hint = `${modeText}. Prompt isolation is enabled for this profile.`; if (sessionContextHint) sessionContextHint.textContent = hint; if (contextProfileHint) contextProfileHint.textContent = `Editing contexts for: ${profile.name}`; } function renderProfileSelectors() { if (!sessionContextProfile || !contextProfileSelect) return; sessionContextProfile.innerHTML = ''; contextProfileSelect.innerHTML = ''; contextProfiles.forEach((profile) => { const sessionOption = document.createElement('option'); sessionOption.value = profile.id; sessionOption.textContent = profile.name; if (profile.id === activeContextProfileId) { sessionOption.selected = true; } sessionContextProfile.appendChild(sessionOption); const contextOption = document.createElement('option'); contextOption.value = profile.id; contextOption.textContent = profile.name; if (profile.id === activeContextProfileId) { contextOption.selected = true; } contextProfileSelect.appendChild(contextOption); }); if (sessionContextProfile.value !== activeContextProfileId) { sessionContextProfile.value = activeContextProfileId; } if (contextProfileSelect.value !== activeContextProfileId) { contextProfileSelect.value = activeContextProfileId; } loadProfileEditor(activeContextProfileId); updateProfileHints(); } async function ensureContextProfiles() { const result = await chrome.storage.sync.get([CONTEXT_PROFILES_STORAGE_KEY, ACTIVE_CONTEXT_PROFILE_STORAGE_KEY]); const savedProfiles = Array.isArray(result[CONTEXT_PROFILES_STORAGE_KEY]) && result[CONTEXT_PROFILES_STORAGE_KEY].length ? result[CONTEXT_PROFILES_STORAGE_KEY] : DEFAULT_CONTEXT_PROFILES; const preferredId = result[ACTIVE_CONTEXT_PROFILE_STORAGE_KEY] || savedProfiles[0].id || DEFAULT_CONTEXT_PROFILE_ID; const resolvedId = savedProfiles.some((profile) => profile.id === preferredId) ? preferredId : savedProfiles[0].id; contextProfiles = savedProfiles; activeContextProfileId = resolvedId; await chrome.storage.sync.set({ [CONTEXT_PROFILES_STORAGE_KEY]: savedProfiles, [ACTIVE_CONTEXT_PROFILE_STORAGE_KEY]: resolvedId }); renderProfileSelectors(); } async function persistProfiles(nextProfiles, nextActiveProfileId) { contextProfiles = nextProfiles; activeContextProfileId = nextActiveProfileId; await chrome.storage.sync.set({ [CONTEXT_PROFILES_STORAGE_KEY]: contextProfiles, [ACTIVE_CONTEXT_PROFILE_STORAGE_KEY]: activeContextProfileId }); renderProfileSelectors(); } async function saveCurrentProfile() { const name = profileNameInput ? profileNameInput.value.trim() : ''; const mode = profileModeSelect ? profileModeSelect.value : 'custom'; const systemPrompt = profilePromptInput ? profilePromptInput.value.trim() : ''; if (!name) { updateProfileManagerStatus('Profile name is required.', 'error'); profileNameInput.focus(); return; } const targetId = draftProfileId || activeContextProfileId || createProfileId(); const existingIndex = contextProfiles.findIndex((profile) => profile.id === targetId); const nextProfile = { id: targetId, name, mode, systemPrompt }; const nextProfiles = [...contextProfiles]; if (existingIndex >= 0) { nextProfiles[existingIndex] = nextProfile; } else { nextProfiles.push(nextProfile); } await persistProfiles(nextProfiles, targetId); updateProfileManagerStatus('Profile saved.', 'success'); } async function createNewProfileDraft() { const newProfile = { id: createProfileId(), name: 'New Context Profile', mode: 'custom', systemPrompt: '' }; await persistProfiles([...contextProfiles, newProfile], newProfile.id); updateProfileManagerStatus('New profile created. Update name and prompt, then click Save Profile.', ''); if (profileNameInput) { profileNameInput.focus(); profileNameInput.select(); } } async function deleteActiveProfile() { if (contextProfiles.length <= 1) { updateProfileManagerStatus('At least one profile is required.', 'error'); return; } const current = getActiveProfile(); if (!current) return; const confirmed = window.confirm(`Delete profile "${current.name}" and its scoped contexts?`); if (!confirmed) return; const nextProfiles = contextProfiles.filter((profile) => profile.id !== current.id); const nextActive = nextProfiles[0].id; await persistProfiles(nextProfiles, nextActive); const localResult = await chrome.storage.local.get([CONTEXTS_BY_PROFILE_STORAGE_KEY]); const byProfile = localResult[CONTEXTS_BY_PROFILE_STORAGE_KEY] || {}; if (byProfile[current.id]) { delete byProfile[current.id]; await chrome.storage.local.set({ [CONTEXTS_BY_PROFILE_STORAGE_KEY]: byProfile }); } await loadContexts(); updateProfileManagerStatus(`Deleted profile "${current.name}".`, 'success'); } async function getContextsForProfile(profileId) { const result = await chrome.storage.local.get([CONTEXTS_BY_PROFILE_STORAGE_KEY, 'contexts']); const byProfile = result[CONTEXTS_BY_PROFILE_STORAGE_KEY] || {}; if (Array.isArray(byProfile[profileId])) { return byProfile[profileId]; } return result.contexts || []; } async function saveContextsForProfile(profileId, contexts) { const result = await chrome.storage.local.get([CONTEXTS_BY_PROFILE_STORAGE_KEY, 'contexts']); const byProfile = result[CONTEXTS_BY_PROFILE_STORAGE_KEY] || {}; byProfile[profileId] = contexts; await chrome.storage.local.set({ [CONTEXTS_BY_PROFILE_STORAGE_KEY]: byProfile }); // Keep legacy key for compatibility with older paths. if (profileId === DEFAULT_CONTEXT_PROFILE_ID) { await chrome.storage.local.set({ contexts }); } } // Load and display saved contexts for the active profile. ensureContextProfiles().then(loadContexts); function setTranscriptText(text) { if (transcriptDiv) transcriptDiv.textContent = text; } function setAIResponseText(text) { if (aiResponseDiv) aiResponseDiv.textContent = text; } // Helper functions function updateModelOptions(provider, selectedModel = null, modelsOverride = null) { const models = modelsOverride || modelCache[provider] || aiProviders[provider].models; modelSelect.innerHTML = ''; models.forEach(model => { const option = document.createElement('option'); option.value = model; option.textContent = model; if (selectedModel === model || (!selectedModel && model === aiProviders[provider].defaultModel)) { option.selected = true; } modelSelect.appendChild(option); }); } function updateApiKeyInput(provider) { const providerConfig = aiProviders[provider]; apiKeyInput.placeholder = providerConfig.apiKeyPlaceholder; apiKeyInput.disabled = !providerConfig.requiresKey; saveApiKeyButton.disabled = !providerConfig.requiresKey; if (!providerConfig.requiresKey) { apiKeyInput.value = ''; updateApiKeyStatus('No API key required', 'success'); } else { updateApiKeyStatus('', ''); } } function updateApiKeyStatus(message, type) { apiKeyStatus.textContent = message; apiKeyStatus.className = `status-message ${type}`; } function updateSttStatus(message, type) { if (!sttStatus) return; sttStatus.textContent = message; sttStatus.className = `status-message ${type}`; } function updateSttSettingsUI(provider, selectedModel, savedKey, savedEndpoint) { if (!sttProviderSelect || !sttModelSelect || !sttApiKeyInput || !saveSttApiKeyButton) return; const cfg = sttProviders[provider] || sttProviders.openai; sttModelSelect.innerHTML = ''; cfg.models.forEach((model) => { const option = document.createElement('option'); option.value = model; option.textContent = model; if (selectedModel === model || (!selectedModel && model === cfg.defaultModel)) { option.selected = true; } sttModelSelect.appendChild(option); }); sttModelSelect.disabled = provider === 'browser'; sttApiKeyInput.disabled = provider === 'browser'; sttApiKeyInput.placeholder = cfg.keyPlaceholder; if (sttEndpointInput) { sttEndpointInput.style.display = cfg.requiresEndpoint ? '' : 'none'; sttEndpointInput.value = savedEndpoint || cfg.defaultEndpoint || ''; } if (sttForcedLanguageInput && sttLanguageModeSelect) { sttForcedLanguageInput.disabled = sttLanguageModeSelect.value !== 'forced'; } if (cfg.requiresKey) { sttApiKeyInput.value = savedKey || ''; if (savedKey) { saveSttApiKeyButton.textContent = 'STT API Key Saved'; saveSttApiKeyButton.disabled = true; updateSttStatus('STT key saved. Tab/Mixed transcription is enabled.', 'success'); } else { saveSttApiKeyButton.textContent = 'Save STT API Key'; saveSttApiKeyButton.disabled = false; updateSttStatus('Tab/Mixed mode requires cloud STT API key.', ''); } } else if (provider === 'local') { sttApiKeyInput.value = savedKey || ''; saveSttApiKeyButton.textContent = savedKey ? 'Local STT Key Saved' : 'Save Local STT Key'; saveSttApiKeyButton.disabled = false; updateSttStatus('Local STT active. Ensure local bridge server is running.', ''); } else { sttApiKeyInput.value = ''; saveSttApiKeyButton.textContent = 'No API Key Needed'; saveSttApiKeyButton.disabled = true; updateSttStatus('Browser STT is best for mic mode and may not capture tab speakers.', ''); } } function updateMicPermissionStatus(message, type) { if (!micPermissionStatus) return; micPermissionStatus.textContent = message; micPermissionStatus.className = `status-message ${type}`; } function updateOverlayStatus(message, type) { if (!overlayStatus) return; overlayStatus.textContent = message; overlayStatus.className = `status-message ${type}`; } function updateInputDeviceStatus(message, type) { if (!inputDeviceStatus) return; inputDeviceStatus.textContent = message; inputDeviceStatus.className = `status-message ${type}`; } function updateTabAccessStatus(message, type) { if (!tabAccessStatus) return; tabAccessStatus.textContent = message; tabAccessStatus.className = `status-message ${type}`; } function pickModel(provider, preferredModel, models) { if (preferredModel && models.includes(preferredModel)) { return preferredModel; } if (aiProviders[provider].defaultModel && models.includes(aiProviders[provider].defaultModel)) { return aiProviders[provider].defaultModel; } return models[0]; } async function refreshModelOptions(provider, preferredModel, apiKey) { if (modelFetchState[provider]) { return; } modelSelect.disabled = true; modelSelect.innerHTML = ''; modelFetchState[provider] = true; try { let models = null; if (provider === 'ollama') { models = await fetchOllamaModels(); } else if (aiProviders[provider].requiresKey && apiKey) { models = await fetchRemoteModels(provider, apiKey); } if (models && models.length) { modelCache[provider] = models; } } catch (error) { console.warn(`Failed to fetch models for ${provider}:`, error); } finally { modelFetchState[provider] = false; const availableModels = modelCache[provider] || aiProviders[provider].models; const selected = pickModel(provider, preferredModel, availableModels); updateModelOptions(provider, selected, availableModels); chrome.storage.sync.set({ selectedModel: selected }); modelSelect.disabled = false; } } async function loadInputDevices(preferredDeviceId = '') { if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { updateInputDeviceStatus('Device enumeration is not supported in this browser.', 'error'); return; } try { const devices = await navigator.mediaDevices.enumerateDevices(); const inputs = devices.filter(device => device.kind === 'audioinput'); const hasLabels = inputs.some(device => device.label); inputDeviceSelect.innerHTML = ''; if (!inputs.length) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No input devices found'; inputDeviceSelect.appendChild(option); inputDeviceSelect.disabled = true; updateInputDeviceStatus('No microphone devices detected.', 'error'); return; } inputs.forEach((device, index) => { const option = document.createElement('option'); option.value = device.deviceId; option.textContent = device.label || `Microphone ${index + 1}`; if (device.deviceId === preferredDeviceId) { option.selected = true; } inputDeviceSelect.appendChild(option); }); inputDeviceSelect.disabled = false; const selectedOption = inputDeviceSelect.options[inputDeviceSelect.selectedIndex]; if (!hasLabels) { updateInputDeviceStatus('Grant mic permission to see device names.', ''); } else { updateInputDeviceStatus(`Selected: ${selectedOption ? selectedOption.textContent : 'Unknown'}`, ''); } } catch (error) { console.warn('Failed to enumerate devices:', error); updateInputDeviceStatus('Failed to list input devices.', 'error'); } } function stopMicMonitor() { if (micMonitorRaf) { cancelAnimationFrame(micMonitorRaf); micMonitorRaf = null; } if (micMonitorSource) { try { micMonitorSource.disconnect(); } catch (error) { console.warn('Failed to disconnect mic monitor source:', error); } micMonitorSource = null; } if (micMonitorAnalyser) { try { micMonitorAnalyser.disconnect(); } catch (error) { console.warn('Failed to disconnect mic monitor analyser:', error); } micMonitorAnalyser = null; } if (micMonitorCtx) { micMonitorCtx.close(); micMonitorCtx = null; } if (micMonitorStream) { micMonitorStream.getTracks().forEach(track => track.stop()); micMonitorStream = null; } if (micLevelBar) { micLevelBar.style.width = '0%'; } } async function startMicMonitor() { if (!micLevelBar || !inputDeviceSelect) return; stopMicMonitor(); updateInputDeviceStatus('Requesting microphone access...', ''); const deviceId = inputDeviceSelect.value; const constraints = deviceId ? { audio: { deviceId: { exact: deviceId } } } : { audio: true }; try { micMonitorStream = await navigator.mediaDevices.getUserMedia(constraints); micMonitorCtx = new AudioContext(); micMonitorAnalyser = micMonitorCtx.createAnalyser(); micMonitorAnalyser.fftSize = 512; micMonitorAnalyser.smoothingTimeConstant = 0.8; micMonitorSource = micMonitorCtx.createMediaStreamSource(micMonitorStream); micMonitorSource.connect(micMonitorAnalyser); const data = new Uint8Array(micMonitorAnalyser.fftSize); const tick = () => { if (!micMonitorAnalyser) return; micMonitorAnalyser.getByteTimeDomainData(data); let sum = 0; for (let i = 0; i < data.length; i++) { const v = (data[i] - 128) / 128; sum += v * v; } const rms = Math.sqrt(sum / data.length); const normalized = Math.min(1, rms * 2.5); micLevelBar.style.width = `${Math.round(normalized * 100)}%`; micMonitorRaf = requestAnimationFrame(tick); }; micMonitorRaf = requestAnimationFrame(tick); const selectedOption = inputDeviceSelect.options[inputDeviceSelect.selectedIndex]; updateInputDeviceStatus(`Mic monitor active: ${selectedOption ? selectedOption.textContent : 'Unknown'}`, 'success'); } catch (error) { console.warn('Failed to start mic monitor:', error); if (error && error.name === 'NotAllowedError') { updateInputDeviceStatus('Microphone permission denied. Click "Request Microphone Permission".', 'error'); } else if (error && error.name === 'NotFoundError') { updateInputDeviceStatus('No microphone found for the selected device.', 'error'); } else { updateInputDeviceStatus('Microphone permission denied or unavailable.', 'error'); } } } async function fetchRemoteModels(provider, apiKey) { if (provider === 'openai') { return fetchOpenAIModels(apiKey); } if (provider === 'anthropic') { return fetchAnthropicModels(apiKey); } if (provider === 'google') { return fetchGoogleModels(apiKey); } if (provider === 'deepseek') { return fetchDeepSeekModels(apiKey); } return []; } async function fetchOpenAIModels(apiKey) { const response = await fetch('https://api.openai.com/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!response.ok) { throw new Error(`OpenAI models request failed: ${response.status}`); } const data = await response.json(); const ids = (data.data || []).map((item) => item.id).filter(Boolean); const chatModels = ids.filter((id) => ( id.startsWith('gpt-') || id.startsWith('o1') || id.startsWith('o3') || id.startsWith('o4') || id.startsWith('o5') )); const models = chatModels.length ? chatModels : ids; return Array.from(new Set(models)).sort(); } async function fetchAnthropicModels(apiKey) { const response = await fetch('https://api.anthropic.com/v1/models', { headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' } }); if (!response.ok) { throw new Error(`Anthropic models request failed: ${response.status}`); } const data = await response.json(); const items = data.data || data.models || []; const ids = items.map((item) => item.id || item.name).filter(Boolean); return Array.from(new Set(ids)).sort(); } async function fetchGoogleModels(apiKey) { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`); if (!response.ok) { throw new Error(`Google models request failed: ${response.status}`); } const data = await response.json(); const models = (data.models || []) .filter((model) => (model.supportedGenerationMethods || []).includes('generateContent')) .map((model) => model.name || '') .map((name) => name.replace(/^models\//, '')) .filter(Boolean); return Array.from(new Set(models)).sort(); } async function fetchDeepSeekModels(apiKey) { const response = await fetch('https://api.deepseek.com/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!response.ok) { throw new Error(`DeepSeek models request failed: ${response.status}`); } const data = await response.json(); const ids = (data.data || []).map((item) => item.id).filter(Boolean); return Array.from(new Set(ids)).sort(); } async function fetchOllamaModels() { const response = await fetch('http://localhost:11434/api/tags'); if (!response.ok) { throw new Error(`Ollama models request failed: ${response.status}`); } const data = await response.json(); const models = (data.models || []).map((model) => model.name).filter(Boolean); return Array.from(new Set(models)).sort(); } // Context Management Functions async function loadContexts() { const contexts = await getContextsForProfile(activeContextProfileId); displayContexts(contexts); updateManageTabCount(contexts.length); } function displayContexts(contexts) { contextList.innerHTML = ''; if (contexts.length === 0) { contextList.innerHTML = '
No context added yet. Add your CV or job description to get better responses!
'; return; } contexts.forEach((context, index) => { const contextItem = document.createElement('div'); contextItem.className = 'context-item'; const info = document.createElement('div'); info.className = 'context-item-info'; const title = document.createElement('div'); title.className = 'context-item-title'; title.textContent = context.title || 'Untitled context'; if (context.type) { const typeBadge = document.createElement('span'); typeBadge.style.fontWeight = '400'; typeBadge.style.color = '#666'; typeBadge.textContent = ` • ${context.type}`; title.appendChild(typeBadge); } const preview = document.createElement('div'); preview.className = 'context-item-preview'; const previewText = context.content || ''; preview.textContent = `${previewText.substring(0, 100)}${previewText.length > 100 ? '...' : ''}`; info.appendChild(title); info.appendChild(preview); const actions = document.createElement('div'); actions.className = 'context-item-actions'; const editButton = document.createElement('button'); editButton.className = 'edit-btn'; editButton.textContent = '✏️ Edit'; editButton.addEventListener('click', () => { startEditingContext(index); }); const deleteButton = document.createElement('button'); deleteButton.className = 'delete-btn danger-btn'; deleteButton.textContent = '🗑️ Delete'; deleteButton.addEventListener('click', () => { deleteContext(index); }); actions.appendChild(editButton); actions.appendChild(deleteButton); contextItem.appendChild(info); contextItem.appendChild(actions); contextList.appendChild(contextItem); }); } function updateManageTabCount(count) { const manageTab = document.querySelector('[data-tab="manage"]'); manageTab.textContent = `Manage (${count})`; } async function saveContext(title, content) { if (!title.trim() || !content.trim()) { alert('Please provide both title and content'); return; } // Optional basic guard for extremely large items (>4MB) const approxBytes = new Blob([content]).size; if (approxBytes > 4 * 1024 * 1024) { alert('This context is too large to store locally. Please split it into smaller parts.'); return; } const contexts = await getContextsForProfile(activeContextProfileId); const nextType = (contextTypeSelect && contextTypeSelect.value) || 'general'; const nowIso = new Date().toISOString(); const existingIndex = editingContextId ? contexts.findIndex((context) => context.id === editingContextId) : -1; if (existingIndex >= 0) { const existing = contexts[existingIndex]; contexts[existingIndex] = { ...existing, title: title.trim(), content: content.trim(), type: nextType, updatedAt: nowIso }; } else { contexts.push({ id: Date.now(), title: title.trim(), content: content.trim(), type: nextType, createdAt: nowIso }); } await saveContextsForProfile(activeContextProfileId, contexts); loadContexts(); // Clear inputs contextTitleInput.value = ''; contextTextInput.value = ''; if (contextTypeSelect) contextTypeSelect.value = 'general'; resetEditingContext(); // Switch to manage tab switchTab('manage'); } async function deleteContext(index) { if (!confirm('Are you sure you want to delete this context?')) return; const contexts = await getContextsForProfile(activeContextProfileId); const target = contexts[index]; contexts.splice(index, 1); await saveContextsForProfile(activeContextProfileId, contexts); if (target && editingContextId && target.id === editingContextId) { resetEditingContext(); } loadContexts(); } async function clearAllContexts() { if (!confirm('Are you sure you want to delete all contexts? This cannot be undone.')) return; await saveContextsForProfile(activeContextProfileId, []); resetEditingContext(); loadContexts(); } function resetEditingContext() { editingContextId = null; if (addContextBtn) { addContextBtn.textContent = 'Save Context'; } } function switchTab(tabName) { // Update tab buttons document.querySelectorAll('.tab-button').forEach(btn => { btn.classList.remove('active'); }); document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); // Update tab content document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tabName}Tab`).classList.add('active'); } async function processFile(file) { const lowerName = (file.name || '').toLowerCase(); let content = ''; if (file.type === 'text/plain' || lowerName.endsWith('.txt')) { content = await readFileAsText(file); } else if (file.type === 'application/pdf' || lowerName.endsWith('.pdf')) { content = await extractPdfText(file); } else if ( file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || lowerName.endsWith('.docx') ) { content = await extractDocxText(file); } else if ( file.type === 'application/msword' || lowerName.endsWith('.doc') ) { throw new Error('Legacy .doc is not supported yet. Please convert to .docx, .pdf, or .txt.'); } else { throw new Error(`Unsupported file type: ${file.name}`); } if (!content || !content.trim()) { throw new Error(`No readable text found in ${file.name}`); } return { title: file.name, content: content.trim() }; } function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(String(e.target.result || '')); reader.onerror = () => reject(new Error('Failed to read file as text')); reader.readAsText(file); }); } function readFileAsArrayBuffer(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = () => reject(new Error('Failed to read file bytes')); reader.readAsArrayBuffer(file); }); } async function extractPdfText(file) { const buffer = await readFileAsArrayBuffer(file); const bytes = new Uint8Array(buffer); const latin = decodeBytes(bytes, 'latin1'); const parts = []; const seen = new Set(); const pushPart = (value) => { const normalized = normalizeExtractedText(value); if (!normalized) return; if (seen.has(normalized)) return; seen.add(normalized); parts.push(normalized); }; // Heuristic 1: Extract UTF-16BE hex strings often embedded in metadata. const utf16HexRegex = //g; let utf16Match; while ((utf16Match = utf16HexRegex.exec(latin)) !== null) { const decoded = decodeUtf16BeHex(utf16Match[1]); if (decoded) pushPart(decoded); } // Heuristic 2: Extract decoded /ecv-data JSON if present. const ecvDataMatch = latin.match(/\/ecv-data\s*/); if (ecvDataMatch && ecvDataMatch[1]) { const decodedJson = decodeUtf16BeHex(ecvDataMatch[1]); if (decodedJson) { try { const obj = JSON.parse(decodedJson); if (isLikelyEnhancvPayload(obj)) { return buildEnhancvResumeText(obj); } pushPart(flattenJsonText(obj)); } catch (_) { pushPart(decodedJson); } } } // Heuristic 3: Decompress FlateDecode streams and extract readable strings. const streamRegex = /<<[\s\S]*?\/Filter\s*\/FlateDecode[\s\S]*?>>\s*stream\r?\n/g; let streamMatch; while ((streamMatch = streamRegex.exec(latin)) !== null) { const streamStart = streamMatch.index + streamMatch[0].length; const endStreamIndex = latin.indexOf('\nendstream', streamStart); if (endStreamIndex === -1) continue; const compressed = bytes.slice(streamStart, endStreamIndex); let decompressed; try { decompressed = await inflateBytes(compressed, 'deflate'); } catch (_) { try { decompressed = await inflateBytes(compressed, 'deflate-raw'); } catch (_) { decompressed = null; } } if (!decompressed) continue; const decompressedText = decodeBytes(decompressed, 'latin1'); extractPdfLikeStrings(decompressedText).forEach(pushPart); } // Heuristic 4: Pull literal strings from uncompressed body. extractPdfLikeStrings(latin).forEach(pushPart); return cleanPdfAggregate(parts.join('\n\n')); } async function extractDocxText(file) { const buffer = await readFileAsArrayBuffer(file); const bytes = new Uint8Array(buffer); const entries = parseZipEntries(bytes); const xmlTargets = entries .filter((entry) => /^word\/(document|header\d+|footer\d+|footnotes)\.xml$/i.test(entry.name)) .sort((a, b) => { const order = (name) => { if (name === 'word/document.xml') return 0; if (name.startsWith('word/header')) return 1; if (name.startsWith('word/footer')) return 2; return 3; }; return order(a.name) - order(b.name) || a.name.localeCompare(b.name); }); if (xmlTargets.length === 0) { throw new Error('Could not find readable DOCX XML content'); } const sections = []; for (const entry of xmlTargets) { const xmlBytes = await unzipEntry(bytes, entry); const xml = decodeBytes(xmlBytes, 'utf-8'); const text = extractTextFromDocxXml(xml); if (text) { sections.push(text); } } return sections.join('\n\n'); } function decodeBytes(bytes, encoding) { return new TextDecoder(encoding, { fatal: false }).decode(bytes); } function decodeUtf16BeHex(hex) { const cleanHex = hex.replace(/[^0-9A-Fa-f]/g, ''); if (cleanHex.length < 4 || cleanHex.length % 2 !== 0) { return ''; } const raw = new Uint8Array(cleanHex.length / 2); for (let i = 0; i < cleanHex.length; i += 2) { raw[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16); } return decodeBytes(raw, 'utf-16be'); } function normalizeExtractedText(value) { if (!value) return ''; const text = String(value) .replace(/\r\n/g, '\n') .replace(/\u0000/g, '') .replace(/[ \t]+\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .trim(); if (!text) return ''; const printable = (text.match(/[A-Za-z0-9]/g) || []).length; if (printable < 8) return ''; if (/^node\d{6,}$/i.test(text)) return ''; if (/[�]/.test(text)) return ''; if (/^[A-Za-z0-9+/=]{40,}$/.test(text)) return ''; return text; } function decodePdfLiteralString(value) { let out = ''; for (let i = 0; i < value.length; i++) { const ch = value[i]; if (ch !== '\\') { out += ch; continue; } const next = value[++i]; if (next === undefined) break; if (next === 'n') out += '\n'; else if (next === 'r') out += '\r'; else if (next === 't') out += '\t'; else if (next === 'b') out += '\b'; else if (next === 'f') out += '\f'; else if (next === '(' || next === ')' || next === '\\') out += next; else if (/[0-7]/.test(next)) { let octal = next; for (let j = 0; j < 2; j++) { if (i + 1 < value.length && /[0-7]/.test(value[i + 1])) { octal += value[++i]; } else { break; } } out += String.fromCharCode(Number.parseInt(octal, 8)); } else { out += next; } } return out; } function extractPdfLikeStrings(source) { const candidates = []; const literalRegex = /\(([^()\r\n]{3,})\)/g; let literalMatch; while ((literalMatch = literalRegex.exec(source)) !== null) { candidates.push(decodePdfLiteralString(literalMatch[1])); } const utf16HexRegex = //g; let hexMatch; while ((hexMatch = utf16HexRegex.exec(source)) !== null) { candidates.push(decodeUtf16BeHex(hexMatch[1])); } return candidates; } async function inflateBytes(data, format) { if (typeof DecompressionStream !== 'function') { throw new Error('DecompressionStream is unavailable'); } const stream = new Response(data).body.pipeThrough(new DecompressionStream(format)); const result = await new Response(stream).arrayBuffer(); return new Uint8Array(result); } function flattenJsonText(input) { const chunks = []; const walk = (value) => { if (value == null) return; if (typeof value === 'string') { const cleaned = value.replace(/<[^>]+>/g, ' ').trim(); if (cleaned) chunks.push(cleaned); return; } if (Array.isArray(value)) { value.forEach(walk); return; } if (typeof value === 'object') { Object.values(value).forEach(walk); } }; walk(input); return chunks.join('\n'); } function isLikelyEnhancvPayload(obj) { if (!obj || typeof obj !== 'object') return false; if (!Array.isArray(obj.sections)) return false; if (!obj.header || typeof obj.header !== 'object') return false; return Boolean(obj.header.name || obj.header.title || obj.header.email); } function buildEnhancvResumeText(data) { const lines = []; const header = data.header || {}; const push = (line = '') => { const clean = cleanInline(line); if (!clean && line !== '') return; lines.push(clean); }; const formatDateRange = (range) => { if (!range || typeof range !== 'object') return ''; const from = formatMonthYear(range.fromMonth, range.fromYear); const to = range.isOngoing ? 'Present' : formatMonthYear(range.toMonth, range.toYear); if (from && to) return `${from} - ${to}`; return from || to || ''; }; push(header.name || data.Title || 'Resume'); if (header.title) push(header.title); const contact = [header.email, header.phone, header.location].filter(Boolean).join(' | '); if (contact) push(contact); if (header.link) push(header.link); for (const section of data.sections || []) { const sectionType = section.__t || ''; const sectionName = cleanInline(section.name || sectionType.replace(/Section$/, '')); if (sectionName) { push(''); push(sectionName.toUpperCase()); } if (Array.isArray(section.items)) { for (const item of section.items) { if (!item || typeof item !== 'object') continue; if (sectionType === 'ExperienceSection') { const role = cleanInline(item.position); const company = cleanInline(item.workplace); const location = cleanInline(item.location); const dateRange = formatDateRange(item.dateRange); const heading = [role, company].filter(Boolean).join(' @ '); if (heading) push(heading); const meta = [location, dateRange].filter(Boolean).join(' | '); if (meta) push(meta); if (Array.isArray(item.bullets)) { item.bullets .map(cleanInline) .filter(Boolean) .forEach((bullet) => push(`- ${bullet}`)); } continue; } if (sectionType === 'EducationSection') { const degree = cleanInline(item.degree); const institution = cleanInline(item.institution); const location = cleanInline(item.location); const dateRange = formatDateRange(item.dateRange); const heading = [degree, institution].filter(Boolean).join(' - '); if (heading) push(heading); const meta = [location, dateRange].filter(Boolean).join(' | '); if (meta) push(meta); continue; } if (sectionType === 'SummarySection') { push(cleanInline(item.text)); continue; } if (sectionType === 'LanguageSection') { const name = cleanInline(item.name); const level = cleanInline(item.levelText); if (name || level) push([name, level].filter(Boolean).join(' - ')); continue; } if (sectionType === 'TechnologySection') { if (Array.isArray(item.tags)) { push(item.tags.map(cleanInline).filter(Boolean).join(', ')); } continue; } if (Array.isArray(item.bullets)) { item.bullets .map(cleanInline) .filter(Boolean) .forEach((bullet) => push(`- ${bullet}`)); } else if (item.title || item.description) { push([cleanInline(item.title), cleanInline(item.description)].filter(Boolean).join(': ')); } } } } return cleanPdfAggregate(lines.join('\n')); } function formatMonthYear(month, year) { if (!year && year !== 0) return ''; const y = String(year); if (month == null || month < 0 || month > 11) return y; const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return `${monthNames[month]} ${y}`; } function cleanInline(value) { if (value == null) return ''; let text = String(value); text = text .replace(//gi, '\n') .replace(/<\/div>/gi, '\n') .replace(/<[^>]+>/g, ' ') .replace(/\u0000/g, '') .replace(/\s+/g, ' ') .trim(); text = decodeXmlEntities(text); text = text.replace(/\s+([,.;:!?])/g, '$1'); return text; } function cleanPdfAggregate(value) { if (!value) return ''; const rawLines = String(value).replace(/\r\n/g, '\n').split('\n'); const out = []; let previousBlank = false; for (const raw of rawLines) { const line = raw.trim(); if (!line) { if (!previousBlank && out.length > 0) { out.push(''); previousBlank = true; } continue; } if (isPdfNoiseLine(line)) continue; out.push(line); previousBlank = false; } return out.join('\n').replace(/\n{3,}/g, '\n\n').trim(); } function isPdfNoiseLine(line) { if (/^%PDF-/i.test(line)) return true; if (/^\d+\s+\d+\s+obj$/i.test(line)) return true; if (/^(endobj|stream|endstream|xref|trailer|startxref)$/i.test(line)) return true; if (/^node\d{6,}$/i.test(line)) return true; if (/^\/[A-Za-z][A-Za-z0-9]+/.test(line)) return true; if (/^[A-Za-z0-9+/=]{50,}$/.test(line)) return true; if (/[�]/.test(line)) return true; if (!/[A-Za-z]/.test(line) && line.length > 45) return true; const odd = (line.match(/[^A-Za-z0-9 .,;:!?'"\-_/()[\]@&%+]/g) || []).length; if (line.length >= 30 && odd / line.length > 0.35) return true; return false; } function parseZipEntries(bytes) { const eocd = findZipEndOfCentralDirectory(bytes); if (eocd < 0) { throw new Error('Invalid DOCX file (EOCD not found)'); } const centralDirSize = readUint32LE(bytes, eocd + 12); const centralDirOffset = readUint32LE(bytes, eocd + 16); const centralDirEnd = centralDirOffset + centralDirSize; const entries = []; let cursor = centralDirOffset; while (cursor + 46 <= centralDirEnd && cursor + 46 <= bytes.length) { if (readUint32LE(bytes, cursor) !== 0x02014b50) { break; } const compression = readUint16LE(bytes, cursor + 10); const compressedSize = readUint32LE(bytes, cursor + 20); const fileNameLength = readUint16LE(bytes, cursor + 28); const extraLength = readUint16LE(bytes, cursor + 30); const commentLength = readUint16LE(bytes, cursor + 32); const localHeaderOffset = readUint32LE(bytes, cursor + 42); const nameStart = cursor + 46; const nameEnd = nameStart + fileNameLength; const name = decodeBytes(bytes.slice(nameStart, nameEnd), 'utf-8'); entries.push({ name, compression, compressedSize, localHeaderOffset }); cursor = nameEnd + extraLength + commentLength; } return entries; } function findZipEndOfCentralDirectory(bytes) { const minIndex = Math.max(0, bytes.length - 0xffff - 22); for (let i = bytes.length - 22; i >= minIndex; i--) { if (readUint32LE(bytes, i) === 0x06054b50) { return i; } } return -1; } async function unzipEntry(zipBytes, entry) { const offset = entry.localHeaderOffset; if (readUint32LE(zipBytes, offset) !== 0x04034b50) { throw new Error(`Invalid local header for ${entry.name}`); } const fileNameLength = readUint16LE(zipBytes, offset + 26); const extraLength = readUint16LE(zipBytes, offset + 28); const dataStart = offset + 30 + fileNameLength + extraLength; const dataEnd = dataStart + entry.compressedSize; const compressed = zipBytes.slice(dataStart, dataEnd); if (entry.compression === 0) { return compressed; } if (entry.compression === 8) { try { return await inflateBytes(compressed, 'deflate-raw'); } catch (_) { return await inflateBytes(compressed, 'deflate'); } } throw new Error(`Unsupported DOCX compression method (${entry.compression}) for ${entry.name}`); } function readUint16LE(bytes, offset) { return bytes[offset] | (bytes[offset + 1] << 8); } function readUint32LE(bytes, offset) { return ( bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24) ) >>> 0; } function extractTextFromDocxXml(xml) { if (!xml) return ''; let text = xml; text = text.replace(//g, '\t'); text = text.replace(//g, '\n'); text = text.replace(//g, '\n'); text = text.replace(/<\/w:p>/g, '\n'); text = text.replace(/<\/w:tr>/g, '\n'); text = text.replace(/<\/w:tc>/g, '\t'); text = text.replace(/<[^>]+>/g, ''); text = decodeXmlEntities(text); text = text .replace(/\r\n/g, '\n') .replace(/[ \t]+\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .trim(); return text; } function decodeXmlEntities(value) { return value .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))) .replace(/&#([0-9]+);/g, (_, num) => String.fromCodePoint(Number.parseInt(num, 10))); } // Multi-device functions async function enableRemoteAccess() { try { remoteStatus.textContent = 'Starting server...'; remoteStatus.className = 'status-message'; // Generate a unique session ID const sessionId = Math.random().toString(36).substring(2, 15); const port = 8765; const accessURL = `http://localhost:${port}?session=${sessionId}`; // Start WebSocket server (we'll implement this) chrome.runtime.sendMessage({ action: 'startRemoteServer', sessionId: sessionId, port: port }, (response) => { if (response.success) { remoteServerActive = true; accessUrl.textContent = accessURL; deviceInfo.style.display = 'block'; remoteStatus.textContent = 'Remote access enabled!'; remoteStatus.className = 'status-message success'; enableRemoteListening.textContent = '🛑 Disable Remote Access'; // Generate QR code (simplified) generateQRCode(accessURL); } else { remoteStatus.textContent = 'Failed to start server: ' + response.error; remoteStatus.className = 'status-message error'; } }); } catch (error) { remoteStatus.textContent = 'Error: ' + error.message; remoteStatus.className = 'status-message error'; } } function disableRemoteAccess() { chrome.runtime.sendMessage({ action: 'stopRemoteServer' }, (response) => { remoteServerActive = false; deviceInfo.style.display = 'none'; remoteStatus.textContent = ''; enableRemoteListening.textContent = '🌐 Enable Remote Access'; }); } function generateQRCode(url) { // Simple QR code placeholder - in production, use a QR code library qrCode.innerHTML = `
QR Code
Scan to access
`; } // Make functions available globally for onclick handlers function startEditingContext(index) { getContextsForProfile(activeContextProfileId).then((contexts) => { const context = contexts[index]; if (context) { editingContextId = context.id; contextTitleInput.value = context.title; contextTextInput.value = context.content; if (contextTypeSelect) contextTypeSelect.value = context.type || 'general'; if (addContextBtn) addContextBtn.textContent = 'Update Context'; switchTab('text'); } }); } // Event listeners aiProviderSelect.addEventListener('change', function() { const selectedProvider = this.value; updateApiKeyInput(selectedProvider); // Load saved API key for this provider chrome.storage.sync.get('apiKeys', (result) => { const apiKeys = result.apiKeys || {}; if (apiKeys[selectedProvider] && aiProviders[selectedProvider].requiresKey) { apiKeyInput.value = apiKeys[selectedProvider]; updateApiKeyStatus('API Key Saved', 'success'); saveApiKeyButton.textContent = 'API Key Saved'; saveApiKeyButton.disabled = true; } else { apiKeyInput.value = ''; saveApiKeyButton.textContent = 'Save API Key'; saveApiKeyButton.disabled = !aiProviders[selectedProvider].requiresKey; } refreshModelOptions(selectedProvider, aiProviders[selectedProvider].defaultModel, apiKeys[selectedProvider]); }); // Save provider selection chrome.storage.sync.set({ aiProvider: selectedProvider }); }); modelSelect.addEventListener('change', function() { chrome.storage.sync.set({ selectedModel: this.value }); }); if (sttProviderSelect) { sttProviderSelect.addEventListener('change', function() { const provider = this.value; chrome.storage.sync.get(['sttApiKeys'], (result) => { const sttApiKeys = result.sttApiKeys || {}; const model = sttProviders[provider].defaultModel; chrome.storage.sync.set({ sttProvider: provider, sttModel: model }, () => { chrome.storage.sync.get(['sttEndpoint'], (saved) => { updateSttSettingsUI(provider, model, sttApiKeys[provider], saved.sttEndpoint || sttProviders.local.defaultEndpoint); }); }); }); }); } if (sttModelSelect) { sttModelSelect.addEventListener('change', function() { chrome.storage.sync.set({ sttModel: this.value }); }); } if (sttLanguageModeSelect) { sttLanguageModeSelect.addEventListener('change', function() { const mode = this.value; if (sttForcedLanguageInput) { sttForcedLanguageInput.disabled = mode !== 'forced'; } chrome.storage.sync.set({ sttLanguageMode: mode }); }); } if (sttForcedLanguageInput) { sttForcedLanguageInput.addEventListener('change', function() { chrome.storage.sync.set({ sttForcedLanguage: this.value.trim().toLowerCase() }); }); } if (sttTaskSelect) { sttTaskSelect.addEventListener('change', function() { chrome.storage.sync.set({ sttTask: this.value }); }); } if (sttVadFilterToggle) { sttVadFilterToggle.addEventListener('change', function() { chrome.storage.sync.set({ sttVadFilter: this.checked }); }); } if (sttBeamSizeInput) { sttBeamSizeInput.addEventListener('change', function() { const next = Math.min(10, Math.max(1, Number(this.value) || 5)); this.value = String(next); chrome.storage.sync.set({ sttBeamSize: next }); }); } if (sttApiKeyInput) { sttApiKeyInput.addEventListener('input', function() { if (!sttProviderSelect || (sttProviderSelect.value !== 'openai' && sttProviderSelect.value !== 'local')) return; saveSttApiKeyButton.textContent = 'Save STT API Key'; saveSttApiKeyButton.disabled = false; updateSttStatus('', ''); }); } if (saveSttApiKeyButton) { saveSttApiKeyButton.addEventListener('click', function() { if (!sttProviderSelect) return; const provider = sttProviderSelect.value; if (provider !== 'openai' && provider !== 'local') return; const apiKey = (sttApiKeyInput ? sttApiKeyInput.value : '').trim(); if (!apiKey && provider === 'openai') { updateSttStatus('Please enter a valid STT API key.', 'error'); return; } chrome.storage.sync.get('sttApiKeys', (result) => { const sttApiKeys = result.sttApiKeys || {}; if (apiKey) { sttApiKeys[provider] = apiKey; } else { delete sttApiKeys[provider]; } chrome.storage.sync.set({ sttApiKeys }, () => { saveSttApiKeyButton.textContent = provider === 'local' ? 'Local STT Key Saved' : 'STT API Key Saved'; saveSttApiKeyButton.disabled = true; updateSttStatus(provider === 'local' ? 'Local STT key saved.' : 'STT API key saved.', 'success'); }); }); }); } if (sttEndpointInput) { sttEndpointInput.addEventListener('change', function() { const endpoint = this.value.trim(); chrome.storage.sync.set({ sttEndpoint: endpoint }, () => { if (sttProviderSelect && sttProviderSelect.value === 'local') { updateSttStatus('Local STT endpoint saved.', 'success'); } }); }); } if (testSttConnectionButton) { testSttConnectionButton.addEventListener('click', function() { updateSttStatus('Testing STT connection...', ''); chrome.runtime.sendMessage({ action: 'stt:testConnection' }, (response) => { if (chrome.runtime.lastError || !response) { updateSttStatus('STT connection test failed.', 'error'); return; } if (!response.success) { updateSttStatus(response.error || 'STT connection test failed.', 'error'); return; } updateSttStatus(response.message || 'STT connection is healthy.', 'success'); }); }); } if (captureModeSelect) { captureModeSelect.addEventListener('change', function() { chrome.storage.sync.set({ captureMode: this.value }); }); } if (autoOpenAssistantWindowToggle) { autoOpenAssistantWindowToggle.addEventListener('change', function() { chrome.storage.sync.set({ autoOpenAssistantWindow: this.checked }); }); } if (autoStartToggle) { autoStartToggle.addEventListener('change', function() { chrome.storage.sync.set({ autoStartEnabled: this.checked }); }); } if (inputDeviceSelect) { inputDeviceSelect.addEventListener('change', function() { const deviceId = this.value; chrome.storage.sync.set({ inputDeviceId: deviceId }); const selectedOption = inputDeviceSelect.options[inputDeviceSelect.selectedIndex]; updateInputDeviceStatus(`Selected: ${selectedOption ? selectedOption.textContent : 'Unknown'}`, ''); if (micMonitorStream) { startMicMonitor(); } }); } if (startMicMonitorBtn) { startMicMonitorBtn.addEventListener('click', function() { startMicMonitor(); }); updateInputDeviceStatus('Click \"Enable Mic Monitor\" to see live input level.', ''); } if (openSettingsBtn) { openSettingsBtn.addEventListener('click', function() { chrome.runtime.sendMessage({ action: 'openSettingsWindow' }); }); } if (sessionAutomationSelect) { sessionAutomationSelect.addEventListener('change', async function() { activeAutomationId = this.value || ''; await chrome.storage.sync.set({ [ACTIVE_AUTOMATION_STORAGE_KEY]: activeAutomationId }); updateAutomationHint(); }); } if (runSelectedAutomationNowBtn) { runSelectedAutomationNowBtn.addEventListener('click', function() { if (sessionAutomationHint) { sessionAutomationHint.textContent = 'Running automation...'; sessionAutomationHint.className = 'status-message'; } chrome.runtime.sendMessage( { action: 'automation:run', trigger: 'manual', automationId: activeAutomationId || null }, (response) => { if (chrome.runtime.lastError || !response || !response.success) { if (sessionAutomationHint) { sessionAutomationHint.textContent = response && response.error ? response.error : 'Automation run failed.'; sessionAutomationHint.className = 'status-message error'; } return; } if (sessionAutomationHint) { const count = Array.isArray(response.results) ? response.results.length : 1; sessionAutomationHint.textContent = `Automation completed (${count} result${count === 1 ? '' : 's'}).`; sessionAutomationHint.className = 'status-message success'; } } ); }); } if (sessionContextProfile) { sessionContextProfile.addEventListener('change', async function() { activeContextProfileId = this.value; if (contextProfileSelect) contextProfileSelect.value = this.value; await chrome.storage.sync.set({ [ACTIVE_CONTEXT_PROFILE_STORAGE_KEY]: activeContextProfileId }); resetEditingContext(); updateProfileHints(); loadProfileEditor(activeContextProfileId); loadContexts(); }); } if (contextProfileSelect) { contextProfileSelect.addEventListener('change', async function() { activeContextProfileId = this.value; if (sessionContextProfile) sessionContextProfile.value = this.value; await chrome.storage.sync.set({ [ACTIVE_CONTEXT_PROFILE_STORAGE_KEY]: activeContextProfileId }); resetEditingContext(); updateProfileHints(); loadProfileEditor(activeContextProfileId); loadContexts(); }); } if (newProfileBtn) { newProfileBtn.addEventListener('click', function() { createNewProfileDraft(); }); } if (saveProfileBtn) { saveProfileBtn.addEventListener('click', function() { saveCurrentProfile(); }); } if (deleteProfileBtn) { deleteProfileBtn.addEventListener('click', function() { deleteActiveProfile(); }); } if (speedModeToggle) { speedModeToggle.addEventListener('change', function() { chrome.storage.sync.set({ speedMode: this.checked }); }); } apiKeyInput.addEventListener('input', function() { if (aiProviders[aiProviderSelect.value].requiresKey) { saveApiKeyButton.textContent = 'Save API Key'; saveApiKeyButton.disabled = false; updateApiKeyStatus('', ''); } }); saveApiKeyButton.addEventListener('click', function() { const apiKey = apiKeyInput.value.trim(); const provider = aiProviderSelect.value; if (!aiProviders[provider].requiresKey) { return; } if (apiKey) { // Save API key for the current provider chrome.storage.sync.get('apiKeys', (result) => { const apiKeys = result.apiKeys || {}; apiKeys[provider] = apiKey; chrome.storage.sync.set({ apiKeys: apiKeys }, () => { saveApiKeyButton.textContent = 'API Key Saved'; saveApiKeyButton.disabled = true; updateApiKeyStatus('API Key Saved', 'success'); refreshModelOptions(provider, modelSelect.value, apiKey); }); }); } else { updateApiKeyStatus('Please enter a valid API key', 'error'); } }); // Context management event listeners document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchTab(tabName); }); }); uploadContextBtn.addEventListener('click', function() { contextFileInput.click(); }); contextFileInput.addEventListener('change', async function() { const files = Array.from(this.files); for (const file of files) { try { const result = await processFile(file); await saveContext(result.title, result.content); } catch (error) { alert('Error processing file: ' + error.message); } } this.value = ''; // Clear file input }); addContextBtn.addEventListener('click', function() { const title = contextTitleInput.value.trim(); const content = contextTextInput.value.trim(); saveContext(title, content); }); clearAllContextBtn.addEventListener('click', clearAllContexts); // Multi-device event listeners enableRemoteListening.addEventListener('click', function() { if (remoteServerActive) { disableRemoteAccess(); } else { enableRemoteAccess(); } }); copyUrlBtn.addEventListener('click', function() { navigator.clipboard.writeText(accessUrl.textContent).then(() => { const originalText = copyUrlBtn.textContent; copyUrlBtn.textContent = '✅ Copied!'; setTimeout(() => { copyUrlBtn.textContent = originalText; }, 2000); }); }); toggleButton.addEventListener('click', function() { isListening = !isListening; toggleButton.textContent = isListening ? 'Stop Listening' : 'Start Listening'; if (isListening) { // Send current AI configuration with start listening const currentProvider = aiProviderSelect.value; const currentModel = modelSelect.value; const captureMode = captureModeSelect ? captureModeSelect.value : 'tab'; chrome.runtime.sendMessage({ action: 'startListening', aiProvider: currentProvider, model: currentModel, captureMode: captureMode, contextProfileId: activeContextProfileId, automationId: activeAutomationId || null }); const consent = window.confirm('Store this session to memory? You can forget it later.'); chrome.runtime.sendMessage({ action: 'session:setConsent', consent }); setTranscriptText('Listening for questions...'); setAIResponseText(`Using ${aiProviders[currentProvider].name} (${currentModel}). The answer will appear here.`); if (storeSessionToggle) { storeSessionToggle.disabled = false; storeSessionToggle.checked = consent; } if (forgetSessionBtn) { forgetSessionBtn.disabled = false; } if (pauseButton) { pauseButton.disabled = false; pauseButton.textContent = 'Pause Listening'; } isPaused = false; chrome.storage.sync.get(['autoOpenAssistantWindow'], (result) => { if (result.autoOpenAssistantWindow) { chrome.runtime.sendMessage({ action: 'openAssistantWindow' }); } }); } else { chrome.runtime.sendMessage({action: 'stopListening'}); setTranscriptText(''); setAIResponseText(''); if (storeSessionToggle) { storeSessionToggle.disabled = true; storeSessionToggle.checked = false; } if (forgetSessionBtn) { forgetSessionBtn.disabled = true; } if (pauseButton) { pauseButton.disabled = true; pauseButton.textContent = 'Pause Listening'; } isPaused = false; } }); if (pauseButton) { pauseButton.addEventListener('click', function() { if (!isListening) return; if (!isPaused) { chrome.runtime.sendMessage({ action: 'pauseListening' }); pauseButton.textContent = 'Resume Listening'; isPaused = true; setTranscriptText('Paused.'); } else { const currentProvider = aiProviderSelect.value; const currentModel = modelSelect.value; const captureMode = captureModeSelect ? captureModeSelect.value : 'tab'; chrome.runtime.sendMessage({ action: 'startListening', aiProvider: currentProvider, model: currentModel, captureMode: captureMode, contextProfileId: activeContextProfileId, automationId: activeAutomationId || null }); pauseButton.textContent = 'Pause Listening'; isPaused = false; setTranscriptText('Listening for questions...'); } }); } if (storeSessionToggle) { storeSessionToggle.addEventListener('change', function() { if (!isListening) { storeSessionToggle.checked = false; return; } if (!storeSessionToggle.checked) { const confirmForget = window.confirm('Turn off storage and forget this session?'); if (!confirmForget) { storeSessionToggle.checked = true; return; } chrome.runtime.sendMessage({ action: 'session:forgetCurrent' }); chrome.runtime.sendMessage({ action: 'session:setConsent', consent: false }); setTranscriptText(''); setAIResponseText('Session forgotten. Listening continues without storage.'); } else { chrome.runtime.sendMessage({ action: 'session:setConsent', consent: true }); } }); } if (forgetSessionBtn) { forgetSessionBtn.addEventListener('click', function() { if (!window.confirm('Forget this session and clear stored data?')) return; chrome.runtime.sendMessage({ action: 'session:forgetCurrent' }); if (isListening) { chrome.runtime.sendMessage({ action: 'stopListening' }); } isListening = false; isPaused = false; toggleButton.textContent = 'Start Listening'; setTranscriptText(''); setAIResponseText(''); if (pauseButton) { pauseButton.disabled = true; pauseButton.textContent = 'Pause Listening'; } if (storeSessionToggle) { storeSessionToggle.disabled = true; storeSessionToggle.checked = false; } if (forgetSessionBtn) { forgetSessionBtn.disabled = true; } }); } if (requestMicPermissionBtn) { requestMicPermissionBtn.addEventListener('click', function() { updateMicPermissionStatus('Requesting microphone permission...', ''); navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { stream.getTracks().forEach(track => track.stop()); updateMicPermissionStatus('Microphone permission granted.', 'success'); if (inputDeviceSelect) { loadInputDevices(inputDeviceSelect.value); } }).catch((error) => { if (error && error.name === 'NotAllowedError') { updateMicPermissionStatus('Microphone permission denied. Please allow access for the extension.', 'error'); } else if (error && error.name === 'NotFoundError') { updateMicPermissionStatus('No microphone found.', 'error'); } else { updateMicPermissionStatus(error && error.message ? error.message : 'Failed to request microphone permission.', 'error'); } }); }); } if (grantTabAccessBtn) { grantTabAccessBtn.addEventListener('click', function() { updateTabAccessStatus('Requesting tab access...', ''); chrome.runtime.sendMessage({ action: 'grantTabAccess' }, (response) => { if (chrome.runtime.lastError) { updateTabAccessStatus('Failed to request tab access. Click the extension icon on the target tab.', 'error'); return; } if (response && response.success) { updateTabAccessStatus('Tab access granted. You can start listening now.', 'success'); } else { updateTabAccessStatus(response && response.error ? response.error : 'Click the extension icon on the target tab to grant access.', 'error'); } }); }); } if (showOverlayBtn) { showOverlayBtn.addEventListener('click', function() { updateOverlayStatus('Restoring transcription overlay...', ''); chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError || !tabs.length) { updateOverlayStatus('No active tab found.', 'error'); return; } chrome.tabs.sendMessage(tabs[0].id, { action: 'showOverlay' }, () => { if (chrome.runtime.lastError) { updateOverlayStatus('Overlay unavailable on this tab. Start listening first.', 'error'); return; } updateOverlayStatus('Overlay restored.', 'success'); }); }); }); } if (navigator.mediaDevices && navigator.mediaDevices.addEventListener) { navigator.mediaDevices.addEventListener('devicechange', () => { if (inputDeviceSelect) { loadInputDevices(inputDeviceSelect.value); } }); } chrome.storage.onChanged.addListener(handleAutomationSettingsChanged); window.addEventListener('focus', loadAutomations); chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.action === 'updateTranscript') { setTranscriptText(request.transcript); } else if (request.action === 'updateAIResponse') { setAIResponseText(request.response); } else if (request.action === 'automation:needsApproval') { const actionsList = Array.isArray(request.actions) ? request.actions.map((action) => `• ${action.label} (${action.toolName})`).join('\\n') : ''; const title = request.automationName ? `Automation: ${request.automationName}` : 'Automation'; const confirmMessage = `Run automation actions now?\\n\\n${title}\\nTrigger: ${request.trigger}\\n${actionsList}`; const approved = window.confirm(confirmMessage); chrome.runtime.sendMessage({ action: approved ? 'automation:approve' : 'automation:reject' }, (response) => { if (chrome.runtime.lastError || !response) return; if (response.success && approved) { setAIResponseText('Automation approved and executed.'); } else if (response.success && !approved) { setAIResponseText('Automation declined.'); } }); } }); });