// 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) => onAsk(q)}>{q} )}
)}
{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) => {t} ;
const cost = (n) => {n} نقطة ;
return (
e.stopPropagation()}>
المستندات
{onUpload && (
{ onClose(); onUpload(); }}>
{ico('📎')}راجع مستنداًارفع PDF أو Word ليبيّن ليكسا المخالفاتِ والمخاطر {cost(LXA_COST.review)}
)}
{ onClose(); onCreate(); }}>
{ico('✍️')}أنشئ عقداًاختر النوع واملأ البيانات — Word أو PDF {cost(LXA_COST.draft)}
);
}
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 => '' + esc(m[2].replace(/\s*:\s*$/, '')) + ' ').join('') + ' ');
const title = lxaDocTitle(body, kind);
return '' + esc(title) + ' '
+ ' '
+ ''
+ out.join('\n') + 'أُعدّ بمساعدة ليكسا — مسودةٌ استرشاديّة تُراجَعُ مع مختصّ قبل الاعتماد. '
+ '