1 line
96 KiB
JSON
1 line
96 KiB
JSON
{"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"}}}} |