From 8f1ab9174a0a778e91bb27cdfc185540372454d4 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 2 Jun 2026 17:42:14 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Claude=20Arabic=20Voice=20Inp?= =?UTF-8?q?ut=20extension=20-=20Arabic=20speech-to-text=20for=20Claude.ai?= =?UTF-8?q?=20with=20Gemini=20AI=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- claude-arabic-voice/background.js | 83 ++++ claude-arabic-voice/content.js | 635 ++++++++++++++++++++++++++ claude-arabic-voice/icons/icon128.svg | 4 + claude-arabic-voice/icons/icon16.svg | 4 + claude-arabic-voice/icons/icon48.svg | 4 + claude-arabic-voice/manifest.json | 43 ++ claude-arabic-voice/popup.html | 96 ++++ claude-arabic-voice/popup.js | 83 ++++ claude-arabic-voice/styles.css | 290 ++++++++++++ 9 files changed, 1242 insertions(+) create mode 100644 claude-arabic-voice/background.js create mode 100644 claude-arabic-voice/content.js create mode 100644 claude-arabic-voice/icons/icon128.svg create mode 100644 claude-arabic-voice/icons/icon16.svg create mode 100644 claude-arabic-voice/icons/icon48.svg create mode 100644 claude-arabic-voice/manifest.json create mode 100644 claude-arabic-voice/popup.html create mode 100644 claude-arabic-voice/popup.js create mode 100644 claude-arabic-voice/styles.css diff --git a/claude-arabic-voice/background.js b/claude-arabic-voice/background.js new file mode 100644 index 0000000..38b98a9 --- /dev/null +++ b/claude-arabic-voice/background.js @@ -0,0 +1,83 @@ +// background.js — Service Worker for Claude Arabic Voice +// Handles Gemini API calls for voice text processing via the existing server proxy + +const SERVER_URL = 'https://cv.intaleqapp.com/cv/server/generate_cv.php'; + +// ─── Message Listener ──────────────────────────────────────────────────────── + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'GEMINI_PROCESS_VOICE') { + handleVoiceProcessing(message.payload) + .then(result => sendResponse({ success: true, data: result })) + .catch(err => sendResponse({ success: false, error: err.message })); + return true; // Keep message channel open for async + } +}); + +// ─── Voice Text Processing with Gemini (via server proxy) ──────────────────── + +async function handleVoiceProcessing({ apiKey, model, text, language }) { + if (!apiKey) { + throw new Error('Gemini API key is required'); + } + + // Detect if text is Arabic + 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 response = await fetch(SERVER_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'generateText', + apiKey: apiKey, + prompt: prompt + }) + }); + + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + const errMsg = errData.error?.message || `HTTP ${response.status}`; + throw new Error(`Gemini API error: ${errMsg}`); + } + + const data = await response.json(); + const resultText = data.candidates?.[0]?.content?.parts?.[0]?.text; + + if (!resultText) { + throw new Error('Empty response from Gemini API'); + } + + return { text: resultText.trim() }; +} + +// ─── Installation Handler ──────────────────────────────────────────────────── + +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + // Set default settings + chrome.storage.sync.set({ + language: 'ar-SA', + autoSend: false, + useGemini: false, + geminiApiKey: '', + geminiModel: 'gemini-flash-lite-latest' + }); + console.log('[ClaudeVoice] ✅ Extension installed with default settings'); + } +}); diff --git a/claude-arabic-voice/content.js b/claude-arabic-voice/content.js new file mode 100644 index 0000000..0b53aa5 --- /dev/null +++ b/claude-arabic-voice/content.js @@ -0,0 +1,635 @@ +// 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(); + } + ); + }); + } + + // ─── Speech Recognition Setup ──────────────────────────────────────────── + function initSpeechRecognition() { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognition) { + console.warn('[ClaudeVoice] Speech recognition not supported in this browser'); + return null; + } + + const recog = new SpeechRecognition(); + recog.continuous = true; + recog.interimResults = true; + recog.lang = settings.language; + recog.maxAlternatives = 1; + + recog.onresult = (event) => { + lastSpeechTime = Date.now(); + interimText = ''; + finalText = ''; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i]; + if (result.isFinal) { + finalText += result[0].transcript + ' '; + } else { + interimText += result[0].transcript; + } + } + + updateInputField(); + + // Reset silence timer on new speech + clearTimeout(silenceTimer); + silenceTimer = setTimeout(() => { + if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { + stopListening(); + } + }, SILENCE_TIMEOUT); + }; + + recog.onerror = (event) => { + console.error('[ClaudeVoice] Recognition error:', event.error); + if (event.error === 'no-speech') { + // No speech detected, restart if still listening + if (isListening) { + try { recog.stop(); } catch (e) { } + setTimeout(() => { + if (isListening) { + try { recog.start(); } catch (e) { } + } + }, 300); + } + return; + } + stopListening(); + showStatus('error', '❌ خطأ: ' + getArabicError(event.error)); + }; + + recog.onend = () => { + // If we're still supposed to be listening, restart + if (isListening) { + try { + recog.start(); + } catch (e) { + console.warn('[ClaudeVoice] Restart failed:', e); + } + } + }; + + return recog; + } + + 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 (!recognition) { + recognition = initSpeechRecognition(); + } + + if (!recognition) { + showStatus('error', '❌ المتصفح لا يدعم التعرف على الصوت'); + return; + } + + // Update language in case it changed + recognition.lang = settings.language; + + try { + recognition.start(); + isListening = true; + lastSpeechTime = Date.now(); + updateMicButton(true); + showStatus('listening', '🎤 جارٍ الاستماع...'); + + // Auto-stop after max time + clearTimeout(autoSendTimer); + autoSendTimer = setTimeout(() => { + if (isListening) stopListening(); + }, MAX_RECORDING_TIME); + + // Silence detection + clearTimeout(silenceTimer); + silenceTimer = setTimeout(() => { + if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { + stopListening(); + } + }, SILENCE_TIMEOUT); + + } catch (e) { + console.error('[ClaudeVoice] Start failed:', e); + showStatus('error', '❌ فشل بدء التسجيل'); + } + } + + async function stopListening() { + if (!recognition) return; + + isListening = false; + clearTimeout(silenceTimer); + clearTimeout(autoSendTimer); + + try { + recognition.stop(); + } catch (e) { /* ignore */ } + + updateMicButton(false); + + // If we have final text, process it + const text = finalText.trim() || interimText.trim(); + if (text) { + showStatus('processing', '⏳ جارٍ المعالجة...'); + + if (settings.useGemini && settings.geminiApiKey) { + await processWithGemini(text); + } else { + // Just insert the text directly + 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 { + const response = await chrome.runtime.sendMessage({ + type: 'GEMINI_PROCESS_VOICE', + payload: { + apiKey: settings.geminiApiKey, + model: settings.geminiModel, + text: text, + language: settings.language + } + }); + + if (response && response.success) { + const processedText = response.data.text; + insertTextIntoClaude(processedText); + showStatus('done', '✅ تمت المعالجة بواسطة Gemini'); + } else { + // Fallback: insert original text + 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(); + 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(); + } + +})(); diff --git a/claude-arabic-voice/icons/icon128.svg b/claude-arabic-voice/icons/icon128.svg new file mode 100644 index 0000000..b3ffbed --- /dev/null +++ b/claude-arabic-voice/icons/icon128.svg @@ -0,0 +1,4 @@ + + + 🎤 + diff --git a/claude-arabic-voice/icons/icon16.svg b/claude-arabic-voice/icons/icon16.svg new file mode 100644 index 0000000..a728497 --- /dev/null +++ b/claude-arabic-voice/icons/icon16.svg @@ -0,0 +1,4 @@ + + + �� + diff --git a/claude-arabic-voice/icons/icon48.svg b/claude-arabic-voice/icons/icon48.svg new file mode 100644 index 0000000..b8a0474 --- /dev/null +++ b/claude-arabic-voice/icons/icon48.svg @@ -0,0 +1,4 @@ + + + 🎤 + diff --git a/claude-arabic-voice/manifest.json b/claude-arabic-voice/manifest.json new file mode 100644 index 0000000..3438431 --- /dev/null +++ b/claude-arabic-voice/manifest.json @@ -0,0 +1,43 @@ +{ + "manifest_version": 3, + "name": "كلود - إملاء صوتي عربي", + "version": "1.0.0", + "description": "يضيف زر مايكروفون لموقع Claude.ai للكتابة بالصوت باللغة العربية - Arabic speech-to-text for Claude.ai", + "permissions": [ + "storage" + ], + "host_permissions": [ + "https://claude.ai/*", + "https://cv.intaleqapp.com/*" + ], + "content_scripts": [ + { + "matches": [ + "https://claude.ai/*" + ], + "js": [ + "content.js" + ], + "css": [ + "styles.css" + ], + "run_at": "document_idle" + } + ], + "background": { + "service_worker": "background.js" + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.svg", + "48": "icons/icon48.svg", + "128": "icons/icon128.svg" + } + }, + "icons": { + "16": "icons/icon16.svg", + "48": "icons/icon48.svg", + "128": "icons/icon128.svg" + } +} \ No newline at end of file diff --git a/claude-arabic-voice/popup.html b/claude-arabic-voice/popup.html new file mode 100644 index 0000000..db1ddac --- /dev/null +++ b/claude-arabic-voice/popup.html @@ -0,0 +1,96 @@ + + + + + + كلود - إملاء صوتي عربي + + + + + + + + diff --git a/claude-arabic-voice/popup.js b/claude-arabic-voice/popup.js new file mode 100644 index 0000000..751a4af --- /dev/null +++ b/claude-arabic-voice/popup.js @@ -0,0 +1,83 @@ +// popup.js — Settings UI for Claude Arabic Voice + +document.addEventListener('DOMContentLoaded', () => { + // ─── Load Settings ─────────────────────────────────────────────────────── + chrome.storage.sync.get( + ['language', 'autoSend', 'useGemini', 'geminiApiKey', 'geminiModel'], + (data) => { + if (data.language) document.getElementById('language').value = data.language; + if (data.autoSend) document.getElementById('autoSend').checked = data.autoSend; + if (data.useGemini) { + document.getElementById('useGemini').checked = data.useGemini; + document.getElementById('geminiSettings').style.display = 'block'; + } + if (data.geminiApiKey) document.getElementById('geminiApiKey').value = data.geminiApiKey; + } + ); + + // ─── Toggle Gemini Settings ────────────────────────────────────────────── + document.getElementById('useGemini').addEventListener('change', (e) => { + document.getElementById('geminiSettings').style.display = e.target.checked ? 'block' : 'none'; + }); + + // ─── Save Settings ─────────────────────────────────────────────────────── + document.getElementById('saveBtn').addEventListener('click', () => { + const settings = { + language: document.getElementById('language').value, + autoSend: document.getElementById('autoSend').checked, + useGemini: document.getElementById('useGemini').checked, + geminiApiKey: document.getElementById('geminiApiKey').value.trim(), + geminiModel: 'gemini-flash-lite-latest' + }; + + chrome.storage.sync.set(settings, () => { + const message = document.getElementById('saveMessage'); + message.textContent = '✅ تم حفظ الإعدادات بنجاح!'; + message.style.color = '#00c853'; + setTimeout(() => { + message.textContent = ''; + }, 2500); + + // Notify content script of settings change + chrome.tabs.query({ url: 'https://claude.ai/*' }, (tabs) => { + tabs.forEach(tab => { + chrome.tabs.sendMessage(tab.id, { + type: 'SETTINGS_UPDATED', + payload: settings + }).catch(() => { /* tab may not have content script */ }); + }); + }); + }); + }); + + // ─── Update Status ────────────────────────────────────────────────────── + function updateStatus() { + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + + chrome.tabs.query({ url: 'https://claude.ai/*' }, (tabs) => { + if (tabs.length === 0) { + statusDot.className = 'status-dot offline'; + statusText.textContent = '🔴 Claude.ai غير مفتوح'; + return; + } + + // Check if content script is loaded + chrome.tabs.sendMessage(tabs[0].id, { type: 'GET_STATUS' }, (response) => { + if (chrome.runtime.lastError) { + statusDot.className = 'status-dot offline'; + statusText.textContent = '🔴 الإضافة غير نشطة - أعد تحميل الصفحة'; + } else if (response && response.isListening) { + statusDot.className = 'status-dot listening'; + statusText.textContent = '🔴 جارٍ الاستماع...'; + } else { + statusDot.className = 'status-dot online'; + statusText.textContent = '✅ جاهز للاستخدام'; + } + }); + }); + } + + updateStatus(); + setInterval(updateStatus, 3000); +}); diff --git a/claude-arabic-voice/styles.css b/claude-arabic-voice/styles.css new file mode 100644 index 0000000..c926e14 --- /dev/null +++ b/claude-arabic-voice/styles.css @@ -0,0 +1,290 @@ +/* ─── Popup Styles ─────────────────────────────────────────────────────────── */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, -apple-system, BlinkMacSystemFont, sans-serif; + background: #1a1a2e; + color: #e0e0e0; + width: 380px; + min-height: 400px; + direction: rtl; +} + +.popup-container { + padding: 16px; +} + +/* ─── Header ──────────────────────────────────────────────────────────────── */ + +.popup-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.popup-icon { + font-size: 32px; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #6c63ff, #4834d4); + border-radius: 12px; +} + +.popup-title h1 { + font-size: 16px; + font-weight: 700; + color: #fff; + margin: 0; +} + +.popup-title .subtitle { + font-size: 11px; + color: #888; + margin: 2px 0 0 0; +} + +/* ─── Status ──────────────────────────────────────────────────────────────── */ + +.status-section { + margin-bottom: 16px; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 8px; + background: rgba(255, 255, 255, 0.05); + padding: 8px 12px; + border-radius: 8px; + font-size: 13px; +} + +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #666; + flex-shrink: 0; +} + +.status-dot.online { + background: #00c853; + box-shadow: 0 0 8px rgba(0, 200, 83, 0.5); +} + +.status-dot.offline { + background: #ff5252; + box-shadow: 0 0 8px rgba(255, 82, 82, 0.5); +} + +.status-dot.listening { + background: #ff1744; + animation: pulse-dot 1.5s infinite; +} + +@keyframes pulse-dot { + 0% { + box-shadow: 0 0 0 0 rgba(255, 23, 68, 0.6); + } + + 50% { + box-shadow: 0 0 0 8px rgba(255, 23, 68, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(255, 23, 68, 0); + } +} + +/* ─── Settings ────────────────────────────────────────────────────────────── */ + +.settings-section { + margin-bottom: 16px; +} + +.settings-section h2 { + font-size: 14px; + font-weight: 600; + color: #ccc; + margin-bottom: 12px; +} + +.settings-section h3 { + font-size: 13px; + font-weight: 600; + color: #bbb; + margin-bottom: 8px; +} + +.setting-group { + margin-bottom: 14px; +} + +.setting-group label { + display: block; + font-size: 12px; + font-weight: 500; + color: #aaa; + margin-bottom: 4px; +} + +.setting-group select, +.setting-group input[type="password"], +.setting-group input[type="text"] { + width: 100%; + padding: 8px 10px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + color: #fff; + font-size: 13px; + font-family: inherit; + outline: none; + transition: border-color 0.2s; +} + +.setting-group select:focus, +.setting-group input:focus { + border-color: #6c63ff; +} + +.setting-group select option { + background: #1a1a2e; + color: #fff; +} + +.setting-hint { + font-size: 11px; + color: #777; + margin-top: 4px; + line-height: 1.4; +} + +.setting-hint a { + color: #6c63ff; + text-decoration: none; +} + +.setting-hint a:hover { + text-decoration: underline; +} + +/* ─── Checkbox ────────────────────────────────────────────────────────────── */ + +.checkbox-group { + margin-bottom: 10px; +} + +.checkbox-label { + display: flex !important; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 13px !important; + color: #ddd !important; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: #6c63ff; + cursor: pointer; +} + +/* ─── Divider ─────────────────────────────────────────────────────────────── */ + +.divider { + padding-top: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +/* ─── Gemini Settings ─────────────────────────────────────────────────────── */ + +.gemini-settings { + margin-top: 10px; + padding: 12px; + background: rgba(108, 99, 255, 0.08); + border: 1px solid rgba(108, 99, 255, 0.2); + border-radius: 8px; +} + +.gemini-settings label { + margin-top: 8px; +} + +.gemini-settings label:first-child { + margin-top: 0; +} + +/* ─── Help Section ────────────────────────────────────────────────────────── */ + +.help-section { + margin-bottom: 16px; + padding: 12px; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; +} + +.help-section h2 { + font-size: 13px; + font-weight: 600; + color: #ccc; + margin-bottom: 8px; +} + +.help-section ol { + padding-right: 20px; + font-size: 12px; + color: #999; + line-height: 1.8; +} + +.help-section ol li strong { + color: #ddd; +} + +/* ─── Footer ──────────────────────────────────────────────────────────────── */ + +.popup-footer { + text-align: center; +} + +.save-btn { + width: 100%; + padding: 10px; + background: linear-gradient(135deg, #6c63ff, #4834d4); + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + font-family: inherit; +} + +.save-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(108, 99, 255, 0.4); +} + +.save-btn:active { + transform: translateY(0); +} + +.save-message { + font-size: 12px; + margin-top: 8px; + min-height: 18px; +} \ No newline at end of file