everydaytok commited on
Commit
28dd9b2
·
verified ·
1 Parent(s): abf20d4

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +164 -210
app.js CHANGED
@@ -1,3 +1,11 @@
 
 
 
 
 
 
 
 
1
  import express from 'express';
2
  import bodyParser from 'body-parser';
3
  import cors from 'cors';
@@ -17,7 +25,7 @@ try {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
19
  // Specific bucket as requested
20
- const bucketName = `hollowpad-ai-default-rtdb.firebaseio.com` // `shago-web.firebasestorage.app`;
21
 
22
  if (admin.apps.length === 0) {
23
  admin.initializeApp({
@@ -42,21 +50,6 @@ try {
42
  const app = express();
43
  const PORT = process.env.PORT || 7860;
44
 
45
- // ---
46
- /*
47
-
48
- app.use((req, res, next) => {
49
- console.log('[REQ]', req.method, req.originalUrl);
50
- console.log('[HEADERS]', req.headers);
51
- console.log('[BODY]', req.body ?? '(unparseable or empty)');
52
- next();
53
- });
54
-
55
- */
56
-
57
- // ---
58
-
59
-
60
  // Load prompts safely
61
  let sysPrompts = {};
62
  try {
@@ -69,45 +62,22 @@ app.use(cors());
69
  app.use(bodyParser.json({ limit: '50mb' }));
70
 
71
  // --- CREDIT CONFIGURATION ---
72
-
73
- // Minimums required to start a request
74
  const MIN_BASIC_REQUIRED = 50;
75
  const MIN_DIAMOND_REQUIRED = 50;
76
-
77
- // Fixed cost for images (Basic credits)
78
  const IMAGE_COST_BASIC = 1000;
79
 
80
- /**
81
- * Checks if user has enough credits of a specific type.
82
- * @param {string} userId
83
- * @param {'basic'|'diamond'} type
84
- */
85
  async function checkMinimumCredits(userId, type = 'basic') {
86
  if (!db) return;
87
 
88
- // Path: users/{uid}/credits/basic OR users/{uid}/credits/diamond
89
  const snap = await db.ref(`users/${userId}/credits/${type}`).once('value');
90
  const credits = snap.val() || 0;
91
-
92
  const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
93
 
94
  if (credits < required) {
95
-
96
- res.status(500).json({
97
- insufficient: true,
98
- error:`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`
99
-
100
- });
101
- // throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`);
102
  }
103
  }
104
 
105
- /**
106
- * Deducts exact tokens from the user's specific wallet.
107
- * @param {string} userId
108
- * @param {number} amount
109
- * @param {'basic'|'diamond'} type
110
- */
111
  async function deductUserCredits(userId, amount, type = 'basic') {
112
  if (!db || !amount || amount <= 0) return;
113
 
@@ -117,20 +87,18 @@ async function deductUserCredits(userId, amount, type = 'basic') {
117
  const current = current_credits || 0;
118
  return Math.max(0, current - amount);
119
  });
120
- console.log(`[Credits] Deducted ${amount} ${type} credits from User ${userId}`);
121
  } catch (err) {
122
- console.error(`[Credits] Failed to deduct ${amount} ${type} from ${userId}:`, err);
123
  }
124
  }
125
 
126
  // --- MIDDLEWARE ---
127
-
128
  const validateRequest = (req, res, next) => {
129
  if (req.path.includes('/admin/cleanup')) return next();
130
 
131
  const { userId, projectId } = req.body;
132
 
133
- // Ensure userId exists for credit checks
134
  if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
135
  return res.status(400).json({ error: "Missing userId" });
136
  }
@@ -138,33 +106,36 @@ const validateRequest = (req, res, next) => {
138
  next();
139
  };
140
 
 
141
  function extractWorkerPrompt(text) {
142
  const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
143
- return match ? match[1].trim() : null;
144
  }
145
 
146
  function formatContext({ hierarchyContext, scriptContext, logContext }) {
147
  let out = "";
148
- if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`;
149
- // if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource?.substring(0, 1000)}...`;
150
- if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
151
- // my own line
152
- if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`;
153
- return out;
154
  }
155
 
156
  function extractPMQuestion(text) {
157
- const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
158
- return match ? match[1].trim() : null;
159
  }
160
 
161
  function extractImagePrompt(text) {
162
- const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
163
- return match ? match[1].trim() : null;
164
  }
165
 
166
- // --- ADMIN ENDPOINTS ---
 
 
 
167
 
 
168
  app.get('/admin/cleanup', async (req, res) => {
169
  try {
170
  const removed = StateManager.cleanupMemory();
@@ -175,23 +146,18 @@ app.get('/admin/cleanup', async (req, res) => {
175
  });
176
 
177
  // --- ONBOARDING ENDPOINTS ---
178
-
179
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
180
  const { description, userId } = req.body;
181
 
182
  if (!description) return res.status(400).json({ error: "Description required" });
183
 
184
  try {
185
- // Analyst uses BASIC models (Flash)
186
  await checkMinimumCredits(userId, 'basic');
187
-
188
- console.log(`[Onboarding] Analyzing idea...`);
189
 
190
  const result = await AIEngine.generateEntryQuestions(description);
191
-
192
  const usage = result.usage?.totalTokenCount || 0;
193
 
194
- // Bill Basic
195
  if (usage > 0) await deductUserCredits(userId, usage, 'basic');
196
 
197
  if (result.status === "REJECTED") {
@@ -204,7 +170,7 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
204
 
205
  } catch (err) {
206
  console.error(err);
207
- if (err.message.includes("Insufficient")) {
208
  return res.status(402).json({ error: err.message });
209
  }
210
  res.status(500).json({ error: "Analysis failed" });
@@ -213,59 +179,22 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
213
 
214
  app.post('/onboarding/create', validateRequest, async (req, res) => {
215
  const { userId, description, answers } = req.body;
216
- let basicTokens = 0; // Create flow uses Grader (Basic) + Image (Basic)
217
 
218
  try {
219
- // Pre-flight check
220
  await checkMinimumCredits(userId, 'basic');
221
 
222
  const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
223
  const projectId = `proj_${Date.now()}_${randomHex(7)}`;
224
 
225
- console.log(`[Onboarding] Grading Project ${projectId}...`);
226
 
227
- // 1. Grading (Basic)
228
  const gradingResult = await AIEngine.gradeProject(description, answers);
229
  basicTokens += (gradingResult.usage?.totalTokenCount || 0);
230
 
231
  const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
232
-
233
  let thumbnailBase64 = null;
234
-
235
- /*
236
- * image generator
237
- if (!isFailure) {
238
- console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
239
- const imagePrompt = `Game Title: ${gradingResult.title}. Core Concept: ${description}`;
240
-
241
- // 2. Image Generation (Basic)
242
- const imgResult = await AIEngine.generateImage(imagePrompt);
243
- if (imgResult) {
244
- thumbnailBase64 = imgResult.image;
245
- // Add token usage for text + fixed cost
246
- basicTokens += (imgResult.usage || 0);
247
- if (imgResult.image) basicTokens += IMAGE_COST_BASIC;
248
- }
249
- } */
250
-
251
- // Upload Logic
252
  let thumbnailUrl = null;
253
- if (thumbnailBase64 && storage) {
254
- try {
255
- const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
256
- const buffer = Buffer.from(base64Data, 'base64');
257
- const bucket = storage.bucket();
258
- const file = bucket.file(`${projectId}/thumbnail.png`);
259
- await file.save(buffer, { metadata: { contentType: 'image/png' } });
260
- await file.makePublic();
261
- thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
262
- } catch (uploadErr) {
263
- console.error("Storage Upload Failed:", uploadErr);
264
- }
265
- }
266
-
267
- const timestamp = Date.now();
268
- const status = isFailure ? "rejected" : "Idle";
269
 
270
  // Save Data Logic
271
  if (!isFailure) {
@@ -277,11 +206,11 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
277
  answers,
278
  stats: gradingResult,
279
  thumbnail: thumbnailUrl,
280
- createdAt: timestamp,
281
- status,
282
- workerHistory: [],
283
  pmHistory: [],
284
- commandQueue: [],
285
  failureCount: 0
286
  };
287
  await StateManager.updateProject(projectId, memoryObject);
@@ -291,38 +220,37 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
291
  await firestore.collection('projects').doc(projectId).set({
292
  id: projectId,
293
  userId: userId,
294
- assets: thumbnailUrl ? [thumbnailUrl] : [],
295
  createdAt: admin.firestore.FieldValue.serverTimestamp()
296
  });
297
  }
298
 
299
  if (db && !isFailure) {
300
  const updates = {};
301
- updates[`projects/${projectId}/info`] = {
302
  id: projectId,
303
  userId,
304
  title: gradingResult.title || "Untitled",
305
  description,
306
  answers,
307
  stats: gradingResult,
308
- createdAt: timestamp,
309
- status
310
  };
311
- if (thumbnailUrl) updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
312
- updates[`projects/${projectId}/state`] = {
313
  workerHistory: [],
314
- pmHistory: [],
315
- commandQueue: [],
316
  failureCount: 0
317
  };
318
  await db.ref().update(updates);
319
  }
320
 
321
- // Deduct Basic Credits
322
  if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic');
323
- console.log("sending");
324
  res.json({
325
- status: 200,
326
  success: !isFailure,
327
  projectId,
328
  stats: gradingResult,
@@ -332,7 +260,7 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
332
 
333
  } catch (err) {
334
  console.error("Create Error:", err);
335
- if (err.message.includes("Insufficient")) {
336
  return res.status(402).json({ error: err.message });
337
  }
338
  res.status(500).json({ error: "Creation failed" });
@@ -342,14 +270,13 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
342
  // --- CORE ENDPOINTS ---
343
 
344
  async function runBackgroundInitialization(projectId, userId, description) {
345
- console.log(`[Background] Starting initialization for ${projectId}`);
346
 
347
- // Separate Counters
348
- let diamondUsage = 0; // For PM
349
- let basicUsage = 0; // For Worker
350
 
351
  try {
352
- const pmHistory = [];
353
 
354
  // 1. Generate GDD (PM -> Diamond)
355
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
@@ -358,8 +285,8 @@ async function runBackgroundInitialization(projectId, userId, description) {
358
  diamondUsage += (gddResult.usage?.totalTokenCount || 0);
359
  const gddText = gddResult.text;
360
 
361
- pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
362
- pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
363
 
364
  // 2. Generate First Task (PM -> Diamond)
365
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
@@ -368,21 +295,21 @@ async function runBackgroundInitialization(projectId, userId, description) {
368
  diamondUsage += (taskResult.usage?.totalTokenCount || 0);
369
  const taskText = taskResult.text;
370
 
371
- pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
372
- pmHistory.push({ role: 'model', parts: [{ text: taskText }] });
373
 
374
  // 3. Initialize Worker (Worker -> Basic)
375
  const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
376
- const workerHistory = [];
377
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
378
 
379
- const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
380
 
381
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
382
  const workerText = workerResult.text;
383
 
384
- workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
385
- workerHistory.push({ role: 'model', parts: [{ text: workerText }] });
386
 
387
  // 4. Update Memory
388
  await StateManager.updateProject(projectId, {
@@ -393,8 +320,7 @@ async function runBackgroundInitialization(projectId, userId, description) {
393
  failureCount: 0
394
  });
395
 
396
- // 5. Queue commands (Handle assets + basic costs)
397
- // We pass userId to deduct image costs (Basic) immediately inside this function
398
  await processAndQueueResponse(projectId, workerText, userId);
399
 
400
  // 6. Update Status
@@ -407,10 +333,10 @@ async function runBackgroundInitialization(projectId, userId, description) {
407
  if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
408
  if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
409
 
410
- console.log(`[Background] Init complete. Diamond: ${diamondUsage}, Basic: ${basicUsage}`);
411
 
412
  } catch (err) {
413
- console.error(`[Background] Init Error for ${projectId}:`, err);
414
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
415
  }
416
  }
@@ -419,7 +345,6 @@ app.post('/new/project', validateRequest, async (req, res) => {
419
  const { userId, projectId, description } = req.body;
420
 
421
  try {
422
- // Init requires BOTH types to be safe
423
  await checkMinimumCredits(userId, 'diamond');
424
  await checkMinimumCredits(userId, 'basic');
425
 
@@ -433,13 +358,14 @@ app.post('/new/project', validateRequest, async (req, res) => {
433
  runBackgroundInitialization(projectId, userId, description);
434
 
435
  } catch (err) {
436
- if (err.message.includes("Insufficient")) {
437
  return res.status(402).json({ error: err.message });
438
  }
439
  res.status(500).json({ error: "Failed to start project" });
440
  }
441
  });
442
 
 
443
  app.post('/project/feedback', async (req, res) => {
444
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
445
 
@@ -451,48 +377,41 @@ app.post('/project/feedback', async (req, res) => {
451
  if (!project) return res.status(404).json({ error: "Project not found." });
452
  if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
453
 
454
- // Basic Check (most interaction is worker)
455
  await checkMinimumCredits(userId, 'basic');
456
 
457
  if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
458
 
 
459
  if (taskComplete) {
460
- console.log(`[${projectId}] ✅ TASK COMPLETE.`);
461
  const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
462
 
463
- // PM Call -> Diamond
464
  const pmResult = await AIEngine.callPM(project.pmHistory, summary);
465
  diamondUsage += (pmResult.usage?.totalTokenCount || 0);
466
  const pmText = pmResult.text;
467
 
468
- project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
469
- project.pmHistory.push({ role: 'model', parts: [{ text: pmText }] });
470
 
471
  const nextInstruction = extractWorkerPrompt(pmText);
472
  if (!nextInstruction) {
473
- await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
474
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
475
-
476
- // Deduct what we used
477
  if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
478
-
479
  return res.json({ success: true, message: "No further tasks. Project Idle." });
480
  }
481
 
482
- const newWorkerHistory = [];
483
  const newPrompt = `New Objective: ${nextInstruction}`;
484
-
485
- // Worker Call -> Basic
486
- const workerResult = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
487
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
488
  const workerText = workerResult.text;
489
 
490
- newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
491
- newWorkerHistory.push({ role: 'model', parts: [{ text: workerText }] });
492
 
493
  await StateManager.updateProject(projectId, {
494
  pmHistory: project.pmHistory,
495
- workerHistory: newWorkerHistory,
496
  failureCount: 0
497
  });
498
 
@@ -500,34 +419,31 @@ app.post('/project/feedback', async (req, res) => {
500
  await processAndQueueResponse(projectId, workerText, userId);
501
 
502
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
503
-
504
- // Final Bill
505
  if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
506
  if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
507
 
508
  return res.json({ success: true, message: "Next Task Assigned" });
509
  }
510
 
511
- // Logic for Failure Handling (PM Guidance)
512
  if (project.failureCount > 3) {
513
  const pmPrompt = (sysPrompts.pm_guidance_prompt || "Analyze logs: {{LOGS}}").replace('{{LOGS}}', logContext?.logs);
514
 
515
- // PM -> Diamond
516
  const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
517
  diamondUsage += (pmResult.usage?.totalTokenCount || 0);
518
  const pmVerdict = pmResult.text;
519
 
520
- if (pmVerdict.includes("[TERMINATE]")) {
521
- const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
522
- const resetHistory = [];
523
- const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
524
 
525
- // Worker -> Basic
526
- const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []);
 
527
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
528
 
529
- resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
530
- resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
531
 
532
  await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
533
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
@@ -537,14 +453,12 @@ app.post('/project/feedback', async (req, res) => {
537
  if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
538
  return res.json({ success: true, message: "Worker Terminated." });
539
  } else {
540
- const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
541
-
542
- // Worker -> Basic
543
- const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []);
544
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
545
 
546
- project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
547
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
548
 
549
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
550
  await processAndQueueResponse(projectId, workerResult.text, userId);
@@ -555,48 +469,91 @@ app.post('/project/feedback', async (req, res) => {
555
  }
556
  }
557
 
558
- // Standard Interaction (Worker Only = Basic)
559
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
560
 
561
- let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
562
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
563
  let responseText = workerResult.text;
564
 
565
- // PM Consultation Check
566
- const pmQuestion = extractPMQuestion(responseText);
567
- if (pmQuestion) {
568
- // PM -> Diamond
569
- const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
570
- const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
 
 
 
571
  diamondUsage += (pmResult.usage?.totalTokenCount || 0);
572
- const pmAnswer = pmResult.text;
573
 
574
- project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
575
- project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
576
 
577
- const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
578
- project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
579
- project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
580
-
581
- // Re-call Worker -> Basic
582
- workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
583
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
584
- responseText = workerResult.text;
585
 
586
- project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
587
- project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
588
- } else {
589
- project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
590
- project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  }
592
 
 
593
  await StateManager.updateProject(projectId, {
594
  workerHistory: project.workerHistory,
595
  pmHistory: project.pmHistory,
596
  failureCount: project.failureCount
597
  });
598
 
599
- await processAndQueueResponse(projectId, responseText, userId);
 
 
 
600
 
601
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
602
 
@@ -608,7 +565,7 @@ app.post('/project/feedback', async (req, res) => {
608
 
609
  } catch (err) {
610
  console.error("AI Error:", err);
611
- if (err.message.includes("Insufficient")) {
612
  return res.status(402).json({ error: err.message });
613
  }
614
  res.status(500).json({ error: "AI Failed" });
@@ -648,19 +605,18 @@ app.post('/human/override', validateRequest, async (req, res) => {
648
  await checkMinimumCredits(userId, 'basic');
649
 
650
  const project = await StateManager.getProject(projectId);
651
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
652
 
653
  if (pruneHistory && project.workerHistory.length >= 2) {
654
  project.workerHistory.pop();
655
  project.workerHistory.pop();
656
  }
657
 
658
- // Worker -> Basic
659
- const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
660
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
661
 
662
- project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
663
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
664
 
665
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
666
  await processAndQueueResponse(projectId, workerResult.text, userId);
@@ -669,36 +625,34 @@ app.post('/human/override', validateRequest, async (req, res) => {
669
 
670
  res.json({ success: true });
671
  } catch (err) {
672
- if (err.message.includes("Insufficient")) {
673
  return res.status(402).json({ error: err.message });
674
  }
675
  res.status(500).json({ error: "Override Failed" });
676
  }
677
  });
678
 
679
- // Helper to handle Asset Parsing & Billing (Basic Credits)
680
  async function processAndQueueResponse(projectId, rawResponse, userId) {
 
 
681
  const imgPrompt = extractImagePrompt(rawResponse);
682
  if (imgPrompt) {
683
- console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
684
 
685
- // 1. Generate Image (returns { image, usage } or null)
686
  const imgResult = await AIEngine.generateImage(imgPrompt);
687
 
688
  if (imgResult && imgResult.image) {
689
- // 2. Bill for image tokens immediately (Basic)
690
  const imgTokens = imgResult.usage?.totalTokenCount || 0;
691
- const totalCost = imgTokens; // imgTokens + IMAGE_COST_BASIC;
692
 
693
  if (userId && totalCost > 0) {
694
  await deductUserCredits(userId, totalCost, 'basic');
695
  }
696
 
697
- // 3. Queue creation command
698
  await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
699
  }
700
  }
701
- // Queue the raw text response for the frontend
702
  await StateManager.queueCommand(projectId, rawResponse);
703
  }
704
 
 
1
+ ---
2
+
3
+ ### 3. The ENTIRE Updated `App.js`
4
+ *(Added `extractRouteToPM` and the massive Interceptor logic inside `/project/feedback`)*
5
+
6
+ ```javascript
7
+ // App.js
8
+
9
  import express from 'express';
10
  import bodyParser from 'body-parser';
11
  import cors from 'cors';
 
25
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
26
 
27
  // Specific bucket as requested
28
+ const bucketName = `hollowpad-ai-default-rtdb.firebaseio.com`
29
 
30
  if (admin.apps.length === 0) {
31
  admin.initializeApp({
 
50
  const app = express();
51
  const PORT = process.env.PORT || 7860;
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  // Load prompts safely
54
  let sysPrompts = {};
55
  try {
 
62
  app.use(bodyParser.json({ limit: '50mb' }));
63
 
64
  // --- CREDIT CONFIGURATION ---
 
 
65
  const MIN_BASIC_REQUIRED = 50;
66
  const MIN_DIAMOND_REQUIRED = 50;
 
 
67
  const IMAGE_COST_BASIC = 1000;
68
 
 
 
 
 
 
69
  async function checkMinimumCredits(userId, type = 'basic') {
70
  if (!db) return;
71
 
 
72
  const snap = await db.ref(`users/${userId}/credits/${type}`).once('value');
73
  const credits = snap.val() || 0;
 
74
  const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
75
 
76
  if (credits < required) {
77
+ throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required} to proceed.`);
 
 
 
 
 
 
78
  }
79
  }
80
 
 
 
 
 
 
 
81
  async function deductUserCredits(userId, amount, type = 'basic') {
82
  if (!db || !amount || amount <= 0) return;
83
 
 
87
  const current = current_credits || 0;
88
  return Math.max(0, current - amount);
89
  });
90
+ console.log(` Deducted ${amount} ${type} credits from User ${userId}`);
91
  } catch (err) {
92
+ console.error(` Failed to deduct ${amount} ${type} from ${userId}:`, err);
93
  }
94
  }
95
 
96
  // --- MIDDLEWARE ---
 
97
  const validateRequest = (req, res, next) => {
98
  if (req.path.includes('/admin/cleanup')) return next();
99
 
100
  const { userId, projectId } = req.body;
101
 
 
102
  if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
103
  return res.status(400).json({ error: "Missing userId" });
104
  }
 
106
  next();
107
  };
108
 
109
+ // --- REGEX HELPERS ---
110
  function extractWorkerPrompt(text) {
111
  const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
112
+ return match ? match.trim() : null;
113
  }
114
 
115
  function formatContext({ hierarchyContext, scriptContext, logContext }) {
116
  let out = "";
117
+ if (scriptContext) out += `\n: ${scriptContext.targetName}\n: ${scriptContext.scriptSource}`;
118
+ if (logContext) out += `\n: ${logContext.logs}`;
119
+ if (hierarchyContext) out += `\n: ${hierarchyContext}`;
120
+ return out;
 
 
121
  }
122
 
123
  function extractPMQuestion(text) {
124
+ const match = text.match(/\/s);
125
+ return match ? match.trim() : null;
126
  }
127
 
128
  function extractImagePrompt(text) {
129
+ const match = text.match(/\/s);
130
+ return match ? match.trim() : null;
131
  }
132
 
133
+ function extractRouteToPM(text) {
134
+ const match = text.match(/\/s);
135
+ return match ? match.trim() : null;
136
+ }
137
 
138
+ // --- ADMIN ENDPOINTS ---
139
  app.get('/admin/cleanup', async (req, res) => {
140
  try {
141
  const removed = StateManager.cleanupMemory();
 
146
  });
147
 
148
  // --- ONBOARDING ENDPOINTS ---
 
149
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
150
  const { description, userId } = req.body;
151
 
152
  if (!description) return res.status(400).json({ error: "Description required" });
153
 
154
  try {
 
155
  await checkMinimumCredits(userId, 'basic');
156
+ console.log(` Analyzing idea...`);
 
157
 
158
  const result = await AIEngine.generateEntryQuestions(description);
 
159
  const usage = result.usage?.totalTokenCount || 0;
160
 
 
161
  if (usage > 0) await deductUserCredits(userId, usage, 'basic');
162
 
163
  if (result.status === "REJECTED") {
 
170
 
171
  } catch (err) {
172
  console.error(err);
173
+ if (err.message && err.message.includes("Insufficient")) {
174
  return res.status(402).json({ error: err.message });
175
  }
176
  res.status(500).json({ error: "Analysis failed" });
 
179
 
180
  app.post('/onboarding/create', validateRequest, async (req, res) => {
181
  const { userId, description, answers } = req.body;
182
+ let basicTokens = 0;
183
 
184
  try {
 
185
  await checkMinimumCredits(userId, 'basic');
186
 
187
  const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
188
  const projectId = `proj_${Date.now()}_${randomHex(7)}`;
189
 
190
+ console.log(` Grading Project ${projectId}...`);
191
 
 
192
  const gradingResult = await AIEngine.gradeProject(description, answers);
193
  basicTokens += (gradingResult.usage?.totalTokenCount || 0);
194
 
195
  const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
 
196
  let thumbnailBase64 = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  let thumbnailUrl = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  // Save Data Logic
200
  if (!isFailure) {
 
206
  answers,
207
  stats: gradingResult,
208
  thumbnail: thumbnailUrl,
209
+ createdAt: Date.now(),
210
+ status: "Idle",
211
+ workerHistory:[],
212
  pmHistory: [],
213
+ commandQueue:[],
214
  failureCount: 0
215
  };
216
  await StateManager.updateProject(projectId, memoryObject);
 
220
  await firestore.collection('projects').doc(projectId).set({
221
  id: projectId,
222
  userId: userId,
223
+ assets: thumbnailUrl ? :[],
224
  createdAt: admin.firestore.FieldValue.serverTimestamp()
225
  });
226
  }
227
 
228
  if (db && !isFailure) {
229
  const updates = {};
230
+ updates = {
231
  id: projectId,
232
  userId,
233
  title: gradingResult.title || "Untitled",
234
  description,
235
  answers,
236
  stats: gradingResult,
237
+ createdAt: Date.now(),
238
+ status: "Idle"
239
  };
240
+ if (thumbnailUrl) updates = { url: thumbnailUrl };
241
+ updates = {
242
  workerHistory: [],
243
+ pmHistory:[],
244
+ commandQueue:[],
245
  failureCount: 0
246
  };
247
  await db.ref().update(updates);
248
  }
249
 
 
250
  if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic');
251
+
252
  res.json({
253
+ status: 200,
254
  success: !isFailure,
255
  projectId,
256
  stats: gradingResult,
 
260
 
261
  } catch (err) {
262
  console.error("Create Error:", err);
263
+ if (err.message && err.message.includes("Insufficient")) {
264
  return res.status(402).json({ error: err.message });
265
  }
266
  res.status(500).json({ error: "Creation failed" });
 
270
  // --- CORE ENDPOINTS ---
271
 
272
  async function runBackgroundInitialization(projectId, userId, description) {
273
+ console.log(` Starting initialization for ${projectId}`);
274
 
275
+ let diamondUsage = 0;
276
+ let basicUsage = 0;
 
277
 
278
  try {
279
+ const pmHistory =[];
280
 
281
  // 1. Generate GDD (PM -> Diamond)
282
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
 
285
  diamondUsage += (gddResult.usage?.totalTokenCount || 0);
286
  const gddText = gddResult.text;
287
 
288
+ pmHistory.push({ role: 'user', parts: });
289
+ pmHistory.push({ role: 'model', parts: });
290
 
291
  // 2. Generate First Task (PM -> Diamond)
292
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
 
295
  diamondUsage += (taskResult.usage?.totalTokenCount || 0);
296
  const taskText = taskResult.text;
297
 
298
+ pmHistory.push({ role: 'user', parts: });
299
+ pmHistory.push({ role: 'model', parts: });
300
 
301
  // 3. Initialize Worker (Worker -> Basic)
302
  const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
303
+ const workerHistory =[];
304
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
305
 
306
+ const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt,[]);
307
 
308
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
309
  const workerText = workerResult.text;
310
 
311
+ workerHistory.push({ role: 'user', parts: });
312
+ workerHistory.push({ role: 'model', parts: });
313
 
314
  // 4. Update Memory
315
  await StateManager.updateProject(projectId, {
 
320
  failureCount: 0
321
  });
322
 
323
+ // 5. Queue commands
 
324
  await processAndQueueResponse(projectId, workerText, userId);
325
 
326
  // 6. Update Status
 
333
  if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
334
  if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
335
 
336
+ console.log(` Init complete. Diamond: ${diamondUsage}, Basic: ${basicUsage}`);
337
 
338
  } catch (err) {
339
+ console.error(` Init Error for ${projectId}:`, err);
340
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
341
  }
342
  }
 
345
  const { userId, projectId, description } = req.body;
346
 
347
  try {
 
348
  await checkMinimumCredits(userId, 'diamond');
349
  await checkMinimumCredits(userId, 'basic');
350
 
 
358
  runBackgroundInitialization(projectId, userId, description);
359
 
360
  } catch (err) {
361
+ if (err.message && err.message.includes("Insufficient")) {
362
  return res.status(402).json({ error: err.message });
363
  }
364
  res.status(500).json({ error: "Failed to start project" });
365
  }
366
  });
367
 
368
+ // MAIN FEEDBACK LOOP
369
  app.post('/project/feedback', async (req, res) => {
370
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
371
 
 
377
  if (!project) return res.status(404).json({ error: "Project not found." });
378
  if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
379
 
 
380
  await checkMinimumCredits(userId, 'basic');
381
 
382
  if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
383
 
384
+ // SCENARIO 1: TASK COMPLETE (Worker asks PM for next job)
385
  if (taskComplete) {
386
+ console.log(` ✅ TASK COMPLETE.`);
387
  const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
388
 
 
389
  const pmResult = await AIEngine.callPM(project.pmHistory, summary);
390
  diamondUsage += (pmResult.usage?.totalTokenCount || 0);
391
  const pmText = pmResult.text;
392
 
393
+ project.pmHistory.push({ role: 'user', parts: });
394
+ project.pmHistory.push({ role: 'model', parts: });
395
 
396
  const nextInstruction = extractWorkerPrompt(pmText);
397
  if (!nextInstruction) {
398
+ await StateManager.updateProject(projectId, { pmHistory: project.pmHistory });
399
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
 
 
400
  if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
 
401
  return res.json({ success: true, message: "No further tasks. Project Idle." });
402
  }
403
 
 
404
  const newPrompt = `New Objective: ${nextInstruction}`;
405
+ const workerResult = await AIEngine.callWorker(project.workerHistory, newPrompt,[]);
 
 
406
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
407
  const workerText = workerResult.text;
408
 
409
+ project.workerHistory.push({ role: 'user', parts: });
410
+ project.workerHistory.push({ role: 'model', parts: });
411
 
412
  await StateManager.updateProject(projectId, {
413
  pmHistory: project.pmHistory,
414
+ workerHistory: project.workerHistory,
415
  failureCount: 0
416
  });
417
 
 
419
  await processAndQueueResponse(projectId, workerText, userId);
420
 
421
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
 
 
422
  if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
423
  if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
424
 
425
  return res.json({ success: true, message: "Next Task Assigned" });
426
  }
427
 
428
+ // SCENARIO 2: FAILURE ESCALATION
429
  if (project.failureCount > 3) {
430
  const pmPrompt = (sysPrompts.pm_guidance_prompt || "Analyze logs: {{LOGS}}").replace('{{LOGS}}', logContext?.logs);
431
 
 
432
  const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
433
  diamondUsage += (pmResult.usage?.totalTokenCount || 0);
434
  const pmVerdict = pmResult.text;
435
 
436
+ if (pmVerdict.includes("")) {
437
+ const fixInstruction = pmVerdict.replace("", "").trim();
438
+ const resetPrompt = `: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
 
439
 
440
+ // Clear History on terminate
441
+ const resetHistory =[];
442
+ const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt,[]);
443
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
444
 
445
+ resetHistory.push({ role: 'user', parts: });
446
+ resetHistory.push({ role: 'model', parts: });
447
 
448
  await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
449
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
 
453
  if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
454
  return res.json({ success: true, message: "Worker Terminated." });
455
  } else {
456
+ const injection = `: ${pmVerdict} \n\nApply this fix now.`;
457
+ const workerResult = await AIEngine.callWorker(project.workerHistory, injection,[]);
 
 
458
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
459
 
460
+ project.workerHistory.push({ role: 'user', parts: });
461
+ project.workerHistory.push({ role: 'model', parts: });
462
 
463
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
464
  await processAndQueueResponse(projectId, workerResult.text, userId);
 
469
  }
470
  }
471
 
472
+ // SCENARIO 3: STANDARD INTERACTION (Worker is the Frontline)
473
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
474
 
475
+ let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images ||[]);
476
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
477
  let responseText = workerResult.text;
478
 
479
+ // INTERCEPTOR A: Worker routes complex task to PM
480
+ const routeTask = extractRouteToPM(responseText);
481
+
482
+ if (routeTask) {
483
+ console.log(` 🔀 Task routed to PM: ${routeTask}`);
484
+
485
+ // PM generates Backend Code (Diamond)
486
+ const pmPrompt = `: ${routeTask}\nWrite the core server/backend logic yourself. Then use WORKER_PROMPT: <task> to delegate the UI/Client aspects to the Worker.`;
487
+ const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
488
  diamondUsage += (pmResult.usage?.totalTokenCount || 0);
489
+ const pmText = pmResult.text;
490
 
491
+ project.pmHistory.push({ role: 'user', parts: });
492
+ project.pmHistory.push({ role: 'model', parts: });
493
 
494
+ // Queue PM's Backend Code to Studio
495
+ await processAndQueueResponse(projectId, pmText, userId);
 
 
 
 
 
 
496
 
497
+ // Did the PM give the Worker UI/Client homework?
498
+ const nextInstruction = extractWorkerPrompt(pmText);
499
+ if (nextInstruction) {
500
+ const delegationPrompt = `: The PM has handled the backend logic. Now execute this UI/Client task: ${nextInstruction}`;
501
+
502
+ const workerDelegationResult = await AIEngine.callWorker(project.workerHistory, delegationPrompt,[]);
503
+ basicUsage += (workerDelegationResult.usage?.totalTokenCount || 0);
504
+ responseText = workerDelegationResult.text;
505
+
506
+ project.workerHistory.push({ role: 'user', parts: });
507
+ project.workerHistory.push({ role: 'model', parts: });
508
+ } else {
509
+ responseText = "System Note: The PM completed the core task without delegating client logic.";
510
+ project.workerHistory.push({ role: 'user', parts: });
511
+ project.workerHistory.push({ role: 'model', parts: });
512
+ }
513
+ }
514
+ // INTERCEPTOR B: Worker asks PM a question
515
+ else {
516
+ const pmQuestion = extractPMQuestion(responseText);
517
+ if (pmQuestion) {
518
+ console.log(` ❓ Worker asked PM: ${pmQuestion}`);
519
+
520
+ const pmConsultPrompt = `: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
521
+ const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
522
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
523
+ const pmAnswer = pmResult.text;
524
+
525
+ project.pmHistory.push({ role: 'user', parts: });
526
+ project.pmHistory.push({ role: 'model', parts: });
527
+
528
+ const injectionMsg = `: ${pmAnswer}`;
529
+ project.workerHistory.push({ role: 'user', parts: });
530
+ project.workerHistory.push({ role: 'model', parts: });
531
+
532
+ // Re-call Worker
533
+ workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg,[]);
534
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
535
+ responseText = workerResult.text;
536
+
537
+ project.workerHistory.push({ role: 'user', parts: });
538
+ project.workerHistory.push({ role: 'model', parts: });
539
+ } else {
540
+ // Standard Worker Execution
541
+ project.workerHistory.push({ role: 'user', parts: });
542
+ project.workerHistory.push({ role: 'model', parts: });
543
+ }
544
  }
545
 
546
+ // Save State & Queue Responses
547
  await StateManager.updateProject(projectId, {
548
  workerHistory: project.workerHistory,
549
  pmHistory: project.pmHistory,
550
  failureCount: project.failureCount
551
  });
552
 
553
+ if (!routeTask) {
554
+ // Only queue the worker response if we didn't route to PM (if routed, PM already queued its code above)
555
+ await processAndQueueResponse(projectId, responseText, userId);
556
+ }
557
 
558
  if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
559
 
 
565
 
566
  } catch (err) {
567
  console.error("AI Error:", err);
568
+ if (err.message && err.message.includes("Insufficient")) {
569
  return res.status(402).json({ error: err.message });
570
  }
571
  res.status(500).json({ error: "AI Failed" });
 
605
  await checkMinimumCredits(userId, 'basic');
606
 
607
  const project = await StateManager.getProject(projectId);
608
+ const overrideMsg = `: ${instruction}`;
609
 
610
  if (pruneHistory && project.workerHistory.length >= 2) {
611
  project.workerHistory.pop();
612
  project.workerHistory.pop();
613
  }
614
 
615
+ const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg,[]);
 
616
  basicUsage += (workerResult.usage?.totalTokenCount || 0);
617
 
618
+ project.workerHistory.push({ role: 'user', parts: });
619
+ project.workerHistory.push({ role: 'model', parts: });
620
 
621
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
622
  await processAndQueueResponse(projectId, workerResult.text, userId);
 
625
 
626
  res.json({ success: true });
627
  } catch (err) {
628
+ if (err.message && err.message.includes("Insufficient")) {
629
  return res.status(402).json({ error: err.message });
630
  }
631
  res.status(500).json({ error: "Override Failed" });
632
  }
633
  });
634
 
635
+ // Helper to handle Asset Parsing & Billing
636
  async function processAndQueueResponse(projectId, rawResponse, userId) {
637
+ if (!rawResponse) return;
638
+
639
  const imgPrompt = extractImagePrompt(rawResponse);
640
  if (imgPrompt) {
641
+ console.log(` 🎨 Generating Asset: ${imgPrompt}`);
642
 
 
643
  const imgResult = await AIEngine.generateImage(imgPrompt);
644
 
645
  if (imgResult && imgResult.image) {
 
646
  const imgTokens = imgResult.usage?.totalTokenCount || 0;
647
+ const totalCost = imgTokens;
648
 
649
  if (userId && totalCost > 0) {
650
  await deductUserCredits(userId, totalCost, 'basic');
651
  }
652
 
 
653
  await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
654
  }
655
  }
 
656
  await StateManager.queueCommand(projectId, rawResponse);
657
  }
658