import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; import { StateManager, initDB } from './stateManager.js'; import { AIEngine } from './aiEngine.js'; import fs from 'fs'; import crypto from "crypto"; import dotenv from 'dotenv'; dotenv.config(); // Initialize Database initDB(); const supabase = StateManager.getSupabaseClient(); const app = express(); const PORT = process.env.PORT || 7860; app.use(cors()); app.use(express.json({ limit: '50mb' })); // --- BILLING CONSTANTS --- const MIN_BASIC_REQUIRED = 100; const MIN_DIAMOND_REQUIRED = 100; // --- STATUS PHASES --- const WORKER_PHASES = [ "Worker: Sorting...", "Worker: Analyzing Request...", "Worker: Reading Context...", "Worker: Checking Hierarchy...", "Worker: Planning Logic...", "Worker: Preparing Environment...", "Worker: Thinking..." ]; const PM_PHASES = [ "Manager: Sorting...", "Manager: Reviewing Request...", "Manager: Analyzing Project Structure...", "Manager: Consulting Guidelines...", "Manager: Formulating Strategy...", "Manager: Delegating Tasks...", "Manager: Thinking..." ]; // --- HELPER FUNCTIONS --- function startStatusLoop(projectId, type = 'worker') { const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES; let index = 0; // Set the initial state immediately StateManager.setStatus(projectId, phases[0]); const interval = setInterval(() => { if (index < phases.length - 1) { index++; StateManager.setStatus(projectId, phases[index]); } else { // Stop the loop once we reach the last entry clearInterval(interval); } }, 3500); return () => clearInterval(interval); }; function extractWorkerPrompt(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/WORKER_PROMPT:\s*(.*)/s); return match ? match[1].trim() : null; } function extractPMQuestion(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/\[ASK_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractImagePrompt(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractRouteToPM(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function formatContext({ hierarchyContext, scriptContext, logContext }) { let parts = []; if (hierarchyContext) { parts.push(`[HIERARCHY_VIEW]\n${hierarchyContext}`); } if (scriptContext && scriptContext.targetName) { // Enforce code block formatting for the script content const source = scriptContext.scriptSource || "(Empty Script)"; parts.push(`[ACTIVE_SCRIPT: ${scriptContext.targetName}]\n\`\`\`lua\n${source}\n\`\`\``); } if (logContext && logContext.logs) { parts.push(`[OUTPUT_LOGS]\n${logContext.logs}`); } if (parts.length === 0) return ""; return `\n\n=== STUDIO CONTEXT ===\n${parts.join("\n\n")}\n======================\n`; } const validateRequest = (req, res, next) => { if (req.path.includes('/admin/cleanup')) return next(); const { userId } = req.body; if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) { return res.status(400).json({ error: "Missing userId" }); } next(); }; // --- BILLING LOGIC --- async function checkMinimumCredits(userId, type = 'basic') { if (!supabase) return; const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); if (!data) return; const credits = data.credits?.[type] || 0; const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED; if (credits < req) { throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`); } } async function deductUserCredits(userId, amount, type = 'basic') { const deduction = Math.floor(parseInt(amount, 10)); if (!supabase || !deduction || deduction <= 0) return; try { const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); if (!data) return; const currentCredits = data.credits || { basic: 0, diamond: 0 }; const currentVal = currentCredits[type] || 0; const newVal = Math.max(0, currentVal - deduction); const updatedCredits = { ...currentCredits, [type]: newVal }; await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId); console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`); } catch (err) { console.error("Deduction failed", err); } } // --- CORE LOGIC: Background Initialization --- async function runBackgroundInitialization(projectId, userId, description) { if (StateManager.isLocked(projectId)) { console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`); return; } StateManager.lock(projectId); console.log(`[Background] Starting initialization for ${projectId}`); let diamondUsage = 0; let basicUsage = 0; try { await StateManager.getProject(projectId); await StateManager.updateProject(projectId, { status: "working", gdd: "", failureCount: 0 }); const pmHistory = []; // --- Step 1: Generate GDD (PM) --- let stopStatus = startStatusLoop(projectId, 'pm'); const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`; const gddResult = await AIEngine.callPM(pmHistory, gddPrompt); stopStatus(); if (!gddResult?.text) throw new Error("PM failed to generate GDD"); const gddText = gddResult.text; diamondUsage += (gddResult.usage?.totalTokenCount || 0); await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt); await StateManager.addHistory(projectId, 'pm', 'model', gddText); pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] }); pmHistory.push({ role: 'model', parts: [{ text: gddText }] }); // --- Step 2: Generate Tasks (PM) --- stopStatus = startStatusLoop(projectId, 'pm'); const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: \nWORKER_PROMPT: "; const taskResult = await AIEngine.callPM(pmHistory, taskPrompt); stopStatus(); if (!taskResult?.text) throw new Error("PM failed to generate Task"); const taskText = taskResult.text; diamondUsage += (taskResult.usage?.totalTokenCount || 0); await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt); await StateManager.addHistory(projectId, 'pm', 'model', taskText); // --- Step 3: Initialize Worker --- const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`; const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`; stopStatus = startStatusLoop(projectId, 'worker'); let workerTextAccumulated = ""; const workerResult = await AIEngine.callWorkerStream( [], initialWorkerPrompt, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Coding..."); workerTextAccumulated += chunk; StateManager.appendStream(projectId, chunk); } ); stopStatus(); basicUsage += (workerResult.usage?.totalTokenCount || 0); if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize"); const finalWorkerText = workerResult.text || workerTextAccumulated; await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt); await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText); await StateManager.updateProject(projectId, { gdd: gddText, status: "idle" }); await processAndQueueResponse(projectId, finalWorkerText, userId); console.log(`[Background] Init complete for ${projectId}`); } catch (err) { console.error(`[Background] Init Error for ${projectId}:`, err.message); await StateManager.updateProject(projectId, { status: "error" }); } finally { StateManager.unlock(projectId); if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); } } // --- CORE LOGIC: Async Feedback Loop --- async function runAsyncFeedback(projectId, userId, fullInput, images = []) { let diamondUsage = 0; let basicUsage = 0; try { console.log(`[${projectId}] Feedback received. Persisting state immediately.`); await Promise.all([ StateManager.updateProject(projectId, { status: "working" }), StateManager.addHistory(projectId, 'worker', 'user', fullInput) ]); const project = await StateManager.getProject(projectId); StateManager.clearSnapshot(projectId); let stopStatus = startStatusLoop(projectId, 'worker'); let firstTurnResponse = ""; let thoughtText = ""; const currentWorkerHistory = [...(project.workerHistory || [])]; // 2. First Turn (Worker Execution) const workerResult = await AIEngine.callWorkerStream( currentWorkerHistory, fullInput, (thought) => { stopStatus(); thoughtText += thought; StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); firstTurnResponse += chunk; StateManager.setStatus(projectId, "Worker: Coding..."); StateManager.appendStream(projectId, chunk); }, images ); stopStatus(); basicUsage += (workerResult.usage?.totalTokenCount || 0); firstTurnResponse = workerResult.text || firstTurnResponse; // 3. Analyze Output (Delegation/Questions) const routeTask = extractRouteToPM(firstTurnResponse); const pmQuestion = extractPMQuestion(firstTurnResponse); let finalResponseToSave = firstTurnResponse; if (routeTask || pmQuestion) { await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] }); let pmPrompt = ""; let pmContextPrefix = ""; if (routeTask) { console.log(`[${projectId}] Routing task to PM...`); pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: to delegate.`; pmContextPrefix = "[PM DELEGATION]:"; } else { console.log(`[${projectId}] Worker consulting PM...`); pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`; pmContextPrefix = "[PM ANSWER]:"; } // 4. Run PM (Diamond Logic) StateManager.clearSnapshot(projectId); stopStatus = startStatusLoop(projectId, 'pm'); let pmResponseText = ""; const pmResult = await AIEngine.callPMStream( project.pmHistory, pmPrompt, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Manager: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Manager: Architecture..."); pmResponseText += chunk; StateManager.appendSnapshotOnly(projectId, chunk); } ); stopStatus(); diamondUsage += (pmResult.usage?.totalTokenCount || 0); pmResponseText = pmResult.text || pmResponseText; await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt); await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText); // --- EXECUTE PM CODE --- await processAndQueueResponse(projectId, pmResponseText, userId); // 5. Run Worker Continuation const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText; const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`; await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt); currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] }); StateManager.clearSnapshot(projectId); stopStatus = startStatusLoop(projectId, 'worker'); let secondTurnResponse = ""; const secondWorkerResult = await AIEngine.callWorkerStream( currentWorkerHistory, workerContinuationPrompt, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Applying Fix..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Finalizing..."); secondTurnResponse += chunk; StateManager.appendStream(projectId, chunk); } ); stopStatus(); basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0); secondTurnResponse = secondWorkerResult.text || secondTurnResponse; await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse); finalResponseToSave = secondTurnResponse; } else { // No delegation, just save the first response await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); } // --- STATUS RESET --- StateManager.setStatus(projectId, "Idle"); // 6. Final Execution & Cleanup await processAndQueueResponse(projectId, finalResponseToSave, userId); await StateManager.updateProject(projectId, { status: "idle" }); } catch (err) { console.error("Async Feedback Error:", err); StateManager.setStatus(projectId, "Error: " + err.message); await StateManager.updateProject(projectId, { status: "error" }); } finally { if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); } } async function processAndQueueResponse(projectId, rawResponse, userId) { if (!rawResponse) return; // Image Generation const imgPrompt = extractImagePrompt(rawResponse); if (imgPrompt) { try { const imgResult = await AIEngine.generateImage(imgPrompt); if (imgResult?.image) { if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image }); } } catch (e) { console.error("Image gen failed", e); } } // Code Execution const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i); if (codeMatch) { await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() }); } // Explicit Read Commands (Parsing what the AI wrote) await StateManager.queueCommand(projectId, rawResponse); } // --- EXPRESS ROUTES --- app.post('/onboarding/analyze', validateRequest, async (req, res) => { const { description, userId } = req.body; if (!description) return res.status(400).json({ error: "Description required" }); try { try { await checkMinimumCredits(userId, 'basic'); } catch (creditErr) { return res.json({ success: false, insufficient: true, error: creditErr.message }); } const result = await AIEngine.generateEntryQuestions(description); if (result.usage?.totalTokenCount > 0) { await deductUserCredits(userId, result.usage.totalTokenCount, 'basic'); } if (result.status === "REJECTED") { return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." }); } res.json({ questions: result.questions }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/onboarding/create', validateRequest, async (req, res) => { const { userId, description, answers } = req.body; try { try { await checkMinimumCredits(userId, 'basic'); await checkMinimumCredits(userId, 'diamond'); } catch (creditErr) { return res.json({ success: false, insufficient: true, error: creditErr.message }); } const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n); const projectId = `proj_${Date.now()}_${randomHex(7)}`; const gradingResult = await AIEngine.gradeProject(description, answers); if (gradingResult.usage?.totalTokenCount) { await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic'); } const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F'; if (!isFailure) { const { error: insertError } = await supabase.from('projects').insert({ id: projectId, user_id: userId, info: { title: gradingResult.title || "Untitled", stats: gradingResult, description, answers, status: "initializing" } }); if (insertError) { console.error("Failed to insert project:", insertError.message); return res.status(500).json({ error: "Database save failed" }); } runBackgroundInitialization(projectId, userId, description); } res.json({ status: 200, success: !isFailure, projectId, stats: gradingResult, title: gradingResult.title || "Untitled" }); } catch (err) { console.error("Create Error:", err); res.status(500).json({ error: err.message }); } }); app.post('/project/feedback', async (req, res) => { const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body; try { const project = await StateManager.getProject(projectId); if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" }); try { await checkMinimumCredits(userId, 'basic'); } catch (creditErr) { return res.json({ success: false, insufficient: true, error: creditErr.message }); } // Pass the context objects into formatContext const formattedContext = formatContext({ hierarchyContext, scriptContext, logContext }); // Append to user prompt const fullInput = `USER: ${prompt || "Automatic Feedback"}${formattedContext}`; // Trigger Async runAsyncFeedback(projectId, userId, fullInput, images || []); res.json({ success: true, message: "Processing started" }); } catch (err) { console.error("Feedback Error:", err); await StateManager.updateProject(projectId, { status: "error" }); res.status(500).json({ error: "Feedback process failed" }); } }); app.post('/project/ping', async (req, res) => { const { projectId, userId, isFrontend } = req.body; if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" }); const project = await StateManager.getProject(projectId); if (!project || project.userId !== userId) return res.json({ action: "IDLE" }); // 1. BASE RESPONSE (Visuals) const response = { action: "IDLE", status: StateManager.getStatus(projectId), // Will now return "Idle" by default snapshot: StateManager.getSnapshot(projectId) }; // 2. EXECUTOR LOGIC (Plugin Only) if (!isFrontend) { const command = await StateManager.popCommand(projectId); if (command) { response.action = command.type; response.target = command.payload; response.code = command.type === 'EXECUTE' ? command.payload : null; } } res.json(response); }); app.post('/human/override', validateRequest, async (req, res) => { const { projectId, instruction, userId } = req.body; try { await checkMinimumCredits(userId, 'basic'); const project = await StateManager.getProject(projectId); res.json({ success: true }); const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`; let overrideText = ""; let stopStatus = startStatusLoop(projectId, 'worker'); const workerResult = await AIEngine.callWorkerStream( project.workerHistory, overrideMsg, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Coding..."); overrideText += chunk; StateManager.appendStream(projectId, chunk); } ); stopStatus(); await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg); await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText); await processAndQueueResponse(projectId, workerResult.text || overrideText, userId); const usage = workerResult.usage?.totalTokenCount || 0; if (usage > 0) await deductUserCredits(userId, usage, 'basic'); } catch (err) { console.error("Override failed", err); } }); app.get('/admin/cleanup', async (req, res) => { try { const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0; res.json({ success: true, removedCount: removed }); } catch (err) { res.status(500).json({ error: "Cleanup failed" }); } }); app.listen(PORT, () => { console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`); }); /* import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; import { StateManager, initDB } from './stateManager.js'; import { AIEngine } from './aiEngine.js'; import fs from 'fs'; import crypto from "crypto"; import dotenv from 'dotenv'; dotenv.config(); // Initialize Database initDB(); const supabase = StateManager.getSupabaseClient(); const app = express(); const PORT = process.env.PORT || 7860; app.use(cors()); app.use(express.json({ limit: '50mb' })); // --- BILLING CONSTANTS --- const MIN_BASIC_REQUIRED = 100; const MIN_DIAMOND_REQUIRED = 100; // --- STATUS PHASES --- const WORKER_PHASES = [ "Worker: Sorting...", "Worker: Analyzing Request...", "Worker: Reading Context...", "Worker: Checking Hierarchy...", "Worker: Planning Logic...", "Worker: Preparing Environment...", "Worker: Thinking..." ]; const PM_PHASES = [ "Manager: Sorting...", "Manager: Reviewing Request...", "Manager: Analyzing Project Structure...", "Manager: Consulting Guidelines...", "Manager: Formulating Strategy...", "Manager: Delegating Tasks...", "Manager: Thinking..." ]; // --- HELPER FUNCTIONS --- function startStatusLoop(projectId, type = 'worker') { const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES; let index = 0; // Set the initial state immediately StateManager.setStatus(projectId, phases[0]); const interval = setInterval(() => { if (index < phases.length - 1) { index++; StateManager.setStatus(projectId, phases[index]); } else { // Stop the loop once we reach the last entry clearInterval(interval); } }, 3500); // 3.5 second delay as requested // Return a cleanup function in case the component unmounts early return () => clearInterval(interval); }; function extractWorkerPrompt(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/WORKER_PROMPT:\s*(.*)/s); return match ? match[1].trim() : null; } function extractPMQuestion(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/\[ASK_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractImagePrompt(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function extractRouteToPM(text) { if (!text || typeof text !== 'string') return null; const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s); return match ? match[1].trim() : null; } function formatContext({ hierarchyContext, scriptContext, logContext }) { let out = ""; if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`; if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`; if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`; return out; } const validateRequest = (req, res, next) => { if (req.path.includes('/admin/cleanup')) return next(); const { userId } = req.body; if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) { return res.status(400).json({ error: "Missing userId" }); } next(); }; // --- BILLING LOGIC --- async function checkMinimumCredits(userId, type = 'basic') { if (!supabase) return; const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); if (!data) return; const credits = data.credits?.[type] || 0; const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED; if (credits < req) { throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`); } } async function deductUserCredits(userId, amount, type = 'basic') { const deduction = Math.floor(parseInt(amount, 10)); if (!supabase || !deduction || deduction <= 0) return; try { const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); if (!data) return; const currentCredits = data.credits || { basic: 0, diamond: 0 }; const currentVal = currentCredits[type] || 0; const newVal = Math.max(0, currentVal - deduction); const updatedCredits = { ...currentCredits, [type]: newVal }; await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId); console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`); } catch (err) { console.error("Deduction failed", err); } } // --- CORE LOGIC: Background Initialization --- async function runBackgroundInitialization(projectId, userId, description) { if (StateManager.isLocked(projectId)) { console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`); return; } StateManager.lock(projectId); console.log(`[Background] Starting initialization for ${projectId}`); let diamondUsage = 0; let basicUsage = 0; try { await StateManager.getProject(projectId); await StateManager.updateProject(projectId, { status: "working", gdd: "", failureCount: 0 }); const pmHistory = []; // --- Step 1: Generate GDD (PM) --- let stopStatus = startStatusLoop(projectId, 'pm'); const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`; const gddResult = await AIEngine.callPM(pmHistory, gddPrompt); stopStatus(); if (!gddResult?.text) throw new Error("PM failed to generate GDD"); const gddText = gddResult.text; diamondUsage += (gddResult.usage?.totalTokenCount || 0); await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt); await StateManager.addHistory(projectId, 'pm', 'model', gddText); pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] }); pmHistory.push({ role: 'model', parts: [{ text: gddText }] }); // --- Step 2: Generate Tasks (PM) --- stopStatus = startStatusLoop(projectId, 'pm'); const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: \nWORKER_PROMPT: "; const taskResult = await AIEngine.callPM(pmHistory, taskPrompt); stopStatus(); if (!taskResult?.text) throw new Error("PM failed to generate Task"); const taskText = taskResult.text; diamondUsage += (taskResult.usage?.totalTokenCount || 0); await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt); await StateManager.addHistory(projectId, 'pm', 'model', taskText); // --- Step 3: Initialize Worker --- const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`; const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`; stopStatus = startStatusLoop(projectId, 'worker'); let workerTextAccumulated = ""; const workerResult = await AIEngine.callWorkerStream( [], initialWorkerPrompt, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Coding..."); workerTextAccumulated += chunk; StateManager.appendStream(projectId, chunk); } ); stopStatus(); basicUsage += (workerResult.usage?.totalTokenCount || 0); if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize"); const finalWorkerText = workerResult.text || workerTextAccumulated; await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt); await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText); await StateManager.updateProject(projectId, { gdd: gddText, status: "idle" }); await processAndQueueResponse(projectId, finalWorkerText, userId); console.log(`[Background] Init complete for ${projectId}`); } catch (err) { console.error(`[Background] Init Error for ${projectId}:`, err.message); await StateManager.updateProject(projectId, { status: "error" }); } finally { StateManager.unlock(projectId); if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); } } // --- CORE LOGIC: Async Feedback Loop --- async function runAsyncFeedback(projectId, userId, fullInput, images = []) { let diamondUsage = 0; let basicUsage = 0; try { console.log(`[${projectId}] Feedback received. Persisting state immediately.`); // 1. Instant Persistence: Saves status and message BEFORE processing // This ensures if user reloads, the state is preserved await Promise.all([ StateManager.updateProject(projectId, { status: "working" }), StateManager.addHistory(projectId, 'worker', 'user', fullInput) ]); const project = await StateManager.getProject(projectId); StateManager.clearSnapshot(projectId); let stopStatus = startStatusLoop(projectId, 'worker'); let firstTurnResponse = ""; let thoughtText = ""; // Clone history to prevent mutation issues // Note: project.workerHistory includes the message we just saved above const currentWorkerHistory = [...(project.workerHistory || [])]; // 2. First Turn (Worker Execution) const workerResult = await AIEngine.callWorkerStream( currentWorkerHistory, fullInput, (thought) => { stopStatus(); thoughtText += thought; StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); firstTurnResponse += chunk; StateManager.setStatus(projectId, "Worker: Coding..."); StateManager.appendStream(projectId, chunk); }, images ); stopStatus(); basicUsage += (workerResult.usage?.totalTokenCount || 0); firstTurnResponse = workerResult.text || firstTurnResponse; // 3. Analyze Output (Delegation/Questions) const routeTask = extractRouteToPM(firstTurnResponse); const pmQuestion = extractPMQuestion(firstTurnResponse); let finalResponseToSave = firstTurnResponse; if (routeTask || pmQuestion) { // Save preliminary response to DB so it doesn't get lost await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] }); let pmPrompt = ""; let pmContextPrefix = ""; if (routeTask) { console.log(`[${projectId}] Routing task to PM...`); pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: to delegate.`; pmContextPrefix = "[PM DELEGATION]:"; } else { console.log(`[${projectId}] Worker consulting PM...`); pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`; pmContextPrefix = "[PM ANSWER]:"; } // 4. Run PM (Diamond Logic) StateManager.clearSnapshot(projectId); stopStatus = startStatusLoop(projectId, 'pm'); let pmResponseText = ""; const pmResult = await AIEngine.callPMStream( project.pmHistory, pmPrompt, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Manager: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Manager: Architecture..."); pmResponseText += chunk; StateManager.appendSnapshotOnly(projectId, chunk); } ); stopStatus(); diamondUsage += (pmResult.usage?.totalTokenCount || 0); pmResponseText = pmResult.text || pmResponseText; await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt); await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText); // --- EXECUTE PM CODE --- // If the PM wrote code (e.g. ServerScriptService logic), execute it immediately await processAndQueueResponse(projectId, pmResponseText, userId); // 5. Run Worker Continuation const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText; const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`; // Add system prompt to history await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt); currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] }); StateManager.clearSnapshot(projectId); stopStatus = startStatusLoop(projectId, 'worker'); let secondTurnResponse = ""; const secondWorkerResult = await AIEngine.callWorkerStream( currentWorkerHistory, workerContinuationPrompt, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Applying Fix..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Finalizing..."); secondTurnResponse += chunk; StateManager.appendStream(projectId, chunk); } ); stopStatus(); // Deduct usage for second turn basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0); secondTurnResponse = secondWorkerResult.text || secondTurnResponse; await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse); finalResponseToSave = secondTurnResponse; } else { // No delegation, just save the first response await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); } StateManager.setStatus(projectId, "Idle"); // 6. Final Execution & Cleanup await processAndQueueResponse(projectId, finalResponseToSave, userId); // Ensure status is idle (this also updates timestamps) await StateManager.updateProject(projectId, { status: "idle" }); } catch (err) { console.error("Async Feedback Error:", err); StateManager.setStatus(projectId, "Error: " + err.message); await StateManager.updateProject(projectId, { status: "error" }); } finally { // Billing if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); } } async function processAndQueueResponse(projectId, rawResponse, userId) { if (!rawResponse) return; // Image Generation const imgPrompt = extractImagePrompt(rawResponse); if (imgPrompt) { try { const imgResult = await AIEngine.generateImage(imgPrompt); if (imgResult?.image) { if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image }); } } catch (e) { console.error("Image gen failed", e); } } // Code Execution // Regex looks for lua/luau code blocks to execute const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i); if (codeMatch) { await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() }); } else { // If there's no code block but response is non-empty, maybe check if it's pure code? // For safety, we usually only execute if wrapped in blocks. // But if you want to support raw text execution: // await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: rawResponse }); } } // --- EXPRESS ROUTES --- app.post('/onboarding/analyze', validateRequest, async (req, res) => { const { description, userId } = req.body; if (!description) return res.status(400).json({ error: "Description required" }); try { // await checkMinimumCredits(userId, 'basic'); try { await checkMinimumCredits(userId, 'basic'); } catch (creditErr) { return res.json({ success: false, insufficient: true, error: creditErr.message }); } const result = await AIEngine.generateEntryQuestions(description); if (result.usage?.totalTokenCount > 0) { await deductUserCredits(userId, result.usage.totalTokenCount, 'basic'); } if (result.status === "REJECTED") { return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." }); } res.json({ questions: result.questions }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/onboarding/create', validateRequest, async (req, res) => { const { userId, description, answers } = req.body; try { try { await checkMinimumCredits(userId, 'basic'); await checkMinimumCredits(userId, 'diamond'); } catch (creditErr) { return res.json({ success: false, insufficient: true, error: creditErr.message }); } const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n); const projectId = `proj_${Date.now()}_${randomHex(7)}`; const gradingResult = await AIEngine.gradeProject(description, answers); if (gradingResult.usage?.totalTokenCount) { await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic'); } const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F'; if (!isFailure) { const { error: insertError } = await supabase.from('projects').insert({ id: projectId, user_id: userId, info: { title: gradingResult.title || "Untitled", stats: gradingResult, description, answers, status: "initializing" } }); if (insertError) { console.error("Failed to insert project:", insertError.message); return res.status(500).json({ error: "Database save failed" }); } runBackgroundInitialization(projectId, userId, description); } res.json({ status: 200, success: !isFailure, projectId, stats: gradingResult, title: gradingResult.title || "Untitled" }); } catch (err) { console.error("Create Error:", err); res.status(500).json({ error: err.message }); } }); app.post('/project/feedback', async (req, res) => { const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body; try { const project = await StateManager.getProject(projectId); if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" }); // --- FIXED CREDIT CHECK --- // Catch credit errors here to return a soft 200 OK so frontend banner shows try { await checkMinimumCredits(userId, 'basic'); } catch (creditErr) { return res.json({ success: false, insufficient: true, error: creditErr.message }); } const context = formatContext({ hierarchyContext, scriptContext, logContext }); const fullInput = `USER: ${prompt || "Automatic Feedback"}${context}`; // Trigger Async runAsyncFeedback(projectId, userId, fullInput, images || []); res.json({ success: true, message: "Processing started" }); } catch (err) { console.error("Feedback Error:", err); await StateManager.updateProject(projectId, { status: "error" }); res.status(500).json({ error: "Feedback process failed" }); } }); app.post('/project/ping', async (req, res) => { const { projectId, userId, isFrontend } = req.body; if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" }); const project = await StateManager.getProject(projectId); if (!project || project.userId !== userId) return res.json({ action: "IDLE" }); // 1. BASE RESPONSE (Visuals) // Both Frontend and Plugin need this to show "Thinking..." or text generation const response = { action: "IDLE", status: StateManager.getStatus(projectId), snapshot: StateManager.getSnapshot(projectId) }; // 2. EXECUTOR LOGIC (Plugin Only) // Only pop commands if this is NOT the passive frontend viewer if (!isFrontend) { const command = await StateManager.popCommand(projectId); // We don't need 'streamBuffers' anymore since we use 'snapshot' for visuals. // But if you have legacy stream logic, we can leave it or remove it. // For this hybrid model, snapshot is king. if (command) { response.action = command.type; response.target = command.payload; response.code = command.type === 'EXECUTE' ? command.payload : null; } } res.json(response); }); app.post('/human/override', validateRequest, async (req, res) => { const { projectId, instruction, userId } = req.body; try { await checkMinimumCredits(userId, 'basic'); const project = await StateManager.getProject(projectId); res.json({ success: true }); const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`; let overrideText = ""; let stopStatus = startStatusLoop(projectId, 'worker'); const workerResult = await AIEngine.callWorkerStream( project.workerHistory, overrideMsg, (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); }, (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Coding..."); overrideText += chunk; StateManager.appendStream(projectId, chunk); } ); stopStatus(); await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg); await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText); await processAndQueueResponse(projectId, workerResult.text || overrideText, userId); const usage = workerResult.usage?.totalTokenCount || 0; if (usage > 0) await deductUserCredits(userId, usage, 'basic'); } catch (err) { console.error("Override failed", err); } }); app.get('/admin/cleanup', async (req, res) => { try { const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0; res.json({ success: true, removedCount: removed }); } catch (err) { res.status(500).json({ error: "Cleanup failed" }); } }); app.listen(PORT, () => { console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`); }); */