Add Claude Arabic Voice Input extension - Arabic speech-to-text for Claude.ai with Gemini AI processing

This commit is contained in:
Hamza-Ayed
2026-06-02 17:42:14 +03:00
parent fa30682463
commit 8f1ab9174a
9 changed files with 1242 additions and 0 deletions

View File

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

View File

@@ -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();
}
})();

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<circle cx="64" cy="64" r="64" fill="#6c63ff"/>
<text x="64" y="90" text-anchor="middle" font-size="72" fill="white">🎤</text>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<circle cx="8" cy="8" r="8" fill="#6c63ff"/>
<text x="8" y="12" text-anchor="middle" font-size="10" fill="white"><EFBFBD><EFBFBD></text>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="24" fill="#6c63ff"/>
<text x="24" y="34" text-anchor="middle" font-size="28" fill="white">🎤</text>
</svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -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"
}
}

View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>كلود - إملاء صوتي عربي</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="popup-container">
<!-- Header -->
<div class="popup-header">
<div class="popup-icon">🎤</div>
<div class="popup-title">
<h1>الإملاء الصوتي لكلود</h1>
<p class="subtitle">Claude Arabic Voice Input</p>
</div>
</div>
<!-- Status -->
<div class="status-section">
<div class="status-indicator" id="statusIndicator">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">جاهز للاستخدام</span>
</div>
</div>
<!-- Settings -->
<div class="settings-section">
<h2>⚙️ الإعدادات</h2>
<!-- Language -->
<div class="setting-group">
<label for="language">لغة التعرف على الصوت</label>
<select id="language">
<option value="ar-SA">العربية (السعودية)</option>
<option value="ar-AE">العربية (الإمارات)</option>
<option value="ar-EG">العربية (مصر)</option>
<option value="ar-JO">العربية (الأردن)</option>
<option value="ar-SY">العربية (سوريا)</option>
<option value="ar-IQ">العربية (العراق)</option>
<option value="ar">العربية (عام)</option>
<option value="en-US">English (US)</option>
</select>
</div>
<!-- Auto Send -->
<div class="setting-group checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="autoSend">
<span>إرسال تلقائي بعد الانتهاء من الكلام</span>
</label>
<p class="setting-hint">بعد التوقف عن الكلام لمدة ثانيتين، يتم إرسال النص تلقائياً</p>
</div>
<!-- Gemini AI -->
<div class="setting-group divider">
<h3>🤖 معالجة النص بـ Gemini AI</h3>
<p class="setting-hint">يحسن دقة النص المنطوق ويصحح الأخطاء الإملائية</p>
<label class="checkbox-label">
<input type="checkbox" id="useGemini">
<span>تفعيل معالجة Gemini</span>
</label>
<div id="geminiSettings" class="gemini-settings" style="display:none;">
<label for="geminiApiKey">مفتاح API</label>
<input type="password" id="geminiApiKey" placeholder="أدخل مفتاح Gemini API">
<p class="setting-hint">استخدم نفس مفتاح API الموجود في إعدادات LinkedIn Analyzer</p>
<p class="setting-hint">النموذج المستخدم: <strong>gemini-flash-lite-latest</strong></p>
</div>
</div>
</div>
<!-- How to use -->
<div class="help-section">
<h2>📖 كيفية الاستخدام</h2>
<ol>
<li>اذهب إلى <strong>claude.ai</strong></li>
<li>ستجد زر المايك 🎤 بجانب حقل الإدخال</li>
<li>اضغط على الزر وابدأ بالتحدث بالعربية</li>
<li>عند التوقف عن الكلام، سيتم إدراج النص تلقائياً</li>
<li>يمكنك تفعيل Gemini لتحسين دقة النص</li>
</ol>
</div>
<!-- Footer -->
<div class="popup-footer">
<button id="saveBtn" class="save-btn">💾 حفظ الإعدادات</button>
<p id="saveMessage" class="save-message"></p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -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);
});

View File

@@ -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;
}