Files
Ai-Interview-Assistant-Chro…/background.js
Ahmed Galadima 56d56395ee feat: Enhance audio capture and monitoring features
- Added "audioCapture" permission to manifest for microphone access.
- Introduced DeepSeek as a new AI provider option in the side panel.
- Implemented a capture mode selection (tab-only, mic-only, mixed) in the side panel.
- Added options to enable/disable the extension and auto-open the assistant window.
- Integrated a mic monitor feature with live input level visualization.
- Included buttons for requesting microphone permission and granting tab access.
- Updated styles for new sections and mic level visualization.
- Enhanced model fetching logic to support DeepSeek and improved error handling.
2026-01-31 21:55:09 +01:00

674 lines
22 KiB
JavaScript

'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.
});
});
});
}