'use strict'; const DEFAULT_AI_CONFIG = { provider: 'openai', model: 'gpt-4o-mini' }; const DEFAULT_CAPTURE_MODE = 'tab'; const LISTENING_PROMPT = 'You are a helpful assistant that answers questions briefly and concisely during interviews. Provide clear, professional responses.'; const AI_SERVICES = { openai: { baseUrl: 'https://api.openai.com/v1/chat/completions', headers: (apiKey) => ({ 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }), formatRequest: (model, question, context = '', options = {}) => ({ model, messages: [ { role: 'system', content: `${LISTENING_PROMPT}${context ? `\n\nContext Information:\n${context}` : ''}` }, { role: 'user', content: question } ], max_tokens: options.maxTokens || 200, temperature: options.temperature ?? 0.7 }), parseResponse: (data) => data.choices[0].message.content.trim() }, anthropic: { baseUrl: 'https://api.anthropic.com/v1/messages', headers: (apiKey) => ({ 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }), formatRequest: (model, question, context = '', options = {}) => ({ model, max_tokens: options.maxTokens || 200, messages: [ { role: 'user', content: `${LISTENING_PROMPT}${context ? `\n\nContext Information:\n${context}` : ''}\n\nQuestion: ${question}` } ] }), parseResponse: (data) => data.content[0].text.trim() }, google: { baseUrl: (apiKey, model) => `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, headers: () => ({ 'Content-Type': 'application/json' }), formatRequest: (model, question, context = '', options = {}) => ({ systemInstruction: { role: 'system', parts: [ { text: `${LISTENING_PROMPT}${context ? `\n\nContext Information:\n${context}` : ''}` } ] }, contents: [ { role: 'user', parts: [{ text: `Question: ${question}` }] } ], generationConfig: { maxOutputTokens: options.maxTokens || 200, temperature: options.temperature ?? 0.7 } }), parseResponse: (data) => data.candidates[0].content.parts[0].text.trim() }, deepseek: { baseUrl: 'https://api.deepseek.com/v1/chat/completions', headers: (apiKey) => ({ 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }), formatRequest: (model, question, context = '', options = {}) => ({ model, messages: [ { role: 'system', content: `${LISTENING_PROMPT}${context ? `\n\nContext Information:\n${context}` : ''}` }, { role: 'user', content: question } ], max_tokens: options.maxTokens || 200, temperature: options.temperature ?? 0.7 }), parseResponse: (data) => data.choices[0].message.content.trim() }, ollama: { baseUrl: 'http://localhost:11434/api/generate', headers: () => ({ 'Content-Type': 'application/json' }), formatRequest: (model, question, context = '', options = {}) => ({ model, prompt: `${LISTENING_PROMPT}${context ? `\n\nContext Information:\n${context}` : ''}\n\nQuestion: ${question}\n\nAnswer:`, stream: false, options: { temperature: options.temperature ?? 0.7, num_predict: options.maxTokens || 200 } }), parseResponse: (data) => data.response.trim() } }; const state = { recognition: undefined, assistantWindowId: null, currentAIConfig: { ...DEFAULT_AI_CONFIG }, currentCaptureMode: DEFAULT_CAPTURE_MODE, remoteServer: null, remoteServerPort: null, activeConnections: new Set(), isActive: true }; chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return handleMessage(request, sender, sendResponse); }); chrome.action.onClicked.addListener((tab) => { chrome.sidePanel.open({ tabId: tab.id }); }); chrome.windows.onRemoved.addListener((windowId) => { if (windowId === state.assistantWindowId) { state.assistantWindowId = null; } }); initializeActiveState(); function handleMessage(request, _sender, sendResponse) { switch (request.action) { case 'startListening': if (!state.isActive) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Extension is inactive. Turn it on in the side panel to start listening.' }); return false; } if (request.aiProvider && request.model) { state.currentAIConfig = { provider: request.aiProvider, model: request.model }; } if (request.captureMode) { state.currentCaptureMode = request.captureMode; } startListening(); return false; case 'stopListening': stopListening(); return false; case 'getAIResponse': getAIResponse(request.question); return false; case 'startRemoteServer': startRemoteServer(request.sessionId, request.port, sendResponse); return true; case 'stopRemoteServer': stopRemoteServer(sendResponse); return true; case 'remoteQuestion': getAIResponse(request.question); return false; case 'grantTabAccess': grantTabAccess(sendResponse); return true; case 'openAssistantWindow': openAssistantWindow(sendResponse); return true; case 'setActiveState': setActiveState(Boolean(request.isActive), sendResponse); return true; default: return false; } } function startListening() { if (state.currentCaptureMode === 'mic') { startMicListening(); return; } if (state.currentCaptureMode === 'mixed') { startMixedListening(); return; } chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError) { console.error('Error querying tabs:', chrome.runtime.lastError); return; } if (!tabs.length) { console.error('No active tab found'); return; } const tab = tabs[0]; if (!isValidCaptureTab(tab)) { const message = 'Error: Cannot capture audio from this page. Please navigate to a regular website.'; console.error('Cannot capture audio from this type of page:', tab.url); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: message }); return; } chrome.tabCapture.getMediaStreamId({ consumerTabId: tab.id }, (streamId) => { if (chrome.runtime.lastError) { const errorMsg = chrome.runtime.lastError.message || 'Unknown error'; const userMessage = buildTabCaptureErrorMessage(errorMsg); console.error('Error getting media stream ID:', chrome.runtime.lastError); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: userMessage }); return; } if (!streamId) { console.error('No stream ID received'); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Failed to get media stream. Please try again.' }); return; } injectContentScriptAndStartCapture(tab.id, streamId); }); }); } function startMicListening() { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError || tabs.length === 0) { console.error('Error querying tabs:', chrome.runtime.lastError); return; } const tab = tabs[0]; if (!isValidCaptureTab(tab)) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Cannot capture audio from this page. Please navigate to a regular website.' }); return; } chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] }, () => { if (chrome.runtime.lastError) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Failed to inject content script. Please refresh the page and try again.' }); return; } chrome.tabs.sendMessage(tab.id, { action: 'startMicCapture' }, () => { if (chrome.runtime.lastError) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Failed to start microphone capture.' }); } else { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Listening for audio (mic-only)...' }); } }); }); }); } function startMixedListening() { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError || tabs.length === 0) { console.error('Error querying tabs:', chrome.runtime.lastError); return; } const tab = tabs[0]; if (!isValidCaptureTab(tab)) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Cannot capture audio from this page. Please navigate to a regular website.' }); return; } chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] }, () => { if (chrome.runtime.lastError) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Failed to inject content script. Please refresh the page and try again.' }); return; } chrome.tabs.sendMessage(tab.id, { action: 'startMixedCapture' }, () => { if (chrome.runtime.lastError) { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Failed to start mixed capture.' }); } else { chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Listening for audio (mixed mode)...' }); } }); }); }); } function injectContentScriptAndStartCapture(tabId, streamId) { chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'] }, () => { if (chrome.runtime.lastError) { console.error('Error injecting content script:', chrome.runtime.lastError); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Error: Failed to inject content script. Please refresh the page and try again.' }); return; } setTimeout(() => { chrome.tabs.sendMessage(tabId, { action: 'startCapture', streamId }, () => { if (chrome.runtime.lastError) { const errorMsg = chrome.runtime.lastError.message || 'Unknown error'; console.error('Error starting capture:', chrome.runtime.lastError); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: `Error: ${errorMsg}. Please make sure microphone permissions are granted.` }); } else { console.log('Capture started successfully'); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Listening for audio... Speak your questions!' }); } }); }, 200); }); } function stopListening() { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError || tabs.length === 0) { console.error('Error querying tabs for stop:', chrome.runtime.lastError); return; } chrome.tabs.sendMessage(tabs[0].id, { action: 'stopCapture' }, () => { if (chrome.runtime.lastError) { console.error('Error stopping capture:', chrome.runtime.lastError); return; } console.log('Capture stopped successfully'); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: 'Stopped listening.' }); }); }); } function isQuestion(text) { const questionWords = ['what', 'when', 'where', 'who', 'why', 'how']; const lowerText = text.toLowerCase(); return questionWords.some((word) => lowerText.includes(word)) || text.includes('?'); } async function getAIResponse(question) { try { const storedConfig = await getAIConfigFromStorage(); if (storedConfig) { state.currentAIConfig = storedConfig; } const { provider, model } = state.currentAIConfig; const service = AI_SERVICES[provider]; const speedMode = await getSpeedModeFromStorage(); if (!service) { throw new Error(`Unsupported AI provider: ${provider}`); } const contextData = await getStoredContexts(); const { systemContexts, generalContexts } = selectContextsForRequest(contextData, speedMode); const systemPromptExtra = systemContexts.length ? systemContexts.map((ctx) => `${ctx.title}:\n${ctx.content}`).join('\n\n---\n\n') : ''; const contextString = generalContexts.length ? generalContexts.map((ctx) => `${ctx.title}:\n${ctx.content}`).join('\n\n---\n\n') : ''; let apiKey = null; if (provider !== 'ollama') { apiKey = await getApiKey(provider); if (!apiKey) { throw new Error(`${provider.charAt(0).toUpperCase() + provider.slice(1)} API key not set`); } } console.log(`Sending request to ${provider} API (${model})...`); let url; let headers; if (provider === 'google') { url = service.baseUrl(apiKey, model); headers = service.headers(); } else { url = service.baseUrl; headers = service.headers(apiKey); } const mergedContextRaw = systemPromptExtra ? `${systemPromptExtra}${contextString ? `\n\n---\n\n${contextString}` : ''}` : contextString; const mergedContext = truncateContext(mergedContextRaw, provider, speedMode); const requestOptions = buildRequestOptions(speedMode); const body = JSON.stringify(service.formatRequest(model, question, mergedContext, requestOptions)); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), speedMode ? 20000 : 30000); const response = await fetch(url, { method: 'POST', headers, body, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); let errorMessage; try { const errorData = JSON.parse(errorText); errorMessage = errorData.error?.message || errorData.message || errorText; } catch { errorMessage = errorText; } throw new Error(`Failed to get response from ${provider}: ${response.status} ${response.statusText}\n${errorMessage}`); } const data = await response.json(); const answer = service.parseResponse(data); chrome.runtime.sendMessage({ action: 'updateAIResponse', response: answer }); broadcastToRemoteDevices('aiResponse', { response: answer, question }); } catch (error) { console.error('Error getting AI response:', error); let errorMessage = error.message; if (error.message.includes('API key')) { errorMessage = `${error.message}. Please check your API key in the settings.`; } else if (error.message.includes('Failed to fetch')) { if (state.currentAIConfig.provider === 'ollama') { errorMessage = 'Failed to connect to Ollama. Make sure Ollama is running locally on port 11434.'; } else { errorMessage = 'Network error. Please check your internet connection.'; } } else if (error.message.includes('aborted')) { errorMessage = 'Request timed out. Try again or enable speed mode.'; } const fullErrorMessage = `Error: ${errorMessage}`; chrome.runtime.sendMessage({ action: 'updateAIResponse', response: fullErrorMessage }); broadcastToRemoteDevices('aiResponse', { response: fullErrorMessage, question }); } } function truncateContext(context, provider, speedMode) { if (!context) return ''; const maxContextCharsByProvider = { deepseek: speedMode ? 30000 : 60000, openai: speedMode ? 50000 : 120000, anthropic: speedMode ? 50000 : 120000, google: speedMode ? 50000 : 120000, ollama: speedMode ? 50000 : 120000 }; const maxChars = maxContextCharsByProvider[provider] || 200000; if (context.length <= maxChars) return context; return `${context.slice(0, maxChars)}\n\n[Context truncated to fit model limits.]`; } function selectContextsForRequest(contexts, speedMode) { const sorted = [...contexts].sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || '')); const systemContexts = sorted.filter((ctx) => ctx.type === 'system'); const generalContexts = sorted.filter((ctx) => ctx.type !== 'system'); const maxGeneralItems = speedMode ? 2 : 4; const maxSystemItems = speedMode ? 1 : 2; const maxItemChars = speedMode ? 4000 : 8000; const trimItem = (ctx) => ({ ...ctx, content: (ctx.content || '').slice(0, maxItemChars) }); return { systemContexts: systemContexts.slice(0, maxSystemItems).map(trimItem), generalContexts: generalContexts.slice(0, maxGeneralItems).map(trimItem) }; } function buildRequestOptions(speedMode) { if (!speedMode) { return { maxTokens: 200, temperature: 0.7 }; } return { maxTokens: 120, temperature: 0.4 }; } function getSpeedModeFromStorage() { return new Promise((resolve) => { chrome.storage.sync.get(['speedMode'], (result) => { if (chrome.runtime.lastError) { resolve(false); return; } resolve(Boolean(result.speedMode)); }); }); } function grantTabAccess(sendResponse) { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError || !tabs.length) { sendResponse({ success: false, error: 'No active tab found.' }); return; } const tabId = tabs[0].id; chrome.sidePanel.open({ tabId }, () => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: 'Click the extension icon on the target tab to grant access.' }); return; } if (chrome.action && chrome.action.openPopup) { chrome.action.openPopup(() => { sendResponse({ success: true }); }); } else { sendResponse({ success: true }); } }); }); } function openAssistantWindow(sendResponse) { if (state.assistantWindowId !== null) { chrome.windows.update(state.assistantWindowId, { focused: true }, () => { sendResponse({ success: true }); }); return; } chrome.windows.create( { url: chrome.runtime.getURL('assistant.html'), type: 'popup', width: 420, height: 320 }, (win) => { if (chrome.runtime.lastError || !win) { sendResponse({ success: false, error: 'Failed to open assistant window.' }); return; } state.assistantWindowId = win.id; sendResponse({ success: true }); } ); } function getAIConfigFromStorage() { return new Promise((resolve) => { chrome.storage.sync.get(['aiProvider', 'selectedModel'], (result) => { if (chrome.runtime.lastError) { resolve(null); return; } const provider = result.aiProvider; const model = result.selectedModel; if (!provider || !model) { resolve(null); return; } resolve({ provider, model }); }); }); } function getApiKey(provider) { return new Promise((resolve) => { chrome.storage.sync.get('apiKeys', (result) => { const apiKeys = result.apiKeys || {}; resolve(apiKeys[provider]); }); }); } function getStoredContexts() { return new Promise((resolve) => { chrome.storage.local.get('contexts', (result) => { resolve(result.contexts || []); }); }); } function startRemoteServer(sessionId, port, sendResponse) { try { state.remoteServerPort = port; console.log(`Starting remote server on port ${port} with session ${sessionId}`); sendResponse({ success: true, message: 'Remote server started (demo mode)', url: `http://localhost:${port}?session=${sessionId}` }); } catch (error) { console.error('Error starting remote server:', error); sendResponse({ success: false, error: error.message }); } } function stopRemoteServer(sendResponse) { state.remoteServer = null; state.remoteServerPort = null; state.activeConnections.clear(); console.log('Remote server stopped'); sendResponse({ success: true }); } function broadcastToRemoteDevices(type, data) { console.log('Broadcasting to remote devices:', type, data); if (state.activeConnections.size > 0) { console.log(`Broadcasting ${type} to ${state.activeConnections.size} connected devices`); } } function isValidCaptureTab(tab) { if (!tab || !tab.url) return false; return !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://'); } function buildTabCaptureErrorMessage(errorMsg) { let userMessage = `Error: ${errorMsg}.`; if (errorMsg.includes('Extension has not been invoked')) { userMessage += ' Click the extension icon on the tab you want to capture, then press Start Listening.'; } else { userMessage += ' Make sure you\'ve granted microphone permissions.'; } return userMessage; } function initializeActiveState() { chrome.storage.sync.get(['extensionActive'], (result) => { if (chrome.runtime.lastError) { state.isActive = true; updateActionBadge(); return; } state.isActive = result.extensionActive !== false; updateActionBadge(); }); } function setActiveState(isActive, sendResponse) { state.isActive = isActive; chrome.storage.sync.set({ extensionActive: isActive }, () => { updateActionBadge(); if (!isActive) { stopListeningAcrossTabs(); } sendResponse({ success: true, isActive }); }); } function updateActionBadge() { if (!chrome.action || !chrome.action.setBadgeText) return; chrome.action.setBadgeText({ text: state.isActive ? 'ON' : 'OFF' }); chrome.action.setBadgeBackgroundColor({ color: state.isActive ? '#2ecc71' : '#e74c3c' }); } function stopListeningAcrossTabs() { chrome.tabs.query({}, (tabs) => { if (chrome.runtime.lastError || !tabs.length) return; tabs.forEach((tab) => { if (!tab.id) return; chrome.tabs.sendMessage(tab.id, { action: 'stopCapture' }, () => { // Ignore errors for tabs without the content script. }); }); }); }