Files

1 line
96 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "aragonaragon/aragon-write", "files": {"frontend/src/components/Settings.jsx": {"0963d89ba3a7b54153700b6c57c236a117b4e102": {"t": 1785746435.0123992, "n": 20036, "h": "01706b9a88338fc6", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle, Power, Play, Eye, EyeOff, Cloud, HardDrive, Plug } from \"lucide-react\";\n\n// Built-in presets for popular OpenAI-compatible providers.\n// Users can edit Base URL/Model after applying, or fill them in manually.\nconst PROVIDER_PRESETS = {\n groq: {\n label: \"Groq\",\n baseUrl: \"https://api.groq.com/openai/v1\",\n model: \"llama-3.3-70b-versatile\",\n hint: \"مجاني وسريع — احصل على المفتاح من console.groq.com\",\n },\n openrouter: {\n label: \"OpenRouter\",\n baseUrl: \"https://openrouter.ai/api/v1\",\n model: \"google/gemini-2.0-flash-exp:free\",\n hint: \"موديلات كثيرة بسعر موحّد — openrouter.ai/keys\",\n },\n deepseek: {\n label: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n model: \"deepseek-chat\",\n hint: \"رخيص وقوي بالعربي — platform.deepseek.com\",\n },\n};\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl, onOllamaStatusChange }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n const [ollamaAction, setOllamaAction] = useState(null); // \"killing\" | \"starting\" | null\n\n // External provider state\n const [showKey, setShowKey] = useState(false);\n const [extModels, setExtModels] = useState([]);\n const [loadingExtModels, setLoadingExtModels] = useState(false);\n const [extTest, setExtTest] = useState(null); // { ok, error } | null\n const [testingExt, setTestingExt] = useState(false);\n\n const fetchModels = async () => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => { fetchModels(); }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels();\n };\n\n const killOllama = async () => {\n setOllamaAction(\"killing\");\n try {\n await fetch(`${apiUrl}/ollama/kill`, { method: \"POST\" });\n setTestStatus(null);\n setModels([]);\n onOllamaStatusChange?.(\"error\");\n } finally {\n setOllamaAction(null);\n }\n };\n\n const startOllama = async () => {\n setOllamaAction(\"starting\");\n try {\n await fetch(`${apiUrl}/ollama/start`, { method: \"POST\" });\n setTimeout(() => {\n fetchModels();\n onOllamaStatusChange?.(\"online\");\n }, 3000);\n } finally {\n setTimeout(() => setOllamaAction(null), 3200);\n }\n };\n\n // ── External provider helpers ──\n const applyPreset = (key) => {\n const preset = PROVIDER_PRESETS[key];\n if (!preset) return;\n onUpdate({\n apiBaseUrl: preset.baseUrl,\n apiModel: preset.model,\n });\n setExtTest(null);\n setExtModels([]);\n };\n\n const fetchExternalModels = async () => {\n if (!settings.apiBaseUrl) {\n setExtTest({ ok: false, error: \"أدخل Base URL أولاً\" });\n return;\n }\n setLoadingExtModels(true);\n setExtTest(null);\n try {\n const res = await fetch(`${apiUrl}/models/external`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ baseUrl: settings.apiBaseUrl, apiKey: settings.apiKey }),\n });\n const data = await res.json();\n if (res.ok && data.models?.length > 0) {\n setExtModels(data.models);\n } else {\n setExtModels([]);\n setExtTest({ ok: false, error: data.error || \"لم يتم العثور على موديلات\" });\n }\n } catch (err) {\n setExtTest({ ok: false, error: err.message || \"فشل جلب الموديلات\" });\n } finally {\n setLoadingExtModels(false);\n }\n };\n\n const testExternalConnection = async () => {\n if (!settings.apiBaseUrl) {\n setExtTest({ ok: false, error: \"أدخل Base URL أولاً\" });\n return;\n }\n setTestingExt(true);\n setExtTest(null);\n try {\n const res = await fetch(`${apiUrl}/health/provider`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ baseUrl: settings.apiBaseUrl, apiKey: settings.apiKey }),\n });\n const data = await res.json();\n setExtTest(data);\n } catch (err) {\n setExtTest({ ok: false, error: err.message || \"تعذّر الاتصال\" });\n } finally {\n setTestingExt(false);\n }\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n\n {/* ── مصدر الذكاء الاصطناعي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">مصدر الذكاء الاصطناعي</div>\n\n <div className=\"provider-toggle\">\n <button\n type=\"button\"\n className={`provider-tab${settings.provider !== \"openai_compat\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ provider: \"ollama\" })}\n >\n <HardDrive size={14} />\n محلي (Ollama)\n </button>\n <button\n type=\"button\"\n className={`provider-tab${settings.provider === \"openai_compat\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ provider: \"openai_compat\" })}\n >\n <Cloud size={14} />\n API خارجي\n </button>\n </div>\n\n {settings.provider === \"openai_compat\" && (\n <>\n <div className=\"settings-field\">\n <label>إعدادات سريعة</label>\n <div className=\"provider-presets\">\n {Object.entries(PROVIDER_PRESETS).map(([key, p]) => (\n <button\n key={key}\n type=\"button\"\n className={`provider-preset${settings.apiBaseUrl === p.baseUrl ? \" active\" : \"\"}`}\n onClick={() => applyPreset(key)}\n title={p.hint}\n >\n {p.label}\n </button>\n ))}\n </div>\n <span className=\"settings-field__hint\">اضغط لتعبئة Base URL والموديل الافتراضي تلقائياً</span>\n </div>\n\n <div className=\"settings-field\">\n <label>Base URL</label>\n <input\n className=\"settings-input\"\n value={settings.apiBaseUrl || \"\"}\n onChange={(e) => onUpdate({ apiBaseUrl: e.target.value })}\n placeholder=\"https://api.groq.com/openai/v1\"\n dir=\"ltr\"\n />\n </div>\n\n <div className=\"settings-field\">\n <label>API Key</label>\n <div className=\"provider-key\">\n <input\n className=\"settings-input\"\n type={showKey ? \"text\" : \"password\"}\n value={settings.apiKey || \"\"}\n onChange={(e) => onUpdate({ apiKey: e.target.value })}\n placeholder=\"sk-...\"\n dir=\"ltr\"\n autoComplete=\"off\"\n />\n <button\n type=\"button\"\n className=\"btn-icon\"\n onClick={() => setShowKey((v) => !v)}\n title={showKey ? \"إخفاء\" : \"إظهار\"}\n >\n {showKey ? <EyeOff size={14} /> : <Eye size={14} />}\n </button>\n </div>\n <span className=\"settings-field__hint\" style={{ color: \"#d97706\" }}>\n ⚠ المفتاح يُحفظ على جهازك في localStorage بدون تشفير. لا تشاركه ولا تستخدمه على جهاز مشترك.\n </span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل</label>\n <button\n type=\"button\"\n className=\"btn-icon\"\n onClick={fetchExternalModels}\n disabled={loadingExtModels || !settings.apiBaseUrl}\n title=\"جلب قائمة الموديلات\"\n >\n <RefreshCw size={14} style={{ animation: loadingExtModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n {extModels.length > 0 ? (\n <select\n className=\"settings-select\"\n value={settings.apiModel || \"\"}\n onChange={(e) => onUpdate({ apiModel: e.target.value })}\n dir=\"ltr\"\n >\n <option value=\"\">— اختر موديل —</option>\n {extModels.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.apiModel || \"\"}\n onChange={(e) => onUpdate({ apiModel: e.target.value })}\n placeholder=\"llama-3.3-70b-versatile\"\n dir=\"ltr\"\n />\n )}\n </div>\n\n <div className=\"settings-field\">\n <button\n type=\"button\"\n className=\"btn btn-secondary\"\n onClick={testExternalConnection}\n disabled={testingExt || !settings.apiBaseUrl}\n style={{ display: \"flex\", alignItems: \"center\", gap: 6 }}\n >\n <Plug size={13} />\n {testingExt ? \"جاري الاختبار...\" : \"اختبار الاتصال\"}\n </button>\n {extTest && (\n <div\n style={{\n display: \"flex\", alignItems: \"center\", gap: 6,\n color: extTest.ok ? \"#34a853\" : \"#ea4335\",\n fontSize: 13, marginTop: 8,\n }}\n >\n {extTest.ok ? <CheckCircle size={14} /> : <AlertCircle size={14} />}\n {extTest.ok ? \"تم الاتصال بنجاح\" : extTest.error}\n </div>\n )}\n </div>\n\n <div\n style={{\n background: \"var(--bg)\",\n border: \"1px solid var(--border)\",\n borderRadius: \"var(--radius-md)\",\n padding: \"10px 12px\",\n fontSize: 12,\n color: \"var(--text-secondary)\",\n lineHeight: 1.8,\n }}\n >\n ℹ <strong>ملاحظات:</strong> بياناتك المكتوبة (المستندات والمشاريع) تبقى محلية على جهازك.\n فقط طلبات المساعد ترسل إلى المزود الخارجي.<br />\n ⚠ <strong>التدقيق الإملائي يتعطّل تلقائياً</strong> في وضع API لتجنّب التكاليف.\n </div>\n </>\n )}\n </div>\n\n {/* ── Ollama ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">العنوان الافتراضي: http://localhost:11434</span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button className=\"btn-icon\" onClick={fetchModels} title=\"تحديث\" disabled={loadingModels}>\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />{modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select className=\"settings-select\" value={settings.model} onChange={(e) => onUpdate({ model: e.target.value })}>\n {models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: gemma4:e4b أو gemma2:9b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: gemma4:e4b (خفيف وسريع)، gemma2:9b (متوازن)، gemma4:26b (الأفضل للأجهزة القوية)\n </span>\n </div>\n\n {/* Kill / Start Ollama */}\n <div className=\"settings-field\">\n <label>تشغيل / إيقاف Ollama</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <button\n className=\"btn btn-secondary\"\n onClick={startOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Play size={13} style={{ color: \"#34a853\" }} />\n {ollamaAction === \"starting\" ? \"جاري التشغيل...\" : \"تشغيل Ollama\"}\n </button>\n <button\n className=\"btn btn-danger\"\n onClick={killOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Power size={13} />\n {ollamaAction === \"killing\" ? \"جاري الإيقاف...\" : \"إيقاف Ollama\"}\n </button>\n </div>\n <span className=\"settings-field__hint\">\n إيقاف Ollama يحرر الذاكرة (RAM) تماماً. شغّله مجدداً عند الحاجة للـ AI.\n </span>\n </div>\n </div>\n\n {/* ── المظهر ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"light\" })}>\n <div className=\"theme-swatch light\" />فاتح\n </button>\n <button className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"dark\" })}>\n <div className=\"theme-swatch dark\" />داكن\n </button>\n <button className={`theme-option${settings.theme === \"sepia\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"sepia\" })}>\n <div className=\"theme-swatch sepia\" />عاجي\n </button>\n </div>\n </div>\n </div>\n\n {/* ── الكتابة ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">الكتابة</div>\n\n <div className=\"settings-field\">\n <label>هدف الكتابة اليومي (كلمة)</label>\n <input\n type=\"number\"\n className=\"settings-input\"\n value={settings.writingGoal || \"\"}\n onChange={(e) => onUpdate({ writingGoal: parseInt(e.target.value) || 0 })}\n placeholder=\"0 = بدون هدف\"\n min=\"0\"\n max=\"50000\"\n style={{ textAlign: \"right\", direction: \"rtl\" }}\n />\n <span className=\"settings-field__hint\">يظهر شريط التقدم في أسفل الشاشة عند تحديد هدف.</span>\n </div>\n\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.typewriterMode || false}\n onChange={(e) => onUpdate({ typewriterMode: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n وضع الآلة الكاتبة\n </label>\n <span className=\"settings-field__hint\">يُثبّت المؤشر في منتصف الشاشة أثناء الكتابة.</span>\n </div>\n </div>\n\n {/* ── التدقيق الإملائي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* ── عن التطبيق ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 المشاريع محفوظة في مجلد على قرصك الصلب</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}, "be7ba8755149d59db2a7f05b0257f7d88e43307c": {"t": 1785764175.7287943, "n": 20036, "h": "01706b9a88338fc6", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle, Power, Play, Eye, EyeOff, Cloud, HardDrive, Plug } from \"lucide-react\";\n\n// Built-in presets for popular OpenAI-compatible providers.\n// Users can edit Base URL/Model after applying, or fill them in manually.\nconst PROVIDER_PRESETS = {\n groq: {\n label: \"Groq\",\n baseUrl: \"https://api.groq.com/openai/v1\",\n model: \"llama-3.3-70b-versatile\",\n hint: \"مجاني وسريع — احصل على المفتاح من console.groq.com\",\n },\n openrouter: {\n label: \"OpenRouter\",\n baseUrl: \"https://openrouter.ai/api/v1\",\n model: \"google/gemini-2.0-flash-exp:free\",\n hint: \"موديلات كثيرة بسعر موحّد — openrouter.ai/keys\",\n },\n deepseek: {\n label: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com/v1\",\n model: \"deepseek-chat\",\n hint: \"رخيص وقوي بالعربي — platform.deepseek.com\",\n },\n};\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl, onOllamaStatusChange }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n const [ollamaAction, setOllamaAction] = useState(null); // \"killing\" | \"starting\" | null\n\n // External provider state\n const [showKey, setShowKey] = useState(false);\n const [extModels, setExtModels] = useState([]);\n const [loadingExtModels, setLoadingExtModels] = useState(false);\n const [extTest, setExtTest] = useState(null); // { ok, error } | null\n const [testingExt, setTestingExt] = useState(false);\n\n const fetchModels = async () => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => { fetchModels(); }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels();\n };\n\n const killOllama = async () => {\n setOllamaAction(\"killing\");\n try {\n await fetch(`${apiUrl}/ollama/kill`, { method: \"POST\" });\n setTestStatus(null);\n setModels([]);\n onOllamaStatusChange?.(\"error\");\n } finally {\n setOllamaAction(null);\n }\n };\n\n const startOllama = async () => {\n setOllamaAction(\"starting\");\n try {\n await fetch(`${apiUrl}/ollama/start`, { method: \"POST\" });\n setTimeout(() => {\n fetchModels();\n onOllamaStatusChange?.(\"online\");\n }, 3000);\n } finally {\n setTimeout(() => setOllamaAction(null), 3200);\n }\n };\n\n // ── External provider helpers ──\n const applyPreset = (key) => {\n const preset = PROVIDER_PRESETS[key];\n if (!preset) return;\n onUpdate({\n apiBaseUrl: preset.baseUrl,\n apiModel: preset.model,\n });\n setExtTest(null);\n setExtModels([]);\n };\n\n const fetchExternalModels = async () => {\n if (!settings.apiBaseUrl) {\n setExtTest({ ok: false, error: \"أدخل Base URL أولاً\" });\n return;\n }\n setLoadingExtModels(true);\n setExtTest(null);\n try {\n const res = await fetch(`${apiUrl}/models/external`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ baseUrl: settings.apiBaseUrl, apiKey: settings.apiKey }),\n });\n const data = await res.json();\n if (res.ok && data.models?.length > 0) {\n setExtModels(data.models);\n } else {\n setExtModels([]);\n setExtTest({ ok: false, error: data.error || \"لم يتم العثور على موديلات\" });\n }\n } catch (err) {\n setExtTest({ ok: false, error: err.message || \"فشل جلب الموديلات\" });\n } finally {\n setLoadingExtModels(false);\n }\n };\n\n const testExternalConnection = async () => {\n if (!settings.apiBaseUrl) {\n setExtTest({ ok: false, error: \"أدخل Base URL أولاً\" });\n return;\n }\n setTestingExt(true);\n setExtTest(null);\n try {\n const res = await fetch(`${apiUrl}/health/provider`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ baseUrl: settings.apiBaseUrl, apiKey: settings.apiKey }),\n });\n const data = await res.json();\n setExtTest(data);\n } catch (err) {\n setExtTest({ ok: false, error: err.message || \"تعذّر الاتصال\" });\n } finally {\n setTestingExt(false);\n }\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n\n {/* ── مصدر الذكاء الاصطناعي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">مصدر الذكاء الاصطناعي</div>\n\n <div className=\"provider-toggle\">\n <button\n type=\"button\"\n className={`provider-tab${settings.provider !== \"openai_compat\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ provider: \"ollama\" })}\n >\n <HardDrive size={14} />\n محلي (Ollama)\n </button>\n <button\n type=\"button\"\n className={`provider-tab${settings.provider === \"openai_compat\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ provider: \"openai_compat\" })}\n >\n <Cloud size={14} />\n API خارجي\n </button>\n </div>\n\n {settings.provider === \"openai_compat\" && (\n <>\n <div className=\"settings-field\">\n <label>إعدادات سريعة</label>\n <div className=\"provider-presets\">\n {Object.entries(PROVIDER_PRESETS).map(([key, p]) => (\n <button\n key={key}\n type=\"button\"\n className={`provider-preset${settings.apiBaseUrl === p.baseUrl ? \" active\" : \"\"}`}\n onClick={() => applyPreset(key)}\n title={p.hint}\n >\n {p.label}\n </button>\n ))}\n </div>\n <span className=\"settings-field__hint\">اضغط لتعبئة Base URL والموديل الافتراضي تلقائياً</span>\n </div>\n\n <div className=\"settings-field\">\n <label>Base URL</label>\n <input\n className=\"settings-input\"\n value={settings.apiBaseUrl || \"\"}\n onChange={(e) => onUpdate({ apiBaseUrl: e.target.value })}\n placeholder=\"https://api.groq.com/openai/v1\"\n dir=\"ltr\"\n />\n </div>\n\n <div className=\"settings-field\">\n <label>API Key</label>\n <div className=\"provider-key\">\n <input\n className=\"settings-input\"\n type={showKey ? \"text\" : \"password\"}\n value={settings.apiKey || \"\"}\n onChange={(e) => onUpdate({ apiKey: e.target.value })}\n placeholder=\"sk-...\"\n dir=\"ltr\"\n autoComplete=\"off\"\n />\n <button\n type=\"button\"\n className=\"btn-icon\"\n onClick={() => setShowKey((v) => !v)}\n title={showKey ? \"إخفاء\" : \"إظهار\"}\n >\n {showKey ? <EyeOff size={14} /> : <Eye size={14} />}\n </button>\n </div>\n <span className=\"settings-field__hint\" style={{ color: \"#d97706\" }}>\n ⚠ المفتاح يُحفظ على جهازك في localStorage بدون تشفير. لا تشاركه ولا تستخدمه على جهاز مشترك.\n </span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل</label>\n <button\n type=\"button\"\n className=\"btn-icon\"\n onClick={fetchExternalModels}\n disabled={loadingExtModels || !settings.apiBaseUrl}\n title=\"جلب قائمة الموديلات\"\n >\n <RefreshCw size={14} style={{ animation: loadingExtModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n {extModels.length > 0 ? (\n <select\n className=\"settings-select\"\n value={settings.apiModel || \"\"}\n onChange={(e) => onUpdate({ apiModel: e.target.value })}\n dir=\"ltr\"\n >\n <option value=\"\">— اختر موديل —</option>\n {extModels.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.apiModel || \"\"}\n onChange={(e) => onUpdate({ apiModel: e.target.value })}\n placeholder=\"llama-3.3-70b-versatile\"\n dir=\"ltr\"\n />\n )}\n </div>\n\n <div className=\"settings-field\">\n <button\n type=\"button\"\n className=\"btn btn-secondary\"\n onClick={testExternalConnection}\n disabled={testingExt || !settings.apiBaseUrl}\n style={{ display: \"flex\", alignItems: \"center\", gap: 6 }}\n >\n <Plug size={13} />\n {testingExt ? \"جاري الاختبار...\" : \"اختبار الاتصال\"}\n </button>\n {extTest && (\n <div\n style={{\n display: \"flex\", alignItems: \"center\", gap: 6,\n color: extTest.ok ? \"#34a853\" : \"#ea4335\",\n fontSize: 13, marginTop: 8,\n }}\n >\n {extTest.ok ? <CheckCircle size={14} /> : <AlertCircle size={14} />}\n {extTest.ok ? \"تم الاتصال بنجاح\" : extTest.error}\n </div>\n )}\n </div>\n\n <div\n style={{\n background: \"var(--bg)\",\n border: \"1px solid var(--border)\",\n borderRadius: \"var(--radius-md)\",\n padding: \"10px 12px\",\n fontSize: 12,\n color: \"var(--text-secondary)\",\n lineHeight: 1.8,\n }}\n >\n ℹ <strong>ملاحظات:</strong> بياناتك المكتوبة (المستندات والمشاريع) تبقى محلية على جهازك.\n فقط طلبات المساعد ترسل إلى المزود الخارجي.<br />\n ⚠ <strong>التدقيق الإملائي يتعطّل تلقائياً</strong> في وضع API لتجنّب التكاليف.\n </div>\n </>\n )}\n </div>\n\n {/* ── Ollama ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">العنوان الافتراضي: http://localhost:11434</span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button className=\"btn-icon\" onClick={fetchModels} title=\"تحديث\" disabled={loadingModels}>\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />{modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select className=\"settings-select\" value={settings.model} onChange={(e) => onUpdate({ model: e.target.value })}>\n {models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: gemma4:e4b أو gemma2:9b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: gemma4:e4b (خفيف وسريع)، gemma2:9b (متوازن)، gemma4:26b (الأفضل للأجهزة القوية)\n </span>\n </div>\n\n {/* Kill / Start Ollama */}\n <div className=\"settings-field\">\n <label>تشغيل / إيقاف Ollama</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <button\n className=\"btn btn-secondary\"\n onClick={startOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Play size={13} style={{ color: \"#34a853\" }} />\n {ollamaAction === \"starting\" ? \"جاري التشغيل...\" : \"تشغيل Ollama\"}\n </button>\n <button\n className=\"btn btn-danger\"\n onClick={killOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Power size={13} />\n {ollamaAction === \"killing\" ? \"جاري الإيقاف...\" : \"إيقاف Ollama\"}\n </button>\n </div>\n <span className=\"settings-field__hint\">\n إيقاف Ollama يحرر الذاكرة (RAM) تماماً. شغّله مجدداً عند الحاجة للـ AI.\n </span>\n </div>\n </div>\n\n {/* ── المظهر ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"light\" })}>\n <div className=\"theme-swatch light\" />فاتح\n </button>\n <button className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"dark\" })}>\n <div className=\"theme-swatch dark\" />داكن\n </button>\n <button className={`theme-option${settings.theme === \"sepia\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"sepia\" })}>\n <div className=\"theme-swatch sepia\" />عاجي\n </button>\n </div>\n </div>\n </div>\n\n {/* ── الكتابة ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">الكتابة</div>\n\n <div className=\"settings-field\">\n <label>هدف الكتابة اليومي (كلمة)</label>\n <input\n type=\"number\"\n className=\"settings-input\"\n value={settings.writingGoal || \"\"}\n onChange={(e) => onUpdate({ writingGoal: parseInt(e.target.value) || 0 })}\n placeholder=\"0 = بدون هدف\"\n min=\"0\"\n max=\"50000\"\n style={{ textAlign: \"right\", direction: \"rtl\" }}\n />\n <span className=\"settings-field__hint\">يظهر شريط التقدم في أسفل الشاشة عند تحديد هدف.</span>\n </div>\n\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.typewriterMode || false}\n onChange={(e) => onUpdate({ typewriterMode: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n وضع الآلة الكاتبة\n </label>\n <span className=\"settings-field__hint\">يُثبّت المؤشر في منتصف الشاشة أثناء الكتابة.</span>\n </div>\n </div>\n\n {/* ── التدقيق الإملائي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* ── عن التطبيق ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 المشاريع محفوظة في مجلد على قرصك الصلب</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}, "35d00dcad922fd1b49f4c42b48b7912b18bd3868": {"t": 1785764178.1948612, "n": 10365, "h": "9cdefad5737822f1", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle, Power, Play } from \"lucide-react\";\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl, onOllamaStatusChange }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n const [ollamaAction, setOllamaAction] = useState(null); // \"killing\" | \"starting\" | null\n\n const fetchModels = async () => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => { fetchModels(); }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels();\n };\n\n const killOllama = async () => {\n setOllamaAction(\"killing\");\n try {\n await fetch(`${apiUrl}/ollama/kill`, { method: \"POST\" });\n setTestStatus(null);\n setModels([]);\n onOllamaStatusChange?.(\"error\");\n } finally {\n setOllamaAction(null);\n }\n };\n\n const startOllama = async () => {\n setOllamaAction(\"starting\");\n try {\n await fetch(`${apiUrl}/ollama/start`, { method: \"POST\" });\n setTimeout(() => {\n fetchModels();\n onOllamaStatusChange?.(\"online\");\n }, 3000);\n } finally {\n setTimeout(() => setOllamaAction(null), 3200);\n }\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n\n {/* ── Ollama ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">العنوان الافتراضي: http://localhost:11434</span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button className=\"btn-icon\" onClick={fetchModels} title=\"تحديث\" disabled={loadingModels}>\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />{modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select className=\"settings-select\" value={settings.model} onChange={(e) => onUpdate({ model: e.target.value })}>\n {models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: gemma4:e4b أو gemma2:9b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: gemma4:e4b (خفيف وسريع)، gemma2:9b (متوازن)، gemma4:26b (الأفضل للأجهزة القوية)\n </span>\n </div>\n\n {/* Kill / Start Ollama */}\n <div className=\"settings-field\">\n <label>تشغيل / إيقاف Ollama</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <button\n className=\"btn btn-secondary\"\n onClick={startOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Play size={13} style={{ color: \"#34a853\" }} />\n {ollamaAction === \"starting\" ? \"جاري التشغيل...\" : \"تشغيل Ollama\"}\n </button>\n <button\n className=\"btn btn-danger\"\n onClick={killOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Power size={13} />\n {ollamaAction === \"killing\" ? \"جاري الإيقاف...\" : \"إيقاف Ollama\"}\n </button>\n </div>\n <span className=\"settings-field__hint\">\n إيقاف Ollama يحرر الذاكرة (RAM) تماماً. شغّله مجدداً عند الحاجة للـ AI.\n </span>\n </div>\n </div>\n\n {/* ── المظهر ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"light\" })}>\n <div className=\"theme-swatch light\" />فاتح\n </button>\n <button className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"dark\" })}>\n <div className=\"theme-swatch dark\" />داكن\n </button>\n <button className={`theme-option${settings.theme === \"sepia\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"sepia\" })}>\n <div className=\"theme-swatch sepia\" />عاجي\n </button>\n </div>\n </div>\n </div>\n\n {/* ── الكتابة ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">الكتابة</div>\n\n <div className=\"settings-field\">\n <label>هدف الكتابة اليومي (كلمة)</label>\n <input\n type=\"number\"\n className=\"settings-input\"\n value={settings.writingGoal || \"\"}\n onChange={(e) => onUpdate({ writingGoal: parseInt(e.target.value) || 0 })}\n placeholder=\"0 = بدون هدف\"\n min=\"0\"\n max=\"50000\"\n style={{ textAlign: \"right\", direction: \"rtl\" }}\n />\n <span className=\"settings-field__hint\">يظهر شريط التقدم في أسفل الشاشة عند تحديد هدف.</span>\n </div>\n\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.typewriterMode || false}\n onChange={(e) => onUpdate({ typewriterMode: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n وضع الآلة الكاتبة\n </label>\n <span className=\"settings-field__hint\">يُثبّت المؤشر في منتصف الشاشة أثناء الكتابة.</span>\n </div>\n </div>\n\n {/* ── التدقيق الإملائي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* ── عن التطبيق ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 المشاريع محفوظة في مجلد على قرصك الصلب</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}, "a60a61b0d628f985eb13396bd3b188e1984e36f2": {"t": 1785764179.736863, "n": 10365, "h": "6b56f45bd616dad6", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle, Power, Play } from \"lucide-react\";\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl, onOllamaStatusChange }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n const [ollamaAction, setOllamaAction] = useState(null); // \"killing\" | \"starting\" | null\n\n const fetchModels = async () => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => { fetchModels(); }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels();\n };\n\n const killOllama = async () => {\n setOllamaAction(\"killing\");\n try {\n await fetch(`${apiUrl}/ollama/kill`, { method: \"POST\" });\n setTestStatus(null);\n setModels([]);\n onOllamaStatusChange?.(\"error\");\n } finally {\n setOllamaAction(null);\n }\n };\n\n const startOllama = async () => {\n setOllamaAction(\"starting\");\n try {\n await fetch(`${apiUrl}/ollama/start`, { method: \"POST\" });\n setTimeout(() => {\n fetchModels();\n onOllamaStatusChange?.(\"online\");\n }, 3000);\n } finally {\n setTimeout(() => setOllamaAction(null), 3200);\n }\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n\n {/* ── Ollama ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">العنوان الافتراضي: http://localhost:11434</span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button className=\"btn-icon\" onClick={fetchModels} title=\"تحديث\" disabled={loadingModels}>\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />{modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select className=\"settings-select\" value={settings.model} onChange={(e) => onUpdate({ model: e.target.value })}>\n {models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: gemma4:e4b أو gemma2:9b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: gemma4:e4b (خفيف وسريع)، gemma2:9b (متوازن)، gemma4:26b (الأفضل للأجهزة القوية)\n </span>\n </div>\n\n {/* Kill / Start Ollama */}\n <div className=\"settings-field\">\n <label>تشغيل / إيقاف Ollama</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <button\n className=\"btn btn-secondary\"\n onClick={startOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Play size={13} style={{ color: \"#34a853\" }} />\n {ollamaAction === \"starting\" ? \"جاري التشغيل...\" : \"تشغيل Ollama\"}\n </button>\n <button\n className=\"btn btn-danger\"\n onClick={killOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Power size={13} />\n {ollamaAction === \"killing\" ? \"جاري الإيقاف...\" : \"إيقاف Ollama\"}\n </button>\n </div>\n <span className=\"settings-field__hint\">\n إيقاف Ollama يحرر الذاكرة (RAM) تماماً. شغّله مجدداً عند الحاجة للـ AI.\n </span>\n </div>\n </div>\n\n {/* ── المظهر ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"light\" })}>\n <div className=\"theme-swatch light\" />فاتح\n </button>\n <button className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"dark\" })}>\n <div className=\"theme-swatch dark\" />داكن\n </button>\n <button className={`theme-option${settings.theme === \"sepia\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"sepia\" })}>\n <div className=\"theme-swatch sepia\" />عاجي\n </button>\n </div>\n </div>\n </div>\n\n {/* ── الكتابة ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">الكتابة</div>\n\n <div className=\"settings-field\">\n <label>هدف الكتابة اليومي (كلمة)</label>\n <input\n type=\"number\"\n className=\"settings-input\"\n value={settings.writingGoal || \"\"}\n onChange={(e) => onUpdate({ writingGoal: parseInt(e.target.value) || 0 })}\n placeholder=\"0 = بدون هدف\"\n min=\"0\"\n max=\"50000\"\n style={{ textAlign: \"right\", direction: \"rtl\" }}\n />\n <span className=\"settings-field__hint\">يظهر شريط التقدم في أسفل الشاشة عند تحديد هدف.</span>\n </div>\n\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.typewriterMode || false}\n onChange={(e) => onUpdate({ typewriterMode: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n وضع الآلة الكاتبة\n </label>\n <span className=\"settings-field__hint\">يُثبّت المؤشر في منتصف الشاشة أثناء الكتابة.</span>\n </div>\n </div>\n\n {/* ── التدقيق الإملائي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* ── عن التطبيق ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 الروايات محفوظة في مجلد على قرصك الصلب</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}, "9fff018884a4e518f15fb228adf19becfceaefc3": {"t": 1785764182.235458, "n": 10317, "h": "625811302a11138d", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle, Power, Play } from \"lucide-react\";\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl, onOllamaStatusChange }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n const [ollamaAction, setOllamaAction] = useState(null); // \"killing\" | \"starting\" | null\n\n const fetchModels = async () => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => { fetchModels(); }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels();\n };\n\n const killOllama = async () => {\n setOllamaAction(\"killing\");\n try {\n await fetch(`${apiUrl}/ollama/kill`, { method: \"POST\" });\n setTestStatus(null);\n setModels([]);\n onOllamaStatusChange?.(\"error\");\n } finally {\n setOllamaAction(null);\n }\n };\n\n const startOllama = async () => {\n setOllamaAction(\"starting\");\n try {\n await fetch(`${apiUrl}/ollama/start`, { method: \"POST\" });\n setTimeout(() => {\n fetchModels();\n onOllamaStatusChange?.(\"online\");\n }, 3000);\n } finally {\n setTimeout(() => setOllamaAction(null), 3200);\n }\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n\n {/* ── Ollama ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">العنوان الافتراضي: http://localhost:11434</span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button className=\"btn-icon\" onClick={fetchModels} title=\"تحديث\" disabled={loadingModels}>\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />{modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select className=\"settings-select\" value={settings.model} onChange={(e) => onUpdate({ model: e.target.value })}>\n {models.map((m) => <option key={m.name} value={m.name}>{m.name}</option>)}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: qwen2.5:7b أو llama3.1:8b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: qwen2.5:7b، Llama 3.1، Gemma2\n </span>\n </div>\n\n {/* Kill / Start Ollama */}\n <div className=\"settings-field\">\n <label>تشغيل / إيقاف Ollama</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <button\n className=\"btn btn-secondary\"\n onClick={startOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Play size={13} style={{ color: \"#34a853\" }} />\n {ollamaAction === \"starting\" ? \"جاري التشغيل...\" : \"تشغيل Ollama\"}\n </button>\n <button\n className=\"btn btn-danger\"\n onClick={killOllama}\n disabled={!!ollamaAction}\n style={{ flex: 1, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6 }}\n >\n <Power size={13} />\n {ollamaAction === \"killing\" ? \"جاري الإيقاف...\" : \"إيقاف Ollama\"}\n </button>\n </div>\n <span className=\"settings-field__hint\">\n إيقاف Ollama يحرر الذاكرة (RAM) تماماً. شغّله مجدداً عند الحاجة للـ AI.\n </span>\n </div>\n </div>\n\n {/* ── المظهر ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"light\" })}>\n <div className=\"theme-swatch light\" />فاتح\n </button>\n <button className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"dark\" })}>\n <div className=\"theme-swatch dark\" />داكن\n </button>\n <button className={`theme-option${settings.theme === \"sepia\" ? \" active\" : \"\"}`} onClick={() => onUpdate({ theme: \"sepia\" })}>\n <div className=\"theme-swatch sepia\" />عاجي\n </button>\n </div>\n </div>\n </div>\n\n {/* ── الكتابة ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">الكتابة</div>\n\n <div className=\"settings-field\">\n <label>هدف الكتابة اليومي (كلمة)</label>\n <input\n type=\"number\"\n className=\"settings-input\"\n value={settings.writingGoal || \"\"}\n onChange={(e) => onUpdate({ writingGoal: parseInt(e.target.value) || 0 })}\n placeholder=\"0 = بدون هدف\"\n min=\"0\"\n max=\"50000\"\n style={{ textAlign: \"right\", direction: \"rtl\" }}\n />\n <span className=\"settings-field__hint\">يظهر شريط التقدم في أسفل الشاشة عند تحديد هدف.</span>\n </div>\n\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.typewriterMode || false}\n onChange={(e) => onUpdate({ typewriterMode: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n وضع الآلة الكاتبة\n </label>\n <span className=\"settings-field__hint\">يُثبّت المؤشر في منتصف الشاشة أثناء الكتابة.</span>\n </div>\n </div>\n\n {/* ── التدقيق الإملائي ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* ── عن التطبيق ── */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 الروايات محفوظة في مجلد على قرصك الصلب</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}, "654b98a215cead9b30ae7557b0f5c8d679d9ad39": {"t": 1785764183.583239, "n": 8934, "h": "7a779a48b9cb61e1", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle } from \"lucide-react\";\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n\n const fetchModels = async (url = ollamaUrl) => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => {\n fetchModels();\n }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels(ollamaUrl);\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n {/* Ollama Settings */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">\n العنوان الافتراضي: http://localhost:11434\n </span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button\n className=\"btn-icon\"\n onClick={() => fetchModels()}\n title=\"تحديث قائمة الموديلات\"\n disabled={loadingModels}\n >\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />\n Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />\n {modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select\n className=\"settings-select\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n >\n {models.map((m) => (\n <option key={m.name} value={m.name}>\n {m.name}\n </option>\n ))}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: qwen2.5:7b أو llama3.1:8b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: qwen2.5:7b، Llama 3.1، Gemma2\n </span>\n </div>\n </div>\n\n {/* Appearance */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button\n className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ theme: \"light\" })}\n >\n <div className=\"theme-swatch light\" />\n فاتح\n </button>\n <button\n className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ theme: \"dark\" })}\n >\n <div className=\"theme-swatch dark\" />\n داكن\n </button>\n <button\n className={`theme-option${settings.theme === \"sepia\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ theme: \"sepia\" })}\n >\n <div className=\"theme-swatch sepia\" />\n عاجي\n </button>\n </div>\n </div>\n </div>\n\n {/* Writing */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">الكتابة</div>\n\n <div className=\"settings-field\">\n <label>هدف الكتابة اليومي (كلمة)</label>\n <input\n type=\"number\"\n className=\"settings-input\"\n value={settings.writingGoal || \"\"}\n onChange={(e) => onUpdate({ writingGoal: parseInt(e.target.value) || 0 })}\n placeholder=\"0 = بدون هدف\"\n min=\"0\"\n max=\"50000\"\n style={{ textAlign: \"right\", direction: \"rtl\" }}\n />\n <span className=\"settings-field__hint\">\n يظهر شريط التقدم في أسفل الشاشة عند تحديد هدف.\n </span>\n </div>\n\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.typewriterMode || false}\n onChange={(e) => onUpdate({ typewriterMode: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n وضع الآلة الكاتبة\n </label>\n <span className=\"settings-field__hint\">\n يُثبّت المؤشر في منتصف الشاشة أثناء الكتابة — أريح للجلسات الطويلة.\n </span>\n </div>\n </div>\n\n {/* Spellcheck */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* About */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 الملفات محفوظة في متصفحك (localStorage)</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}, "35abf5177fb84adc3a86a2aa760e2b62585ee324": {"t": 1785764184.920802, "n": 7150, "h": "dfdc4afeffc258f8", "c": "import { useEffect, useState } from \"react\";\nimport { X, RefreshCw, CheckCircle, AlertCircle } from \"lucide-react\";\n\nexport default function Settings({ settings, onUpdate, onClose, apiUrl }) {\n const [models, setModels] = useState([]);\n const [loadingModels, setLoadingModels] = useState(false);\n const [modelError, setModelError] = useState(null);\n const [ollamaUrl, setOllamaUrl] = useState(settings.ollamaUrl);\n const [testStatus, setTestStatus] = useState(null);\n\n const fetchModels = async (url = ollamaUrl) => {\n setLoadingModels(true);\n setModelError(null);\n try {\n const res = await fetch(`${apiUrl}/models`);\n const data = await res.json();\n if (data.models?.length > 0) {\n setModels(data.models);\n setTestStatus(\"success\");\n } else {\n setModels([]);\n setModelError(\"لم يتم العثور على موديلات. تأكد من تشغيل Ollama وتحميل موديل.\");\n setTestStatus(\"error\");\n }\n } catch {\n setModelError(\"تعذر الاتصال بـ Ollama. تأكد من تشغيله على المنفذ الصحيح.\");\n setTestStatus(\"error\");\n } finally {\n setLoadingModels(false);\n }\n };\n\n useEffect(() => {\n fetchModels();\n }, []); // eslint-disable-line\n\n const saveOllamaUrl = () => {\n onUpdate({ ollamaUrl });\n fetchModels(ollamaUrl);\n };\n\n return (\n <div className=\"modal-overlay\" onClick={(e) => e.target === e.currentTarget && onClose()}>\n <div className=\"modal\">\n <div className=\"modal__header\">\n <h2 className=\"modal__title\">الإعدادات</h2>\n <button className=\"btn-icon\" onClick={onClose}><X size={18} /></button>\n </div>\n\n <div className=\"modal__body\">\n {/* Ollama Settings */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">إعدادات Ollama (الذكاء الاصطناعي)</div>\n\n <div className=\"settings-field\">\n <label>عنوان Ollama URL</label>\n <div style={{ display: \"flex\", gap: 8 }}>\n <input\n className=\"settings-input\"\n value={ollamaUrl}\n onChange={(e) => setOllamaUrl(e.target.value)}\n placeholder=\"http://localhost:11434\"\n />\n <button className=\"btn btn-secondary\" onClick={saveOllamaUrl} style={{ flexShrink: 0 }}>\n حفظ\n </button>\n </div>\n <span className=\"settings-field__hint\">\n العنوان الافتراضي: http://localhost:11434\n </span>\n </div>\n\n <div className=\"settings-field\">\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n <label>الموديل النشط</label>\n <button\n className=\"btn-icon\"\n onClick={() => fetchModels()}\n title=\"تحديث قائمة الموديلات\"\n disabled={loadingModels}\n >\n <RefreshCw size={14} style={{ animation: loadingModels ? \"spin 1s linear infinite\" : \"none\" }} />\n </button>\n </div>\n\n {testStatus === \"success\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#34a853\", fontSize: 13, marginBottom: 8 }}>\n <CheckCircle size={14} />\n Ollama يعمل — {models.length} موديل متاح\n </div>\n )}\n {testStatus === \"error\" && (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, color: \"#ea4335\", fontSize: 13, marginBottom: 8 }}>\n <AlertCircle size={14} />\n {modelError}\n </div>\n )}\n\n {models.length > 0 ? (\n <select\n className=\"settings-select\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n >\n {models.map((m) => (\n <option key={m.name} value={m.name}>\n {m.name}\n </option>\n ))}\n </select>\n ) : (\n <input\n className=\"settings-input\"\n value={settings.model}\n onChange={(e) => onUpdate({ model: e.target.value })}\n placeholder=\"مثال: qwen2.5:7b أو llama3.1:8b\"\n />\n )}\n <span className=\"settings-field__hint\">\n الموديلات الموصى بها للعربية: qwen2.5:7b، Llama 3.1، Gemma2\n </span>\n </div>\n </div>\n\n {/* Appearance */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">المظهر</div>\n\n <div className=\"settings-field\">\n <label>وضع العرض</label>\n <div className=\"theme-options\">\n <button\n className={`theme-option${settings.theme === \"light\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ theme: \"light\" })}\n >\n <div className=\"theme-swatch light\" />\n فاتح\n </button>\n <button\n className={`theme-option${settings.theme === \"dark\" ? \" active\" : \"\"}`}\n onClick={() => onUpdate({ theme: \"dark\" })}\n >\n <div className=\"theme-swatch dark\" />\n داكن\n </button>\n </div>\n </div>\n </div>\n\n {/* Spellcheck */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">التدقيق الإملائي</div>\n <div className=\"settings-field\">\n <label style={{ display: \"flex\", alignItems: \"center\", gap: 10, cursor: \"pointer\" }}>\n <input\n type=\"checkbox\"\n checked={settings.spellcheckEnabled}\n onChange={(e) => onUpdate({ spellcheckEnabled: e.target.checked })}\n style={{ width: 16, height: 16 }}\n />\n تفعيل التدقيق الإملائي التلقائي\n </label>\n <span className=\"settings-field__hint\">\n يستخدم Ollama لفحص الكلمات العربية. قد يستغرق وقتاً عند الكتابة.\n </span>\n </div>\n </div>\n\n {/* About */}\n <div className=\"settings-section\">\n <div className=\"settings-section__title\">عن التطبيق</div>\n <div style={{ fontSize: 13, color: \"var(--text-secondary)\", lineHeight: 2 }}>\n <div>🔒 محلي 100% — بدون إنترنت، بدون سحابة، بدون تسجيل</div>\n <div>💾 الملفات محفوظة في متصفحك (localStorage)</div>\n <div>🤖 يتكامل مع Ollama لتشغيل موديلات AI محلية</div>\n <div>📝 محرر نصوص WYSIWYG مع دعم كامل للعربية RTL</div>\n </div>\n </div>\n </div>\n\n <div className=\"modal__footer\">\n <button className=\"btn btn-primary\" onClick={onClose}>تم</button>\n </div>\n </div>\n </div>\n );\n}\n"}}}}