require('dotenv').config(); const express = require('express'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; const GEMINI_MODEL = 'gemini-2.5-flash-lite'; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${encodeURIComponent(GEMINI_API_KEY)}`; app.use(express.json({ limit: '2mb' })); app.use(express.static(path.join(__dirname, 'public'))); // In-memory session store: sessionId -> { title, history, functionPoints, useCases, scope, actors } const sessions = new Map(); function newId() { return 'sess_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36); } const SYSTEM_PROMPT = `أنت "محلل نظم ذكي" خبير في هندسة البرمجيات. مهمتك إجراء مقابلة تفاعلية مع المستخدم باللغة العربية الفصحى لاستخلاص متطلبات نظام برمجي بشكل كامل. التزم بالمراحل التالية بالترتيب: 1) التمهيد: افهم اسم المشروع، نطاقه العام، وأهم المستخدمين والأهداف. 2) الاستكشاف (تأخذ وتعطي): اطرح سؤالاً واحداً فقط في كل رد، استوضح التفاصيل، استخرج كل وظيفة (Function Point). 3) صياغة حالات الاستخدام: حدد الـ Actors والسيناريوهات الرئيسية والاستثنائية. 4) التقييم: قيّم تعقيد كل وظيفة (بسيطة/متوسطة/معقدة). 5) عند اكتمال المتطلبات، أعلم المستخدم بأنه يستطيع توليد التقرير. قواعد صارمة: - سؤال واحد فقط في كل رد، قصير وواضح. - لا تكرر ما قاله المستخدم. - بعد كل رد للمستخدم، حدّث القائمة الداخلية للوظائف وحالات الاستخدام. يجب أن تُخرج ردك دائماً كـ JSON صالح (بدون أي نص خارج الـ JSON، بدون أسوار كود) بالشكل التالي بالضبط: { "reply": "نص ردك العربي للمستخدم (سؤال واحد أو ملخص قصير)", "stage": "intro|exploration|usecase|evaluation|done", "scope": "وصف موجز لنطاق المشروع (يتطور مع الحوار)", "actors": ["قائمة الـ Actors المستخرجين"], "functionPoints": [ { "id": "FP-01", "name": "اسم الوظيفة", "description": "وصف موجز", "complexity": "بسيطة|متوسطة|معقدة", "fpScore": 3 } ], "useCases": [ { "id": "UC-01", "title": "عنوان حالة الاستخدام", "actor": "الفاعل", "preconditions": "...", "mainFlow": ["خطوة 1","خطوة 2"], "alternateFlow": ["..."] } ], "isComplete": false } قيّم fpScore: بسيطة=3، متوسطة=4، معقدة=6. أعد دائماً القائمة الكاملة المحدّثة (لا الفروقات فقط).`; async function callGemini(history, userMessage) { const contents = [ { role: 'user', parts: [{ text: SYSTEM_PROMPT }] }, { role: 'model', parts: [{ text: '{"reply":"تمام، أنا جاهز.","stage":"intro","scope":"","actors":[],"functionPoints":[],"useCases":[],"isComplete":false}' }] }, ...history.flatMap(m => ([{ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.role === 'assistant' ? JSON.stringify(m.payload) : m.text }] }])), { role: 'user', parts: [{ text: userMessage }] } ]; const res = await fetch(GEMINI_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents, generationConfig: { temperature: 0.7, responseMimeType: 'application/json' } }) }); if (!res.ok) { const errTxt = await res.text(); throw new Error(`Gemini ${res.status}: ${errTxt.slice(0, 200)}`); } const data = await res.json(); const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || '{}'; let parsed; try { parsed = JSON.parse(text); } catch { parsed = { reply: text, stage: 'exploration', scope: '', actors: [], functionPoints: [], useCases: [], isComplete: false }; } return parsed; } app.post('/api/start', async (req, res) => { try { const { title } = req.body || {}; if (!title || !title.trim()) return res.status(400).json({ error: 'يرجى إدخال عنوان المشروع' }); const sessionId = newId(); const session = { id: sessionId, title: title.trim(), history: [], scope: '', actors: [], functionPoints: [], useCases: [], isComplete: false, createdAt: new Date().toISOString() }; sessions.set(sessionId, session); const opener = `بدأنا مشروعاً جديداً بعنوان: "${title}". رجاءً عرّفني بالمجال العام للمشروع وأهم المستخدمين المستهدفين منه.`; const payload = { reply: opener, stage: 'intro', scope: '', actors: [], functionPoints: [], useCases: [], isComplete: false }; session.history.push({ role: 'assistant', payload }); res.json({ sessionId, ...payload }); } catch (e) { console.error(e); res.status(500).json({ error: 'فشل بدء الجلسة' }); } }); app.post('/api/message', async (req, res) => { try { const { sessionId, message } = req.body || {}; const session = sessions.get(sessionId); if (!session) return res.status(404).json({ error: 'الجلسة غير موجودة' }); if (!message || !message.trim()) return res.status(400).json({ error: 'الرسالة فارغة' }); session.history.push({ role: 'user', text: message.trim() }); if (!GEMINI_API_KEY) { const fake = { reply: '(وضع تجريبي) يُرجى إضافة GEMINI_API_KEY في ملف .env لتفعيل الذكاء الاصطناعي.', stage: 'exploration', scope: session.scope, actors: session.actors, functionPoints: session.functionPoints, useCases: session.useCases, isComplete: false }; session.history.push({ role: 'assistant', payload: fake }); return res.json(fake); } const payload = await callGemini(session.history.slice(0, -1), message.trim()); // update session state session.scope = payload.scope || session.scope; session.actors = payload.actors || session.actors; session.functionPoints = payload.functionPoints || session.functionPoints; session.useCases = payload.useCases || session.useCases; session.isComplete = !!payload.isComplete; session.history.push({ role: 'assistant', payload }); res.json(payload); } catch (e) { console.error('message error:', e.message); res.status(500).json({ error: e.message || 'خطأ في المعالجة' }); } }); app.get('/api/session/:id', (req, res) => { const s = sessions.get(req.params.id); if (!s) return res.status(404).json({ error: 'not found' }); res.json(s); }); // ----- Report HTML builder ----- function buildReportHtml(s, { forPdf = false } = {}) { const totalFP = (s.functionPoints || []).reduce((sum, f) => sum + (Number(f.fpScore) || 0), 0); const ucp = computeUCP(s); return ` تقرير متطلبات: ${escapeHtml(s.title)} ${forPdf ? '' : ``}

