everydaytok commited on
Commit
86366a6
·
verified ·
1 Parent(s): e8e0c9e

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +415 -4
app.js CHANGED
@@ -21,6 +21,417 @@ app.use(bodyParser.json({ limit: '50mb' }));
21
  const MIN_BASIC_REQUIRED = 50;
22
  const MIN_DIAMOND_REQUIRED = 50;
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  // --- STATUS PHASES ---
25
  const WORKER_PHASES = [
26
  "Worker: Sorting...",
@@ -455,9 +866,9 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
455
  if (result.usage?.totalTokenCount > 0) {
456
  await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
457
  }
458
- /* const usage = result.usage?.totalTokenCount || 0;
459
- if (usage > 0) await deductUserCredits(userId, usage, 'basic');
460
- */
461
 
462
  if (result.status === "REJECTED") {
463
  return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." });
@@ -638,4 +1049,4 @@ app.get('/admin/cleanup', async (req, res) => {
638
 
639
  app.listen(PORT, () => {
640
  console.log(`AI Backend Running on ${PORT} (Supabase Edition)`);
641
- });
 
21
  const MIN_BASIC_REQUIRED = 50;
22
  const MIN_DIAMOND_REQUIRED = 50;
23
 
24
+ // --- STATUS PHASES ---
25
+ const WORKER_PHASES = [
26
+ "Worker: Sorting...",
27
+ "Worker: Analyzing Request...",
28
+ "Worker: Reading Context...",
29
+ "Worker: Checking Hierarchy...",
30
+ "Worker: Planning Logic...",
31
+ "Worker: Preparing Environment...",
32
+ "Worker: Thinking..."
33
+ ];
34
+
35
+ const PM_PHASES = [
36
+ "Manager: Sorting...",
37
+ "Manager: Reviewing Request...",
38
+ "Manager: Analyzing Project Structure...",
39
+ "Manager: Consulting Guidelines...",
40
+ "Manager: Formulating Strategy...",
41
+ "Manager: Delegating Tasks...",
42
+ "Manager: Thinking..."
43
+ ];
44
+
45
+ function startStatusLoop(projectId, type = 'worker') {
46
+ const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
47
+ let index = 0;
48
+
49
+ StateManager.setStatus(projectId, phases[0]);
50
+
51
+ const interval = setInterval(() => {
52
+ index++;
53
+ if (index < phases.length) {
54
+ StateManager.setStatus(projectId, phases[index]);
55
+ } else {
56
+ clearInterval(interval);
57
+ }
58
+ }, 3500);
59
+
60
+ return () => clearInterval(interval);
61
+ }
62
+
63
+ async function checkMinimumCredits(userId, type = 'basic') {
64
+ if (!supabase) return;
65
+
66
+ const { data, error } = await supabase
67
+ .from('users')
68
+ .select('credits')
69
+ .eq('id', userId)
70
+ .single();
71
+
72
+ if (error || !data) return;
73
+
74
+ const credits = data.credits?.[type] || 0;
75
+ const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
76
+
77
+ if (credits < required) {
78
+ throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required}.`);
79
+ }
80
+ }
81
+
82
+ async function deductUserCredits(userId, amount, type = 'basic') {
83
+ if (!supabase || !amount || amount <= 0) return;
84
+
85
+ try {
86
+ const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
87
+ if (!data) return;
88
+
89
+ const currentCredits = data.credits || {};
90
+ const currentVal = currentCredits[type] || 0;
91
+ const newVal = Math.max(0, currentVal - amount);
92
+
93
+ const updatedCredits = { ...currentCredits, [type]: newVal };
94
+
95
+ await supabase
96
+ .from('users')
97
+ .update({ credits: updatedCredits })
98
+ .eq('id', userId);
99
+
100
+ console.log(`[Credits] Deducted ${amount} ${type} from User ${userId}. Remaining: ${newVal}`);
101
+ } catch (err) {
102
+ console.error(`[Credits] Failed to deduct from ${userId}:`, err);
103
+ }
104
+ }
105
+
106
+ const validateRequest = (req, res, next) => {
107
+ if (req.path.includes('/admin/cleanup')) return next();
108
+ const { userId } = req.body;
109
+ if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
110
+ return res.status(400).json({ error: "Missing userId" });
111
+ }
112
+ next();
113
+ };
114
+
115
+ function extractWorkerPrompt(text) {
116
+ if (!text || typeof text !== 'string') return null;
117
+ const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
118
+ return match ? match[1].trim() : null;
119
+ }
120
+
121
+ function extractPMQuestion(text) {
122
+ if (!text || typeof text !== 'string') return null;
123
+ const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
124
+ return match ? match[1].trim() : null;
125
+ }
126
+
127
+ function extractImagePrompt(text) {
128
+ if (!text || typeof text !== 'string') return null;
129
+ const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
130
+ return match ? match[1].trim() : null;
131
+ }
132
+
133
+ function extractRouteToPM(text) {
134
+ if (!text || typeof text !== 'string') return null;
135
+ const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s);
136
+ return match ? match[1].trim() : null;
137
+ }
138
+
139
+ function formatContext({ hierarchyContext, scriptContext, logContext }) {
140
+ let out = "";
141
+ if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`;
142
+ if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
143
+ if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`;
144
+ return out;
145
+ }
146
+
147
+ async function runBackgroundInitialization(projectId, userId, description) {
148
+ if (StateManager.isLocked(projectId)) return;
149
+ StateManager.lock(projectId);
150
+
151
+ let diamondUsage = 0;
152
+ let basicUsage = 0;
153
+
154
+ try {
155
+ await StateManager.getProject(projectId);
156
+ await StateManager.updateProject(projectId, { status: "working", failureCount: 0 });
157
+
158
+ const pmHistory = [];
159
+ let stopStatus = startStatusLoop(projectId, 'pm');
160
+
161
+ const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`;
162
+ const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
163
+ stopStatus();
164
+
165
+ if (!gddResult?.text) throw new Error("PM failed to generate GDD");
166
+ diamondUsage += (gddResult.usage?.totalTokenCount || 0);
167
+
168
+ await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt);
169
+ await StateManager.addHistory(projectId, 'pm', 'model', gddResult.text);
170
+
171
+ pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
172
+ pmHistory.push({ role: 'model', parts: [{ text: gddResult.text }] });
173
+
174
+ stopStatus = startStatusLoop(projectId, 'pm');
175
+ const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
176
+ const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
177
+ stopStatus();
178
+
179
+ if (!taskResult?.text) throw new Error("PM failed to generate Task");
180
+ diamondUsage += (taskResult.usage?.totalTokenCount || 0);
181
+
182
+ await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt);
183
+ await StateManager.addHistory(projectId, 'pm', 'model', taskResult.text);
184
+
185
+ const initialWorkerInstruction = extractWorkerPrompt(taskResult.text) || `Initialize structure for: ${description}`;
186
+ const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
187
+
188
+ stopStatus = startStatusLoop(projectId, 'worker');
189
+ const workerResult = await AIEngine.callWorkerStream(
190
+ [],
191
+ initialWorkerPrompt,
192
+ (thought) => { StateManager.appendSnapshotOnly(projectId, thought); },
193
+ (chunk) => { StateManager.appendStream(projectId, chunk); }
194
+ );
195
+ stopStatus();
196
+
197
+ if (!workerResult?.text) throw new Error("Worker failed to initialize");
198
+ basicUsage += (workerResult.usage?.totalTokenCount || 0);
199
+
200
+ await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt);
201
+ await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text);
202
+
203
+ await StateManager.updateProject(projectId, { gdd: gddResult.text, status: "idle" });
204
+ await processAndQueueResponse(projectId, workerResult.text, userId);
205
+
206
+ } catch (err) {
207
+ console.error(`[Background] Init Error:`, err.message);
208
+ await StateManager.updateProject(projectId, { status: "error" });
209
+ } finally {
210
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
211
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
212
+ StateManager.unlock(projectId);
213
+ }
214
+ }
215
+
216
+ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
217
+ let diamondUsage = 0;
218
+ let basicUsage = 0;
219
+
220
+ try {
221
+ // 1. Persistence First
222
+ await Promise.all([
223
+ StateManager.updateProject(projectId, { status: "working" }),
224
+ StateManager.addHistory(projectId, 'worker', 'user', fullInput)
225
+ ]);
226
+
227
+ const project = await StateManager.getProject(projectId);
228
+ StateManager.clearSnapshot(projectId);
229
+ let stopStatus = startStatusLoop(projectId, 'worker');
230
+
231
+ const currentWorkerHistory = [...(project.workerHistory || [])];
232
+
233
+ // 2. Worker Turn 1
234
+ const firstTurn = await AIEngine.callWorkerStream(
235
+ currentWorkerHistory,
236
+ fullInput,
237
+ (thought) => {
238
+ stopStatus();
239
+ StateManager.setStatus(projectId, "Worker: Thinking...");
240
+ StateManager.appendSnapshotOnly(projectId, thought);
241
+ },
242
+ (chunk) => {
243
+ stopStatus();
244
+ StateManager.setStatus(projectId, "Worker: Coding...");
245
+ StateManager.appendStream(projectId, chunk);
246
+ },
247
+ images
248
+ );
249
+ basicUsage += (firstTurn.usage?.totalTokenCount || 0);
250
+
251
+ const routeTask = extractRouteToPM(firstTurn.text);
252
+ const pmQuestion = extractPMQuestion(firstTurn.text);
253
+
254
+ let finalResponseToExecute = firstTurn.text;
255
+
256
+ if (routeTask || pmQuestion) {
257
+ await StateManager.addHistory(projectId, 'worker', 'model', firstTurn.text);
258
+ currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurn.text }] });
259
+
260
+ let pmPrompt = routeTask ?
261
+ `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.` :
262
+ `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`;
263
+
264
+ // 3. PM Turn
265
+ StateManager.clearSnapshot(projectId);
266
+ stopStatus = startStatusLoop(projectId, 'pm');
267
+ const pmResult = await AIEngine.callPMStream(
268
+ project.pmHistory,
269
+ pmPrompt,
270
+ (thought) => {
271
+ stopStatus();
272
+ StateManager.setStatus(projectId, "Manager: Thinking...");
273
+ StateManager.appendSnapshotOnly(projectId, thought);
274
+ },
275
+ (chunk) => {
276
+ stopStatus();
277
+ StateManager.setStatus(projectId, "Manager: Architecture...");
278
+ StateManager.appendSnapshotOnly(projectId, chunk);
279
+ }
280
+ );
281
+ diamondUsage += (pmResult.usage?.totalTokenCount || 0);
282
+
283
+ await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt);
284
+ await StateManager.addHistory(projectId, 'pm', 'model', pmResult.text);
285
+
286
+ // Execute PM Code
287
+ await processAndQueueResponse(projectId, pmResult.text, userId);
288
+
289
+ // 4. Worker Turn 2 (Continuation)
290
+ const nextInstruction = extractWorkerPrompt(pmResult.text) || pmResult.text;
291
+ const workerContPrompt = `[PM RESPONSE]: ${nextInstruction}\n\nBased on this, finalize the task.`;
292
+
293
+ await StateManager.addHistory(projectId, 'worker', 'user', workerContPrompt);
294
+ currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContPrompt }] });
295
+
296
+ StateManager.clearSnapshot(projectId);
297
+ stopStatus = startStatusLoop(projectId, 'worker');
298
+ const secondTurn = await AIEngine.callWorkerStream(
299
+ currentWorkerHistory,
300
+ workerContPrompt,
301
+ (thought) => {
302
+ stopStatus();
303
+ StateManager.setStatus(projectId, "Worker: Applying...");
304
+ StateManager.appendSnapshotOnly(projectId, thought);
305
+ },
306
+ (chunk) => {
307
+ stopStatus();
308
+ StateManager.setStatus(projectId, "Worker: Finalizing...");
309
+ StateManager.appendStream(projectId, chunk);
310
+ }
311
+ );
312
+ basicUsage += (secondTurn.usage?.totalTokenCount || 0);
313
+
314
+ await StateManager.addHistory(projectId, 'worker', 'model', secondTurn.text);
315
+ finalResponseToExecute = secondTurn.text;
316
+ } else {
317
+ await StateManager.addHistory(projectId, 'worker', 'model', firstTurn.text);
318
+ }
319
+
320
+ await processAndQueueResponse(projectId, finalResponseToExecute, userId);
321
+ await StateManager.updateProject(projectId, { status: "idle" });
322
+
323
+ } catch (err) {
324
+ console.error("Async Feedback Error:", err);
325
+ StateManager.setStatus(projectId, "Error: " + err.message);
326
+ await StateManager.updateProject(projectId, { status: "error" });
327
+ } finally {
328
+ // --- ONLY DEDUCT HERE ---
329
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
330
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
331
+ }
332
+ }
333
+
334
+ async function processAndQueueResponse(projectId, rawResponse, userId) {
335
+ if (!rawResponse) return;
336
+ const imgPrompt = extractImagePrompt(rawResponse);
337
+ if (imgPrompt) {
338
+ const imgResult = await AIEngine.generateImage(imgPrompt);
339
+ if (imgResult?.image) {
340
+ await deductUserCredits(userId, 1000, 'basic');
341
+ await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
342
+ }
343
+ }
344
+ await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: rawResponse });
345
+ }
346
+
347
+ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
348
+ const { description, userId } = req.body;
349
+ try {
350
+ await checkMinimumCredits(userId, 'basic');
351
+ const result = await AIEngine.generateEntryQuestions(description);
352
+
353
+ if (result.usage?.totalTokenCount > 0) {
354
+ await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
355
+ }
356
+
357
+ if (result.status === "REJECTED") return res.json({ rejected: true, reason: result.reason });
358
+ res.json({ questions: result.questions });
359
+ } catch (err) { res.status(500).json({ error: err.message }); }
360
+ });
361
+
362
+ app.post('/onboarding/create', validateRequest, async (req, res) => {
363
+ const { userId, description, answers } = req.body;
364
+ try {
365
+ await checkMinimumCredits(userId, 'basic');
366
+ await checkMinimumCredits(userId, 'diamond');
367
+
368
+ const projectId = `proj_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
369
+ const gradingResult = await AIEngine.gradeProject(description, answers);
370
+
371
+ if (gradingResult.usage?.totalTokenCount > 0) {
372
+ await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic');
373
+ }
374
+
375
+ if (gradingResult.feasibility >= 30 && gradingResult.rating !== 'F') {
376
+ await supabase.from('projects').insert({
377
+ id: projectId, user_id: userId,
378
+ info: { title: gradingResult.title || "Untitled", stats: gradingResult, description, answers, status: "initializing" }
379
+ });
380
+ runBackgroundInitialization(projectId, userId, description);
381
+ }
382
+ res.json({ status: 200, success: true, projectId, stats: gradingResult });
383
+ } catch (err) { res.status(500).json({ error: err.message }); }
384
+ });
385
+
386
+ app.post('/project/feedback', async (req, res) => {
387
+ const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body;
388
+ try {
389
+ const project = await StateManager.getProject(projectId);
390
+ if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
391
+ await checkMinimumCredits(userId, 'basic');
392
+ const context = formatContext({ hierarchyContext, scriptContext, logContext });
393
+ runAsyncFeedback(projectId, userId, `USER: ${prompt || "Automatic Feedback"}${context}`, images || []);
394
+ res.json({ success: true });
395
+ } catch (err) { res.status(500).json({ error: err.message }); }
396
+ });
397
+
398
+ app.post('/project/ping', async (req, res) => {
399
+ const { projectId, userId, isFrontend } = req.body;
400
+ const project = await StateManager.getProject(projectId);
401
+ if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
402
+ if (isFrontend) return res.json({ status: StateManager.getStatus(projectId), snapshot: StateManager.getSnapshot(projectId) });
403
+ const command = await StateManager.popCommand(projectId);
404
+ const streamData = StateManager.popStream(projectId);
405
+ res.json({ action: streamData ? "STREAM_APPEND" : (command?.type || "IDLE"), stream: streamData, target: command?.payload, code: command?.type === 'EXECUTE' ? command.payload : null });
406
+ });
407
+
408
+ app.get('/admin/cleanup', async (req, res) => { res.json({ success: true, removedCount: StateManager.cleanupMemory() }); });
409
+
410
+ app.listen(PORT, () => console.log(`AI Backend Running on ${PORT}`));
411
+
412
+ /* import express from 'express';
413
+ import bodyParser from 'body-parser';
414
+ import cors from 'cors';
415
+ import { StateManager, initDB } from './stateManager.js';
416
+ import { AIEngine } from './aiEngine.js';
417
+ import fs from 'fs';
418
+ import crypto from "crypto";
419
+ import dotenv from 'dotenv';
420
+
421
+ dotenv.config();
422
+
423
+ initDB();
424
+ const supabase = StateManager.getSupabaseClient();
425
+
426
+ const app = express();
427
+ const PORT = process.env.PORT || 7860;
428
+
429
+ app.use(cors());
430
+ app.use(bodyParser.json({ limit: '50mb' }));
431
+
432
+ const MIN_BASIC_REQUIRED = 50;
433
+ const MIN_DIAMOND_REQUIRED = 50;
434
+
435
  // --- STATUS PHASES ---
436
  const WORKER_PHASES = [
437
  "Worker: Sorting...",
 
866
  if (result.usage?.totalTokenCount > 0) {
867
  await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
868
  }
869
+ // const usage = result.usage?.totalTokenCount || 0;
870
+ // if (usage > 0) await deductUserCredits(userId, usage, 'basic');
871
+ //
872
 
873
  if (result.status === "REJECTED") {
874
  return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." });
 
1049
 
1050
  app.listen(PORT, () => {
1051
  console.log(`AI Backend Running on ${PORT} (Supabase Edition)`);
1052
+ }); */