let recognition; let assistantWindowId = null; let currentAIConfig = { provider: 'openai', model: 'gpt-4o-mini' }; // AI Service configurations const aiServices = { openai: { baseUrl: 'https://api.openai.com/v1/chat/completions', headers: (apiKey) => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }), formatRequest: (model, question, context = '') => ({ model: model, messages: [ { role: "system", content: `You are a helpful assistant that answers questions briefly and concisely during interviews. Provide clear, professional responses. ${context ? `\n\nContext Information:\n${context}` : ''}` }, { role: "user", content: question } ], max_tokens: 200, 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 = '') => ({ model: model, max_tokens: 200, messages: [ { role: "user", content: `You are a helpful assistant that answers questions briefly and concisely during interviews. Provide clear, professional responses.${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 = '') => ({ // Use systemInstruction for instructions/context, and user role for the question systemInstruction: { role: 'system', parts: [{ text: `You are a helpful assistant that answers questions briefly and concisely during interviews. Provide clear, professional responses.` + (context ? `\n\nContext Information:\n${context}` : '') }] }, contents: [{ role: 'user', parts: [{ text: `Question: ${question}` }] }], generationConfig: { maxOutputTokens: 200, temperature: 0.7 } }), parseResponse: (data) => data.candidates[0].content.parts[0].text.trim() }, ollama: { baseUrl: 'http://localhost:11434/api/generate', headers: () => ({ 'Content-Type': 'application/json' }), formatRequest: (model, question, context = '') => ({ model: model, prompt: `You are a helpful assistant that answers questions briefly and concisely during interviews. Provide clear, professional responses.${context ? `\n\nContext Information:\n${context}` : ''}\n\nQuestion: ${question}\n\nAnswer:`, stream: false, options: { temperature: 0.7, num_predict: 200 } }), parseResponse: (data) => data.response.trim() } }; // Multi-device server state let remoteServer = null; let remoteServerPort = null; let activeConnections = new Set(); chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.action === 'startListening') { if (request.aiProvider && request.model) { currentAIConfig = { provider: request.aiProvider, model: request.model }; } startListening(); } else if (request.action === 'stopListening') { stopListening(); } else if (request.action === 'getAIResponse') { getAIResponse(request.question); } else if (request.action === 'startRemoteServer') { startRemoteServer(request.sessionId, request.port, sendResponse); return true; // Keep message channel open for async response } else if (request.action === 'stopRemoteServer') { stopRemoteServer(sendResponse); return true; } else if (request.action === 'remoteQuestion') { // Handle questions from remote devices getAIResponse(request.question); } }); chrome.action.onClicked.addListener((tab) => { chrome.sidePanel.open({ tabId: tab.id }); }); chrome.windows.onRemoved.addListener((windowId) => { if (windowId === assistantWindowId) { assistantWindowId = null; } }); function startListening() { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (chrome.runtime.lastError) { console.error('Error querying tabs:', chrome.runtime.lastError); return; } if (tabs.length === 0) { console.error('No active tab found'); return; } const activeTabId = tabs[0].id; if (typeof activeTabId === 'undefined') { console.error('Active tab ID is undefined'); return; } // Check if the current tab is a valid web page (not chrome:// or extension pages) const tab = tabs[0]; if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) { console.error('Cannot capture audio from this type of page:', tab.url); chrome.runtime.sendMessage({action: 'updateAIResponse', response: 'Error: Cannot capture audio from this page. Please navigate to a regular website.'}); return; } chrome.tabCapture.getMediaStreamId({ consumerTabId: activeTabId }, (streamId) => { if (chrome.runtime.lastError) { console.error('Error getting media stream ID:', chrome.runtime.lastError); const errorMsg = chrome.runtime.lastError.message || 'Unknown error'; chrome.runtime.sendMessage({action: 'updateAIResponse', response: `Error: ${errorMsg}. Make sure you've granted microphone permissions.`}); 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(activeTabId, streamId); }); }); } function injectContentScriptAndStartCapture(tabId, streamId) { chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'] }, (injectionResults) => { 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; } // Wait a bit to ensure the content script is fully loaded setTimeout(() => { chrome.tabs.sendMessage(tabId, { action: 'startCapture', streamId: streamId }, (response) => { if (chrome.runtime.lastError) { console.error('Error starting capture:', chrome.runtime.lastError); const errorMsg = chrome.runtime.lastError.message || 'Unknown error'; 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); // Increased timeout slightly for better reliability }); } 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' }, (response) => { if (chrome.runtime.lastError) { console.error('Error stopping capture:', chrome.runtime.lastError); // Don't show error to user for stop operation, just log it } else { console.log('Capture stopped successfully'); chrome.runtime.sendMessage({action: 'updateAIResponse', response: 'Stopped listening.'}); } }); }); } function isQuestion(text) { // Simple check for question words or question mark 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 { provider, model } = currentAIConfig; const service = aiServices[provider]; if (!service) { throw new Error(`Unsupported AI provider: ${provider}`); } // Get saved contexts to include in the prompt const contextData = await getStoredContexts(); const systemContexts = contextData.filter(c => c.type === 'system'); const generalContexts = contextData.filter(c => c.type !== 'system'); const systemPromptExtra = systemContexts.length > 0 ? systemContexts.map(ctx => `${ctx.title}:\n${ctx.content}`).join('\n\n---\n\n') : ''; const contextString = generalContexts.length > 0 ? generalContexts.map(ctx => `${ctx.title}:\n${ctx.content}`).join('\n\n---\n\n') : ''; // Get API key for the current provider (skip for Ollama) 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})...`); // Prepare request configuration let url, headers, body; if (provider === 'google') { url = service.baseUrl(apiKey, model); headers = service.headers(); } else { url = service.baseUrl; headers = service.headers(apiKey); } // Inject system prompt extras into question or dedicated field depending on provider // For consistency we keep a single system message including systemPromptExtra const mergedContext = systemPromptExtra ? `${systemPromptExtra}${contextString ? '\n\n---\n\n' + contextString : ''}` : contextString; body = JSON.stringify(service.formatRequest(model, question, mergedContext)); const response = await fetch(url, { method: 'POST', headers: headers, body: body }); 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); // Send response to both local UI and remote devices chrome.runtime.sendMessage({action: 'updateAIResponse', response: answer}); broadcastToRemoteDevices('aiResponse', { response: answer, question: question }); } catch (error) { console.error('Error getting AI response:', error); // Provide more specific error messages 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 (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.'; } } const fullErrorMessage = 'Error: ' + errorMessage; chrome.runtime.sendMessage({action: 'updateAIResponse', response: fullErrorMessage}); broadcastToRemoteDevices('aiResponse', { response: fullErrorMessage, question: question }); } } async function getApiKey(provider) { return new Promise((resolve) => { chrome.storage.sync.get('apiKeys', (result) => { const apiKeys = result.apiKeys || {}; resolve(apiKeys[provider]); }); }); } async function getStoredContexts() { return new Promise((resolve) => { chrome.storage.local.get('contexts', (result) => { resolve(result.contexts || []); }); }); } // Multi-device server functions async function startRemoteServer(sessionId, port, sendResponse) { try { // Note: Chrome extensions can't directly create HTTP servers // This is a simplified implementation that would need a companion app // For now, we'll simulate the server functionality remoteServerPort = port; console.log(`Starting remote server on port ${port} with session ${sessionId}`); // In a real implementation, you would: // 1. Start a local HTTP/WebSocket server // 2. Handle incoming connections // 3. Route audio data and responses // For this demo, we'll just track the state 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) { remoteServer = null; remoteServerPort = null; activeConnections.clear(); console.log('Remote server stopped'); sendResponse({ success: true }); } function broadcastToRemoteDevices(type, data) { // In a real implementation, this would send data to all connected WebSocket clients console.log('Broadcasting to remote devices:', type, data); // For demo purposes, we'll just log the broadcast if (activeConnections.size > 0) { console.log(`Broadcasting ${type} to ${activeConnections.size} connected devices`); } }