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 ? `
${discrepancy.evidence.map((item) => `- ${esc(item)}
`).join('')}
` : ''}
التوصية: ${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 || '')}${(sprint.focus || []).map((item) => `- ${esc(item)}
`).join('')}
`;
});
}
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]));
}