Initial setup

This commit is contained in:
2025-09-23 21:12:15 +02:00
commit 246506b177
20 changed files with 2275 additions and 0 deletions

378
background.js Normal file
View 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`);
}
}