177 lines
6.1 KiB
JavaScript
177 lines
6.1 KiB
JavaScript
const $ = (id) => document.getElementById(id);
|
|
const state = { sessionId: null, isComplete: false };
|
|
|
|
const stageLabels = {
|
|
intro: 'تمهيد', exploration: 'استكشاف', usecase: 'حالات الاستخدام',
|
|
evaluation: 'تقييم', done: 'مكتمل'
|
|
};
|
|
|
|
$('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', e => {
|
|
e.target.style.height = 'auto';
|
|
e.target.style.height = Math.min(e.target.scrollHeight, 140) + 'px';
|
|
});
|
|
$('export-btn').addEventListener('click', () => {
|
|
if (state.sessionId) window.open(`/report/${state.sessionId}`, '_blank');
|
|
});
|
|
|
|
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;
|
|
$('welcome').classList.add('hidden');
|
|
$('app').classList.remove('hidden');
|
|
$('project-name').textContent = title;
|
|
addAgentMessage(data.reply, true);
|
|
updateState(data);
|
|
} catch (e) {
|
|
alert('خطأ: ' + e.message);
|
|
$('start-btn').disabled = false;
|
|
}
|
|
}
|
|
|
|
async function sendMessage() {
|
|
const input = $('input');
|
|
const text = input.value.trim();
|
|
if (!text || !state.sessionId) return;
|
|
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);
|
|
} catch (e) {
|
|
thinkingEl.querySelector('.bubble').textContent = '⚠️ ' + e.message;
|
|
thinkingEl.classList.remove('thinking');
|
|
} finally {
|
|
$('send-btn').disabled = false;
|
|
$('input').focus();
|
|
}
|
|
}
|
|
|
|
function addUserMessage(text) {
|
|
const el = document.createElement('div');
|
|
el.className = 'msg user';
|
|
el.innerHTML = `<div class="avatar">أنا</div><div class="bubble"></div>`;
|
|
el.querySelector('.bubble').textContent = text;
|
|
$('messages').appendChild(el);
|
|
scrollMsgs();
|
|
}
|
|
|
|
function addAgentMessage(text, typewriter = false, thinking = false) {
|
|
document.querySelectorAll('.msg.agent.fresh').forEach(e => e.classList.remove('fresh'));
|
|
const el = document.createElement('div');
|
|
el.className = 'msg agent fresh' + (thinking ? ' thinking' : '');
|
|
el.innerHTML = `<div class="avatar">AI</div><div class="bubble${thinking ? ' typing' : ''}"></div>`;
|
|
$('messages').appendChild(el);
|
|
const bubble = el.querySelector('.bubble');
|
|
if (typewriter) typeText(bubble, text);
|
|
else bubble.textContent = text;
|
|
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 / text.length));
|
|
const tick = () => {
|
|
if (i < text.length) {
|
|
cursor.insertAdjacentText('beforebegin', text[i]);
|
|
i++;
|
|
scrollMsgs();
|
|
setTimeout(tick, speed);
|
|
} else {
|
|
cursor.remove();
|
|
}
|
|
};
|
|
tick();
|
|
}
|
|
|
|
function scrollMsgs() {
|
|
const m = $('messages');
|
|
m.scrollTop = m.scrollHeight;
|
|
}
|
|
|
|
function updateState(data) {
|
|
const fps = data.functionPoints || [];
|
|
const ucs = data.useCases || [];
|
|
const actors = data.actors || [];
|
|
$('fp-count').textContent = fps.length;
|
|
$('uc-count').textContent = ucs.length;
|
|
$('fp-total').textContent = fps.reduce((s, f) => s + (Number(f.fpScore) || 0), 0);
|
|
$('stage-pill').textContent = stageLabels[data.stage] || data.stage || '—';
|
|
|
|
// Tree
|
|
const tree = $('tree');
|
|
if (!actors.length && !fps.length) {
|
|
tree.innerHTML = '<div class="tree-empty">ستظهر الـ Actors والوظائف هنا تلقائياً.</div>';
|
|
} else {
|
|
let html = '';
|
|
if (actors.length) {
|
|
actors.forEach(a => {
|
|
html += `<div class="tree-actor">${esc(a)}</div>`;
|
|
});
|
|
}
|
|
fps.forEach(f => {
|
|
html += `<div class="tree-fp">${esc(f.id || '')} · ${esc(f.name || '')}</div>`;
|
|
});
|
|
tree.innerHTML = html;
|
|
}
|
|
|
|
// Live preview
|
|
const prev = $('preview-content');
|
|
let html = '';
|
|
if (data.scope) html += `<h3>نطاق النظام</h3><div class="scope">${esc(data.scope)}</div>`;
|
|
if (actors.length) {
|
|
html += `<h3>المستخدمون</h3><div class="scope">${actors.map(esc).join(' · ')}</div>`;
|
|
}
|
|
if (fps.length) {
|
|
html += `<h3>المتطلبات الوظيفية (${fps.length})</h3>`;
|
|
fps.forEach(f => {
|
|
html += `<div class="uc-prev"><b>${esc(f.id || '')}</b> ${esc(f.name || '')} <span style="color:#888">— ${esc(f.complexity || '')} (FP ${esc(String(f.fpScore || ''))})</span></div>`;
|
|
});
|
|
}
|
|
if (ucs.length) {
|
|
html += `<h3>حالات الاستخدام (${ucs.length})</h3>`;
|
|
ucs.forEach(u => {
|
|
html += `<div class="uc-prev"><b>${esc(u.id || '')}</b> ${esc(u.title || '')}<br><span style="color:#888">الفاعل: ${esc(u.actor || '')}</span></div>`;
|
|
});
|
|
}
|
|
prev.innerHTML = html || '<div class="preview-empty">سيتشكّل النص الهندسي للتقرير هنا أثناء الحوار.</div>';
|
|
|
|
// Show export when there's enough content or marked complete
|
|
if (data.isComplete || (fps.length >= 3 && ucs.length >= 1)) {
|
|
$('export-btn').classList.remove('hidden');
|
|
}
|
|
state.isComplete = !!data.isComplete;
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]));
|
|
} |