document.addEventListener('DOMContentLoaded', function() { const toggleButton = document.getElementById('toggleListening'); 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 requestMicPermissionBtn = document.getElementById('requestMicPermission'); const showOverlayBtn = document.getElementById('showOverlay'); 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 extensionActiveToggle = document.getElementById('extensionActiveToggle'); 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'); // 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'); let isListening = false; let remoteServerActive = false; let micMonitorStream = null; let micMonitorCtx = null; let micMonitorSource = null; let micMonitorAnalyser = null; let micMonitorRaf = 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 modelCache = {}; const modelFetchState = {}; // Load saved settings chrome.storage.sync.get(['aiProvider', 'selectedModel', 'apiKeys', 'speedMode', 'captureMode', 'autoOpenAssistantWindow', 'inputDeviceId', 'extensionActive'], (result) => { const savedProvider = result.aiProvider || 'openai'; const savedModel = result.selectedModel || aiProviders[savedProvider].defaultModel; const savedApiKeys = result.apiKeys || {}; const speedMode = Boolean(result.speedMode); const captureMode = result.captureMode || 'tab'; const autoOpenAssistantWindow = Boolean(result.autoOpenAssistantWindow); const savedInputDeviceId = result.inputDeviceId || ''; const extensionActive = result.extensionActive !== false; aiProviderSelect.value = savedProvider; if (captureModeSelect) captureModeSelect.value = captureMode; if (speedModeToggle) speedModeToggle.checked = speedMode; if (autoOpenAssistantWindowToggle) autoOpenAssistantWindowToggle.checked = autoOpenAssistantWindow; if (extensionActiveToggle) extensionActiveToggle.checked = extensionActive; refreshModelOptions(savedProvider, savedModel, savedApiKeys[savedProvider]); updateApiKeyInput(savedProvider); 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); } }); // Load and display saved contexts loadContexts(); // 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 updateMicPermissionStatus(message, type) { if (!micPermissionStatus) return; micPermissionStatus.textContent = message; micPermissionStatus.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 result = await chrome.storage.local.get('contexts'); const contexts = result.contexts || []; displayContexts(contexts); updateManageTabCount(contexts.length); } function displayContexts(contexts) { contextList.innerHTML = ''; if (contexts.length === 0) { contextList.innerHTML = '