Initial setup
This commit is contained in:
378
background.js
Normal file
378
background.js
Normal file
@@ -0,0 +1,378 @@
|
||||
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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user