وثيقة متطلبات النظام

المشروع: ${escapeHtml(s.title)}   ·   التاريخ: ${new Date(s.createdAt).toLocaleDateString('ar-EG')}

1. مقدمة ونطاق النظام

${escapeHtml(s.scope || 'لم يُحدَّد بعد.')}

2. المستخدمون (Actors)

3. المتطلبات الوظيفية (Function Points)

${(s.functionPoints || []).map(f => ``).join('') || ''}
الرمزالوظيفةالوصفالتعقيدFP
${escapeHtml(f.id || '')} ${escapeHtml(f.name || '')} ${escapeHtml(f.description || '')} ${escapeHtml(f.complexity || '')} ${escapeHtml(String(f.fpScore || ''))}

4. توصيف حالات الاستخدام (Use Cases)

${(s.useCases || []).map(uc => `
${escapeHtml(uc.id || '')} — ${escapeHtml(uc.title || '')}
الفاعل: ${escapeHtml(uc.actor || '')}
الشروط المسبقة: ${escapeHtml(uc.preconditions || '')}
السيناريو الرئيسي:
    ${(uc.mainFlow || []).map(s => `
  1. ${escapeHtml(s)}
  2. `).join('')}
السيناريو البديل:
`).join('') || '

'}

5. التقدير الأولي للحجم

عدد الوظائف: ${(s.functionPoints || []).length}
عدد حالات الاستخدام: ${(s.useCases || []).length}
إجمالي نقاط الوظائف (Total FP): ${totalFP}
نقاط حالات الاستخدام (UCP): ${ucp.UCP}  ·  الجهد التقديري: ${ucp.effortHours} ساعة (~${ucp.effortDays} يوم عمل)
{/* Function Points breakdown by complexity */} ${(() => { const fps = s.functionPoints || []; const simple = fps.filter(f => f.complexity === 'بسيطة').length; const medium = fps.filter(f => f.complexity === 'متوسطة').length; const complex = fps.filter(f => f.complexity === 'معقدة').length; return `

تفصيل نقاط الوظائف حسب التعقيد:

التعقيد العدد FP لكل وظيفة المجموع
بسيطة ${simple} 3 ${simple * 3}
متوسطة ${medium} 4 ${medium * 4}
معقدة ${complex} 6 ${complex * 6}
الإجمالي ${fps.length} ${totalFP}
`; })()}

6. حساب نقاط حالات الاستخدام (Use Case Points)

يعتمد الحساب على منهجية Karner. تُصنَّف الـ Actors والـ Use Cases آلياً ثم تُطبَّق المعاملات الفنية والبيئية.

6.1 وزن الـ Actors (UAW)

${ucp.actorRows.map(a => ``).join('') || ''}
الفاعلالتصنيفالمبررالوزن
${escapeHtml(a.name)} ${escapeHtml(a.type)} ${escapeHtml(a.reason)} ${a.weight}
إجمالي UAW${ucp.UAW}

القاعدة: بسيط (واجهة برمجية API) = 1، متوسط (بروتوكول/سطر أوامر) = 2، معقد (مستخدم بشري عبر واجهة رسومية) = 3.

6.2 وزن الـ Use Cases (UUCW)

${ucp.ucRows.map(u => ``).join('') || ''}
الرمزعدد العمليات (Transactions)التصنيفالوزن
${escapeHtml(u.id)} — ${escapeHtml(u.title)} ${u.transactions} ${escapeHtml(u.type)} ${u.weight}
إجمالي UUCW${ucp.UUCW}

القاعدة: بسيط (≤ 3 خطوات) = 5، متوسط (4–7 خطوات) = 10، معقد (> 7 خطوات) = 15.

6.3 المعامل الفني (TCF)

TCF = 0.6 + (0.01 × TFactor) ، حيث TFactor = مجموع 13 عاملاً فنياً (افتراضي = ${ucp.TFactor}).

TCF = 0.6 + (0.01 × ${ucp.TFactor}) = ${ucp.TCF}

6.4 المعامل البيئي (ECF)

ECF = 1.4 + (−0.03 × EFactor) ، حيث EFactor = مجموع 8 عوامل بيئية (افتراضي = ${ucp.EFactor}).

ECF = 1.4 + (−0.03 × ${ucp.EFactor}) = ${ucp.ECF}

6.5 الحساب النهائي

UUCP = UAW + UUCW = ${ucp.UAW} + ${ucp.UUCW} = ${ucp.UUCP}
UCP = UUCP × TCF × ECF = ${ucp.UUCP} × ${ucp.TCF} × ${ucp.ECF} = ${ucp.UCP}
الجهد التقديري = UCP × 20 ساعة/نقطة = ${ucp.effortHours} ساعة عمل (~${ucp.effortDays} يوم بمعدل 8 ساعات).
`; } // Use Case Points (Karner) calculator function computeUCP(s) { // --- Actors classification --- const actorRows = (s.actors || []).map(name => { const n = String(name || '').toLowerCase(); let type, weight, reason; if (/api|نظام خارجي|external|service|بروتوكول/.test(n)) { type = 'بسيط'; weight = 1; reason = 'واجهة برمجية / نظام خارجي'; } else if (/cli|سطر أوامر|tcp|protocol|قاعدة بيانات|database/.test(n)) { type = 'متوسط'; weight = 2; reason = 'تفاعل عبر بروتوكول أو سطر أوامر'; } else { type = 'معقد'; weight = 3; reason = 'مستخدم بشري عبر واجهة رسومية'; } return { name, type, weight, reason }; }); const UAW = actorRows.reduce((a, b) => a + b.weight, 0); // --- Use cases classification by # of transactions (mainFlow steps) --- const ucRows = (s.useCases || []).map(uc => { const transactions = Array.isArray(uc.mainFlow) ? uc.mainFlow.length : 0; let type, weight; if (transactions <= 3) { type = 'بسيط'; weight = 5; } else if (transactions <= 7) { type = 'متوسط'; weight = 10; } else { type = 'معقد'; weight = 15; } return { id: uc.id || '', title: uc.title || '', transactions, type, weight }; }); const UUCW = ucRows.reduce((a, b) => a + b.weight, 0); const UUCP = UAW + UUCW; const TFactor = 30; // معدل افتراضي للعوامل الفنية const EFactor = 17; // معدل افتراضي للعوامل البيئية const TCF = +(0.6 + 0.01 * TFactor).toFixed(2); const ECF = +(1.4 + -0.03 * EFactor).toFixed(2); const UCP = +(UUCP * TCF * ECF).toFixed(2); const effortHours = +(UCP * 20).toFixed(1); const effortDays = +(effortHours / 8).toFixed(1); return { actorRows, ucRows, UAW, UUCW, UUCP, TFactor, EFactor, TCF, ECF, UCP, effortHours, effortDays }; } // HTML preview app.get('/report/:id', (req, res) => { const s = sessions.get(req.params.id); if (!s) return res.status(404).send('الجلسة غير موجودة'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(buildReportHtml(s, { forPdf: false })); }); // PDF: عبر طباعة المتصفح (window.print) من صفحة التقرير — لا حاجة لمكتبات ثقيلة. function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } app.listen(PORT, () => { console.log(`✨ Smart System Analyst Agent running at http://localhost:${PORT}`); if (!GEMINI_API_KEY) console.warn('⚠️ GEMINI_API_KEY غير مضبوط — ستعمل الواجهة في الوضع التجريبي.'); });