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.scope || 'لم يُحدَّد بعد.')}
| الرمز | الوظيفة | الوصف | التعقيد | FP |
|---|---|---|---|---|
| ${escapeHtml(f.id || '')} | ${escapeHtml(f.name || '')} | ${escapeHtml(f.description || '')} | ${escapeHtml(f.complexity || '')} | ${escapeHtml(String(f.fpScore || ''))} |
| — | ||||
—
'}| التعقيد | العدد | FP لكل وظيفة | المجموع |
|---|---|---|---|
| بسيطة | ${simple} | 3 | ${simple * 3} |
| متوسطة | ${medium} | 4 | ${medium * 4} |
| معقدة | ${complex} | 6 | ${complex * 6} |
| الإجمالي | ${fps.length} | ${totalFP} |
يعتمد الحساب على منهجية Karner. تُصنَّف الـ Actors والـ Use Cases آلياً ثم تُطبَّق المعاملات الفنية والبيئية.
| الفاعل | التصنيف | المبرر | الوزن |
|---|---|---|---|
| ${escapeHtml(a.name)} | ${escapeHtml(a.type)} | ${escapeHtml(a.reason)} | ${a.weight} |
| — | |||
| إجمالي UAW | ${ucp.UAW} | ||
القاعدة: بسيط (واجهة برمجية API) = 1، متوسط (بروتوكول/سطر أوامر) = 2، معقد (مستخدم بشري عبر واجهة رسومية) = 3.
| الرمز | عدد العمليات (Transactions) | التصنيف | الوزن |
|---|---|---|---|
| ${escapeHtml(u.id)} — ${escapeHtml(u.title)} | ${u.transactions} | ${escapeHtml(u.type)} | ${u.weight} |
| — | |||
| إجمالي UUCW | ${ucp.UUCW} | ||
القاعدة: بسيط (≤ 3 خطوات) = 5، متوسط (4–7 خطوات) = 10، معقد (> 7 خطوات) = 15.
TCF = 0.6 + (0.01 × TFactor) ، حيث TFactor = مجموع 13 عاملاً فنياً (افتراضي = ${ucp.TFactor}).
TCF = 0.6 + (0.01 × ${ucp.TFactor}) = ${ucp.TCF}
ECF = 1.4 + (−0.03 × EFactor) ، حيث EFactor = مجموع 8 عوامل بيئية (افتراضي = ${ucp.EFactor}).
ECF = 1.4 + (−0.03 × ${ucp.EFactor}) = ${ucp.ECF}