Initial setup
This commit is contained in:
473
sidepanel.js
Normal file
473
sidepanel.js
Normal file
@@ -0,0 +1,473 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toggleButton = document.getElementById('toggleListening');
|
||||
const transcriptDiv = document.getElementById('transcript');
|
||||
const aiResponseDiv = document.getElementById('aiResponse');
|
||||
const apiKeyInput = document.getElementById('apiKeyInput');
|
||||
const saveApiKeyButton = document.getElementById('saveApiKey');
|
||||
const aiProviderSelect = document.getElementById('aiProvider');
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const apiKeyStatus = document.getElementById('apiKeyStatus');
|
||||
|
||||
// Context management elements
|
||||
const contextFileInput = document.getElementById('contextFileInput');
|
||||
const uploadContextBtn = document.getElementById('uploadContextBtn');
|
||||
const contextTextInput = document.getElementById('contextTextInput');
|
||||
const contextTypeSelect = document.getElementById('contextTypeSelect');
|
||||
const contextTitleInput = document.getElementById('contextTitleInput');
|
||||
const addContextBtn = document.getElementById('addContextBtn');
|
||||
const contextList = document.getElementById('contextList');
|
||||
const clearAllContextBtn = document.getElementById('clearAllContextBtn');
|
||||
|
||||
// Multi-device elements
|
||||
const enableRemoteListening = document.getElementById('enableRemoteListening');
|
||||
const remoteStatus = document.getElementById('remoteStatus');
|
||||
const deviceInfo = document.getElementById('deviceInfo');
|
||||
const accessUrl = document.getElementById('accessUrl');
|
||||
const copyUrlBtn = document.getElementById('copyUrlBtn');
|
||||
const qrCode = document.getElementById('qrCode');
|
||||
|
||||
let isListening = false;
|
||||
let remoteServerActive = false;
|
||||
|
||||
// AI Provider configurations
|
||||
const aiProviders = {
|
||||
openai: {
|
||||
name: 'OpenAI',
|
||||
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
||||
defaultModel: 'gpt-4o-mini',
|
||||
apiKeyPlaceholder: 'Enter your OpenAI API Key',
|
||||
requiresKey: true
|
||||
},
|
||||
anthropic: {
|
||||
name: 'Anthropic',
|
||||
models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'],
|
||||
defaultModel: 'claude-3-5-sonnet-20241022',
|
||||
apiKeyPlaceholder: 'Enter your Anthropic API Key',
|
||||
requiresKey: true
|
||||
},
|
||||
google: {
|
||||
name: 'Google',
|
||||
models: ['gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-pro'],
|
||||
defaultModel: 'gemini-1.5-flash',
|
||||
apiKeyPlaceholder: 'Enter your Google AI API Key',
|
||||
requiresKey: true
|
||||
},
|
||||
ollama: {
|
||||
name: 'Ollama',
|
||||
models: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'phi3'],
|
||||
defaultModel: 'llama3.2',
|
||||
apiKeyPlaceholder: 'No API key required (Local)',
|
||||
requiresKey: false
|
||||
}
|
||||
};
|
||||
|
||||
// Load saved settings
|
||||
chrome.storage.sync.get(['aiProvider', 'selectedModel', 'apiKeys'], (result) => {
|
||||
const savedProvider = result.aiProvider || 'openai';
|
||||
const savedModel = result.selectedModel || aiProviders[savedProvider].defaultModel;
|
||||
const savedApiKeys = result.apiKeys || {};
|
||||
|
||||
aiProviderSelect.value = savedProvider;
|
||||
updateModelOptions(savedProvider, savedModel);
|
||||
updateApiKeyInput(savedProvider);
|
||||
|
||||
if (savedApiKeys[savedProvider] && aiProviders[savedProvider].requiresKey) {
|
||||
apiKeyInput.value = savedApiKeys[savedProvider];
|
||||
updateApiKeyStatus('API Key Saved', 'success');
|
||||
saveApiKeyButton.textContent = 'API Key Saved';
|
||||
saveApiKeyButton.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Load and display saved contexts
|
||||
loadContexts();
|
||||
|
||||
// Helper functions
|
||||
function updateModelOptions(provider, selectedModel = null) {
|
||||
const models = aiProviders[provider].models;
|
||||
modelSelect.innerHTML = '';
|
||||
|
||||
models.forEach(model => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model;
|
||||
option.textContent = model;
|
||||
if (selectedModel === model || (!selectedModel && model === aiProviders[provider].defaultModel)) {
|
||||
option.selected = true;
|
||||
}
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function updateApiKeyInput(provider) {
|
||||
const providerConfig = aiProviders[provider];
|
||||
apiKeyInput.placeholder = providerConfig.apiKeyPlaceholder;
|
||||
apiKeyInput.disabled = !providerConfig.requiresKey;
|
||||
saveApiKeyButton.disabled = !providerConfig.requiresKey;
|
||||
|
||||
if (!providerConfig.requiresKey) {
|
||||
apiKeyInput.value = '';
|
||||
updateApiKeyStatus('No API key required', 'success');
|
||||
} else {
|
||||
updateApiKeyStatus('', '');
|
||||
}
|
||||
}
|
||||
|
||||
function updateApiKeyStatus(message, type) {
|
||||
apiKeyStatus.textContent = message;
|
||||
apiKeyStatus.className = `status-message ${type}`;
|
||||
}
|
||||
|
||||
// Context Management Functions
|
||||
async function loadContexts() {
|
||||
const result = await chrome.storage.local.get('contexts');
|
||||
const contexts = result.contexts || [];
|
||||
displayContexts(contexts);
|
||||
updateManageTabCount(contexts.length);
|
||||
}
|
||||
|
||||
function displayContexts(contexts) {
|
||||
contextList.innerHTML = '';
|
||||
if (contexts.length === 0) {
|
||||
contextList.innerHTML = '<div class="no-contexts">No context added yet. Add your CV or job description to get better responses!</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
contexts.forEach((context, index) => {
|
||||
const contextItem = document.createElement('div');
|
||||
contextItem.className = 'context-item';
|
||||
contextItem.innerHTML = `
|
||||
<div class="context-item-info">
|
||||
<div class="context-item-title">${context.title} ${context.type ? `<span style="font-weight: 400; color: #666;">• ${context.type}</span>` : ''}</div>
|
||||
<div class="context-item-preview">${context.content.substring(0, 100)}${context.content.length > 100 ? '...' : ''}</div>
|
||||
</div>
|
||||
<div class="context-item-actions">
|
||||
<button onclick="editContext(${index})" class="edit-btn">✏️ Edit</button>
|
||||
<button onclick="deleteContext(${index})" class="delete-btn danger-btn">🗑️ Delete</button>
|
||||
</div>
|
||||
`;
|
||||
contextList.appendChild(contextItem);
|
||||
});
|
||||
}
|
||||
|
||||
function updateManageTabCount(count) {
|
||||
const manageTab = document.querySelector('[data-tab="manage"]');
|
||||
manageTab.textContent = `Manage (${count})`;
|
||||
}
|
||||
|
||||
async function saveContext(title, content) {
|
||||
if (!title.trim() || !content.trim()) {
|
||||
alert('Please provide both title and content');
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional basic guard for extremely large items (>4MB)
|
||||
const approxBytes = new Blob([content]).size;
|
||||
if (approxBytes > 4 * 1024 * 1024) {
|
||||
alert('This context is too large to store locally. Please split it into smaller parts.');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await chrome.storage.local.get('contexts');
|
||||
const contexts = result.contexts || [];
|
||||
|
||||
contexts.push({
|
||||
id: Date.now(),
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
type: (contextTypeSelect && contextTypeSelect.value) || 'general',
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
await chrome.storage.local.set({ contexts: contexts });
|
||||
loadContexts();
|
||||
|
||||
// Clear inputs
|
||||
contextTitleInput.value = '';
|
||||
contextTextInput.value = '';
|
||||
if (contextTypeSelect) contextTypeSelect.value = 'general';
|
||||
|
||||
// Switch to manage tab
|
||||
switchTab('manage');
|
||||
}
|
||||
|
||||
async function deleteContext(index) {
|
||||
if (!confirm('Are you sure you want to delete this context?')) return;
|
||||
|
||||
const result = await chrome.storage.local.get('contexts');
|
||||
const contexts = result.contexts || [];
|
||||
contexts.splice(index, 1);
|
||||
|
||||
await chrome.storage.local.set({ contexts: contexts });
|
||||
loadContexts();
|
||||
}
|
||||
|
||||
async function clearAllContexts() {
|
||||
if (!confirm('Are you sure you want to delete all contexts? This cannot be undone.')) return;
|
||||
|
||||
await chrome.storage.local.set({ contexts: [] });
|
||||
loadContexts();
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
// Update tab buttons
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
||||
|
||||
// Update tab content
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${tabName}Tab`).classList.add('active');
|
||||
}
|
||||
|
||||
async function processFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
const content = e.target.result;
|
||||
resolve({
|
||||
title: file.name,
|
||||
content: content
|
||||
});
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
reject(new Error('Failed to read file'));
|
||||
};
|
||||
|
||||
if (file.type === 'text/plain') {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
// For other file types, we'll need to extract text
|
||||
// This is a simplified version - in production, you'd want proper PDF/DOC parsing
|
||||
reader.readAsText(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Multi-device functions
|
||||
async function enableRemoteAccess() {
|
||||
try {
|
||||
remoteStatus.textContent = 'Starting server...';
|
||||
remoteStatus.className = 'status-message';
|
||||
|
||||
// Generate a unique session ID
|
||||
const sessionId = Math.random().toString(36).substring(2, 15);
|
||||
const port = 8765;
|
||||
const accessURL = `http://localhost:${port}?session=${sessionId}`;
|
||||
|
||||
// Start WebSocket server (we'll implement this)
|
||||
chrome.runtime.sendMessage({
|
||||
action: 'startRemoteServer',
|
||||
sessionId: sessionId,
|
||||
port: port
|
||||
}, (response) => {
|
||||
if (response.success) {
|
||||
remoteServerActive = true;
|
||||
accessUrl.textContent = accessURL;
|
||||
deviceInfo.style.display = 'block';
|
||||
remoteStatus.textContent = 'Remote access enabled!';
|
||||
remoteStatus.className = 'status-message success';
|
||||
enableRemoteListening.textContent = '🛑 Disable Remote Access';
|
||||
|
||||
// Generate QR code (simplified)
|
||||
generateQRCode(accessURL);
|
||||
} else {
|
||||
remoteStatus.textContent = 'Failed to start server: ' + response.error;
|
||||
remoteStatus.className = 'status-message error';
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
remoteStatus.textContent = 'Error: ' + error.message;
|
||||
remoteStatus.className = 'status-message error';
|
||||
}
|
||||
}
|
||||
|
||||
function disableRemoteAccess() {
|
||||
chrome.runtime.sendMessage({ action: 'stopRemoteServer' }, (response) => {
|
||||
remoteServerActive = false;
|
||||
deviceInfo.style.display = 'none';
|
||||
remoteStatus.textContent = '';
|
||||
enableRemoteListening.textContent = '🌐 Enable Remote Access';
|
||||
});
|
||||
}
|
||||
|
||||
function generateQRCode(url) {
|
||||
// Simple QR code placeholder - in production, use a QR code library
|
||||
qrCode.innerHTML = `
|
||||
<div style="border: 2px solid #333; padding: 10px; display: inline-block;">
|
||||
<div style="font-size: 8px; font-family: monospace;">QR Code</div>
|
||||
<div style="font-size: 6px;">Scan to access</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Make functions available globally for onclick handlers
|
||||
window.editContext = function(index) {
|
||||
chrome.storage.local.get('contexts', (result) => {
|
||||
const contexts = result.contexts || [];
|
||||
const context = contexts[index];
|
||||
if (context) {
|
||||
contextTitleInput.value = context.title;
|
||||
contextTextInput.value = context.content;
|
||||
if (contextTypeSelect) contextTypeSelect.value = context.type || 'general';
|
||||
switchTab('text');
|
||||
// Remove the old context when editing
|
||||
deleteContext(index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteContext = deleteContext;
|
||||
|
||||
// Event listeners
|
||||
aiProviderSelect.addEventListener('change', function() {
|
||||
const selectedProvider = this.value;
|
||||
updateModelOptions(selectedProvider);
|
||||
updateApiKeyInput(selectedProvider);
|
||||
|
||||
// Save provider selection
|
||||
chrome.storage.sync.set({
|
||||
aiProvider: selectedProvider,
|
||||
selectedModel: aiProviders[selectedProvider].defaultModel
|
||||
});
|
||||
|
||||
// Load saved API key for this provider
|
||||
chrome.storage.sync.get('apiKeys', (result) => {
|
||||
const apiKeys = result.apiKeys || {};
|
||||
if (apiKeys[selectedProvider] && aiProviders[selectedProvider].requiresKey) {
|
||||
apiKeyInput.value = apiKeys[selectedProvider];
|
||||
updateApiKeyStatus('API Key Saved', 'success');
|
||||
saveApiKeyButton.textContent = 'API Key Saved';
|
||||
saveApiKeyButton.disabled = true;
|
||||
} else {
|
||||
apiKeyInput.value = '';
|
||||
saveApiKeyButton.textContent = 'Save API Key';
|
||||
saveApiKeyButton.disabled = !aiProviders[selectedProvider].requiresKey;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
modelSelect.addEventListener('change', function() {
|
||||
chrome.storage.sync.set({ selectedModel: this.value });
|
||||
});
|
||||
|
||||
apiKeyInput.addEventListener('input', function() {
|
||||
if (aiProviders[aiProviderSelect.value].requiresKey) {
|
||||
saveApiKeyButton.textContent = 'Save API Key';
|
||||
saveApiKeyButton.disabled = false;
|
||||
updateApiKeyStatus('', '');
|
||||
}
|
||||
});
|
||||
|
||||
saveApiKeyButton.addEventListener('click', function() {
|
||||
const apiKey = apiKeyInput.value.trim();
|
||||
const provider = aiProviderSelect.value;
|
||||
|
||||
if (!aiProviders[provider].requiresKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
// Save API key for the current provider
|
||||
chrome.storage.sync.get('apiKeys', (result) => {
|
||||
const apiKeys = result.apiKeys || {};
|
||||
apiKeys[provider] = apiKey;
|
||||
|
||||
chrome.storage.sync.set({ apiKeys: apiKeys }, () => {
|
||||
saveApiKeyButton.textContent = 'API Key Saved';
|
||||
saveApiKeyButton.disabled = true;
|
||||
updateApiKeyStatus('API Key Saved', 'success');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
updateApiKeyStatus('Please enter a valid API key', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Context management event listeners
|
||||
document.querySelectorAll('.tab-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const tabName = this.getAttribute('data-tab');
|
||||
switchTab(tabName);
|
||||
});
|
||||
});
|
||||
|
||||
uploadContextBtn.addEventListener('click', function() {
|
||||
contextFileInput.click();
|
||||
});
|
||||
|
||||
contextFileInput.addEventListener('change', async function() {
|
||||
const files = Array.from(this.files);
|
||||
for (const file of files) {
|
||||
try {
|
||||
const result = await processFile(file);
|
||||
await saveContext(result.title, result.content);
|
||||
} catch (error) {
|
||||
alert('Error processing file: ' + error.message);
|
||||
}
|
||||
}
|
||||
this.value = ''; // Clear file input
|
||||
});
|
||||
|
||||
addContextBtn.addEventListener('click', function() {
|
||||
const title = contextTitleInput.value.trim();
|
||||
const content = contextTextInput.value.trim();
|
||||
saveContext(title, content);
|
||||
});
|
||||
|
||||
clearAllContextBtn.addEventListener('click', clearAllContexts);
|
||||
|
||||
// Multi-device event listeners
|
||||
enableRemoteListening.addEventListener('click', function() {
|
||||
if (remoteServerActive) {
|
||||
disableRemoteAccess();
|
||||
} else {
|
||||
enableRemoteAccess();
|
||||
}
|
||||
});
|
||||
|
||||
copyUrlBtn.addEventListener('click', function() {
|
||||
navigator.clipboard.writeText(accessUrl.textContent).then(() => {
|
||||
const originalText = copyUrlBtn.textContent;
|
||||
copyUrlBtn.textContent = '✅ Copied!';
|
||||
setTimeout(() => {
|
||||
copyUrlBtn.textContent = originalText;
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
toggleButton.addEventListener('click', function() {
|
||||
isListening = !isListening;
|
||||
toggleButton.textContent = isListening ? 'Stop Listening' : 'Start Listening';
|
||||
|
||||
if (isListening) {
|
||||
// Send current AI configuration with start listening
|
||||
const currentProvider = aiProviderSelect.value;
|
||||
const currentModel = modelSelect.value;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
action: 'startListening',
|
||||
aiProvider: currentProvider,
|
||||
model: currentModel
|
||||
});
|
||||
transcriptDiv.textContent = 'Listening for questions...';
|
||||
aiResponseDiv.textContent = `Using ${aiProviders[currentProvider].name} (${currentModel}). The answer will appear here.`;
|
||||
} else {
|
||||
chrome.runtime.sendMessage({action: 'stopListening'});
|
||||
transcriptDiv.textContent = '';
|
||||
aiResponseDiv.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
if (request.action === 'updateTranscript') {
|
||||
transcriptDiv.textContent = request.transcript;
|
||||
} else if (request.action === 'updateAIResponse') {
|
||||
aiResponseDiv.textContent = request.response;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user