// AssistantView.jsx — AI Legal Assistant v2.0 // Features: Quick Start (ask / upload / create), doc type cards, save-to-files, credit display, focus mode /* ── الافتتاحيّةُ الهادئة (calm1 للتطبيق، desk1 لسطح المكتب أيضاً): عنوانٌ وسطرٌ وثلاثةُ أسئلةٍ مقترحة ── (حلّت محلَّ البطاقةِ الداكنة وبطاقتَي «راجِع/أنشئ» على سطح المكتب — المستنداتُ من زرِّ «+» في الصندوق) */ const LXC_CSS = ` .lxc{min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:24px 8px 12px;animation:fadeIn .4s ease both} .lxc-t{font-family:'Noto Kufi Arabic','IBM Plex Sans Arabic',sans-serif;font-weight:700;font-size:30px;line-height:1.35;margin:0;color:#efe7d6} .lxc-t b{color:#e07a52} .lxc-p{margin:10px auto 0;max-width:30ch;font-size:15px;line-height:1.8;color:#c9c2b3} .lxc-s{display:flex;flex-direction:column;align-items:stretch;gap:8px;margin-top:26px;width:100%;max-width:420px} .lxc-s button{border:0;background:#16211d;font:inherit;font-size:14px;line-height:1.6;color:#c9c2b3;padding:10px 14px;border-radius:14px;cursor:pointer;text-align:right} .lxc-s button:active{background:#1d2a25} .lxc-d{margin-top:22px;font-size:11px;line-height:1.6;color:#9a9384} html.lx-app .lxa-msgs{padding:16px 16px 20px!important} html.lx-app .lxa-msgs>*{max-width:720px;margin-inline:auto} html.lx-app .lxa-head{box-shadow:none!important;background:transparent!important;padding:4px 8px!important;min-height:44px} html.lx-app .lxa-head .lxa-ibtn{box-shadow:none;width:40px;height:40px;color:#c9c2b3} html.lx-app .lxa-composer{background:transparent!important;box-shadow:none!important;padding:6px 12px 8px!important} .lxa-pts{flex:1;min-width:0;text-align:center;font-size:12px;color:#9a9384;border:0;background:none;font-family:inherit;cursor:pointer;padding:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .lxa-pts b{color:#c9c2b3;font-weight:600} .lxa-plus{flex-shrink:0;width:40px;height:40px;border-radius:50%;border:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;color:#c9c2b3;transition:background .15s,color .15s} .lxa-plus[aria-expanded="true"]{background:rgba(224,122,82,.14);color:#e07a52} html.lx-app .lxa-send{border-radius:50%} html.lx-app .lxa-stop{border-radius:20px} html.lx-app .lxa-ta{padding:9px 4px!important} /* desk1 — سطحُ المكتب بروحِ التطبيق: رأسٌ شفّاف، عمودُ قراءةٍ في الوسط، صندوقٌ بيضاويّ بزرّ «+» */ .lxc-tip{margin-top:18px;font-size:12.5px;color:#8f9a94;line-height:1.7} .lxc-tip b{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;margin:0 3px;vertical-align:-4px;background:rgba(239,231,214,.07);color:#c9c2b3;font-weight:600} html:not(.lx-app) .lxc{animation:none;padding:24px 8px 8px} html:not(.lx-app) .lxc-t{font-size:34px} html:not(.lx-app) .lxc-p{max-width:44ch} html:not(.lx-app) .lxc-s{max-width:520px} html:not(.lx-app) .lxc-s button{transition:background .15s,color .15s} html:not(.lx-app) .lxc-s button:hover{background:#1d2a25;color:#efe7d6} html:not(.lx-app) .lxc-d{display:none} html:not(.lx-app) .lxa-head{background:transparent!important;box-shadow:none!important;padding:8px 16px!important;min-height:52px} html:not(.lx-app) .lxa-head .lxa-ibtn{box-shadow:none;color:#a8a69e} html:not(.lx-app) .lxa-head .lxa-ibtn:hover{color:#efe7d6} html:not(.lx-app) .lxa-msgs{padding:8px 32px 24px!important} html:not(.lx-app) .lxa-msgs>*{max-width:800px;margin-inline:auto} html:not(.lx-app) .lxa-composer{background:transparent!important;box-shadow:none!important;padding:6px 32px calc(12px + env(safe-area-inset-bottom,0px))!important} html:not(.lx-app) .lxa-composer>*{max-width:800px;margin-inline:auto} html:not(.lx-app) .lxa-send{border-radius:50%} html:not(.lx-app) .lxa-stop{border-radius:20px} html:not(.lx-app) .lxa-ta{padding:8px 6px!important} html:not(.lx-app) .lxa-plus:hover{background:rgba(239,231,214,.06);color:#efe7d6} /* سجلُّ المحادثات: لوحٌ خفيف بلا كتلٍ ملوّنة */ html:not(.lx-app) .lxa-hist{background:transparent!important;border-left:1px solid rgba(239,231,214,.06)!important} html:not(.lx-app) .lxa-hist-top,html:not(.lx-app) .lxa-hist-foot{background:transparent!important;border:0!important} html:not(.lx-app) .lxa-hist-new{background:transparent!important;color:#efe7d6!important;box-shadow:0 0 0 1px rgba(239,231,214,.12)!important;justify-content:flex-start!important;font-weight:500!important} html:not(.lx-app) .lxa-hist-new:hover{background:rgba(239,231,214,.05)!important} html:not(.lx-app) .lxa-hist-foot input{background:rgba(239,231,214,.04)!important;box-shadow:none!important} html:not(.lx-app) .lxa-conv[aria-current="true"]{background:rgba(239,231,214,.07);color:#efe7d6} `; function LxAssistCalm({ onAsk, desk }) { // نفسُ «قد تسأل أيضاً» (/ai/suggestions) — ثلاثةٌ فقط، ولا شيءَ لمن لم يسأل بعد const [sugg, setSugg] = React.useState([]); React.useEffect(() => { let alive = true; window.api && window.api.ai && window.api.ai.suggestions && window.api.ai.suggestions().then(r => { if (alive) setSugg(((r && r.questions) || []).slice(0, 3)); }).catch(() => {}); return () => { alive = false; }; }, []); return (

