const $ = (id) => document.getElementById(id); const STORAGE_KEY = 'ssa_last_session_id'; const state = { sessionId: null, current: null, drafts: [], }; const stageLabels = { intro: 'تمهيد', exploration: 'استكشاف', usecase: 'حالات الاستخدام', evaluation: 'تقييم', done: 'مكتمل', }; 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').addEventListener('input', autoGrow); $('srs-text').addEventListener('input', autoGrow); $('actors-editor').addEventListener('input', autoGrow); $('usecases-editor').addEventListener('input', autoGrow); $('io-editor').addEventListener('input', autoGrow); $('scope-editor').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', () => { const file = $('srs-file').files?.[0]; $('srs-file-name').textContent = file ? `الملف المختار: ${file.name}` : 'لم يتم اختيار ملف بعد.'; }); loadDrafts(); } function autoGrow(event) { const target = event.target; if (!target || target.tagName !== 'TEXTAREA') return; target.style.height = 'auto'; target.style.height = Math.min(target.scrollHeight, target.classList.contains('editor-textarea') ? 220 : 160) + 'px'; } 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); addAgentMessage(data.reply || 'تم بدء الجلسة.', true); updateState(data); await loadDrafts(); $('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); showResumeBanner(data.resumeMessage || 'أهلاً بك مجدداً، يمكننا متابعة العمل من آخر نقطة توقفنا عندها.'); $('input').focus(); 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 = ''; input.style.height = 'auto'; $('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 = $('srs-file').files?.[0]; let content = $('srs-text').value.trim(); let filename = ''; if (!content && file) { filename = file.name; content = await file.text(); } else if (file) { filename = file.name; } if (!content.trim()) { alert('أرفق ملفاً نصياً بسيطاً أو الصق نص المتطلبات أولاً.'); return; } if (looksBinary(content)) { alert('الملف يبدو غير نصي. في هذه النسخة يُفضَّل رفع ملف نصي بسيط أو لصق محتوى SRS يدوياً.'); return; } addUserMessage(filename ? `📄 تم إرسال ملف SRS للتحليل: ${filename}` : '📄 تم إرسال نص SRS للتحليل'); const thinkingEl = addAgentMessage('أحلّل المستند وأستخرج Actors وUse Cases وInputs/Outputs...', false, true); try { 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(); addAgentMessage(data.reply || 'تم تحليل المستند.', true); updateState(data); await loadDrafts(); } catch (error) { thinkingEl.querySelector('.bubble').textContent = `⚠️ ${error.message}`; thinkingEl.classList.remove('thinking'); } } 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(); } 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); 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])); }