- 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.
674 lines
22 KiB
JavaScript
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.
|
|
});
|
|
});
|
|
});
|
|
}
|