اسأل ليكسا.

اكتب سؤالك القانوني بلغتك، وليكسا تجيبك من النظام بالمصدر.

{sugg.length > 0 && (
{sugg.map((q, i) => )}
)} {desk &&
لمراجعة عقدٍ أو إنشاءِ مستند: زرّ في صندوق الكتابة
}
ليكسا تقدّم معلوماتٍ قانونيّةً عامّة، وليست استشارةً قانونيّةً رسميّة.
); } /* ورقةُ المستندات (وضع التطبيق): زرُّ «+» في صندوقِ الكتابة يفتحُ خياري المراجعة والإنشاء بكلفتهما */ function LxaDocSheet({ open, onClose, onUpload, onCreate }) { React.useEffect(() => { if (!open) return; const k = e => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', k); return () => document.removeEventListener('keydown', k); }, [open, onClose]); if (!open) return null; const ico = (t) => ; const cost = (n) => {n} نقطة; return (
e.stopPropagation()}>
); } const ASSISTANT_INIT = [{ id: 'welcome', role: 'assistant', text: 'مرحباً، أنا ليكسا — مساعدك القانوني الذكي.\n\nأستطيع مساعدتك في فهم حقوقك، تحليل النصوص القانونية، والإجابة على أسئلتك القانونية بلغة واضحة وبسيطة.\n\nكيف يمكنني مساعدتك اليوم؟', }]; const ASSISTANT_DOC_TYPES = [ { group: 'العقود التجارية والعمل', items: [ { id: 'work-contract', label: 'عقد عمل', icon: '👤' }, { id: 'rent-contract', label: 'عقد إيجار', icon: '🏢' }, { id: 'partnership', label: 'عقد شراكة', icon: '🤝' }, { id: 'services', label: 'عقد خدمات / مقاولة', icon: '⚙️' }, ] }, { group: 'الاتفاقيات القانونية', items: [ { id: 'nda', label: 'اتفاقية سرية (NDA)', icon: '🔒' }, { id: 'poa', label: 'توكيل رسمي', icon: '📋' }, { id: 'sale', label: 'عقد بيع', icon: '✍️' }, ] }, { group: 'المراسلات والإشعارات', items: [ { id: 'legal-notice', label: 'خطاب إنذار قانوني', icon: '✉️' }, { id: 'legal-memo', label: 'مذكرة قانونية', icon: '📄' }, ] }, ]; /* ── Doc Schemas — fields per doc type ───────────────── */ const DOC_SCHEMAS = { 'work-contract': { fields: [ { id: 'employer', label: 'صاحب العمل', placeholder: 'اسم الجهة أو الفرد' }, { id: 'employee', label: 'الموظف', placeholder: 'الاسم الكامل' }, { id: 'jobTitle', label: 'المسمى الوظيفي', placeholder: 'مثال: محاسب، مهندس…' }, { id: 'salary', label: 'الراتب الشهري', placeholder: 'بالريال السعودي', type: 'number' }, { id: 'probation', label: 'فترة التجربة (بالأشهر)', placeholder: '3 أشهر', type: 'number' }, { id: 'duration', label: 'مدة العقد', placeholder: 'محدد المدة / غير محدد المدة' }, { id: 'leave', label: 'الإجازة السنوية (بالأيام)', placeholder: '21 يوماً', type: 'number' }, ] }, 'rent-contract': { fields: [ { id: 'lessor', label: 'المؤجر', placeholder: 'اسم المؤجر' }, { id: 'lessee', label: 'المستأجر', placeholder: 'اسم المستأجر' }, { id: 'propType', label: 'نوع العقار', placeholder: 'سكني / تجاري / مكتب' }, { id: 'address', label: 'العنوان الكامل', placeholder: 'المدينة، الحي، الشارع' }, { id: 'area', label: 'المساحة (م²)', placeholder: 'مثال: 250', type: 'number' }, { id: 'deedNo', label: 'رقم الصك (اختياري)', placeholder: 'إن وُجد' }, { id: 'duration', label: 'مدة العقد', placeholder: 'مثال: سنة' }, { id: 'startDate', label: 'تاريخ البداية', type: 'date' }, { id: 'amount', label: 'القيمة السنوية (ر.س)', placeholder: 'مثال: 60000', type: 'number' }, { id: 'paymentMethod', label: 'طريقة السداد', placeholder: 'سنوي / نصف سنوي / ربع سنوي / شهري' }, ], extras: [ { id: 'incInsurance', label: 'يشمل التأمين على العقار' }, { id: 'allowSublet', label: 'يحق التأجير من الباطن' }, { id: 'lessorMaint', label: 'الصيانة على المؤجر' }, ], }, 'partnership': { fields: [ { id: 'partners', label: 'أسماء الشركاء', placeholder: 'افصل بفاصلة' }, { id: 'shares', label: 'النسب', placeholder: 'مثال: 50% / 30% / 20%' }, { id: 'capital', label: 'رأس المال (ر.س)', placeholder: 'مثال: 500,000', type: 'number' }, { id: 'partnershipType', label: 'نوع الشراكة', placeholder: 'تضامن / محدودة / توصية' }, { id: 'management', label: 'الإدارة', placeholder: 'من يدير الشركة' }, ] }, 'services': { fields: [ { id: 'provider', label: 'مقدم الخدمة', placeholder: 'الاسم أو الجهة' }, { id: 'client', label: 'العميل', placeholder: 'اسم العميل' }, { id: 'serviceType', label: 'نوع الخدمة', placeholder: 'وصف موجز' }, { id: 'duration', label: 'مدة العقد', placeholder: 'مثال: 6 أشهر' }, { id: 'amount', label: 'القيمة الإجمالية (ر.س)', type: 'number' }, ] }, 'nda': { fields: [ { id: 'parties', label: 'الأطراف', placeholder: 'الطرف الأول والطرف الثاني' }, { id: 'infoType', label: 'نوع المعلومات السرية', placeholder: 'تجارية / تقنية / مالية' }, { id: 'duration', label: 'مدة السرية', placeholder: 'مثال: 5 سنوات' }, { id: 'exceptions', label: 'الاستثناءات (اختياري)', placeholder: 'معلومات معروفة للعموم…' }, ] }, 'poa': { fields: [ { id: 'principal', label: 'الموكل', placeholder: 'الاسم الكامل + رقم الهوية' }, { id: 'agent', label: 'الوكيل', placeholder: 'الاسم الكامل + رقم الهوية' }, { id: 'scope', label: 'نطاق الوكالة', placeholder: 'محدد بمهمة معينة أم عام' }, { id: 'duration', label: 'مدة الوكالة', placeholder: 'مثال: سنة من تاريخ التوقيع' }, ] }, 'sale': { fields: [ { id: 'seller', label: 'البائع', placeholder: 'الاسم الكامل' }, { id: 'buyer', label: 'المشتري', placeholder: 'الاسم الكامل' }, { id: 'item', label: 'المبيع', placeholder: 'وصف الشيء المباع' }, { id: 'price', label: 'السعر (ر.س)', type: 'number' }, { id: 'paymentTerms', label: 'شروط الدفع', placeholder: 'كاش / مقسط / دفعة مقدمة + الباقي…' }, { id: 'warranty', label: 'الضمانات (اختياري)', placeholder: 'مثال: ضمان 12 شهر' }, ] }, 'legal-notice': { fields: [ { id: 'sender', label: 'المُنذِر', placeholder: 'اسمك أو جهتك' }, { id: 'recipient', label: 'المُنذَر', placeholder: 'الطرف المرسل إليه' }, { id: 'subject', label: 'الموضوع', placeholder: 'مثال: مطالبة بسداد دين' }, { id: 'demand', label: 'المطلوب', placeholder: 'الإجراء المطلوب من الطرف الآخر' }, { id: 'deadline', label: 'المهلة', placeholder: 'مثال: 15 يوم من تاريخ الإنذار' }, ] }, 'legal-memo': { fields: [ { id: 'subject', label: 'الموضوع', placeholder: 'موضوع المذكرة' }, { id: 'parties', label: 'الأطراف', placeholder: 'المدعي والمدعى عليه' }, { id: 'facts', label: 'الوقائع', placeholder: 'سرد موجز للوقائع', multiline: true }, { id: 'requests', label: 'الطلبات', placeholder: 'ما تطلبه من المحكمة', multiline: true }, ] }, 'custom': { fields: [ { id: 'docType', label: 'نوع المستند', placeholder: 'مثال: اتفاقية إنهاء، إقرار…' }, { id: 'parties', label: 'الأطراف', placeholder: 'الأطراف المعنية' }, { id: 'subject', label: 'الموضوع', placeholder: 'موضوع المستند' }, { id: 'details', label: 'التفاصيل', placeholder: 'كل التفاصيل المطلوبة في المستند', multiline: true }, ] }, }; /* Analysis options for uploaded documents */ const ANALYSIS_OPTIONS = [ { id: 'comprehensive', icon: '🔍', label: 'مراجعة شاملة', desc: 'تحليل كامل + نقاط قوة + مخاطر + توصيات' }, { id: 'risks', icon: '⚠️', label: 'كشف المخاطر القانونية', desc: 'البنود الخطرة أو المجحفة' }, { id: 'compliance', icon: '⚖️', label: 'التحقق من المطابقة للأنظمة السعودية', desc: 'هل المستند مطابق للأنظمة' }, { id: 'summary', icon: '📋', label: 'تلخيص المستند', desc: 'نقاط أساسية مختصرة' }, { id: 'edits', icon: '🔄', label: 'اقتراح تعديلات', desc: 'لتحسين المستند لصالحك' }, { id: 'specific', icon: '❓', label: 'سؤال محدد', desc: 'اسأل سؤالاً عن المستند' }, ]; /* ── مستنداتُ المساعد: «رفع مستند» (mode=review) و«إنشاء مستند» (mode=draft) ───────────── كلفتُها بالنقاط (access.POINTS في الخادم)؛ الخادمُ يبدأُ ردَّهما بوسم «⟦lexa:draft⟧» / «⟦lexa:review⟧»، ورسالةُ المستخدمِ في المراجعة تحملُ نصَّ المستندِ بين … (يُخفى هنا ويظهرُ شارة). */ const LXA_COST = { draft: 30, review: 25, ask: 10 }; const LXA_MAX_UPLOAD = 10 * 1024 * 1024; function lxaDocMeta(text) { const m = String(text || '').match(/^⟦lexa:(draft|review)⟧\s*/); return m ? { kind: m[1], body: String(text).slice(m[0].length) } : { kind: null, body: String(text || '') }; } // fix4 (U1/U2): رسائلُ المساعدِ المحفوظةُ التي ليست إجابة — اعتذارُ فشلٍ/انشغال (النقاطُ رُدّت) أو «أوقفتَ الإجابة» const LXA_STOP_PREFIX = 'أوقفتَ الإجابة'; const LXA_FAIL_PREFIXES = ['تعذّر توليدُ الإجابةِ الآن', 'الخادمُ مشغولٌ بإجاباتٍ أخرى']; function lxaIsStoppedMsg(m) { return !!m && m.role === 'assistant' && typeof m.content === 'string' && m.content.startsWith(LXA_STOP_PREFIX); } function lxaIsFailedMsg(m) { return !!m && m.role === 'assistant' && !!m.is_stub && !m.credits_used && typeof m.content === 'string' && LXA_FAIL_PREFIXES.some(p => m.content.startsWith(p)); } // نصُّ ملاحظةِ الإيقاف: النقاطُ لا تُردّ (المساعدُ كان قد بدأ العمل) — الأدمنُ بلا نقاط function lxaStoppedNote() { const u = window.__sanadCurrentUser; return (u && u.is_admin) ? 'أوقفتَ الإجابة قبل اكتمالها.' : 'أوقفتَ الإجابة قبل اكتمالها — خُصمت نقاط السؤال لأنّ المساعد كان قد بدأ العمل.'; } function lxaSplitUser(text) { const t = String(text || ''); const m = t.match(/\n*]*>[\s\S]*?<\/lexa-doc>\s*/); if (!m) return { text: t, doc: null }; const unesc = (s) => s.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); return { text: (t.slice(0, m.index) + t.slice(m.index + m[0].length)).trim(), doc: { name: unesc(m[1] || 'مستند'), chars: m[2] ? +m[2] : null } }; } function lxaDocTitle(body, kind) { const h = String(body || '').match(/^#\s+(.+)$/m); return (h ? h[1].replace(/\*\*/g, '').trim() : '') || (kind === 'review' ? 'تقرير مراجعة مستند — ليكسا' : 'مستند — ليكسا'); } function lxaPlain(body) { // نصٌّ نظيفٌ للنسخ: بلا وسمٍ ولا روابطِ ⟦…⟧ return String(body || '').replace(/^⟦lexa:\w+⟧\s*/, '').replace(/\s*⟦[^⟧]*⟧/g, ''); } async function lxaDownloadDocx(body, kind) { const title = lxaDocTitle(body, kind); const blob = await window.api.ai.exportDocx(title, body); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = title.replace(/[\\/:*?"<>|#]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80) + '.docx'; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1500); } // PDF: صفحةٌ مطبوعةٌ عربيّةٌ من اليمين ← «حفظ بصيغة PDF» من نافذةِ الطباعة (المتصفّحُ يشكّلُ الحروفَ العربيّة صحيحاً) function lxaPrintHtml(body, kind) { const esc = (s) => String(s).replace(/&/g, '&').replace(//g, '>'); const t = lxaPlain(body); const cut = t.search(/\n-{3,}\nالمصادر:/); const main = (cut >= 0 ? t.slice(0, cut) : t).replace(/\s*\[\d{1,2}\](?:\s*\[\d{1,2}\])*/g, ''); const srcs = cut >= 0 ? t.slice(cut).split('\n').map(l => l.match(/^\[(\d+)\]\s*(.+)$/)).filter(Boolean) : []; const inl = (s) => esc(s).replace(/\*\*([^*]+)\*\*/g, '$1'); const out = []; let list = null; const flush = () => { if (list) { out.push(''); list = null; } }; t && main.split('\n').forEach(raw => { const s = raw.trim(); if (!s) { flush(); return; } let m; if (/^[-_*]{3,}$/.test(s)) { flush(); out.push('
'); return; } if ((m = s.match(/^#\s+(.*)$/))) { flush(); out.push('

' + inl(m[1]) + '

'); return; } if ((m = s.match(/^#{2,4}\s+(.*)$/))) { flush(); out.push('

' + inl(m[1]) + '

'); return; } if ((m = s.match(/^>\s?(.*)$/))) { flush(); out.push('
' + inl(m[1]) + '
'); return; } if ((m = s.match(/^[-•*]\s+(.*)$/))) { (list = list || []).push('
  • ' + inl(m[1]) + '
  • '); return; } flush(); out.push('

    ' + inl(s) + '

    '); }); flush(); if (srcs.length) out.push('

    المراجع النظاميّة (من قاعدة ليكسا)

      ' + srcs.map(m => '
    1. ' + esc(m[2].replace(/\s*:\s*$/, '')) + '
    2. ').join('') + '
    '); const title = lxaDocTitle(body, kind); return '' + esc(title) + '' + '' + '' + out.join('\n') + '
    أُعدّ بمساعدة ليكسا — مسودةٌ استرشاديّة تُراجَعُ مع مختصّ قبل الاعتماد.
    ' + '