const $ = (id) => document.getElementById(id); const STORAGE_KEY = 'ssa_last_session_id'; const state = { sessionId: null, current: null, drafts: [], ui: { workbenchOpen: false, activeTab: 'srs', }, }; const stageLabels = { intro: 'تمهيد', exploration: 'استكشاف', usecase: 'حالات الاستخدام', evaluation: 'تقييم', done: 'مكتمل', }; const textareaLimits = { input: { min: 46, max: 180 }, 'srs-text': { min: 92, max: 240 }, 'actors-editor': { min: 72, max: 180 }, 'usecases-editor': { min: 72, max: 200 }, 'io-editor': { min: 88, max: 220 }, 'scope-editor': { min: 72, max: 180 }, }; document.addEventListener('DOMContentLoaded', init); function init() { $('start-btn').addEventListener('click', startSession); $('project-title').addEventListener('keydown', (e) => { if (e.key === 'Enter') startSession(); }); $('send-btn').addEventListener('click', sendMessage); $('input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); ['input', 'srs-text', 'actors-editor', 'usecases-editor', 'io-editor', 'scope-editor'].forEach((id) => { $(id).addEventListener('input', autoGrow); }); $('analyze-srs-btn').addEventListener('click', analyzeSrs); $('confirm-srs-btn').addEventListener('click', confirmStructure); $('save-planning-btn').addEventListener('click', savePlanning); $('export-btn').addEventListener('click', () => { if (state.sessionId) window.open(`/report/${state.sessionId}`, '_blank'); }); $('srs-file').addEventListener('change', updateSrsFileSelection); configurePdfEngine(); updateSrsFileSelection(); $('workspace-collapse').addEventListener('click', () => setWorkbenchOpen(!state.ui.workbenchOpen)); document.querySelectorAll('[data-workspace-tab]').forEach((button) => { button.addEventListener('click', () => setWorkbenchTab(button.dataset.workspaceTab, true)); }); setWorkbenchTab('srs'); setWorkbenchOpen(false); resizeAllTextareas(); loadDrafts(); } function configurePdfEngine() { if (window.pdfjsLib?.GlobalWorkerOptions) { window.pdfjsLib.GlobalWorkerOptions.workerSrc = '/vendor/pdfjs/pdf.worker.min.js'; } } function getSelectedSrsFile() { return $('srs-file').files?.[0] || null; } function isPdfFile(file) { return Boolean(file) && (/\.pdf$/i.test(file.name || '') || (file.type || '').toLowerCase() === 'application/pdf'); } function updateSrsFileSelection(statusText = '') { const file = getSelectedSrsFile(); const fileName = $('srs-file-name'); const note = $('srs-source-note'); if (statusText) { fileName.textContent = statusText; } else if (file) { fileName.textContent = `الملف المختار: ${file.name}${isPdfFile(file) ? ' — PDF' : ''}`; } else { fileName.textContent = 'لم يتم اختيار ملف بعد.'; } if (note) { note.textContent = file && isPdfFile(file) ? 'سيتم استخراج النص من ملف PDF قبل التحليل. إذا كان الملف صورة ممسوحة ضوئياً فقد تحتاج إلى OCR أو لصق النص يدوياً.' : 'يدعم TXT / MD / CSV / JSON / SRS / REQ إضافةً إلى PDF النصي. يمكنك أيضاً لصق المتطلبات مباشرة.'; } } function setSrsProcessingState(isBusy, statusText = '') { const button = $('analyze-srs-btn'); button.disabled = Boolean(isBusy); button.textContent = isBusy ? 'جارٍ تحليل المستند...' : 'حلّل الملف/النص'; updateSrsFileSelection(statusText); } async function readSrsSource({ file, manualText }) { if (file) { if (isPdfFile(file)) { updateSrsFileSelection(`جارٍ استخراج النص من PDF: ${file.name}...`); const content = await extractPdfText(file, ({ currentPage, totalPages }) => { updateSrsFileSelection(`جارٍ استخراج النص من PDF: الصفحة ${currentPage}/${totalPages} — ${file.name}`); }); if (!content.trim()) { throw new Error('تعذر استخراج نص واضح من ملف PDF. إذا كان الملف صورة ممسوحة ضوئياً فاستخدم OCR أو الصق النص يدوياً.'); } return content; } const content = await file.text(); if (looksBinary(content)) { throw new Error('الملف المختار غير نصي مقروء. استخدم PDF نصي أو ملف TXT/MD/CSV/JSON، أو الصق النص يدوياً.'); } return content; } return String(manualText || '').trim(); } async function extractPdfText(file, onProgress) { const pdfjs = window.pdfjsLib; if (!pdfjs?.getDocument) { throw new Error('محرّك قراءة PDF غير جاهز حالياً. حدّث الصفحة ثم جرّب مرة أخرى.'); } pdfjs.GlobalWorkerOptions.workerSrc = '/vendor/pdfjs/pdf.worker.min.js'; const data = await file.arrayBuffer(); const pdf = await pdfjs.getDocument({ data }).promise; const pages = []; for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) { if (typeof onProgress === 'function') { onProgress({ currentPage: pageNumber, totalPages: pdf.numPages }); } const page = await pdf.getPage(pageNumber); const textContent = await page.getTextContent(); const pageText = extractPdfPageText(textContent); if (pageText) pages.push(pageText); } return pages.join('\n\n').replace(/\n{3,}/g, '\n\n').trim(); } function extractPdfPageText(textContent) { const lines = []; let currentLine = []; let lastY = null; const flushLine = () => { const line = currentLine.join(' ').replace(/\s+/g, ' ').trim(); if (line) lines.push(line); currentLine = []; }; (textContent?.items || []).forEach((item) => { const raw = String(item?.str ?? '').replace(/\s+/g, ' ').trim(); const y = Number(item?.transform?.[5] ?? lastY ?? 0); if (lastY !== null && Math.abs(y - lastY) > 4) { flushLine(); } if (raw) { currentLine.push(raw); lastY = y; } if (item?.hasEOL) { flushLine(); lastY = null; } }); flushLine(); return lines.join('\n').trim(); } function autoGrow(event) { resizeTextarea(event.target); } function resizeTextarea(target) { if (!target || target.tagName !== 'TEXTAREA') return; const limits = textareaLimits[target.id] || { min: 72, max: 180 }; if (target.offsetParent === null) { target.style.height = `${limits.min}px`; target.style.overflowY = 'hidden'; return; } target.style.height = '0px'; const nextHeight = Math.max(limits.min, Math.min(target.scrollHeight, limits.max)); target.style.height = `${nextHeight}px`; target.style.overflowY = target.scrollHeight > limits.max ? 'auto' : 'hidden'; } function resizeAllTextareas() { document.querySelectorAll('textarea').forEach((textarea) => resizeTextarea(textarea)); } function setWorkbenchOpen(open, options = {}) { const { activeTab, focusChat = false } = options; if (activeTab) setWorkbenchTab(activeTab); state.ui.workbenchOpen = Boolean(open); const body = $('workspace-body'); const panel = $('workspace-panel'); const toggle = $('workspace-collapse'); const toggleText = $('workspace-collapse-text'); const stateBadge = $('workspace-state'); panel.classList.toggle('collapsed', !state.ui.workbenchOpen); body.classList.toggle('hidden', !state.ui.workbenchOpen); toggle.setAttribute('aria-expanded', state.ui.workbenchOpen ? 'true' : 'false'); toggleText.textContent = state.ui.workbenchOpen ? 'إخفاء اللوحة' : 'فتح اللوحة'; stateBadge.textContent = state.ui.workbenchOpen ? 'مفتوحة' : 'مطوية'; updateWorkbenchSummary(state.current); if (state.ui.workbenchOpen) { requestAnimationFrame(() => { resizeAllTextareas(); focusWorkbenchField(); }); } else if (focusChat) { requestAnimationFrame(() => $('input').focus()); } } function setWorkbenchTab(tab, openIfCollapsed = false) { state.ui.activeTab = tab === 'proposal' ? 'proposal' : 'srs'; document.querySelectorAll('[data-workspace-tab]').forEach((button) => { const active = button.dataset.workspaceTab === state.ui.activeTab; button.classList.toggle('active', active); button.setAttribute('aria-selected', active ? 'true' : 'false'); }); document.querySelectorAll('.workspace-card').forEach((panel) => { const active = panel.dataset.panel === state.ui.activeTab; panel.classList.toggle('active', active); panel.hidden = !active; }); if (openIfCollapsed && !state.ui.workbenchOpen) { setWorkbenchOpen(true); return; } if (state.ui.workbenchOpen) { requestAnimationFrame(() => { resizeAllTextareas(); focusWorkbenchField(); }); } } function focusWorkbenchField() { const targetId = state.ui.activeTab === 'proposal' ? 'actors-editor' : 'srs-text'; $(targetId)?.focus(); } function updateWorkbenchSummary(data = state.current) { const summaryEl = $('workspace-summary'); if (!summaryEl) return; if (!data) { summaryEl.textContent = 'ارفع SRS أو راجع الاقتراح من هنا بدون أن تزاحم مساحة الشات.'; return; } const source = data.srsDraft || data; const counts = source.counts || { actors: (source.actors || []).length, useCases: (source.useCases || []).length, inputs: (source.inputOutputs || []).filter((item) => item.type === 'input').length, outputs: (source.inputOutputs || []).filter((item) => item.type === 'output').length, }; const parts = []; parts.push(`المرحلة: ${stageLabels[data.stage] || data.stage || '—'}`); if (counts.actors || counts.useCases || counts.inputs || counts.outputs) { parts.push(`${counts.actors || 0} Actors · ${counts.useCases || 0} Use Cases · ${counts.inputs || 0} Inputs · ${counts.outputs || 0} Outputs`); } else { parts.push('ابدأ برفع SRS أو لصق المتطلبات لتوليد الاقتراح.'); } parts.push(data.needsStructureConfirmation ? 'الاقتراح جاهز للمراجعة والاعتماد.' : 'يمكنك إبقاء اللوحة مطوية والتركيز على المحادثة.'); summaryEl.textContent = parts.join(' — '); } async function loadDrafts() { try { const res = await fetch('/api/sessions'); const data = await res.json(); state.drafts = Array.isArray(data.sessions) ? data.sessions : []; renderDraftLists(); } catch (error) { console.error('loadDrafts error', error); } } function renderDraftLists() { renderDraftList('welcome-drafts', true); renderDraftList('sidebar-drafts', false); } function renderDraftList(containerId, compact = false) { const container = $(containerId); const drafts = state.drafts || []; const lastId = localStorage.getItem(STORAGE_KEY); if (!drafts.length) { container.innerHTML = '
لا توجد مسودات محفوظة بعد.
'; return; } container.innerHTML = drafts.map((draft) => { const active = draft.id === state.sessionId; const last = draft.id === lastId; const counts = draft.counts || {}; return ` `; }).join(''); container.querySelectorAll('.draft-card').forEach((btn) => { btn.addEventListener('click', () => resumeSession(btn.dataset.sessionId)); }); } function formatDate(value, compact = false) { if (!value) return 'بدون تاريخ'; const date = new Date(value); if (Number.isNaN(date.getTime())) return 'بدون تاريخ'; const formatter = new Intl.DateTimeFormat('ar-EG', { year: 'numeric', month: compact ? 'short' : 'long', day: 'numeric', hour: compact ? undefined : 'numeric', minute: compact ? undefined : '2-digit', }); return formatter.format(date); } function showApp(title) { $('welcome').classList.add('hidden'); $('app').classList.remove('hidden'); $('project-name').textContent = title || '—'; } function showResumeBanner(text) { const banner = $('resume-banner'); banner.textContent = text; banner.classList.remove('hidden'); } function hideResumeBanner() { $('resume-banner').classList.add('hidden'); $('resume-banner').textContent = ''; } async function startSession() { const title = $('project-title').value.trim(); if (!title) { $('project-title').focus(); return; } $('start-btn').disabled = true; try { const res = await fetch('/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'فشل بدء الجلسة'); state.sessionId = data.sessionId; localStorage.setItem(STORAGE_KEY, data.sessionId); state.current = data; clearMessages(); hideResumeBanner(); showApp(title); setWorkbenchTab('srs'); setWorkbenchOpen(false); addAgentMessage(data.reply || 'تم بدء الجلسة.', true); updateState(data); await loadDrafts(); resizeAllTextareas(); $('input').focus(); } catch (error) { alert(`خطأ: ${error.message}`); } finally { $('start-btn').disabled = false; } } async function resumeSession(sessionId) { try { const res = await fetch(`/api/session/${encodeURIComponent(sessionId)}`); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'تعذر استئناف الجلسة'); state.sessionId = data.sessionId; state.current = data; localStorage.setItem(STORAGE_KEY, data.sessionId); showApp(data.title); renderHistory(data.history || []); updateState(data); if (data.needsStructureConfirmation) { setWorkbenchOpen(true, { activeTab: 'proposal' }); } else { setWorkbenchTab('srs'); setWorkbenchOpen(false); $('input').focus(); } showResumeBanner(data.resumeMessage || 'أهلاً بك مجدداً، يمكننا متابعة العمل من آخر نقطة توقفنا عندها.'); await loadDrafts(); } catch (error) { alert(`خطأ: ${error.message}`); if (String(error.message).includes('غير موجودة')) { localStorage.removeItem(STORAGE_KEY); } } } async function sendMessage() { const input = $('input'); const text = input.value.trim(); if (!text || !state.sessionId) return; hideResumeBanner(); addUserMessage(text); input.value = ''; resizeTextarea(input); $('send-btn').disabled = true; const thinkingEl = addAgentMessage('يفكّر...', false, true); try { const res = await fetch('/api/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: state.sessionId, message: text }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'فشل الإرسال'); thinkingEl.remove(); addAgentMessage(data.reply || 'تم تحديث التحليل.', true); updateState(data); await loadDrafts(); } catch (error) { thinkingEl.querySelector('.bubble').textContent = `⚠️ ${error.message}`; thinkingEl.classList.remove('thinking'); } finally { $('send-btn').disabled = false; $('input').focus(); } } async function analyzeSrs() { if (!state.sessionId) { alert('ابدأ جلسة أولاً ثم ارفع ملف SRS أو الصق النص.'); return; } const file = getSelectedSrsFile(); const filename = file?.name || ''; let thinkingEl = null; try { setSrsProcessingState(true); const content = await readSrsSource({ file, manualText: $('srs-text').value.trim() }); if (!content.trim()) { throw new Error('أرفق ملفاً أو الصق نص المتطلبات أولاً.'); } if (looksBinary(content)) { throw new Error('المحتوى المقروء غير صالح للتحليل كنص. استخدم PDF نصي أو ألصق النص مباشرة.'); } addUserMessage(filename ? `📄 تم إرسال ملف SRS للتحليل: ${filename}` : '📄 تم إرسال نص SRS للتحليل'); thinkingEl = addAgentMessage('أحلّل المستند وأستخرج Actors وUse Cases وInputs/Outputs...', false, true); const res = await fetch('/api/analyze-srs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: state.sessionId, content, filename }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'فشل تحليل المستند'); thinkingEl.remove(); thinkingEl = null; addAgentMessage(data.reply || 'تم تحليل المستند.', true); updateState(data); setWorkbenchTab('proposal'); setWorkbenchOpen(true); await loadDrafts(); } catch (error) { if (thinkingEl) { thinkingEl.querySelector('.bubble').textContent = `⚠️ ${error.message}`; thinkingEl.classList.remove('thinking'); } else { alert(`خطأ: ${error.message}`); } } finally { setSrsProcessingState(false); } } async function confirmStructure() { if (!state.sessionId) return; const actors = parseSimpleList($('actors-editor').value); const useCases = parseUseCases($('usecases-editor').value, state.current?.useCases || []); const inputOutputs = parseInputOutputs($('io-editor').value, state.current?.inputOutputs || []); const scope = $('scope-editor').value.trim(); if (!actors.length && !useCases.length && !inputOutputs.length && !scope) { alert('لا توجد بيانات لاعتمادها بعد. حلّل SRS أولاً أو أدخل القوائم يدوياً.'); return; } addUserMessage('✅ تم اعتماد/تعديل الهيكل الأولي للمشروع.'); const thinkingEl = addAgentMessage('أثبّت القوائم وأعيد حساب التقدير...', false, true); try { const res = await fetch('/api/confirm-structure', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: state.sessionId, scope, actors, useCases, inputOutputs }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'فشل اعتماد الاقتراح'); thinkingEl.remove(); addAgentMessage(data.reply || 'تم اعتماد الهيكل الأولي.', true); updateState(data); await loadDrafts(); setWorkbenchOpen(false, { focusChat: true }); } catch (error) { thinkingEl.querySelector('.bubble').textContent = `⚠️ ${error.message}`; thinkingEl.classList.remove('thinking'); } } async function savePlanning() { if (!state.sessionId) return; $('planning-status').textContent = 'جارٍ تحديث خطة التنفيذ...'; try { const res = await fetch(`/api/session/${encodeURIComponent(state.sessionId)}/planning`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ developers: Number($('team-size').value || 3), sprintWeeks: Number($('sprint-weeks').value || 2), }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'فشل تحديث التخطيط'); updateState(data); $('planning-status').textContent = 'تم تحديث الخطة والمدة المتوقعة بنجاح.'; await loadDrafts(); } catch (error) { $('planning-status').textContent = `⚠️ ${error.message}`; } } function clearMessages() { $('messages').innerHTML = ''; } function renderHistory(history) { clearMessages(); history.forEach((entry) => { if (entry.role === 'user') addUserMessage(entry.text || '', false); else if (entry.role === 'assistant') addAgentMessage(entry.payload?.reply || '', false, false, false); }); scrollMsgs(); } function addUserMessage(text, scroll = true) { const el = document.createElement('div'); el.className = 'msg user'; el.innerHTML = `
أنا
`; el.querySelector('.bubble').textContent = text; $('messages').appendChild(el); if (scroll) scrollMsgs(); return el; } function addAgentMessage(text, typewriter = false, thinking = false, scroll = true) { document.querySelectorAll('.msg.agent.fresh').forEach((node) => node.classList.remove('fresh')); const el = document.createElement('div'); el.className = `msg agent fresh${thinking ? ' thinking' : ''}`; el.innerHTML = `
AI
`; $('messages').appendChild(el); const bubble = el.querySelector('.bubble'); if (typewriter) typeText(bubble, text); else bubble.textContent = text; if (scroll) scrollMsgs(); return el; } function typeText(el, text) { el.textContent = ''; const cursor = document.createElement('span'); cursor.className = 'cursor'; el.appendChild(cursor); let i = 0; const speed = Math.max(8, Math.min(28, 1200 / Math.max(text.length, 12))); const tick = () => { if (i < text.length) { cursor.insertAdjacentText('beforebegin', text[i]); i += 1; scrollMsgs(); setTimeout(tick, speed); } else { cursor.remove(); } }; tick(); } function scrollMsgs() { const messages = $('messages'); messages.scrollTop = messages.scrollHeight; } function updateState(data) { state.current = data; state.sessionId = data.sessionId || data.id || state.sessionId; localStorage.setItem(STORAGE_KEY, state.sessionId); const fps = data.functionPoints || []; const ucs = data.useCases || []; const ios = data.inputOutputs || []; const actors = data.actors || []; const analytics = data.analytics || {}; const planning = data.planning || {}; $('project-name').textContent = data.title || $('project-name').textContent; $('fp-count').textContent = fps.length; $('uc-count').textContent = ucs.length; $('io-count').textContent = ios.length; $('fp-total').textContent = analytics.fp?.totalFP ?? fps.reduce((sum, item) => sum + (Number(item.fpScore) || 0), 0); $('stage-pill').textContent = stageLabels[data.stage] || data.stage || '—'; $('team-size').value = planning.developers || 3; $('sprint-weeks').value = planning.sprintWeeks || 2; fillProposalEditors(data); renderTree(actors, fps, ios); renderPreview(data); updateWorkbenchSummary(data); resizeAllTextareas(); if (data.isComplete || ucs.length >= 1 || fps.length >= 2) $('export-btn').classList.remove('hidden'); } function fillProposalEditors(data) { const source = data.srsDraft || data; $('scope-editor').value = source.summary || source.scope || data.scope || ''; $('actors-editor').value = (source.actors || []).join('\n'); $('usecases-editor').value = (source.useCases || []).map((uc) => `${uc.title || ''} | ${uc.actor || 'المستخدم'}`).join('\n'); $('io-editor').value = (source.inputOutputs || []).map((item) => [ item.type === 'output' ? 'Output' : 'Input', item.name || '', item.source || '', item.destination || '', item.description || '', ].join(' | ')).join('\n'); const counts = source.counts || { actors: (source.actors || []).length, useCases: (source.useCases || []).length, inputs: (source.inputOutputs || []).filter((item) => item.type === 'input').length, outputs: (source.inputOutputs || []).filter((item) => item.type === 'output').length, }; if (counts.actors || counts.useCases || counts.inputs || counts.outputs) { $('proposal-summary').textContent = `Actors: ${counts.actors || 0} · Use Cases: ${counts.useCases || 0} · Inputs: ${counts.inputs || 0} · Outputs: ${counts.outputs || 0}`; } else { $('proposal-summary').textContent = 'ستظهر هنا أعداد Actors وUse Cases وInputs/Outputs بعد تحليل المستند.'; } const status = $('proposal-status'); if (data.needsStructureConfirmation) { status.textContent = 'بانتظار الاعتماد'; status.classList.remove('dim'); } else if ((source.actors || []).length || (source.useCases || []).length) { status.textContent = 'قابل للتعديل'; status.classList.remove('dim'); } else { status.textContent = 'بانتظار تحليل'; status.classList.add('dim'); } } function renderTree(actors, fps, ios) { const tree = $('tree'); if (!actors.length && !fps.length && !ios.length) { tree.innerHTML = '
ستظهر الـ Actors والوظائف وهياكل الـ IO هنا تلقائياً.
'; return; } let html = ''; actors.forEach((actor) => { html += `
${esc(actor)}
`; }); fps.forEach((fp) => { html += `
${esc(fp.id || '')} · ${esc(fp.name || '')}
`; }); ios.slice(0, 8).forEach((item) => { html += `
${item.type === 'output' ? 'Output' : 'Input'} · ${esc(item.name || '')}
`; }); tree.innerHTML = html; } function renderPreview(data) { const prev = $('preview-content'); const actors = data.actors || []; const fps = data.functionPoints || []; const ucs = data.useCases || []; const ios = data.inputOutputs || []; const analytics = data.analytics || {}; const fp = analytics.fp || {}; const ucp = analytics.ucp || {}; const discrepancy = analytics.discrepancy || {}; const plan = analytics.plan || {}; let html = ''; html += `
${previewMetric(fp.totalFP ?? 0, 'Total FP', `~ ${fp.effortHours ?? 0} ساعة`) } ${previewMetric(ucp.UCP ?? 0, 'UCP', `~ ${ucp.effortHours ?? 0} ساعة`) } ${previewMetric(discrepancy.deltaPercent ?? 0, 'Gap', `${discrepancy.dominantMethod || '—'} الأعلى`) } ${previewMetric(plan.totalSprints ?? 0, 'Sprints', `${plan.calendarWeeks ?? 0} أسبوع`) }
`; if (data.scope) { html += `

نطاق النظام

${esc(data.scope)}
`; } if (actors.length) { html += `

Actors (${actors.length})

${actors.map((actor) => `${esc(actor)}`).join('')}
`; } if (ios.length) { html += `

Inputs / Outputs (${ios.length})

`; ios.forEach((item) => { html += `
${item.type === 'output' ? 'Output' : 'Input'} ${esc(item.name || '')}
${esc(item.source || '')} → ${esc(item.destination || '')}
`; }); } if (fps.length) { html += `

المتطلبات الوظيفية (${fps.length})

`; fps.forEach((fpItem) => { html += `
${esc(fpItem.id || '')} ${esc(fpItem.name || '')} — ${esc(fpItem.complexity || '')} (FP ${esc(String(fpItem.fpScore || ''))})
`; }); } if (ucs.length) { html += `

حالات الاستخدام (${ucs.length})

`; ucs.forEach((uc) => { html += `
${esc(uc.id || '')} ${esc(uc.title || '')}
الفاعل: ${esc(uc.actor || '')}
`; }); } if (discrepancy.narrative) { html += `

تحليل الفرق بين FP وUCP

`; html += `
الفرق: ${esc(String(discrepancy.deltaPercent ?? 0))}%
الطريقة الأعلى: ${esc(discrepancy.dominantMethod || '—')}
${esc(discrepancy.narrative || '')}
${Array.isArray(discrepancy.evidence) && discrepancy.evidence.length ? `` : ''}
التوصية: ${esc(discrepancy.recommendation || '')}
`; } if (plan.milestones?.length) { html += `

WBS / Milestones

`; plan.milestones.forEach((item) => { html += `
${esc(item.name || '')}${esc(String(item.percent || 0))}% · ${esc(String(item.hours || 0))} ساعة · ${esc(item.owner || '')}
`; }); } if (plan.sprintPlan?.length) { html += `

Sprint Breakdown

`; plan.sprintPlan.forEach((sprint) => { html += `
${esc(sprint.name || '')}${esc(sprint.goal || '')}
`; }); } if (plan.staffing?.length) { html += `

توزيع الفريق

`; plan.staffing.forEach((member) => { html += `
${esc(member.name || '')}${esc(member.focus || '')} · ${esc(String(member.loadHours || 0))} ساعة
`; }); } prev.innerHTML = html || '
سيتشكل هنا ملخص النطاق، Catalog الـ IO، تحليل الفجوة بين FP/UCP، وخطة التنفيذ الأولية.
'; } function previewMetric(value, label, meta) { return `
${esc(label)}
${esc(String(value))}
${esc(meta || '')}
`; } function parseSimpleList(text) { return text .split(/\n+/) .map((line) => line.trim()) .filter(Boolean); } function parseUseCases(text, previous = []) { return parseSimpleList(text).map((line, index) => { const prev = previous[index] || {}; const parts = line.split('|').map((part) => part.trim()); const title = parts[0] || prev.title || `حالة استخدام ${index + 1}`; const actor = parts[1] || prev.actor || 'المستخدم'; const mainFlow = parts[2] ? parts[2].split('>').map((step) => step.trim()).filter(Boolean) : (Array.isArray(prev.mainFlow) && prev.mainFlow.length ? prev.mainFlow : defaultMainFlow(title, actor)); const alternateFlow = parts[3] ? parts[3].split('>').map((step) => step.trim()).filter(Boolean) : (Array.isArray(prev.alternateFlow) ? prev.alternateFlow : []); return { id: prev.id || `UC-${String(index + 1).padStart(2, '0')}`, title, actor, preconditions: prev.preconditions || `يملك ${actor} الصلاحية اللازمة.`, mainFlow, alternateFlow, }; }); } function parseInputOutputs(text, previous = []) { return parseSimpleList(text).map((line, index) => { const prev = previous[index] || {}; const parts = line.split('|').map((part) => part.trim()); const typeRaw = (parts[0] || prev.type || 'input').toLowerCase(); const type = /output|out|مخرج|إخراج/.test(typeRaw) ? 'output' : 'input'; return { id: prev.id || `IO-${String(index + 1).padStart(2, '0')}`, type, name: parts[1] || prev.name || `${type === 'output' ? 'مخرج' : 'مدخل'} ${index + 1}`, source: parts[2] || prev.source || (type === 'input' ? 'المستخدم' : 'النظام'), destination: parts[3] || prev.destination || (type === 'input' ? 'النظام' : 'المستخدم'), description: parts[4] || prev.description || '', }; }); } function defaultMainFlow(title, actor) { return [ `${actor} يبدأ مسار ${title}.`, 'يدخل البيانات أو يحدد الخيارات المطلوبة.', 'يعالج النظام الطلب ويعرض النتيجة أو التأكيد.', ]; } function looksBinary(text) { if (!text) return false; let suspicious = 0; const sample = text.slice(0, 600); for (let i = 0; i < sample.length; i += 1) { const code = sample.charCodeAt(i); if (code === 0 || (code < 9) || (code > 13 && code < 32)) suspicious += 1; } return suspicious / Math.max(sample.length, 1) > 0.12; } function esc(value) { return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); }