everydaytok commited on
Commit
a842e18
·
verified ·
1 Parent(s): 14e4f14

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +650 -43
app.js CHANGED
@@ -60,26 +60,665 @@ function startStatusLoop(projectId, type = 'worker') {
60
  // Stop the loop once we reach the last entry
61
  clearInterval(interval);
62
  }
63
- }, 3500); // 3.5 second delay as requested
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
- // Return a cleanup function in case the component unmounts early
66
- return () => clearInterval(interval);
67
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- /*
70
  function startStatusLoop(projectId, type = 'worker') {
71
  const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
72
  let index = 0;
73
 
 
74
  StateManager.setStatus(projectId, phases[0]);
75
 
76
  const interval = setInterval(() => {
77
- index = (index + 1) % phases.length;
78
- StateManager.setStatus(projectId, phases[index]);
79
- }, 2000); // Slightly faster updates for better UI feedback
 
 
 
 
 
80
 
 
81
  return () => clearInterval(interval);
82
- } */
 
83
 
84
  function extractWorkerPrompt(text) {
85
  if (!text || typeof text !== 'string') return null;
@@ -580,39 +1219,6 @@ app.post('/project/feedback', async (req, res) => {
580
  }
581
  });
582
 
583
- /*
584
- app.post('/project/ping', async (req, res) => {
585
- const { projectId, userId, isFrontend } = req.body;
586
- if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" });
587
-
588
- const project = await StateManager.getProject(projectId);
589
- if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
590
-
591
- if (isFrontend) {
592
- return res.json({
593
- status: StateManager.getStatus(projectId),
594
- snapshot: StateManager.getSnapshot(projectId)
595
- });
596
- }
597
-
598
- const command = await StateManager.popCommand(projectId);
599
- const streamData = StateManager.popStream(projectId);
600
-
601
- let response = { action: "IDLE", stream: streamData || null };
602
-
603
- if (streamData && streamData.length > 0) {
604
- response.action = "STREAM_APPEND";
605
- }
606
-
607
- if (command) {
608
- response.action = command.type;
609
- response.target = command.payload;
610
- response.code = command.type === 'EXECUTE' ? command.payload : null;
611
- }
612
-
613
- res.json(response);
614
- });
615
- */
616
 
