everydaycats commited on
Commit
8086716
·
verified ·
1 Parent(s): 993fdee

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +300 -108
app.js CHANGED
@@ -48,10 +48,65 @@ const sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
48
  app.use(cors());
49
  app.use(bodyParser.json({ limit: '50mb' }));
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  const validateRequest = (req, res, next) => {
52
- if (req.path.includes('/onboarding') || req.path.includes('/admin/cleanup')) return next();
 
 
53
  const { userId, projectId } = req.body;
54
- if (!userId || !projectId) return res.status(400).json({ error: "Missing ID" });
 
 
 
 
 
55
  next();
56
  };
57
 
@@ -95,13 +150,25 @@ app.get('/admin/cleanup', async (req, res) => {
95
  // --- ONBOARDING ENDPOINTS ---
96
 
97
  app.post('/onboarding/analyze', validateRequest, async (req, res) => {
98
- const { description } = req.body;
99
  if (!description) return res.status(400).json({ error: "Description required" });
 
 
 
100
 
101
  try {
 
 
 
102
  console.log(`[Onboarding] Analyzing idea...`);
 
 
103
  const result = await AIEngine.generateEntryQuestions(description);
 
104
 
 
 
 
105
  if (result.status === "REJECTED") {
106
  return res.json({
107
  rejected: true,
@@ -111,37 +178,57 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
111
  res.json({ questions: result.questions });
112
  } catch (err) {
113
  console.error(err);
 
 
 
114
  res.status(500).json({ error: "Analysis failed" });
115
  }
116
  });
117
 
118
  app.post('/onboarding/create', validateRequest, async (req, res) => {
119
  const { userId, description, answers } = req.body;
 
 
 
 
 
120
 
121
- const randomHex = (n = 6) => {
122
- const bytes = Math.ceil(n / 2);
123
- return crypto.randomBytes(bytes).toString("hex").slice(0, n);
124
- };
125
 
126
- const projectId = `proj_${Date.now()}_${randomHex(7)}`;
127
 
128
- try {
129
  console.log(`[Onboarding] Grading Project ${projectId}...`);
 
 
 
 
130
 
131
  const grading = await AIEngine.gradeProject(description, answers);
 
 
132
  const isFailure = grading.feasibility < 30 || grading.rating === 'F';
133
 
134
  let thumbnailBase64 = null;
135
 
136
  if (isFailure) {
137
  console.log(`[Onboarding] ❌ Project Failed Grading. Skipping Image.`);
138
-
139
  } else {
140
  console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
141
  const imagePrompt = `Game Title: ${grading.title}. Core Concept: ${description}`;
 
 
 
 
142
  thumbnailBase64 = await AIEngine.generateImage(imagePrompt);
 
 
 
143
  }
144
 
 
145
  let thumbnailUrl = null;
146
  if (thumbnailBase64 && storage) {
147
  try {
@@ -227,6 +314,9 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
227
  await StateManager.updateProject(projectId, memoryObject);
228
  }
229
 
 
 
 
230
  res.json({
231
  success: !isFailure,
232
  projectId,
@@ -237,6 +327,9 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
237
 
238
  } catch (err) {
239
  console.error("Create Error:", err);
 
 
 
240
  res.status(500).json({ error: "Creation failed" });
241
  }
242
  });
@@ -246,19 +339,27 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
246
  // NEW: Helper function to run initialization in background
247
  async function runBackgroundInitialization(projectId, userId, description) {
248
  console.log(`[Background] Starting initialization for ${projectId}`);
 
 
249
  try {
250
  const pmHistory = [];
251
 
252
  // 1. Generate GDD
253
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
 
 
254
  const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
 
255
 
256
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
257
  pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
258
 
259
  // 2. Generate First Task
260
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
 
 
261
  const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
 
262
 
263
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
264
  pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
@@ -267,7 +368,10 @@ async function runBackgroundInitialization(projectId, userId, description) {
267
  const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
268
  const workerHistory = [];
269
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
 
 
270
  const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
 
271
 
272
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
273
  workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
@@ -281,146 +385,199 @@ async function runBackgroundInitialization(projectId, userId, description) {
281
  failureCount: 0
282
  });
283
 
284
- await processAndQueueResponse(projectId, workerResponse);
 
 
 
 
 
285
 
286
- // 5. Update Status to IDLE (Ready for user input)
287
  if(db) await db.ref(`projects/${projectId}/info`).update({
288
  status: "IDLE",
289
  lastUpdated: Date.now()
290
  });
 
 
 
291
 
292
- console.log(`[Background] Initialization complete for ${projectId}`);
293
 
294
  } catch (err) {
295
  console.error(`[Background] Init Error for ${projectId}:`, err);
 
 
296
  if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
297
  }
298
  }
299
 
300
- app.post('/new/project', validateRequest, (req, res) => {
301
  const { userId, projectId, description } = req.body;
302
 
303
- // 1. Immediately acknowledge request
304
- // We set status to 'working' or 'initializing' immediately so frontend shows a loader
305
- if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
306
- console.log("Received new project request ")
307
- res.json({
308
- success: true,
309
- message: "Project initialization started in background."
310
- });
311
-
312
- // 2. Trigger background process (no await)
313
- runBackgroundInitialization(projectId, userId, description);
 
 
 
 
 
 
 
 
 
 
 
 
314
  });
315
 
316
  app.post('/project/feedback', async (req, res) => {
317
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
 
318
 
 
319
  const project = await StateManager.getProject(projectId);
320
-
321
  if (!project) return res.status(404).json({ error: "Project not found." });
322
 
323
  if (project.userId !== userId) {
324
  console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
325
  return res.status(403).json({ error: "Unauthorized: You do not own this project." });
326
  }
327
-
328
- // Update status to working immediately
329
- if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
330
-
331
- if (taskComplete) {
332
- console.log(`[${projectId}] ✅ TASK COMPLETE.`);
333
- const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
334
- const pmResponse = await AIEngine.callPM(project.pmHistory, summary);
335
 
336
- project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
337
- project.pmHistory.push({ role: 'model', parts: [{ text: pmResponse }] });
338
-
339
- const nextInstruction = extractWorkerPrompt(pmResponse);
340
- if (!nextInstruction) {
341
- await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
342
- if(db) {
343
- await db.ref(`projects/${projectId}/info`).update({
344
- status: "IDLE",
345
- lastUpdated: Date.now()
346
- });
347
- }
348
- return res.json({ success: true, message: "No further tasks. Project Idle." });
349
- }
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
- const newWorkerHistory = [];
352
- const newPrompt = `New Objective: ${nextInstruction}`;
353
- const workerResponse = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
354
 
355
- newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
356
- newWorkerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
357
 
358
- await StateManager.updateProject(projectId, {
359
- pmHistory: project.pmHistory,
360
- workerHistory: newWorkerHistory,
361
- failureCount: 0
362
- });
363
 
364
- StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
365
- await processAndQueueResponse(projectId, workerResponse);
366
-
367
- // Still working on next task
368
- if(db) {
369
- await db.ref(`projects/${projectId}/info`).update({
370
- status: "working",
371
- lastUpdated: Date.now()
372
  });
373
- }
374
- return res.json({ success: true, message: "Next Task Assigned" });
375
- }
376
 
377
- let isFailure = false;
378
- if (logContext?.logs) {
379
- const errs = ["Error", "Exception", "failed", "Stack Begin", "Infinite yield"];
380
- if (errs.some(k => logContext.logs.includes(k))) {
381
- isFailure = true;
382
- project.failureCount = (project.failureCount || 0) + 1;
383
- }
384
- }
 
 
385
 
386
- if (project.failureCount > 3) {
387
- const pmPrompt = sysPrompts.pm_guidance_prompt.replace('{{LOGS}}', logContext?.logs);
388
- const pmVerdict = await AIEngine.callPM(project.pmHistory, pmPrompt);
389
 
390
- if (pmVerdict.includes("[TERMINATE]")) {
391
- const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
392
- const resetHistory = [];
393
- const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
394
- const workerResp = await AIEngine.callWorker(resetHistory, resetPrompt, []);
395
-
396
- resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
397
- resetHistory.push({ role: 'model', parts: [{ text: workerResp }] });
398
 
399
- await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
400
- StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
401
- await processAndQueueResponse(projectId, workerResp);
402
- return res.json({ success: true, message: "Worker Terminated." });
403
- } else {
404
- const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
405
- const workerResp = await AIEngine.callWorker(project.workerHistory, injection, []);
406
-
407
- project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
408
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResp }] });
409
-
410
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
411
- await processAndQueueResponse(projectId, workerResp);
412
- return res.json({ success: true, message: "PM Guidance Applied." });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  }
414
- }
415
 
416
- try {
417
  const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
 
 
418
  let response = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
 
419
 
420
  const pmQuestion = extractPMQuestion(response);
421
  if (pmQuestion) {
422
  const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
 
 
423
  const pmAnswer = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
 
424
 
425
  project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
426
  project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
@@ -429,7 +586,10 @@ app.post('/project/feedback', async (req, res) => {
429
  project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
430
  project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
431
 
 
432
  response = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
 
 
433
  project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
434
  project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
435
  } else {
@@ -443,7 +603,7 @@ app.post('/project/feedback', async (req, res) => {
443
  failureCount: project.failureCount
444
  });
445
 
446
- await processAndQueueResponse(projectId, response);
447
 
448
  // --- UPDATED: Set status to IDLE and update lastUpdated ---
449
  if(db) {
@@ -453,9 +613,18 @@ app.post('/project/feedback', async (req, res) => {
453
  });
454
  }
455
 
 
 
 
456
  res.json({ success: true });
457
  } catch (err) {
458
  console.error("AI Error:", err);
 
 
 
 
 
 
459
  res.status(500).json({ error: "AI Failed" });
460
  }
461
  });
@@ -496,26 +665,49 @@ app.post('/project/ping', async (req, res) => {
496
  });
497
 
498
  app.post('/human/override', validateRequest, async (req, res) => {
499
- const { projectId, instruction, pruneHistory } = req.body;
 
 
 
 
 
 
 
 
 
500
  const project = await StateManager.getProject(projectId);
501
  const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
 
 
502
  if (pruneHistory && project.workerHistory.length >= 2) {
503
  project.workerHistory.pop();
504
  project.workerHistory.pop();
505
  }
506
  const response = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
 
 
507
  project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
508
  project.workerHistory.push({ role: 'model', parts: [{ text: response }] });
509
  await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
510
- await processAndQueueResponse(projectId, response);
 
 
 
511
  res.json({ success: true });
512
  });
513
 
514
- async function processAndQueueResponse(projectId, rawResponse) {
 
515
  const imgPrompt = extractImagePrompt(rawResponse);
516
  if (imgPrompt) {
517
  console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
518
  const base64Image = await AIEngine.generateImage(imgPrompt);
 
 
 
 
 
 
519
  if (base64Image) {
520
  await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: base64Image });
521
  }
 
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
+ }
95
+ }
96
+
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();
111
  };
112
 
 
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,
 
178
  res.json({ questions: result.questions });
179
  } catch (err) {
180
  console.error(err);
181
+ if (err.message.includes("Insufficient credits")) {
182
+ return res.status(402).json({ error: err.message });
183
+ }
184
  res.status(500).json({ error: "Analysis failed" });
185
  }
186
  });
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 {
 
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,
 
327
 
328
  } catch (err) {
329
  console.error("Create Error:", err);
330
+ if (err.message.includes("Insufficient credits")) {
331
+ return res.status(402).json({ error: err.message });
332
+ }
333
  res.status(500).json({ error: "Creation failed" });
334
  }
335
  });
 
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 }] });
 
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 }] });
 
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
 
414
+ 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
+ }
438
+ res.status(500).json({ error: "Failed to start project" });
439
+ }
440
  });
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,
498
+ workerHistory: newWorkerHistory,
499
+ failureCount: 0
 
 
 
 
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"];
520
+ if (errs.some(k => logContext.logs.includes(k))) {
521
+ isFailure = true;
522
+ project.failureCount = (project.failureCount || 0) + 1;
523
+ }
524
+ }
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 }] });
 
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 {
 
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) {
 
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
+ }
628
  res.status(500).json({ error: "AI Failed" });
629
  }
630
  });
 
665
  });
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
  }