everydaytok commited on
Commit
3c25306
·
verified ·
1 Parent(s): 3b0d86e

Rename app.js to app.py

Browse files
Files changed (2) hide show
  1. app.js +0 -702
  2. app.py +84 -0
app.js DELETED
@@ -1,702 +0,0 @@
1
- import express from 'express';
2
- import bodyParser from 'body-parser';
3
- import cors from 'cors';
4
- import { StateManager, initDB } from './stateManager.js';
5
- import { AIEngine } from './aiEngine.js';
6
- import fs from 'fs';
7
- import admin from 'firebase-admin';
8
- import crypto from "crypto";
9
-
10
- // --- FIREBASE SETUP ---
11
- let db = null;
12
- let firestore = null;
13
- let storage = null;
14
-
15
- try {
16
- if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
17
- const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
-
19
- // Specific bucket as requested
20
- const bucketName = `shago-web.firebasestorage.app`;
21
-
22
- if (admin.apps.length === 0) {
23
- admin.initializeApp({
24
- credential: admin.credential.cert(serviceAccount),
25
- databaseURL: "https://shago-web-default-rtdb.firebaseio.com",
26
- storageBucket: bucketName
27
- });
28
- }
29
-
30
- db = admin.database();
31
- firestore = admin.firestore();
32
- storage = admin.storage();
33
-
34
- initDB(db);
35
- console.log("🔥 Firebase Connected (RTDB, Firestore, Storage) & StateManager Linked");
36
- } else {
37
- console.warn("⚠️ Memory-Only mode.");
38
- }
39
- } catch (e) { console.error("Firebase Init Error:", e); }
40
-
41
-
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 {
63
- sysPrompts = JSON.parse(fs.readFileSync('./prompts.json', 'utf8'));
64
- } catch (e) {
65
- console.error("Failed to load prompts.json:", e);
66
- }
67
-
68
- 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
-
114
- try {
115
- const ref = db.ref(`users/${userId}/credits/${type}`);
116
- await ref.transaction((current_credits) => {
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
- }
137
-
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?.substring(0, 1000)}...`;
149
- if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
150
- return out;
151
- }
152
-
153
- function extractPMQuestion(text) {
154
- const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
155
- return match ? match[1].trim() : null;
156
- }
157
-
158
- function extractImagePrompt(text) {
159
- const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
160
- return match ? match[1].trim() : null;
161
- }
162
-
163
- // --- ADMIN ENDPOINTS ---
164
-
165
- app.get('/admin/cleanup', async (req, res) => {
166
- try {
167
- const removed = StateManager.cleanupMemory();
168
- res.json({ success: true, removedCount: removed });
169
- } catch (err) {
170
- res.status(500).json({ error: "Cleanup failed" });
171
- }
172
- });
173
-
174
- // --- ONBOARDING ENDPOINTS ---
175
-
176
- app.post('/onboarding/analyze', validateRequest, async (req, res) => {
177
- const { description, userId } = req.body;
178
-
179
- if (!description) return res.status(400).json({ error: "Description required" });
180
-
181
- try {
182
- // Analyst uses BASIC models (Flash)
183
- await checkMinimumCredits(userId, 'basic');
184
-
185
- console.log(`[Onboarding] Analyzing idea...`);
186
-
187
- const result = await AIEngine.generateEntryQuestions(description);
188
-
189
- const usage = result.usage?.totalTokenCount || 0;
190
-
191
- // Bill Basic
192
- if (usage > 0) await deductUserCredits(userId, usage, 'basic');
193
-
194
- if (result.status === "REJECTED") {
195
- return res.json({
196
- rejected: true,
197
- reason: result.reason || "Idea violates TOS."
198
- });
199
- }
200
- res.json({ questions: result.questions });
201
-
202
- } catch (err) {
203
- console.error(err);
204
- if (err.message.includes("Insufficient")) {
205
- return res.status(402).json({ error: err.message });
206
- }
207
- res.status(500).json({ error: "Analysis failed" });
208
- }
209
- });
210
-
211
- app.post('/onboarding/create', validateRequest, async (req, res) => {
212
- const { userId, description, answers } = req.body;
213
- let basicTokens = 0; // Create flow uses Grader (Basic) + Image (Basic)
214
-
215
- try {
216
- // Pre-flight check
217
- await checkMinimumCredits(userId, 'basic');
218
-
219
- const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
220
- const projectId = `proj_${Date.now()}_${randomHex(7)}`;
221
-
222
- console.log(`[Onboarding] Grading Project ${projectId}...`);
223
-
224
- // 1. Grading (Basic)
225
- const gradingResult = await AIEngine.gradeProject(description, answers);
226
- basicTokens += (gradingResult.usage?.totalTokenCount || 0);
227
-
228
- const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
229
-
230
- let thumbnailBase64 = null;
231
-
232
- if (!isFailure) {
233
- console.log(`[Onboarding] ✅ Passed. Generating Thumbnail...`);
234
- const imagePrompt = `Game Title: ${gradingResult.title}. Core Concept: ${description}`;
235
-
236
- // 2. Image Generation (Basic)
237
- const imgResult = await AIEngine.generateImage(imagePrompt);
238
- if (imgResult) {
239
- thumbnailBase64 = imgResult.image;
240
- // Add token usage for text + fixed cost
241
- basicTokens += (imgResult.usage || 0);
242
- if (imgResult.image) basicTokens += IMAGE_COST_BASIC;
243
- }
244
- }
245
-
246
- // Upload Logic
247
- let thumbnailUrl = null;
248
- if (thumbnailBase64 && storage) {
249
- try {
250
- const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
251
- const buffer = Buffer.from(base64Data, 'base64');
252
- const bucket = storage.bucket();
253
- const file = bucket.file(`${projectId}/thumbnail.png`);
254
- await file.save(buffer, { metadata: { contentType: 'image/png' } });
255
- await file.makePublic();
256
- thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
257
- } catch (uploadErr) {
258
- console.error("Storage Upload Failed:", uploadErr);
259
- }
260
- }
261
-
262
- const timestamp = Date.now();
263
- const status = isFailure ? "rejected" : "Idle";
264
-
265
- // Save Data Logic
266
- if (!isFailure) {
267
- const memoryObject = {
268
- id: projectId,
269
- userId,
270
- title: gradingResult.title || "Untitled",
271
- description,
272
- answers,
273
- stats: gradingResult,
274
- thumbnail: thumbnailUrl,
275
- createdAt: timestamp,
276
- status,
277
- workerHistory: [],
278
- pmHistory: [],
279
- commandQueue: [],
280
- failureCount: 0
281
- };
282
- await StateManager.updateProject(projectId, memoryObject);
283
- }
284
-
285
- if (firestore && !isFailure) {
286
- await firestore.collection('projects').doc(projectId).set({
287
- id: projectId,
288
- userId: userId,
289
- assets: thumbnailUrl ? [thumbnailUrl] : [],
290
- createdAt: admin.firestore.FieldValue.serverTimestamp()
291
- });
292
- }
293
-
294
- if (db && !isFailure) {
295
- const updates = {};
296
- updates[`projects/${projectId}/info`] = {
297
- id: projectId,
298
- userId,
299
- title: gradingResult.title || "Untitled",
300
- description,
301
- answers,
302
- stats: gradingResult,
303
- createdAt: timestamp,
304
- status
305
- };
306
- if (thumbnailUrl) updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
307
- updates[`projects/${projectId}/state`] = {
308
- workerHistory: [],
309
- pmHistory: [],
310
- commandQueue: [],
311
- failureCount: 0
312
- };
313
- await db.ref().update(updates);
314
- }
315
-
316
- // Deduct Basic Credits
317
- if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic');
318
- console.log("sending");
319
- res.json({
320
- status: 200,
321
- success: !isFailure,
322
- projectId,
323
- stats: gradingResult,
324
- title: gradingResult.title || "Untitled",
325
- thumbnail: thumbnailBase64
326
- });
327
-
328
- } catch (err) {
329
- console.error("Create Error:", err);
330
- if (err.message.includes("Insufficient")) {
331
- return res.status(402).json({ error: err.message });
332
- }
333
- res.status(500).json({ error: "Creation failed" });
334
- }
335
- });
336
-
337
- // --- CORE ENDPOINTS ---
338
-
339
- async function runBackgroundInitialization(projectId, userId, description) {
340
- console.log(`[Background] Starting initialization for ${projectId}`);
341
-
342
- // Separate Counters
343
- let diamondUsage = 0; // For PM
344
- let basicUsage = 0; // For Worker
345
-
346
- try {
347
- const pmHistory = [];
348
-
349
- // 1. Generate GDD (PM -> Diamond)
350
- const gddPrompt = `Create a comprehensive GDD for: ${description}`;
351
- const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
352
-
353
- diamondUsage += (gddResult.usage?.totalTokenCount || 0);
354
- const gddText = gddResult.text;
355
-
356
- pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
357
- pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
358
-
359
- // 2. Generate First Task (PM -> Diamond)
360
- const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
361
- const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
362
-
363
- diamondUsage += (taskResult.usage?.totalTokenCount || 0);
364
- const taskText = taskResult.text;
365
-
366
- pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
367
- pmHistory.push({ role: 'model', parts: [{ text: taskText }] });
368
-
369
- // 3. Initialize Worker (Worker -> Basic)
370
- const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
371
- const workerHistory = [];
372
- const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
373
-
374
- const workerResult = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
375
-
376
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
377
- const workerText = workerResult.text;
378
-
379
- workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
380
- workerHistory.push({ role: 'model', parts: [{ text: workerText }] });
381
-
382
- // 4. Update Memory
383
- await StateManager.updateProject(projectId, {
384
- userId,
385
- pmHistory,
386
- workerHistory,
387
- gdd: gddText,
388
- failureCount: 0
389
- });
390
-
391
- // 5. Queue commands (Handle assets + basic costs)
392
- // We pass userId to deduct image costs (Basic) immediately inside this function
393
- await processAndQueueResponse(projectId, workerText, userId);
394
-
395
- // 6. Update Status
396
- if(db) await db.ref(`projects/${projectId}/info`).update({
397
- status: "IDLE",
398
- lastUpdated: Date.now()
399
- });
400
-
401
- // 7. Deduct Accumulated Credits
402
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
403
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
404
-
405
- console.log(`[Background] Init complete. Diamond: ${diamondUsage}, Basic: ${basicUsage}`);
406
-
407
- } catch (err) {
408
- console.error(`[Background] Init Error for ${projectId}:`, err);
409
- if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
410
- }
411
- }
412
-
413
- app.post('/new/project', validateRequest, async (req, res) => {
414
- const { userId, projectId, description } = req.body;
415
-
416
- try {
417
- // Init requires BOTH types to be safe
418
- await checkMinimumCredits(userId, 'diamond');
419
- await checkMinimumCredits(userId, 'basic');
420
-
421
- if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
422
-
423
- res.json({
424
- success: true,
425
- message: "Project initialization started in background."
426
- });
427
-
428
- runBackgroundInitialization(projectId, userId, description);
429
-
430
- } catch (err) {
431
- if (err.message.includes("Insufficient")) {
432
- return res.status(402).json({ error: err.message });
433
- }
434
- res.status(500).json({ error: "Failed to start project" });
435
- }
436
- });
437
-
438
- app.post('/project/feedback', async (req, res) => {
439
- const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
440
-
441
- let diamondUsage = 0;
442
- let basicUsage = 0;
443
-
444
- try {
445
- const project = await StateManager.getProject(projectId);
446
- if (!project) return res.status(404).json({ error: "Project not found." });
447
- if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
448
-
449
- // Basic Check (most interaction is worker)
450
- await checkMinimumCredits(userId, 'basic');
451
-
452
- if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
453
-
454
- if (taskComplete) {
455
- console.log(`[${projectId}] ✅ TASK COMPLETE.`);
456
- const summary = `Worker completed the previous task. Logs: ${logContext?.logs || "Clean"}. \nGenerate the NEXT task using 'WORKER_PROMPT:' format.`;
457
-
458
- // PM Call -> Diamond
459
- const pmResult = await AIEngine.callPM(project.pmHistory, summary);
460
- diamondUsage += (pmResult.usage?.totalTokenCount || 0);
461
- const pmText = pmResult.text;
462
-
463
- project.pmHistory.push({ role: 'user', parts: [{ text: summary }] });
464
- project.pmHistory.push({ role: 'model', parts: [{ text: pmText }] });
465
-
466
- const nextInstruction = extractWorkerPrompt(pmText);
467
- if (!nextInstruction) {
468
- await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
469
- if(db) await db.ref(`projects/${projectId}/info`).update({ status: "IDLE", lastUpdated: Date.now() });
470
-
471
- // Deduct what we used
472
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
473
-
474
- return res.json({ success: true, message: "No further tasks. Project Idle." });
475
- }
476
-
477
- const newWorkerHistory = [];
478
- const newPrompt = `New Objective: ${nextInstruction}`;
479
-
480
- // Worker Call -> Basic
481
- const workerResult = await AIEngine.callWorker(newWorkerHistory, newPrompt, []);
482
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
483
- const workerText = workerResult.text;
484
-
485
- newWorkerHistory.push({ role: 'user', parts: [{ text: newPrompt }] });
486
- newWorkerHistory.push({ role: 'model', parts: [{ text: workerText }] });
487
-
488
- await StateManager.updateProject(projectId, {
489
- pmHistory: project.pmHistory,
490
- workerHistory: newWorkerHistory,
491
- failureCount: 0
492
- });
493
-
494
- StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
495
- await processAndQueueResponse(projectId, workerText, userId);
496
-
497
- if(db) await db.ref(`projects/${projectId}/info`).update({ status: "working", lastUpdated: Date.now() });
498
-
499
- // Final Bill
500
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
501
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
502
-
503
- return res.json({ success: true, message: "Next Task Assigned" });
504
- }
505
-
506
- // Logic for Failure Handling (PM Guidance)
507
- if (project.failureCount > 3) {
508
- const pmPrompt = (sysPrompts.pm_guidance_prompt || "Analyze logs: {{LOGS}}").replace('{{LOGS}}', logContext?.logs);
509
-
510
- // PM -> Diamond
511
- const pmResult = await AIEngine.callPM(project.pmHistory, pmPrompt);
512
- diamondUsage += (pmResult.usage?.totalTokenCount || 0);
513
- const pmVerdict = pmResult.text;
514
-
515
- if (pmVerdict.includes("[TERMINATE]")) {
516
- const fixInstruction = pmVerdict.replace("[TERMINATE]", "").trim();
517
- const resetHistory = [];
518
- const resetPrompt = `[SYSTEM]: Previous worker terminated. \nNew Objective: ${fixInstruction}`;
519
-
520
- // Worker -> Basic
521
- const workerResult = await AIEngine.callWorker(resetHistory, resetPrompt, []);
522
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
523
-
524
- resetHistory.push({ role: 'user', parts: [{ text: resetPrompt }] });
525
- resetHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
526
-
527
- await StateManager.updateProject(projectId, { workerHistory: resetHistory, failureCount: 0 });
528
- StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "print('SYSTEM: Worker Reset')" });
529
- await processAndQueueResponse(projectId, workerResult.text, userId);
530
-
531
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
532
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
533
- return res.json({ success: true, message: "Worker Terminated." });
534
- } else {
535
- const injection = `[PM GUIDANCE]: ${pmVerdict} \n\nApply this fix now.`;
536
-
537
- // Worker -> Basic
538
- const workerResult = await AIEngine.callWorker(project.workerHistory, injection, []);
539
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
540
-
541
- project.workerHistory.push({ role: 'user', parts: [{ text: injection }] });
542
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
543
-
544
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory, failureCount: 1 });
545
- await processAndQueueResponse(projectId, workerResult.text, userId);
546
-
547
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
548
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
549
- return res.json({ success: true, message: "PM Guidance Applied." });
550
- }
551
- }
552
-
553
- // Standard Interaction (Worker Only = Basic)
554
- const fullInput = `USER: ${prompt || "Automatic Feedback"}` + formatContext({ hierarchyContext, scriptContext, logContext });
555
-
556
- let workerResult = await AIEngine.callWorker(project.workerHistory, fullInput, images || []);
557
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
558
- let responseText = workerResult.text;
559
-
560
- // PM Consultation Check
561
- const pmQuestion = extractPMQuestion(responseText);
562
- if (pmQuestion) {
563
- // PM -> Diamond
564
- const pmConsultPrompt = `[WORKER CONSULTATION]: The Worker asks: "${pmQuestion}"\nProvide a technical answer to unblock them.`;
565
- const pmResult = await AIEngine.callPM(project.pmHistory, pmConsultPrompt);
566
- diamondUsage += (pmResult.usage?.totalTokenCount || 0);
567
- const pmAnswer = pmResult.text;
568
-
569
- project.pmHistory.push({ role: 'user', parts: [{ text: pmConsultPrompt }] });
570
- project.pmHistory.push({ role: 'model', parts: [{ text: pmAnswer }] });
571
-
572
- const injectionMsg = `[PM RESPONSE]: ${pmAnswer}`;
573
- project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
574
- project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
575
-
576
- // Re-call Worker -> Basic
577
- workerResult = await AIEngine.callWorker(project.workerHistory, injectionMsg, []);
578
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
579
- responseText = workerResult.text;
580
-
581
- project.workerHistory.push({ role: 'user', parts: [{ text: injectionMsg }] });
582
- project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
583
- } else {
584
- project.workerHistory.push({ role: 'user', parts: [{ text: fullInput }] });
585
- project.workerHistory.push({ role: 'model', parts: [{ text: responseText }] });
586
- }
587
-
588
- await StateManager.updateProject(projectId, {
589
- workerHistory: project.workerHistory,
590
- pmHistory: project.pmHistory,
591
- failureCount: project.failureCount
592
- });
593
-
594
- await processAndQueueResponse(projectId, responseText, userId);
595
-
596
- if(db) await db.ref(`projects/${projectId}/info`).update({ status: "idle", lastUpdated: Date.now() });
597
-
598
- // Final Billing
599
- if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
600
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
601
-
602
- res.json({ success: true });
603
-
604
- } catch (err) {
605
- console.error("AI Error:", err);
606
- if (err.message.includes("Insufficient")) {
607
- return res.status(402).json({ error: err.message });
608
- }
609
- res.status(500).json({ error: "AI Failed" });
610
- }
611
- });
612
-
613
- app.post('/project/ping', async (req, res) => {
614
- const { projectId, userId } = req.body;
615
- if (!projectId || !userId) return res.status(400).json({ error: "Missing ID fields" });
616
-
617
- const project = await StateManager.getProject(projectId);
618
- if (!project) return res.status(404).json({ action: "IDLE", error: "Project not found" });
619
- if (project.userId !== userId) return res.status(403).json({ error: "Unauthorized" });
620
-
621
- const command = await StateManager.popCommand(projectId);
622
-
623
- if (command) {
624
- if (command.payload === "CLEAR_CONSOLE") {
625
- res.json({ action: "CLEAR_LOGS" });
626
- } else {
627
- res.json({
628
- action: command.type,
629
- target: command.payload,
630
- code: command.type === 'EXECUTE' ? command.payload : null
631
- });
632
- }
633
- } else {
634
- res.json({ action: "IDLE" });
635
- }
636
- });
637
-
638
- app.post('/human/override', validateRequest, async (req, res) => {
639
- const { projectId, instruction, pruneHistory, userId } = req.body;
640
- let basicUsage = 0;
641
-
642
- try {
643
- await checkMinimumCredits(userId, 'basic');
644
-
645
- const project = await StateManager.getProject(projectId);
646
- const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
647
-
648
- if (pruneHistory && project.workerHistory.length >= 2) {
649
- project.workerHistory.pop();
650
- project.workerHistory.pop();
651
- }
652
-
653
- // Worker -> Basic
654
- const workerResult = await AIEngine.callWorker(project.workerHistory, overrideMsg, []);
655
- basicUsage += (workerResult.usage?.totalTokenCount || 0);
656
-
657
- project.workerHistory.push({ role: 'user', parts: [{ text: overrideMsg }] });
658
- project.workerHistory.push({ role: 'model', parts: [{ text: workerResult.text }] });
659
-
660
- await StateManager.updateProject(projectId, { workerHistory: project.workerHistory });
661
- await processAndQueueResponse(projectId, workerResult.text, userId);
662
-
663
- if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
664
-
665
- res.json({ success: true });
666
- } catch (err) {
667
- if (err.message.includes("Insufficient")) {
668
- return res.status(402).json({ error: err.message });
669
- }
670
- res.status(500).json({ error: "Override Failed" });
671
- }
672
- });
673
-
674
- // Helper to handle Asset Parsing & Billing (Basic Credits)
675
- async function processAndQueueResponse(projectId, rawResponse, userId) {
676
- const imgPrompt = extractImagePrompt(rawResponse);
677
- if (imgPrompt) {
678
- console.log(`[${projectId}] 🎨 Generating Asset: ${imgPrompt}`);
679
-
680
- // 1. Generate Image (returns { image, usage } or null)
681
- const imgResult = await AIEngine.generateImage(imgPrompt);
682
-
683
- if (imgResult && imgResult.image) {
684
- // 2. Bill for image tokens immediately (Basic)
685
- const imgTokens = imgResult.usage?.totalTokenCount || 0;
686
- const totalCost = imgTokens; // imgTokens + IMAGE_COST_BASIC;
687
-
688
- if (userId && totalCost > 0) {
689
- await deductUserCredits(userId, totalCost, 'basic');
690
- }
691
-
692
- // 3. Queue creation command
693
- await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
694
- }
695
- }
696
- // Queue the raw text response for the frontend
697
- await StateManager.queueCommand(projectId, rawResponse);
698
- }
699
-
700
- app.listen(PORT, () => {
701
- console.log(`AI Backend Running on ${PORT}`);
702
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import io, base64
4
+ from fastapi import FastAPI
5
+ from fastapi.responses import HTMLResponse
6
+ from pydantic import BaseModel
7
+ from sklearn.decomposition import PCA
8
+ from sklearn.cluster import DBSCAN
9
+
10
+ app = FastAPI()
11
+
12
+ class VectorSystem:
13
+ def get_center(self, data, mode='global'):
14
+ if mode == 'cluster':
15
+ clustering = DBSCAN(eps=0.5, min_samples=3).fit(data)
16
+ labels = clustering.labels_
17
+ if len(set(labels)) > (1 if -1 in labels else 0):
18
+ # Pick the largest non-noise cluster
19
+ largest = max(set(labels) - {-1}, key=lambda l: np.sum(labels == l))
20
+ selected = data[labels == largest]
21
+ return np.mean(selected, axis=0), "Clustered"
22
+ return np.mean(data, axis=0), "Global"
23
+
24
+ # --- THE VISUALIZER ---
25
+ def generate_plot(mode='global', scenario='split'):
26
+ # 1. Generate High-Dim Data (128-dim)
27
+ np.random.seed(42)
28
+ if scenario == 'split':
29
+ c1 = np.random.normal(0, 0.1, (20, 128))
30
+ c2 = np.random.normal(1, 0.1, (20, 128))
31
+ data = np.vstack([c1, c2])
32
+ else:
33
+ data = np.random.normal(0.5, 0.05, (40, 128))
34
+
35
+ # 2. Find Center
36
+ sys = VectorSystem()
37
+ center_vec, label = sys.get_center(data, mode)
38
+
39
+ # 3. PCA Projection to 2D
40
+ pca = PCA(n_components=2)
41
+ all_points = np.vstack([data, center_vec])
42
+ projected = pca.fit_transform(all_points)
43
+
44
+ pts_2d = projected[:-1]
45
+ center_2d = projected[-1]
46
+
47
+ # 4. Plotting
48
+ plt.figure(figsize=(6, 4))
49
+ plt.scatter(pts_2d[:, 0], pts_2d[:, 1], alpha=0.5, c='blue', label='Input Vectors')
50
+ plt.scatter(center_2d[0], center_2d[1], c='red', s=100, marker='X', label=f'Predicted {label} Center')
51
+ plt.title(f"Space Visualization ({scenario.capitalize()} Data)")
52
+ plt.legend()
53
+
54
+ buf = io.BytesIO()
55
+ plt.savefig(buf, format='png')
56
+ plt.close()
57
+ return base64.b64encode(buf.getvalue()).decode('utf-8')
58
+
59
+ @app.get("/", response_class=HTMLResponse)
60
+ async def root():
61
+ # Show two scenarios side-by-side
62
+ img_global = generate_plot('global', 'split')
63
+ img_cluster = generate_plot('cluster', 'split')
64
+
65
+ return f"""
66
+ <html>
67
+ <body style="font-family:sans-serif; text-align:center; background:#1a1a1a; color:white;">
68
+ <h1>Vector Convergence Visualizer</h1>
69
+ <div style="display:flex; justify-content:center; gap:20px;">
70
+ <div>
71
+ <h3>Global Mode</h3>
72
+ <p>Lands in the "dead zone" between clusters.</p>
73
+ <img src="data:image/png;base64,{img_global}" style="border-radius:10px;"/>
74
+ </div>
75
+ <div>
76
+ <h3>Cluster Mode</h3>
77
+ <p>Detects the density and "gravitates" to one group.</p>
78
+ <img src="data:image/png;base64,{img_cluster}" style="border-radius:10px;"/>
79
+ </div>
80
+ </div>
81
+ <p style="margin-top:30px;">This demonstrates 128-dimensional relationships projected into 2D.</p>
82
+ </body>
83
+ </html>
84
+ """