617
  app.post('/project/ping', async (req, res) => {
618
  const { projectId, userId, isFrontend } = req.body;
@@ -702,4 +1308,5 @@ app.get('/admin/cleanup', async (req, res) => {
702
 
703
  app.listen(PORT, () => {
704
  console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`);
705
- });
 
 
60
  // Stop the loop once we reach the last entry
61
  clearInterval(interval);
62
  }
63
+ }, 3500);
64
+
65
+ return () => clearInterval(interval);
66
+ };
67
+
68
+ function extractWorkerPrompt(text) {
69
+ if (!text || typeof text !== 'string') return null;
70
+ const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
71
+ return match ? match[1].trim() : null;
72
+ }
73
+
74
+ function extractPMQuestion(text) {
75
+ if (!text || typeof text !== 'string') return null;
76
+ const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
77
+ return match ? match[1].trim() : null;
78
+ }
79
+
80
+ function extractImagePrompt(text) {
81
+ if (!text || typeof text !== 'string') return null;
82
+ const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
83
+ return match ? match[1].trim() : null;
84
+ }
85
+
86
+ function extractRouteToPM(text) {
87
+ if (!text || typeof text !== 'string') return null;
88
+ const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s);
89
+ return match ? match[1].trim() : null;
90
+ }
91
+
92
+ function formatContext({ hierarchyContext, scriptContext, logContext }) {
93
+ let parts = [];
94
+
95
+ if (hierarchyContext) {
96
+ parts.push(`[HIERARCHY_VIEW]\n${hierarchyContext}`);
97
+ }
98
+
99
+ if (scriptContext && scriptContext.targetName) {
100
+ // Enforce code block formatting for the script content
101
+ const source = scriptContext.scriptSource || "(Empty Script)";
102
+ parts.push(`[ACTIVE_SCRIPT: ${scriptContext.targetName}]\n\`\`\`lua\n${source}\n\`\`\``);
103
+ }
104
+
105
+ if (logContext && logContext.logs) {
106
+ parts.push(`[OUTPUT_LOGS]\n${logContext.logs}`);
107
+ }
108
+
109
+ if (parts.length === 0) return "";
110
+
111
+ return `\n\n=== STUDIO CONTEXT ===\n${parts.join("\n\n")}\n======================\n`;
112
+ }
113
+
114
+ const validateRequest = (req, res, next) => {
115
+ if (req.path.includes('/admin/cleanup')) return next();
116
+ const { userId } = req.body;
117
+ if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
118
+ return res.status(400).json({ error: "Missing userId" });
119
+ }
120
+ next();
121
+ };
122
+
123
+ // --- BILLING LOGIC ---
124
+
125
+ async function checkMinimumCredits(userId, type = 'basic') {
126
+ if (!supabase) return;
127
+ const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
128
+ if (!data) return;
129
+
130
+ const credits = data.credits?.[type] || 0;
131
+ const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
132
+
133
+ if (credits < req) {
134
+ throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`);
135
+ }
136
+ }
137
+
138
+ async function deductUserCredits(userId, amount, type = 'basic') {
139
+ const deduction = Math.floor(parseInt(amount, 10));
140
+ if (!supabase || !deduction || deduction <= 0) return;
141
+
142
+ try {
143
+ const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
144
+ if (!data) return;
145
+
146
+ const currentCredits = data.credits || { basic: 0, diamond: 0 };
147
+ const currentVal = currentCredits[type] || 0;
148
+ const newVal = Math.max(0, currentVal - deduction);
149
+
150
+ const updatedCredits = { ...currentCredits, [type]: newVal };
151
+
152
+ await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId);
153
+ console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`);
154
+ } catch (err) {
155
+ console.error("Deduction failed", err);
156
+ }
157
+ }
158
+
159
+ // --- CORE LOGIC: Background Initialization ---
160
+
161
+ async function runBackgroundInitialization(projectId, userId, description) {
162
+ if (StateManager.isLocked(projectId)) {
163
+ console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`);
164
+ return;
165
+ }
166
+
167
+ StateManager.lock(projectId);
168
+ console.log(`[Background] Starting initialization for ${projectId}`);
169
+
170
+ let diamondUsage = 0;
171
+ let basicUsage = 0;
172
+
173
+ try {
174
+ await StateManager.getProject(projectId);
175
+
176
+ await StateManager.updateProject(projectId, {
177
+ status: "working",
178
+ gdd: "",
179
+ failureCount: 0
180
+ });
181
+
182
+ const pmHistory = [];
183
+
184
+ // --- Step 1: Generate GDD (PM) ---
185
+ let stopStatus = startStatusLoop(projectId, 'pm');
186
+ const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`;
187
+ const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
188
+ stopStatus();
189
+
190
+ if (!gddResult?.text) throw new Error("PM failed to generate GDD");
191
+
192
+ const gddText = gddResult.text;
193
+ diamondUsage += (gddResult.usage?.totalTokenCount || 0);
194
+
195
+ await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt);
196
+ await StateManager.addHistory(projectId, 'pm', 'model', gddText);
197
+
198
+ pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
199
+ pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
200
+
201
+ // --- Step 2: Generate Tasks (PM) ---
202
+ stopStatus = startStatusLoop(projectId, 'pm');
203
+ const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
204
+ const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
205
+ stopStatus();
206
+
207
+ if (!taskResult?.text) throw new Error("PM failed to generate Task");
208
+
209
+ const taskText = taskResult.text;
210
+ diamondUsage += (taskResult.usage?.totalTokenCount || 0);
211
+
212
+ await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt);
213
+ await StateManager.addHistory(projectId, 'pm', 'model', taskText);
214
+
215
+ // --- Step 3: Initialize Worker ---
216
+ const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
217
+ const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
218
+
219
+ stopStatus = startStatusLoop(projectId, 'worker');
220
+
221
+ let workerTextAccumulated = "";
222
+
223
+ const workerResult = await AIEngine.callWorkerStream(
224
+ [],
225
+ initialWorkerPrompt,
226
+ (thought) => {
227
+ stopStatus();
228
+ StateManager.setStatus(projectId, "Worker: Thinking...");
229
+ StateManager.appendSnapshotOnly(projectId, thought);
230
+ },
231
+ (chunk) => {
232
+ stopStatus();
233
+ StateManager.setStatus(projectId, "Worker: Coding...");
234
+ workerTextAccumulated += chunk;
235
+ StateManager.appendStream(projectId, chunk);
236
+ }
237
+ );
238
+ stopStatus();
239
+
240
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
241
+
242
+ if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize");
243
+ const finalWorkerText = workerResult.text || workerTextAccumulated;
244
+
245
+ await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt);
246
+ await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText);
247
+
248
+ await StateManager.updateProject(projectId, {
249
+ gdd: gddText,
250
+ status: "idle"
251
+ });
252
+
253
+ await processAndQueueResponse(projectId, finalWorkerText, userId);
254
+ console.log(`[Background] Init complete for ${projectId}`);
255
+
256
+ } catch (err) {
257
+ console.error(`[Background] Init Error for ${projectId}:`, err.message);
258
+ await StateManager.updateProject(projectId, { status: "error" });
259
+ } finally {
260
+ StateManager.unlock(projectId);
261
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
262
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
263
+ }
264
+ }
265
+
266
+ // --- CORE LOGIC: Async Feedback Loop ---
267
+
268
+ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
269
+ let diamondUsage = 0;
270
+ let basicUsage = 0;
271
+
272
+ try {
273
+ console.log(`[${projectId}] Feedback received. Persisting state immediately.`);
274
+
275
+ await Promise.all([
276
+ StateManager.updateProject(projectId, { status: "working" }),
277
+ StateManager.addHistory(projectId, 'worker', 'user', fullInput)
278
+ ]);
279
+
280
+ const project = await StateManager.getProject(projectId);
281
+
282
+ StateManager.clearSnapshot(projectId);
283
+ let stopStatus = startStatusLoop(projectId, 'worker');
284
+
285
+ let firstTurnResponse = "";
286
+ let thoughtText = "";
287
+
288
+ const currentWorkerHistory = [...(project.workerHistory || [])];
289
+
290
+ // 2. First Turn (Worker Execution)
291
+ const workerResult = await AIEngine.callWorkerStream(
292
+ currentWorkerHistory,
293
+ fullInput,
294
+ (thought) => {
295
+ stopStatus();
296
+ thoughtText += thought;
297
+ StateManager.setStatus(projectId, "Worker: Thinking...");
298
+ StateManager.appendSnapshotOnly(projectId, thought);
299
+ },
300
+ (chunk) => {
301
+ stopStatus();
302
+ firstTurnResponse += chunk;
303
+ StateManager.setStatus(projectId, "Worker: Coding...");
304
+ StateManager.appendStream(projectId, chunk);
305
+ },
306
+ images
307
+ );
308
+ stopStatus();
309
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
310
+ firstTurnResponse = workerResult.text || firstTurnResponse;
311
+
312
+ // 3. Analyze Output (Delegation/Questions)
313
+ const routeTask = extractRouteToPM(firstTurnResponse);
314
+ const pmQuestion = extractPMQuestion(firstTurnResponse);
315
+ let finalResponseToSave = firstTurnResponse;
316
+
317
+ if (routeTask || pmQuestion) {
318
+ await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
319
+ currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] });
320
+
321
+ let pmPrompt = "";
322
+ let pmContextPrefix = "";
323
+
324
+ if (routeTask) {
325
+ console.log(`[${projectId}] Routing task to PM...`);
326
+ pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.`;
327
+ pmContextPrefix = "[PM DELEGATION]:";
328
+ } else {
329
+ console.log(`[${projectId}] Worker consulting PM...`);
330
+ pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`;
331
+ pmContextPrefix = "[PM ANSWER]:";
332
+ }
333
+
334
+ // 4. Run PM (Diamond Logic)
335
+ StateManager.clearSnapshot(projectId);
336
+ stopStatus = startStatusLoop(projectId, 'pm');
337
+
338
+ let pmResponseText = "";
339
+
340
+ const pmResult = await AIEngine.callPMStream(
341
+ project.pmHistory,
342
+ pmPrompt,
343
+ (thought) => {
344
+ stopStatus();
345
+ StateManager.setStatus(projectId, "Manager: Thinking...");
346
+ StateManager.appendSnapshotOnly(projectId, thought);
347
+ },
348
+ (chunk) => {
349
+ stopStatus();
350
+ StateManager.setStatus(projectId, "Manager: Architecture...");
351
+ pmResponseText += chunk;
352
+ StateManager.appendSnapshotOnly(projectId, chunk);
353
+ }
354
+ );
355
+ stopStatus();
356
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
357
+ pmResponseText = pmResult.text || pmResponseText;
358
+
359
+ await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt);
360
+ await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
361
+
362
+ // --- EXECUTE PM CODE ---
363
+ await processAndQueueResponse(projectId, pmResponseText, userId);
364
+
365
+ // 5. Run Worker Continuation
366
+ const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText;
367
+ const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`;
368
+
369
+ await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt);
370
+ currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] });
371
+
372
+ StateManager.clearSnapshot(projectId);
373
+ stopStatus = startStatusLoop(projectId, 'worker');
374
+
375
+ let secondTurnResponse = "";
376
+
377
+ const secondWorkerResult = await AIEngine.callWorkerStream(
378
+ currentWorkerHistory,
379
+ workerContinuationPrompt,
380
+ (thought) => {
381
+ stopStatus();
382
+ StateManager.setStatus(projectId, "Worker: Applying Fix...");
383
+ StateManager.appendSnapshotOnly(projectId, thought);
384
+ },
385
+ (chunk) => {
386
+ stopStatus();
387
+ StateManager.setStatus(projectId, "Worker: Finalizing...");
388
+ secondTurnResponse += chunk;
389
+ StateManager.appendStream(projectId, chunk);
390
+ }
391
+ );
392
+ stopStatus();
393
+
394
+ basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0);
395
+ secondTurnResponse = secondWorkerResult.text || secondTurnResponse;
396
+
397
+ await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse);
398
+ finalResponseToSave = secondTurnResponse;
399
+
400
+ } else {
401
+ // No delegation, just save the first response
402
+ await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
403
+ }
404
+
405
+ StateManager.setStatus(projectId, "Idle");
406
+
407
+ // 6. Final Execution & Cleanup
408
+ await processAndQueueResponse(projectId, finalResponseToSave, userId);
409
+
410
+ await StateManager.updateProject(projectId, { status: "idle" });
411
+
412
+ } catch (err) {
413
+ console.error("Async Feedback Error:", err);
414
+ StateManager.setStatus(projectId, "Error: " + err.message);
415
+ await StateManager.updateProject(projectId, { status: "error" });
416
+ } finally {
417
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
418
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
419
+ }
420
+ }
421
+
422
+ async function processAndQueueResponse(projectId, rawResponse, userId) {
423
+ if (!rawResponse) return;
424
+
425
+ // Image Generation
426
+ const imgPrompt = extractImagePrompt(rawResponse);
427
+ if (imgPrompt) {
428
+ try {
429
+ const imgResult = await AIEngine.generateImage(imgPrompt);
430
+ if (imgResult?.image) {
431
+ if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee
432
+ await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
433
+ }
434
+ } catch (e) {
435
+ console.error("Image gen failed", e);
436
+ }
437
+ }
438
+
439
+ // Code Execution
440
+ const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
441
+ if (codeMatch) {
442
+ await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() });
443
+ }
444
+
445
+ // Explicit Read Commands (Parsing what the AI wrote)
446
+ // This allows the AI to output [READ_SCRIPT: Game.ServerScriptService.Main]
447
+ // And have the backend parse it for the plugin to pick up.
448
+ // (Note: StateManager.queueCommand handles the parsing logic too, but we call it here to trigger it)
449
+ await StateManager.queueCommand(projectId, rawResponse);
450
+ }
451
+
452
+ // --- EXPRESS ROUTES ---
453
+
454
+ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
455
+ const { description, userId } = req.body;
456
+ if (!description) return res.status(400).json({ error: "Description required" });
457
+
458
+ try {
459
+ try {
460
+ await checkMinimumCredits(userId, 'basic');
461
+ } catch (creditErr) {
462
+ return res.json({ success: false, insufficient: true, error: creditErr.message });
463
+ }
464
+
465
+ const result = await AIEngine.generateEntryQuestions(description);
466
+
467
+ if (result.usage?.totalTokenCount > 0) {
468
+ await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
469
+ }
470
+
471
+ if (result.status === "REJECTED") {
472
+ return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." });
473
+ }
474
+
475
+ res.json({ questions: result.questions });
476
+ } catch (err) {
477
+ res.status(500).json({ error: err.message });
478
+ }
479
+ });
480
 
481
+ app.post('/onboarding/create', validateRequest, async (req, res) => {
482
+ const { userId, description, answers } = req.body;
483
+
484
+ try {
485
+
486
+ try {
487
+ await checkMinimumCredits(userId, 'basic');
488
+ await checkMinimumCredits(userId, 'diamond');
489
+ } catch (creditErr) {
490
+ return res.json({ success: false, insufficient: true, error: creditErr.message });
491
+ }
492
+
493
+ const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
494
+ const projectId = `proj_${Date.now()}_${randomHex(7)}`;
495
+
496
+ const gradingResult = await AIEngine.gradeProject(description, answers);
497
+
498
+ if (gradingResult.usage?.totalTokenCount) {
499
+ await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic');
500
+ }
501
+
502
+ const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
503
+
504
+ if (!isFailure) {
505
+ const { error: insertError } = await supabase.from('projects').insert({
506
+ id: projectId,
507
+ user_id: userId,
508
+ info: {
509
+ title: gradingResult.title || "Untitled",
510
+ stats: gradingResult,
511
+ description,
512
+ answers,
513
+ status: "initializing"
514
+ }
515
+ });
516
+
517
+ if (insertError) {
518
+ console.error("Failed to insert project:", insertError.message);
519
+ return res.status(500).json({ error: "Database save failed" });
520
+ }
521
+
522
+ runBackgroundInitialization(projectId, userId, description);
523
+ }
524
+
525
+ res.json({
526
+ status: 200,
527
+ success: !isFailure,
528
+ projectId,
529
+ stats: gradingResult,
530
+ title: gradingResult.title || "Untitled"
531
+ });
532
+
533
+ } catch (err) {
534
+ console.error("Create Error:", err);
535
+ res.status(500).json({ error: err.message });
536
+ }
537
+ });
538
+
539
+ app.post('/project/feedback', async (req, res) => {
540
+ const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body;
541
+
542
+ try {
543
+ const project = await StateManager.getProject(projectId);
544
+ if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
545
+
546
+ try {
547
+ await checkMinimumCredits(userId, 'basic');
548
+ } catch (creditErr) {
549
+ return res.json({ success: false, insufficient: true, error: creditErr.message });
550
+ }
551
+
552
+ // Pass the context objects into formatContext
553
+ const formattedContext = formatContext({ hierarchyContext, scriptContext, logContext });
554
+
555
+ // Append to user prompt
556
+ const fullInput = `USER: ${prompt || "Automatic Feedback"}${formattedContext}`;
557
+
558
+ // Trigger Async
559
+ runAsyncFeedback(projectId, userId, fullInput, images || []);
560
+
561
+ res.json({ success: true, message: "Processing started" });
562
+
563
+ } catch (err) {
564
+ console.error("Feedback Error:", err);
565
+ await StateManager.updateProject(projectId, { status: "error" });
566
+ res.status(500).json({ error: "Feedback process failed" });
567
+ }
568
+ });
569
+
570
+ app.post('/project/ping', async (req, res) => {
571
+ const { projectId, userId, isFrontend } = req.body;
572
+ if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" });
573
+
574
+ const project = await StateManager.getProject(projectId);
575
+ if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
576
+
577
+ // 1. BASE RESPONSE (Visuals)
578
+ const response = {
579
+ action: "IDLE",
580
+ status: StateManager.getStatus(projectId),
581
+ snapshot: StateManager.getSnapshot(projectId)
582
+ };
583
+
584
+ // 2. EXECUTOR LOGIC (Plugin Only)
585
+ if (!isFrontend) {
586
+ const command = await StateManager.popCommand(projectId);
587
+
588
+ if (command) {
589
+ response.action = command.type;
590
+ response.target = command.payload;
591
+ response.code = command.type === 'EXECUTE' ? command.payload : null;
592
+ }
593
+ }
594
+
595
+ res.json(response);
596
+ });
597
+
598
+
599
+ app.post('/human/override', validateRequest, async (req, res) => {
600
+ const { projectId, instruction, userId } = req.body;
601
+ try {
602
+ await checkMinimumCredits(userId, 'basic');
603
+ const project = await StateManager.getProject(projectId);
604
+
605
+ res.json({ success: true });
606
+
607
+ const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
608
+
609
+ let overrideText = "";
610
+ let stopStatus = startStatusLoop(projectId, 'worker');
611
+
612
+ const workerResult = await AIEngine.callWorkerStream(
613
+ project.workerHistory,
614
+ overrideMsg,
615
+ (thought) => {
616
+ stopStatus();
617
+ StateManager.setStatus(projectId, "Worker: Thinking...");
618
+ StateManager.appendSnapshotOnly(projectId, thought);
619
+ },
620
+ (chunk) => {
621
+ stopStatus();
622
+ StateManager.setStatus(projectId, "Worker: Coding...");
623
+ overrideText += chunk;
624
+ StateManager.appendStream(projectId, chunk);
625
+ }
626
+ );
627
+ stopStatus();
628
+
629
+ await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg);
630
+ await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText);
631
+ await processAndQueueResponse(projectId, workerResult.text || overrideText, userId);
632
+
633
+ const usage = workerResult.usage?.totalTokenCount || 0;
634
+ if (usage > 0) await deductUserCredits(userId, usage, 'basic');
635
+
636
+ } catch (err) {
637
+ console.error("Override failed", err);
638
+ }
639
+ });
640
+
641
+ app.get('/admin/cleanup', async (req, res) => {
642
+ try {
643
+ const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0;
644
+ res.json({ success: true, removedCount: removed });
645
+ } catch (err) {
646
+ res.status(500).json({ error: "Cleanup failed" });
647
+ }
648
+ });
649
+
650
+ app.listen(PORT, () => {
651
+ console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`);
652
+ });
653
+
654
+ /* import express from 'express';
655
+ import bodyParser from 'body-parser';
656
+ import cors from 'cors';
657
+ import { StateManager, initDB } from './stateManager.js';
658
+ import { AIEngine } from './aiEngine.js';
659
+ import fs from 'fs';
660
+ import crypto from "crypto";
661
+ import dotenv from 'dotenv';
662
+
663
+ dotenv.config();
664
+
665
+ // Initialize Database
666
+ initDB();
667
+ const supabase = StateManager.getSupabaseClient();
668
+
669
+ const app = express();
670
+ const PORT = process.env.PORT || 7860;
671
+
672
+ app.use(cors());
673
+ app.use(express.json({ limit: '50mb' }));
674
+
675
+ // --- BILLING CONSTANTS ---
676
+ const MIN_BASIC_REQUIRED = 100;
677
+ const MIN_DIAMOND_REQUIRED = 100;
678
+
679
+ // --- STATUS PHASES ---
680
+ const WORKER_PHASES = [
681
+ "Worker: Sorting...",
682
+ "Worker: Analyzing Request...",
683
+ "Worker: Reading Context...",
684
+ "Worker: Checking Hierarchy...",
685
+ "Worker: Planning Logic...",
686
+ "Worker: Preparing Environment...",
687
+ "Worker: Thinking..."
688
+ ];
689
+
690
+ const PM_PHASES = [
691
+ "Manager: Sorting...",
692
+ "Manager: Reviewing Request...",
693
+ "Manager: Analyzing Project Structure...",
694
+ "Manager: Consulting Guidelines...",
695
+ "Manager: Formulating Strategy...",
696
+ "Manager: Delegating Tasks...",
697
+ "Manager: Thinking..."
698
+ ];
699
 
700
+ // --- HELPER FUNCTIONS ---
701
  function startStatusLoop(projectId, type = 'worker') {
702
  const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
703
  let index = 0;
704
 
705
+ // Set the initial state immediately
706
  StateManager.setStatus(projectId, phases[0]);
707
 
708
  const interval = setInterval(() => {
709
+ if (index < phases.length - 1) {
710
+ index++;
711
+ StateManager.setStatus(projectId, phases[index]);
712
+ } else {
713
+ // Stop the loop once we reach the last entry
714
+ clearInterval(interval);
715
+ }
716
+ }, 3500); // 3.5 second delay as requested
717
 
718
+ // Return a cleanup function in case the component unmounts early
719
  return () => clearInterval(interval);
720
+ };
721
+
722
 
723
  function extractWorkerPrompt(text) {
724
  if (!text || typeof text !== 'string') return null;
 
1219
  }
1220
  });
1221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
 
1223
  app.post('/project/ping', async (req, res) => {
1224
  const { projectId, userId, isFrontend } = req.body;
 
1308
 
1309
  app.listen(PORT, () => {
1310
  console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`);
1311
+ });
1312
+ */