everydaycats commited on
Commit
adab8e7
·
verified ·
1 Parent(s): 9e7d11c

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +222 -295
app.js CHANGED
@@ -1,7 +1,7 @@
1
  import express from 'express';
2
  import bodyParser from 'body-parser';
3
  import cors from 'cors';
4
- import { StateManager, initDB } from './stateManager.js'; // Import initDB
5
  import { AIEngine } from './aiEngine.js';
6
  import fs from 'fs';
7
  import admin from 'firebase-admin';
@@ -16,14 +16,13 @@ try {
16
  if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
19
- // We assume the project ID is available in the service account to construct the bucket name
20
  const bucketName = `shago-web.firebasestorage.app`;
21
 
22
  if (admin.apps.length === 0) {
23
  admin.initializeApp({
24
  credential: admin.credential.cert(serviceAccount),
25
  databaseURL: "https://shago-web-default-rtdb.firebaseio.com",
26
- storageBucket: bucketName // Required for Storage
27
  });
28
  }
29
 
@@ -31,7 +30,6 @@ try {
31
  firestore = admin.firestore();
32
  storage = admin.storage();
33
 
34
- // Pass DB instance to StateManager
35
  initDB(db);
36
  console.log("🔥 Firebase Connected (RTDB, Firestore, Storage) & StateManager Linked");
37
  } else {
@@ -48,47 +46,39 @@ const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
48
  app.use(cors());
49
  app.use(bodyParser.json({ limit: '50mb' }));
50
 
51
- // --- CREDIT SYSTEM HELPERS ---
52
-
53
  const MIN_CREDITS_REQUIRED = 20;
54
- const IMAGE_GENERATION_COST = 1000; // Fixed token cost for an image
55
 
56
  /**
57
- * Estimates token count based on text length (Standard: ~4 chars = 1 token)
 
58
  */
59
- function estimateTokens(text) {
60
- if (!text) return 0;
61
- if (typeof text !== 'string') text = JSON.stringify(text);
62
- return Math.ceil(text.length / 4);
63
- }
64
-
65
- /**
66
- * Checks if user has enough credits. Throws error if insufficient.
67
- */
68
- async function checkUserCredits(userId) {
69
- if (!db) return; // Skip if no DB connected (Dev mode)
70
-
71
  const snap = await db.ref(`users/${userId}/credits`).once('value');
72
  const credits = snap.val() || 0;
73
 
74
  if (credits < MIN_CREDITS_REQUIRED) {
75
- throw new Error(`Insufficient credits. You have ${credits}, but ${MIN_CREDITS_REQUIRED} are required.`);
76
  }
77
- return credits;
78
  }
79
 
80
  /**
81
- * Deducts tokens from the user's account.
82
  */
83
  async function deductUserCredits(userId, amount) {
84
- if (!db || amount <= 0) return;
85
 
86
  try {
87
  const ref = db.ref(`users/${userId}/credits`);
88
  await ref.transaction((current_credits) => {
89
- return (current_credits || 0) - amount;
 
 
 
 
90
  });
91
- console.log(`[Credits] Deducted ${amount} from User ${userId}`);
92
  } catch (err) {
93
  console.error(`[Credits] Failed to deduct ${amount} from ${userId}:`, err);
94
  }
@@ -97,14 +87,13 @@ async function deductUserCredits(userId, amount) {
97
  // --- MIDDLEWARE ---
98
 
99
  const validateRequest = (req, res, next) => {
100
- // Modified: Onboarding endpoints might now need userId for credit checks
101
  if (req.path.includes('/admin/cleanup')) return next();
102
 
103
  const { userId, projectId } = req.body;
104
 
105
- // For specific endpoints, ensure IDs exist
106
- if ((req.path.includes('/new/project') || req.path.includes('/project/feedback')) && (!userId || !projectId)) {
107
- return res.status(400).json({ error: "Missing userId or projectId" });
108
  }
109
 
110
  next();
@@ -134,14 +123,10 @@ function extractImagePrompt(text) {
134
 
135
  // --- ADMIN ENDPOINTS ---
136
 
137
- /**
138
- * CLEANUP MEMORY
139
- * Call this via Cron Job or manually to free up RAM
140
- */
141
  app.get('/admin/cleanup', async (req, res) => {
142
  try {
143
  const removed = StateManager.cleanupMemory();
144
- res.json({ success: true, removedCount: removed, message: `Cleaned ${removed} inactive projects from memory.` });
145
  } catch (err) {
146
  res.status(500).json({ error: "Cleanup failed" });
147
  }
@@ -150,32 +135,32 @@ app.get('/admin/cleanup', async (req, res) => {
150
  // --- ONBOARDING ENDPOINTS ---
151
 
152
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
153
- const { description, userId } = req.body; // userId now required for credit check
154
  if (!description) return res.status(400).json({ error: "Description required" });
155
- if (!userId) return res.status(400).json({ error: "User ID required for analysis" });
156
-
157
- let tokenUsage = 0;
158
 
159
  try {
160
- // 1. Credit Check
161
- await checkUserCredits(userId);
162
 
163
  console.log(`[Onboarding] Analyzing idea...`);
164
- tokenUsage += estimateTokens(description);
165
-
 
166
  const result = await AIEngine.generateEntryQuestions(description);
167
- tokenUsage += estimateTokens(JSON.stringify(result));
168
 
169
- // 2. Deduct Credits
170
- await deductUserCredits(userId, tokenUsage);
 
 
171
 
172
  if (result.status === "REJECTED") {
173
  return res.json({
174
  rejected: true,
175
- reason: result.reason || "Idea violates TOS or guidelines."
176
  });
177
  }
178
  res.json({ questions: result.questions });
 
179
  } catch (err) {
180
  console.error(err);
181
  if (err.message.includes("Insufficient credits")) {
@@ -187,64 +172,48 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
187
 
188
  app.post('/onboarding/create', validateRequest, async (req, res) => {
189
  const { userId, description, answers } = req.body;
190
- let tokenUsage = 0;
191
 
192
  try {
193
- // 1. Credit Check
194
- await checkUserCredits(userId);
195
-
196
- const randomHex = (n = 6) => {
197
- const bytes = Math.ceil(n / 2);
198
- return crypto.randomBytes(bytes).toString("hex").slice(0, n);
199
- };
200
 
 
201
  const projectId = `proj_${Date.now()}_${randomHex(7)}`;
202
 
203
  console.log(`[Onboarding] Grading Project ${projectId}...`);
204
-
205
- // Track Input Usage
206
- tokenUsage += estimateTokens(description);
207
- tokenUsage += estimateTokens(JSON.stringify(answers));
208
 
209
- const grading = await AIEngine.gradeProject(description, answers);
210
- tokenUsage += estimateTokens(JSON.stringify(grading));
 
211
 
212
- const isFailure = grading.feasibility < 30 || grading.rating === 'F';
213
 
214
  let thumbnailBase64 = null;
215
 
216
- if (isFailure) {
217
- console.log(`[Onboarding] ❌ Project Failed Grading. Skipping Image.`);
218
- } else {
219
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
220
- const imagePrompt = `Game Title: ${grading.title}. Core Concept: ${description}`;
221
-
222
- // Track Prompt Usage
223
- tokenUsage += estimateTokens(imagePrompt);
224
 
225
- thumbnailBase64 = await AIEngine.generateImage(imagePrompt);
226
-
227
- // Track Image Cost
228
- if (thumbnailBase64) tokenUsage += IMAGE_GENERATION_COST;
 
 
229
  }
230
 
231
- // --- Database & Storage Logic ---
232
  let thumbnailUrl = null;
233
  if (thumbnailBase64 && storage) {
234
  try {
235
- console.log(`[Onboarding] 📤 Uploading Thumbnail to Storage...`);
236
  const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
237
  const buffer = Buffer.from(base64Data, 'base64');
238
  const bucket = storage.bucket();
239
  const file = bucket.file(`${projectId}/thumbnail.png`);
240
-
241
- await file.save(buffer, {
242
- metadata: { contentType: 'image/png' }
243
- });
244
-
245
  await file.makePublic();
246
  thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
247
- console.log(`[Onboarding] 🖼️ Thumbnail URL: ${thumbnailUrl}`);
248
  } catch (uploadErr) {
249
  console.error("Storage Upload Failed:", uploadErr);
250
  }
@@ -253,75 +222,66 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
253
  const timestamp = Date.now();
254
  const status = isFailure ? "rejected" : "Idle";
255
 
256
- const memoryObject = {
257
- id: projectId,
258
- userId,
259
- title: grading.title || "Untitled Project",
260
- description,
261
- answers,
262
- stats: grading,
263
- thumbnail: thumbnailUrl,
264
- createdAt: timestamp,
265
- status,
266
- workerHistory: [],
267
- pmHistory: [],
268
- commandQueue: [],
269
- failureCount: 0
270
- };
 
 
 
 
271
 
 
272
  if (firestore && !isFailure) {
273
- try {
274
- await firestore.collection('projects').doc(projectId).set({
275
- id: projectId,
276
- userId: userId,
277
- assets: thumbnailUrl ? [thumbnailUrl] : [],
278
- createdAt: admin.firestore.FieldValue.serverTimestamp()
279
- });
280
- console.log(`[Onboarding] 📝 Firestore Doc Created`);
281
- } catch (fsErr) {
282
- console.error("Firestore Write Failed:", fsErr);
283
- }
284
  }
285
 
286
- if (db && !isFailure) {
287
  const updates = {};
288
  updates[`projects/${projectId}/info`] = {
289
  id: projectId,
290
  userId,
291
- title: grading.title || "Untitled Project",
292
  description,
293
  answers,
294
- stats: grading,
295
  createdAt: timestamp,
296
  status
297
  };
298
-
299
- if (thumbnailUrl) {
300
- updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
301
- }
302
-
303
  updates[`projects/${projectId}/state`] = {
304
  workerHistory: [],
305
  pmHistory: [],
306
  commandQueue: [],
307
  failureCount: 0
308
  };
309
-
310
  await db.ref().update(updates);
311
  }
312
-
313
- if (!isFailure) {
314
- await StateManager.updateProject(projectId, memoryObject);
315
- }
316
 
317
- // 2. Deduct Credits
318
- await deductUserCredits(userId, tokenUsage);
319
 
320
  res.json({
321
  success: !isFailure,
322
  projectId,
323
- stats: grading,
324
- title: grading.title || "Untitled Project",
325
  thumbnail: thumbnailBase64
326
  });
327
 
@@ -336,78 +296,74 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
336
 
337
  // --- CORE ENDPOINTS ---
338
 
339
- // NEW: Helper function to run initialization in background
340
  async function runBackgroundInitialization(projectId, userId, description) {
341
  console.log(`[Background] Starting initialization for ${projectId}`);
342
- let tokenUsage = 0;
343
 
344
  try {
345
  const pmHistory = [];
346
 
347
  // 1. Generate GDD
348
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
349
- tokenUsage += estimateTokens(gddPrompt);
350
-
351
- const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
352
- tokenUsage += estimateTokens(gddResponse);
353
 
 
 
 
354
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
355
- pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
356
 
357
  // 2. Generate First Task
358
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
359
- tokenUsage += estimateTokens(taskPrompt);
360
-
361
- const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
362
- tokenUsage += estimateTokens(taskResponse);
363
 
364
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
365
- pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
366
 
367
  // 3. Initialize Worker
368
- const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
369
  const workerHistory = [];
370
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
371
- tokenUsage += estimateTokens(initialWorkerPrompt);
372
-
373
- const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
374
- tokenUsage += estimateTokens(workerResponse);
 
375
 
376
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
377
- workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
378
 
379
- // 4. Update Memory & DB
380
  await StateManager.updateProject(projectId, {
381
  userId,
382
  pmHistory,
383
  workerHistory,
384
- gdd: gddResponse,
385
  failureCount: 0
386
  });
387
 
388
- // Queue response (Asset generation logic inside here is separate, handled by main queue but we can check usage there too if needed)
389
- // For now, we assume processAndQueueResponse handles assets separately or we charge later.
390
- // If processAndQueueResponse generates an image, we should charge.
391
- // Let's modify processAndQueueResponse to return cost or handle it there.
392
- // For simplicity, we stick to text cost here.
393
- await processAndQueueResponse(projectId, workerResponse, userId); // Passed userId to handle image cost in queue
394
 
395
- // 5. Update Status to IDLE
396
  if(db) await db.ref(`projects/${projectId}/info`).update({
397
  status: "IDLE",
398
  lastUpdated: Date.now()
399
  });
400
-
401
- // 6. Deduct accumulated credits
402
- await deductUserCredits(userId, tokenUsage);
403
 
404
- console.log(`[Background] Initialization complete for ${projectId}. Tokens used: ${tokenUsage}`);
 
 
 
405
 
406
  } catch (err) {
407
  console.error(`[Background] Init Error for ${projectId}:`, err);
408
- // Even if it failed, we should deduct what was used
409
- // if (tokenUsage > 0) await deductUserCredits(userId, tokenUsage);
410
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
 
411
  }
412
  }
413
 
@@ -415,23 +371,18 @@ app.post('/new/project', validateRequest, async (req, res) => {
415
  const { userId, projectId, description } = req.body;
416
 
417
  try {
418
- // 1. Credit Check (Prevent starting if low balance)
419
- await checkUserCredits(userId);
420
 
421
- // 2. Immediately acknowledge request
422
  if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
423
- console.log("Received new project request");
424
 
425
  res.json({
426
  success: true,
427
  message: "Project initialization started in background."
428
  });
429
 
430
- // 3. Trigger background process
431
  runBackgroundInitialization(projectId, userId, description);
432
 
433
  } catch (err) {
434
- console.error("New Project Error:", err);
435
  if (err.message.includes("Insufficient credits")) {
436
  return res.status(402).json({ error: err.message });
437
  }
@@ -441,57 +392,49 @@ app.post('/new/project', validateRequest, async (req, res) => {
441
 
442
  app.post('/project/feedback', async (req, res) => {
443
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
444
- let tokenUsage = 0;
445
 
446
- // 1. Basic Validations
447
- const project = await StateManager.getProject(projectId);
448
- if (!project) return res.status(404).json({ error: "Project not found." });
449
-
450
- if (project.userId !== userId) {
451
- console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
452
- return res.status(403).json({ error: "Unauthorized: You do not own this project." });
453
- }
454
-
455
  try {
456
- // 2. Credit Check
457
- await checkUserCredits(userId);
 
 
 
 
458
 
459
- // Update status to working immediately
460
  if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
461
 
462
  if (taskComplete) {
463
  console.log(`[${projectId}] ✅ TASK COMPLETE.`);
464
  const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
465
- tokenUsage += estimateTokens(summary);
466
-
467
- const pmResponse = await AIEngine.callPM(project.pmHistory, summary);
468
- tokenUsage += estimateTokens(pmResponse);
469
 
470
  project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
471
- project.pmHistory.push({ role: 'model', parts: [{ text: pmResponse }] });
472
 
473
- const nextInstruction = extractWorkerPrompt(pmResponse);
474
  if (!nextInstruction) {
475
  await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
476
- if(db) {
477
- await db.ref(`projects/${projectId}/info`).update({
478
- status: "IDLE",
479
- lastUpdated: Date.now()
480
- });
481
- }
482
- await deductUserCredits(userId, tokenUsage);
483
  return res.json({ success: true, message: "No further tasks. Project Idle." });
484
  }
485
 
486
  const newWorkerHistory = [];
487
  const newPrompt = `New Objective: ${nextInstruction}`;
488
- tokenUsage += estimateTokens(newPrompt);
489
-
490
- const workerResponse = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
491
- tokenUsage += estimateTokens(workerResponse);
492
 
493
  newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
494
- newWorkerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
495
 
496
  await StateManager.updateProject(projectId, {
497
  pmHistory: project.pmHistory,
@@ -500,20 +443,16 @@ app.post('/project/feedback', async (req, res) => {
500
  });
501
 
502
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
503
- await processAndQueueResponse(projectId, workerResponse, userId); // Handle usage inside or separately? We'll track image usage here if it occurs.
504
 
505
- // Still working on next task
506
- if(db) {
507
- await db.ref(`projects/${projectId}/info`).update({
508
- status: "working",
509
- lastUpdated: Date.now()
510
- });
511
- }
512
 
513
- await deductUserCredits(userId, tokenUsage);
 
514
  return res.json({ success: true, message: "Next Task Assigned" });
515
  }
516
 
 
517
  let isFailure = false;
518
  if (logContext?.logs) {
519
  const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"];
@@ -525,76 +464,74 @@ app.post('/project/feedback', async (req, res) => {
525
 
526
  if (project.failureCount > 3) {
527
  const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
528
- tokenUsage += estimateTokens(pmPrompt);
529
-
530
- const pmVerdict = await AIEngine.callPM(project.pmHistory, pmPrompt);
531
- tokenUsage += estimateTokens(pmVerdict);
532
 
533
  if (pmVerdict.includes("[TERMINATE]")) {
534
  const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
535
  const resetHistory = [];
536
  const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
537
- tokenUsage += estimateTokens(resetPrompt);
538
-
539
- const workerResp = await AIEngine.callWorker(resetHistory, resetPrompt, []);
540
- tokenUsage += estimateTokens(workerResp);
541
 
542
  resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
543
- resetHistory.push({ role: 'model', parts: [{ text: workerResp }] });
544
 
545
  await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
546
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
547
- await processAndQueueResponse(projectId, workerResp, userId);
548
- await deductUserCredits(userId, tokenUsage);
 
549
  return res.json({ success: true, message: "Worker Terminated." });
550
  } else {
551
  const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
552
- tokenUsage += estimateTokens(injection);
 
553
 
554
- const workerResp = await AIEngine.callWorker(project.workerHistory, injection, []);
555
- tokenUsage += estimateTokens(workerResp);
556
-
557
  project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
558
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResp }] });
559
 
560
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
561
- await processAndQueueResponse(projectId, workerResp, userId);
562
- await deductUserCredits(userId, tokenUsage);
 
563
  return res.json({ success: true, message: "PM Guidance Applied." });
564
  }
565
  }
566
 
567
- // Standard Logic
568
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
569
- tokenUsage += estimateTokens(fullInput);
570
-
571
- let response = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
572
- tokenUsage += estimateTokens(response);
573
 
574
- const pmQuestion = extractPMQuestion(response);
 
 
 
 
575
  if (pmQuestion) {
576
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
577
- tokenUsage += estimateTokens(pmConsultPrompt);
578
-
579
- const pmAnswer = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
580
- tokenUsage += estimateTokens(pmAnswer);
581
 
582
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
583
  project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
584
 
585
  const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
586
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
587
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
588
 
589
- tokenUsage += estimateTokens(injectionMsg);
590
- response = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
591
- tokenUsage += estimateTokens(response);
 
592
 
593
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
594
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
595
  } else {
596
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
597
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
598
  }
599
 
600
  await StateManager.updateProject(projectId, {
@@ -603,25 +540,17 @@ app.post('/project/feedback', async (req, res) => {
603
  failureCount: project.failureCount
604
  });
605
 
606
- await processAndQueueResponse(projectId, response, userId);
607
 
608
- // --- UPDATED: Set status to IDLE and update lastUpdated ---
609
- if(db) {
610
- await db.ref(`projects/${projectId}/info`).update({
611
- status: "idle",
612
- lastUpdated: Date.now()
613
- });
614
- }
615
 
616
- // Final Deduction
617
- await deductUserCredits(userId, tokenUsage);
618
 
619
  res.json({ success: true });
 
620
  } catch (err) {
621
  console.error("AI Error:", err);
622
- // Even if it failed, deduct used tokens
623
- // if (tokenUsage > 0) await deductUserCredits(userId, tokenUsage);
624
-
625
  if (err.message.includes("Insufficient credits")) {
626
  return res.status(402).json({ error: err.message });
627
  }
@@ -631,21 +560,11 @@ app.post('/project/feedback', async (req, res) => {
631
 
632
  app.post('/project/ping', async (req, res) => {
633
  const { projectId, userId } = req.body;
634
-
635
- if (!projectId || !userId) {
636
- return res.status(400).json({ error: "Missing ID fields" });
637
- }
638
 
639
  const project = await StateManager.getProject(projectId);
640
-
641
- if (!project) {
642
- return res.status(404).json({ action: "IDLE", error: "Project not found" });
643
- }
644
-
645
- if (project.userId !== userId) {
646
- console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
647
- return res.status(403).json({ error: "Unauthorized: You do not own this project." });
648
- }
649
 
650
  const command = await StateManager.popCommand(projectId);
651
 
@@ -666,52 +585,60 @@ app.post('/project/ping', async (req, res) => {
666
 
667
  app.post('/human/override', validateRequest, async (req, res) => {
668
  const { projectId, instruction, pruneHistory, userId } = req.body;
669
- // Assuming userId is passed, we check credits
670
- if (userId) {
671
- try {
672
- await checkUserCredits(userId);
673
- } catch (e) {
674
- return res.status(402).json({ error: e.message });
675
- }
676
- }
677
 
678
- const project = await StateManager.getProject(projectId);
679
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
680
- let tokenUsage = estimateTokens(overrideMsg);
681
 
682
- if (pruneHistory && project.workerHistory.length >= 2) {
683
- project.workerHistory.pop();
684
- project.workerHistory.pop();
685
- }
686
- const response = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
687
- tokenUsage += estimateTokens(response);
688
 
689
- project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
690
- project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
691
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
692
- await processAndQueueResponse(projectId, response, userId);
693
-
694
- if (userId) await deductUserCredits(userId, tokenUsage);
695
 
696
- res.json({ success: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  });
698
 
699
- // Added userId param to handle asset cost if image generated
700
  async function processAndQueueResponse(projectId, rawResponse, userId) {
701
  const imgPrompt = extractImagePrompt(rawResponse);
702
  if (imgPrompt) {
703
  console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
704
- const base64Image = await AIEngine.generateImage(imgPrompt);
705
 
706
- // Deduct for image generation here if userId is available
707
- if (base64Image && userId) {
708
- await deductUserCredits(userId, IMAGE_GENERATION_COST);
709
- }
 
 
 
 
 
710
 
711
- if (base64Image) {
712
- await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: base64Image });
713
  }
714
  }
 
715
  await StateManager.queueCommand(projectId, rawResponse);
716
  }
717
 
 
1
  import express from 'express';
2
  import bodyParser from 'body-parser';
3
  import cors from 'cors';
4
+ import { StateManager, initDB } from './stateManager.js';
5
  import { AIEngine } from './aiEngine.js';
6
  import fs from 'fs';
7
  import admin from 'firebase-admin';
 
16
  if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
 
19
  const bucketName = `shago-web.firebasestorage.app`;
20
 
21
  if (admin.apps.length === 0) {
22
  admin.initializeApp({
23
  credential: admin.credential.cert(serviceAccount),
24
  databaseURL: "https://shago-web-default-rtdb.firebaseio.com",
25
+ storageBucket: bucketName
26
  });
27
  }
28
 
 
30
  firestore = admin.firestore();
31
  storage = admin.storage();
32
 
 
33
  initDB(db);
34
  console.log("🔥 Firebase Connected (RTDB, Firestore, Storage) & StateManager Linked");
35
  } else {
 
46
  app.use(cors());
47
  app.use(bodyParser.json({ limit: '50mb' }));
48
 
49
+ // --- CREDIT HELPERS ---
 
50
  const MIN_CREDITS_REQUIRED = 20;
 
51
 
52
  /**
53
+ * Checks if user has enough credits to START an operation.
54
+ * Throws if < 20.
55
  */
56
+ async function checkMinimumCredits(userId) {
57
+ if (!db) return;
 
 
 
 
 
 
 
 
 
 
58
  const snap = await db.ref(`users/${userId}/credits`).once('value');
59
  const credits = snap.val() || 0;
60
 
61
  if (credits < MIN_CREDITS_REQUIRED) {
62
+ throw new Error(`Insufficient credits. You have ${credits}, need minimum ${MIN_CREDITS_REQUIRED} to proceed.`);
63
  }
 
64
  }
65
 
66
  /**
67
+ * Deducts exact tokens from the user's account.
68
  */
69
  async function deductUserCredits(userId, amount) {
70
+ if (!db || !amount || amount <= 0) return;
71
 
72
  try {
73
  const ref = db.ref(`users/${userId}/credits`);
74
  await ref.transaction((current_credits) => {
75
+ const current = current_credits || 0;
76
+ // Allow going negative if the bill is huge, or cap at 0?
77
+ // Usually valid to go slightly negative on final bill, or cap at 0.
78
+ // Here we assume we just subtract.
79
+ return Math.max(0, current - amount);
80
  });
81
+ console.log(`[Credits] Deducted ${amount} tokens from User ${userId}`);
82
  } catch (err) {
83
  console.error(`[Credits] Failed to deduct ${amount} from ${userId}:`, err);
84
  }
 
87
  // --- MIDDLEWARE ---
88
 
89
  const validateRequest = (req, res, next) => {
 
90
  if (req.path.includes('/admin/cleanup')) return next();
91
 
92
  const { userId, projectId } = req.body;
93
 
94
+ // Ensure userId exists for credit checks
95
+ if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
96
+ return res.status(400).json({ error: "Missing userId" });
97
  }
98
 
99
  next();
 
123
 
124
  // --- ADMIN ENDPOINTS ---
125
 
 
 
 
 
126
  app.get('/admin/cleanup', async (req, res) => {
127
  try {
128
  const removed = StateManager.cleanupMemory();
129
+ res.json({ success: true, removedCount: removed });
130
  } catch (err) {
131
  res.status(500).json({ error: "Cleanup failed" });
132
  }
 
135
  // --- ONBOARDING ENDPOINTS ---
136
 
137
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
138
+ const { description, userId } = req.body;
139
  if (!description) return res.status(400).json({ error: "Description required" });
 
 
 
140
 
141
  try {
142
+ // 1. Pre-flight Check
143
+ await checkMinimumCredits(userId);
144
 
145
  console.log(`[Onboarding] Analyzing idea...`);
146
+
147
+ // 2. AI Call
148
+ // Result is { questions: [...], usage: { totalTokenCount: ... }, ... }
149
  const result = await AIEngine.generateEntryQuestions(description);
 
150
 
151
+ const usage = result.usage?.totalTokenCount || 0;
152
+
153
+ // 3. Billing (Only on success)
154
+ if (usage > 0) await deductUserCredits(userId, usage);
155
 
156
  if (result.status === "REJECTED") {
157
  return res.json({
158
  rejected: true,
159
+ reason: result.reason || "Idea violates TOS."
160
  });
161
  }
162
  res.json({ questions: result.questions });
163
+
164
  } catch (err) {
165
  console.error(err);
166
  if (err.message.includes("Insufficient credits")) {
 
172
 
173
  app.post('/onboarding/create', validateRequest, async (req, res) => {
174
  const { userId, description, answers } = req.body;
175
+ let totalTokens = 0;
176
 
177
  try {
178
+ // 1. Pre-flight Check
179
+ await checkMinimumCredits(userId);
 
 
 
 
 
180
 
181
+ const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
182
  const projectId = `proj_${Date.now()}_${randomHex(7)}`;
183
 
184
  console.log(`[Onboarding] Grading Project ${projectId}...`);
 
 
 
 
185
 
186
+ // 2. Grading
187
+ const gradingResult = await AIEngine.gradeProject(description, answers);
188
+ totalTokens += (gradingResult.usage?.totalTokenCount || 0);
189
 
190
+ const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
191
 
192
  let thumbnailBase64 = null;
193
 
194
+ if (!isFailure) {
 
 
195
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
196
+ const imagePrompt = `Game Title: ${gradingResult.title}. Core Concept: ${description}`;
 
 
 
197
 
198
+ // 3. Image Generation
199
+ const imgResult = await AIEngine.generateImage(imagePrompt);
200
+ if (imgResult) {
201
+ thumbnailBase64 = imgResult.image;
202
+ totalTokens += (imgResult.usage?.totalTokenCount || 0);
203
+ }
204
  }
205
 
206
+ // Upload Logic
207
  let thumbnailUrl = null;
208
  if (thumbnailBase64 && storage) {
209
  try {
 
210
  const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
211
  const buffer = Buffer.from(base64Data, 'base64');
212
  const bucket = storage.bucket();
213
  const file = bucket.file(`${projectId}/thumbnail.png`);
214
+ await file.save(buffer, { metadata: { contentType: 'image/png' } });
 
 
 
 
215
  await file.makePublic();
216
  thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
 
217
  } catch (uploadErr) {
218
  console.error("Storage Upload Failed:", uploadErr);
219
  }
 
222
  const timestamp = Date.now();
223
  const status = isFailure ? "rejected" : "Idle";
224
 
225
+ // Save Data
226
+ if (!isFailure) {
227
+ const memoryObject = {
228
+ id: projectId,
229
+ userId,
230
+ title: gradingResult.title || "Untitled",
231
+ description,
232
+ answers,
233
+ stats: gradingResult,
234
+ thumbnail: thumbnailUrl,
235
+ createdAt: timestamp,
236
+ status,
237
+ workerHistory: [],
238
+ pmHistory: [],
239
+ commandQueue: [],
240
+ failureCount: 0
241
+ };
242
+ await StateManager.updateProject(projectId, memoryObject);
243
+ }
244
 
245
+ // DB Updates
246
  if (firestore && !isFailure) {
247
+ await firestore.collection('projects').doc(projectId).set({
248
+ id: projectId,
249
+ userId: userId,
250
+ assets: thumbnailUrl ? [thumbnailUrl] : [],
251
+ createdAt: admin.firestore.FieldValue.serverTimestamp()
252
+ });
 
 
 
 
 
253
  }
254
 
255
+ if (db && !isFailure) {
256
  const updates = {};
257
  updates[`projects/${projectId}/info`] = {
258
  id: projectId,
259
  userId,
260
+ title: gradingResult.title || "Untitled",
261
  description,
262
  answers,
263
+ stats: gradingResult,
264
  createdAt: timestamp,
265
  status
266
  };
267
+ if (thumbnailUrl) updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
 
 
 
 
268
  updates[`projects/${projectId}/state`] = {
269
  workerHistory: [],
270
  pmHistory: [],
271
  commandQueue: [],
272
  failureCount: 0
273
  };
 
274
  await db.ref().update(updates);
275
  }
 
 
 
 
276
 
277
+ // 4. Billing (Only on success)
278
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
279
 
280
  res.json({
281
  success: !isFailure,
282
  projectId,
283
+ stats: gradingResult,
284
+ title: gradingResult.title || "Untitled",
285
  thumbnail: thumbnailBase64
286
  });
287
 
 
296
 
297
  // --- CORE ENDPOINTS ---
298
 
 
299
  async function runBackgroundInitialization(projectId, userId, description) {
300
  console.log(`[Background] Starting initialization for ${projectId}`);
301
+ let totalTokens = 0;
302
 
303
  try {
304
  const pmHistory = [];
305
 
306
  // 1. Generate GDD
307
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
308
+ const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
 
 
 
309
 
310
+ totalTokens += (gddResult.usage?.totalTokenCount || 0);
311
+ const gddText = gddResult.text;
312
+
313
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
314
+ pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
315
 
316
  // 2. Generate First Task
317
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
318
+ const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
319
+
320
+ totalTokens += (taskResult.usage?.totalTokenCount || 0);
321
+ const taskText = taskResult.text;
322
 
323
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
324
+ pmHistory.push({ role: 'model', parts: [{ text: taskText }] });
325
 
326
  // 3. Initialize Worker
327
+ const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
328
  const workerHistory = [];
329
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
330
+
331
+ const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
332
+
333
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
334
+ const workerText = workerResult.text;
335
 
336
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
337
+ workerHistory.push({ role: 'model', parts: [{ text: workerText }] });
338
 
339
+ // 4. Update Memory
340
  await StateManager.updateProject(projectId, {
341
  userId,
342
  pmHistory,
343
  workerHistory,
344
+ gdd: gddText,
345
  failureCount: 0
346
  });
347
 
348
+ // 5. Queue commands (Handle assets + their costs)
349
+ // We pass userId to deduct image costs immediately inside this function
350
+ await processAndQueueResponse(projectId, workerText, userId);
 
 
 
351
 
352
+ // 6. Update Status
353
  if(db) await db.ref(`projects/${projectId}/info`).update({
354
  status: "IDLE",
355
  lastUpdated: Date.now()
356
  });
 
 
 
357
 
358
+ // 7. Deduct accumulated text tokens
359
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
360
+
361
+ console.log(`[Background] Init complete. Text Tokens: ${totalTokens}`);
362
 
363
  } catch (err) {
364
  console.error(`[Background] Init Error for ${projectId}:`, err);
 
 
365
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
366
+ // No deduction on failure
367
  }
368
  }
369
 
 
371
  const { userId, projectId, description } = req.body;
372
 
373
  try {
374
+ await checkMinimumCredits(userId);
 
375
 
 
376
  if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
 
377
 
378
  res.json({
379
  success: true,
380
  message: "Project initialization started in background."
381
  });
382
 
 
383
  runBackgroundInitialization(projectId, userId, description);
384
 
385
  } catch (err) {
 
386
  if (err.message.includes("Insufficient credits")) {
387
  return res.status(402).json({ error: err.message });
388
  }
 
392
 
393
  app.post('/project/feedback', async (req, res) => {
394
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
395
+ let totalTokens = 0;
396
 
 
 
 
 
 
 
 
 
 
397
  try {
398
+ const project = await StateManager.getProject(projectId);
399
+ if (!project) return res.status(404).json({ error: "Project not found." });
400
+ if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
401
+
402
+ // 1. Pre-flight Check
403
+ await checkMinimumCredits(userId);
404
 
 
405
  if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
406
 
407
  if (taskComplete) {
408
  console.log(`[${projectId}] ✅ TASK COMPLETE.`);
409
  const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
410
+
411
+ const pmResult = await AIEngine.callPM(project.pmHistory, summary);
412
+ totalTokens += (pmResult.usage?.totalTokenCount || 0);
413
+ const pmText = pmResult.text;
414
 
415
  project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
416
+ project.pmHistory.push({ role: 'model', parts: [{ text: pmText }] });
417
 
418
+ const nextInstruction = extractWorkerPrompt(pmText);
419
  if (!nextInstruction) {
420
  await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
421
+ if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
422
+
423
+ // Deduct what we used so far
424
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
425
+
 
 
426
  return res.json({ success: true, message: "No further tasks. Project Idle." });
427
  }
428
 
429
  const newWorkerHistory = [];
430
  const newPrompt = `New Objective: ${nextInstruction}`;
431
+
432
+ const workerResult = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
433
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
434
+ const workerText = workerResult.text;
435
 
436
  newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
437
+ newWorkerHistory.push({ role: 'model', parts: [{ text: workerText }] });
438
 
439
  await StateManager.updateProject(projectId, {
440
  pmHistory: project.pmHistory,
 
443
  });
444
 
445
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
446
+ await processAndQueueResponse(projectId, workerText, userId);
447
 
448
+ if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
 
 
 
 
 
 
449
 
450
+ // Billing
451
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
452
  return res.json({ success: true, message: "Next Task Assigned" });
453
  }
454
 
455
+ // Logic for Failure Handling (PM Guidance) or Standard Feedback
456
  let isFailure = false;
457
  if (logContext?.logs) {
458
  const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"];
 
464
 
465
  if (project.failureCount > 3) {
466
  const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
467
+ const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
468
+ totalTokens += (pmResult.usage?.totalTokenCount || 0);
469
+ const pmVerdict = pmResult.text;
 
470
 
471
  if (pmVerdict.includes("[TERMINATE]")) {
472
  const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
473
  const resetHistory = [];
474
  const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
475
+
476
+ const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []);
477
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
 
478
 
479
  resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
480
+ resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
481
 
482
  await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
483
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
484
+ await processAndQueueResponse(projectId, workerResult.text, userId);
485
+
486
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
487
  return res.json({ success: true, message: "Worker Terminated." });
488
  } else {
489
  const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
490
+ const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []);
491
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
492
 
 
 
 
493
  project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
494
+ project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
495
 
496
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
497
+ await processAndQueueResponse(projectId, workerResult.text, userId);
498
+
499
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
500
  return res.json({ success: true, message: "PM Guidance Applied." });
501
  }
502
  }
503
 
504
+ // Standard Interaction
505
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
 
 
 
 
506
 
507
+ let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
508
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
509
+ let responseText = workerResult.text;
510
+
511
+ const pmQuestion = extractPMQuestion(responseText);
512
  if (pmQuestion) {
513
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
514
+ const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
515
+ totalTokens += (pmResult.usage?.totalTokenCount || 0);
516
+ const pmAnswer = pmResult.text;
 
517
 
518
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
519
  project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
520
 
521
  const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
522
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
523
+ project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
524
 
525
+ // Re-call worker with answer
526
+ workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
527
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
528
+ responseText = workerResult.text;
529
 
530
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
531
+ project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
532
  } else {
533
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
534
+ project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
535
  }
536
 
537
  await StateManager.updateProject(projectId, {
 
540
  failureCount: project.failureCount
541
  });
542
 
543
+ await processAndQueueResponse(projectId, responseText, userId);
544
 
545
+ if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
 
 
 
 
 
 
546
 
547
+ // Final Billing
548
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
549
 
550
  res.json({ success: true });
551
+
552
  } catch (err) {
553
  console.error("AI Error:", err);
 
 
 
554
  if (err.message.includes("Insufficient credits")) {
555
  return res.status(402).json({ error: err.message });
556
  }
 
560
 
561
  app.post('/project/ping', async (req, res) => {
562
  const { projectId, userId } = req.body;
563
+ if (!projectId || !userId) return res.status(400).json({ error: "Missing ID fields" });
 
 
 
564
 
565
  const project = await StateManager.getProject(projectId);
566
+ if (!project) return res.status(404).json({ action: "IDLE", error: "Project not found" });
567
+ if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
 
 
 
 
 
 
 
568
 
569
  const command = await StateManager.popCommand(projectId);
570
 
 
585
 
586
  app.post('/human/override', validateRequest, async (req, res) => {
587
  const { projectId, instruction, pruneHistory, userId } = req.body;
588
+ let totalTokens = 0;
 
 
 
 
 
 
 
589
 
590
+ try {
591
+ await checkMinimumCredits(userId);
 
592
 
593
+ const project = await StateManager.getProject(projectId);
594
+ const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
 
 
 
 
595
 
596
+ if (pruneHistory && project.workerHistory.length >= 2) {
597
+ project.workerHistory.pop();
598
+ project.workerHistory.pop();
599
+ }
 
 
600
 
601
+ const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
602
+ totalTokens += (workerResult.usage?.totalTokenCount || 0);
603
+
604
+ project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
605
+ project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
606
+
607
+ await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
608
+ await processAndQueueResponse(projectId, workerResult.text, userId);
609
+
610
+ if (totalTokens > 0) await deductUserCredits(userId, totalTokens);
611
+
612
+ res.json({ success: true });
613
+ } catch (err) {
614
+ if (err.message.includes("Insufficient credits")) {
615
+ return res.status(402).json({ error: err.message });
616
+ }
617
+ res.status(500).json({ error: "Override Failed" });
618
+ }
619
  });
620
 
621
+ // Helper to handle Asset Parsing & Billing
622
  async function processAndQueueResponse(projectId, rawResponse, userId) {
623
  const imgPrompt = extractImagePrompt(rawResponse);
624
  if (imgPrompt) {
625
  console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
 
626
 
627
+ // 1. Generate Image (returns { image, usage } or null)
628
+ const imgResult = await AIEngine.generateImage(imgPrompt);
629
+
630
+ if (imgResult && imgResult.image) {
631
+ // 2. Bill for image tokens immediately
632
+ const imgTokens = imgResult.usage?.totalTokenCount || 0;
633
+ if (userId && imgTokens > 0) {
634
+ await deductUserCredits(userId, imgTokens);
635
+ }
636
 
637
+ // 3. Queue creation command
638
+ await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
639
  }
640
  }
641
+ // Queue the raw text response for the frontend
642
  await StateManager.queueCommand(projectId, rawResponse);
643
  }
644