652 lines
26 KiB
JavaScript
652 lines
26 KiB
JavaScript
// content.js — Arabic Voice Input for Claude.ai
|
|
// Injects a microphone button that uses Web Speech API for Arabic speech recognition
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Prevent double injection
|
|
if (window.__claudeArabicVoiceLoaded) return;
|
|
window.__claudeArabicVoiceLoaded = true;
|
|
|
|
// ─── State ───────────────────────────────────────────────────────────────
|
|
let recognition = null;
|
|
let isListening = false;
|
|
let micButton = null;
|
|
let statusIndicator = null;
|
|
let interimText = '';
|
|
let finalText = '';
|
|
let autoSendTimer = null;
|
|
let silenceTimer = null;
|
|
let lastSpeechTime = 0;
|
|
let isGeminiProcessing = false;
|
|
|
|
const SILENCE_TIMEOUT = 2000; // 2 seconds of silence = auto-stop
|
|
const MAX_RECORDING_TIME = 30000; // 30 seconds max recording
|
|
|
|
// ─── Settings ────────────────────────────────────────────────────────────
|
|
let settings = {
|
|
language: 'ar-SA', // Default: Arabic (Saudi Arabia)
|
|
autoSend: false, // Auto-send after stopping
|
|
useGemini: false, // Use Gemini AI to refine text
|
|
geminiApiKey: '',
|
|
geminiModel: 'gemini-flash-lite-latest'
|
|
};
|
|
|
|
// ─── Load Settings ───────────────────────────────────────────────────────
|
|
async function loadSettings() {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.sync.get(
|
|
['language', 'autoSend', 'useGemini', 'geminiApiKey', 'geminiModel'],
|
|
(data) => {
|
|
if (data.language) settings.language = data.language;
|
|
if (data.autoSend !== undefined) settings.autoSend = data.autoSend;
|
|
if (data.useGemini !== undefined) settings.useGemini = data.useGemini;
|
|
if (data.geminiApiKey) settings.geminiApiKey = data.geminiApiKey;
|
|
if (data.geminiModel) settings.geminiModel = data.geminiModel;
|
|
resolve();
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
// ─── Iframe Integration ──────────────────────────────────────────────────
|
|
let speechIframe = null;
|
|
|
|
function initSpeechIframe() {
|
|
if (speechIframe) return;
|
|
|
|
speechIframe = document.createElement('iframe');
|
|
speechIframe.id = 'claude-voice-speech-iframe';
|
|
speechIframe.style.cssText = `
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
left: -9999px;
|
|
top: -9999px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
border: none;
|
|
`;
|
|
speechIframe.src = chrome.runtime.getURL('claude-arabic-voice/speech.html');
|
|
speechIframe.allow = 'microphone';
|
|
document.body.appendChild(speechIframe);
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (event.source !== speechIframe.contentWindow) return;
|
|
|
|
const message = event.data;
|
|
if (message.type === 'SPEECH_START_SUCCESS') {
|
|
isListening = true;
|
|
lastSpeechTime = Date.now();
|
|
interimText = '';
|
|
finalText = '';
|
|
updateMicButton(true);
|
|
showStatus('listening', '🎤 جارٍ الاستماع...');
|
|
|
|
clearTimeout(autoSendTimer);
|
|
autoSendTimer = setTimeout(() => {
|
|
if (isListening) stopListening();
|
|
}, MAX_RECORDING_TIME);
|
|
|
|
clearTimeout(silenceTimer);
|
|
silenceTimer = setTimeout(checkSilence, SILENCE_TIMEOUT);
|
|
} else if (message.type === 'SPEECH_RESULT') {
|
|
lastSpeechTime = Date.now();
|
|
interimText = message.payload.interimText || '';
|
|
finalText = message.payload.finalText || '';
|
|
|
|
updateInputField();
|
|
|
|
clearTimeout(silenceTimer);
|
|
silenceTimer = setTimeout(checkSilence, SILENCE_TIMEOUT);
|
|
} else if (message.type === 'SPEECH_ERROR') {
|
|
console.error('[ClaudeVoice] Recognition error:', message.payload.error);
|
|
if (message.payload.error === 'no-speech') {
|
|
return;
|
|
}
|
|
stopListening();
|
|
|
|
if (message.payload.error === 'not-allowed') {
|
|
showStatus('error', '❌ الرجاء منح الصلاحية (تم فتح صفحة جديدة)');
|
|
chrome.runtime.sendMessage({ type: 'OPEN_PERMISSION_PAGE' });
|
|
} else {
|
|
showStatus('error', '❌ خطأ: ' + getArabicError(message.payload.error));
|
|
}
|
|
} else if (message.type === 'SPEECH_END') {
|
|
if (isListening) stopListening();
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkSilence() {
|
|
if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) {
|
|
stopListening();
|
|
}
|
|
}
|
|
|
|
function getArabicError(error) {
|
|
const errors = {
|
|
'no-speech': 'لم يتم اكتشاف كلام',
|
|
'aborted': 'تم الإلغاء',
|
|
'audio-capture': 'الميكروفون غير متاح',
|
|
'network': 'خطأ في الشبكة',
|
|
'not-allowed': 'الرجاء السماح باستخدام الميكروفون',
|
|
'service-not-allowed': 'الخدمة غير متاحة',
|
|
'bad-grammar': 'خطأ في القواعد'
|
|
};
|
|
return errors[error] || error;
|
|
}
|
|
|
|
// ─── Start / Stop Listening ─────────────────────────────────────────────
|
|
async function startListening() {
|
|
await loadSettings();
|
|
if (!speechIframe) initSpeechIframe();
|
|
|
|
try {
|
|
speechIframe.contentWindow.postMessage({
|
|
type: 'START_RECORDING',
|
|
payload: { language: settings.language }
|
|
}, '*');
|
|
} catch (e) {
|
|
console.error('[ClaudeVoice] Start message failed:', e);
|
|
showStatus('error', '❌ فشل الاتصال بالإضافة');
|
|
}
|
|
}
|
|
|
|
async function stopListening() {
|
|
if (!isListening) return;
|
|
|
|
isListening = false;
|
|
clearTimeout(silenceTimer);
|
|
clearTimeout(autoSendTimer);
|
|
|
|
if (speechIframe) {
|
|
speechIframe.contentWindow.postMessage({ type: 'STOP_RECORDING' }, '*');
|
|
}
|
|
updateMicButton(false);
|
|
|
|
const text = finalText.trim() || interimText.trim();
|
|
if (text) {
|
|
showStatus('processing', '⏳ جارٍ المعالجة...');
|
|
|
|
if (settings.useGemini && settings.geminiApiKey) {
|
|
await processWithGemini(text);
|
|
} else {
|
|
insertTextIntoClaude(text);
|
|
showStatus('done', '✅ تم الإدراج');
|
|
setTimeout(() => hideStatus(), 2000);
|
|
}
|
|
} else {
|
|
showStatus('idle', '🎤 اضغط للتحدث');
|
|
setTimeout(() => hideStatus(), 1500);
|
|
}
|
|
}
|
|
|
|
// ─── Update Claude Input ─────────────────────────────────────────────────
|
|
function updateInputField() {
|
|
const fullText = (finalText + interimText).trim();
|
|
if (!fullText) return;
|
|
|
|
// Find Claude's input area
|
|
const inputArea = findClaudeInput();
|
|
if (!inputArea) return;
|
|
|
|
// For interim results, we update a placeholder
|
|
// For final results, we insert the text
|
|
if (interimText) {
|
|
// Show interim in a floating preview
|
|
showInterimPreview(interimText);
|
|
} else {
|
|
hideInterimPreview();
|
|
}
|
|
}
|
|
|
|
function findClaudeInput() {
|
|
// Claude.ai uses a contenteditable div or textarea
|
|
// Try multiple selectors as Claude's UI may change
|
|
const selectors = [
|
|
'[contenteditable="true"][role="textbox"]',
|
|
'[contenteditable="true"].ProseMirror',
|
|
'div[contenteditable="true"]',
|
|
'textarea[placeholder*="Message"]',
|
|
'textarea[placeholder*="Ask"]',
|
|
'textarea[placeholder*="مراسلة"]',
|
|
'textarea[placeholder*="اسأل"]',
|
|
'.ProseMirror[contenteditable="true"]',
|
|
'div[role="textbox"][contenteditable="true"]'
|
|
];
|
|
|
|
for (const sel of selectors) {
|
|
const el = document.querySelector(sel);
|
|
if (el) return el;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findSendButton() {
|
|
const selectors = [
|
|
'button[aria-label*="Send"]',
|
|
'button[aria-label*="إرسال"]',
|
|
'button[data-testid*="send"]',
|
|
'button[class*="send"]',
|
|
'button[class*="Send"]',
|
|
'form button[type="submit"]',
|
|
// Claude's specific send button
|
|
'button:has(svg[class*="send"])',
|
|
'button:has(svg[data-icon*="arrow"])'
|
|
];
|
|
|
|
for (const sel of selectors) {
|
|
const el = document.querySelector(sel);
|
|
if (el) return el;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function insertTextIntoClaude(text) {
|
|
const inputArea = findClaudeInput();
|
|
if (!inputArea) {
|
|
showStatus('error', '❌ لم يتم العثور على حقل الإدخال');
|
|
return false;
|
|
}
|
|
|
|
// Check if it's a contenteditable div (ProseMirror) or textarea
|
|
if (inputArea.isContentEditable || inputArea.tagName === 'DIV') {
|
|
// For contenteditable (Claude's ProseMirror editor)
|
|
// Insert text at cursor position or append
|
|
const selection = window.getSelection();
|
|
const range = document.createRange();
|
|
|
|
// Focus the input area
|
|
inputArea.focus();
|
|
|
|
// Try to place cursor at end
|
|
if (inputArea.lastChild) {
|
|
range.setStartAfter(inputArea.lastChild);
|
|
range.setEndAfter(inputArea.lastChild);
|
|
} else {
|
|
range.selectNodeContents(inputArea);
|
|
range.collapse(false);
|
|
}
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
|
|
// Insert text
|
|
document.execCommand('insertText', false, text);
|
|
|
|
// Dispatch input event for React/ProseMirror to detect
|
|
inputArea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
inputArea.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
} else if (inputArea.tagName === 'TEXTAREA' || inputArea.tagName === 'INPUT') {
|
|
// For textarea/input
|
|
const start = inputArea.selectionStart || inputArea.value.length;
|
|
const end = inputArea.selectionEnd || inputArea.value.length;
|
|
inputArea.value = inputArea.value.substring(0, start) + text + inputArea.value.substring(end);
|
|
inputArea.selectionStart = inputArea.selectionEnd = start + text.length;
|
|
inputArea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
inputArea.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
// Auto-send if enabled
|
|
if (settings.autoSend) {
|
|
setTimeout(() => {
|
|
const sendBtn = findSendButton();
|
|
if (sendBtn && !sendBtn.disabled) {
|
|
sendBtn.click();
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ─── Interim Preview ─────────────────────────────────────────────────────
|
|
let previewEl = null;
|
|
|
|
function showInterimPreview(text) {
|
|
if (!previewEl) {
|
|
previewEl = document.createElement('div');
|
|
previewEl.id = 'claude-voice-interim';
|
|
previewEl.style.cssText = `
|
|
position: fixed;
|
|
bottom: 100px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0,0,0,0.85);
|
|
color: #fff;
|
|
padding: 12px 20px;
|
|
border-radius: 12px;
|
|
font-size: 16px;
|
|
max-width: 600px;
|
|
width: 90%;
|
|
text-align: center;
|
|
z-index: 999999;
|
|
direction: rtl;
|
|
font-family: 'Segoe UI', Tahoma, sans-serif;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
pointer-events: none;
|
|
`;
|
|
document.body.appendChild(previewEl);
|
|
}
|
|
previewEl.textContent = '🎤 ' + text + ' ▊';
|
|
previewEl.style.display = 'block';
|
|
}
|
|
|
|
function hideInterimPreview() {
|
|
if (previewEl) {
|
|
previewEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ─── Gemini AI Processing ────────────────────────────────────────────────
|
|
async function processWithGemini(text) {
|
|
isGeminiProcessing = true;
|
|
|
|
try {
|
|
let processedText = null;
|
|
|
|
// Try direct fetch to server (more reliable than chrome.runtime.sendMessage)
|
|
try {
|
|
const isArabic = /[\u0600-\u06FF]/.test(text);
|
|
const langName = isArabic ? 'Arabic' : 'the detected language';
|
|
|
|
const prompt = `You are a text refinement assistant. Your task is to:
|
|
|
|
1. Correct any speech recognition errors in the following text
|
|
2. Fix punctuation, capitalization, and formatting
|
|
3. Keep the original meaning and content intact
|
|
4. Do NOT add any new information or commentary
|
|
5. Return ONLY the corrected text, nothing else
|
|
|
|
The text is in ${langName}. Preserve the original language.
|
|
|
|
TEXT TO REFINE:
|
|
${text}
|
|
|
|
CORRECTED TEXT:`;
|
|
|
|
const directResponse = await fetch('https://cv.intaleqapp.com/cv/server/generate_cv.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'generateText',
|
|
apiKey: settings.geminiApiKey,
|
|
prompt: prompt
|
|
})
|
|
});
|
|
|
|
if (directResponse.ok) {
|
|
const rawText = await directResponse.text();
|
|
let data;
|
|
try {
|
|
data = JSON.parse(rawText);
|
|
} catch (e) {
|
|
data = rawText;
|
|
}
|
|
|
|
if (data && data.candidates && data.candidates[0]) {
|
|
processedText = data.candidates[0].content?.parts?.[0]?.text;
|
|
} else if (typeof data === 'string') {
|
|
processedText = data;
|
|
}
|
|
} else {
|
|
console.warn('[ClaudeVoice] Server fetch failed:', directResponse.status);
|
|
}
|
|
} catch (fetchErr) {
|
|
console.warn('[ClaudeVoice] Server fetch error:', fetchErr);
|
|
}
|
|
|
|
if (processedText) {
|
|
insertTextIntoClaude(processedText);
|
|
showStatus('done', '✅ تمت المعالجة بواسطة Gemini');
|
|
} else {
|
|
insertTextIntoClaude(text);
|
|
showStatus('done', '✅ تم الإدراج (بدون معالجة)');
|
|
}
|
|
} catch (e) {
|
|
console.error('[ClaudeVoice] Gemini error:', e);
|
|
insertTextIntoClaude(text);
|
|
showStatus('done', '✅ تم الإدراج (بدون معالجة)');
|
|
}
|
|
|
|
isGeminiProcessing = false;
|
|
setTimeout(() => hideStatus(), 2000);
|
|
}
|
|
|
|
// ─── UI: Mic Button ──────────────────────────────────────────────────────
|
|
function createMicButton() {
|
|
if (micButton) return;
|
|
|
|
micButton = document.createElement('button');
|
|
micButton.id = 'claude-voice-mic-btn';
|
|
micButton.innerHTML = '🎤';
|
|
micButton.title = 'إملاء صوتي عربي - اضغط للتحدث';
|
|
micButton.setAttribute('aria-label', 'إملاء صوتي عربي');
|
|
|
|
micButton.addEventListener('click', () => {
|
|
if (isListening) {
|
|
stopListening();
|
|
} else {
|
|
startListening();
|
|
}
|
|
});
|
|
|
|
// Status indicator
|
|
statusIndicator = document.createElement('div');
|
|
statusIndicator.id = 'claude-voice-status';
|
|
statusIndicator.textContent = '🎤 اضغط للتحدث';
|
|
statusIndicator.style.display = 'none';
|
|
|
|
// Container
|
|
const container = document.createElement('div');
|
|
container.id = 'claude-voice-container';
|
|
container.appendChild(micButton);
|
|
container.appendChild(statusIndicator);
|
|
|
|
document.body.appendChild(container);
|
|
|
|
// Position the button near Claude's input area
|
|
positionMicButton();
|
|
}
|
|
|
|
function positionMicButton() {
|
|
// Try to find the input area and position near it
|
|
const observer = new MutationObserver(() => {
|
|
const inputArea = findClaudeInput();
|
|
if (inputArea && micButton) {
|
|
const rect = inputArea.getBoundingClientRect();
|
|
if (rect && rect.top > 0) {
|
|
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
|
|
micButton.style.right = '20px';
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
attributes: true,
|
|
attributeFilter: ['style', 'class']
|
|
});
|
|
|
|
// Initial positioning
|
|
setTimeout(() => {
|
|
const inputArea = findClaudeInput();
|
|
if (inputArea && micButton) {
|
|
const rect = inputArea.getBoundingClientRect();
|
|
if (rect && rect.top > 0) {
|
|
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
|
|
}
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
function updateMicButton(listening) {
|
|
if (!micButton) return;
|
|
|
|
if (listening) {
|
|
micButton.classList.add('listening');
|
|
micButton.innerHTML = '🔴';
|
|
micButton.title = 'إيقاف التسجيل';
|
|
} else {
|
|
micButton.classList.remove('listening');
|
|
micButton.innerHTML = '🎤';
|
|
micButton.title = 'إملاء صوتي عربي - اضغط للتحدث';
|
|
}
|
|
}
|
|
|
|
function showStatus(type, message) {
|
|
if (!statusIndicator) return;
|
|
statusIndicator.textContent = message;
|
|
statusIndicator.className = 'claude-voice-status-' + type;
|
|
statusIndicator.style.display = 'block';
|
|
}
|
|
|
|
function hideStatus() {
|
|
if (!statusIndicator) return;
|
|
statusIndicator.style.display = 'none';
|
|
}
|
|
|
|
// ─── Inject Styles ───────────────────────────────────────────────────────
|
|
function injectStyles() {
|
|
// Styles are loaded from styles.css via manifest
|
|
// But we also inject some critical inline styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
#claude-voice-container {
|
|
position: fixed;
|
|
z-index: 999999;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
bottom: 120px;
|
|
right: 20px;
|
|
}
|
|
#claude-voice-mic-btn {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: linear-gradient(135deg, #6c63ff, #4834d4);
|
|
color: white;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 15px rgba(108, 99, 255, 0.4);
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
#claude-voice-mic-btn:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 6px 20px rgba(108, 99, 255, 0.6);
|
|
}
|
|
#claude-voice-mic-btn.listening {
|
|
background: linear-gradient(135deg, #ff4444, #cc0000);
|
|
animation: claude-voice-pulse 1.5s infinite;
|
|
box-shadow: 0 4px 20px rgba(255, 68, 68, 0.6);
|
|
}
|
|
@keyframes claude-voice-pulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0.6); }
|
|
50% { box-shadow: 0 0 0 15px rgba(255, 68, 68, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0); }
|
|
}
|
|
#claude-voice-status {
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
padding: 6px 14px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-family: 'Segoe UI', Tahoma, sans-serif;
|
|
white-space: nowrap;
|
|
direction: rtl;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
pointer-events: none;
|
|
}
|
|
.claude-voice-status-listening {
|
|
background: rgba(255, 68, 68, 0.9) !important;
|
|
}
|
|
.claude-voice-status-processing {
|
|
background: rgba(255, 165, 0, 0.9) !important;
|
|
}
|
|
.claude-voice-status-done {
|
|
background: rgba(0, 200, 83, 0.9) !important;
|
|
}
|
|
.claude-voice-status-error {
|
|
background: rgba(255, 0, 0, 0.9) !important;
|
|
}
|
|
.claude-voice-status-idle {
|
|
background: rgba(108, 99, 255, 0.9) !important;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
// ─── Message Listener (for popup communication) ─────────────────────────
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message.type === 'GET_STATUS') {
|
|
sendResponse({ isListening });
|
|
return true;
|
|
}
|
|
if (message.type === 'SETTINGS_UPDATED') {
|
|
if (message.payload.language) settings.language = message.payload.language;
|
|
if (message.payload.autoSend !== undefined) settings.autoSend = message.payload.autoSend;
|
|
if (message.payload.useGemini !== undefined) settings.useGemini = message.payload.useGemini;
|
|
if (message.payload.geminiApiKey) settings.geminiApiKey = message.payload.geminiApiKey;
|
|
if (message.payload.geminiModel) settings.geminiModel = message.payload.geminiModel;
|
|
console.log('[ClaudeVoice] Settings updated:', settings);
|
|
sendResponse({ success: true });
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// ─── Initialize ──────────────────────────────────────────────────────────
|
|
async function init() {
|
|
await loadSettings();
|
|
injectStyles();
|
|
initSpeechIframe(); // Inject iframe immediately
|
|
createMicButton();
|
|
|
|
// Re-position on scroll and resize
|
|
window.addEventListener('scroll', () => {
|
|
const inputArea = findClaudeInput();
|
|
if (inputArea && micButton) {
|
|
const rect = inputArea.getBoundingClientRect();
|
|
if (rect && rect.top > 0) {
|
|
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
|
|
}
|
|
}
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
const inputArea = findClaudeInput();
|
|
if (inputArea && micButton) {
|
|
const rect = inputArea.getBoundingClientRect();
|
|
if (rect && rect.top > 0) {
|
|
micButton.style.bottom = (window.innerHeight - rect.top + 10) + 'px';
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('[ClaudeVoice] ✅ Arabic voice input extension loaded');
|
|
console.log('[ClaudeVoice] Language:', settings.language);
|
|
console.log('[ClaudeVoice] Gemini:', settings.useGemini ? 'Enabled' : 'Disabled');
|
|
}
|
|
|
|
// Wait for page to be ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
})();
|