378 lines
14 KiB
JavaScript
378 lines
14 KiB
JavaScript
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`);
|
|
}
|
|
} |