everydaytok commited on
Commit
ac15076
·
verified ·
1 Parent(s): faf50b6

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +519 -75
app.js CHANGED
@@ -3,162 +3,606 @@ 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 crypto from "crypto";
7
  import dotenv from 'dotenv';
8
 
9
  dotenv.config();
 
10
  initDB();
11
  const supabase = StateManager.getSupabaseClient();
 
12
  const app = express();
13
  const PORT = process.env.PORT || 7860;
 
14
  app.use(cors());
15
  app.use(bodyParser.json({ limit: '50mb' }));
16
 
17
- const WORKER_PHASES = ["Worker: Analyzing Context...", "Worker: Triage Task...", "Worker: Planning Logic...", "Worker: Thinking..."];
18
- const PM_PHASES = ["Manager: Reviewing Request...", "Manager: Formulating Strategy...", "Manager: Preparing Delegation...", "Manager: Thinking..."];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  function startStatusLoop(projectId, type = 'worker') {
21
  const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
22
  let index = 0;
 
23
  StateManager.setStatus(projectId, phases[0]);
 
24
  const interval = setInterval(() => {
25
  index++;
26
- if (index < phases.length) StateManager.setStatus(projectId, phases[index]);
27
- else clearInterval(interval);
 
 
 
28
  }, 1500);
 
29
  return () => clearInterval(interval);
30
  }
31
 
32
  async function checkMinimumCredits(userId, type = 'basic') {
33
- const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
34
- if (!data || (data.credits?.[type] || 0) < 50) throw new Error(`Insufficient credits.`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  async function deductUserCredits(userId, amount, type = 'basic') {
 
 
38
  try {
39
  const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
40
- const newVal = Math.max(0, (data?.credits?.[type] || 0) - amount);
41
- await supabase.from('users').update({ credits: { ...data.credits, [type]: newVal } }).eq('id', userId);
42
- } catch (e) { console.error("Credit Error:", e.message); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
- const formatContext = ({ hierarchyContext, scriptContext, logContext }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  let out = "";
47
  if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`;
48
  if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
49
  if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`;
50
  return out;
51
- };
52
 
53
- const extractTags = (text) => ({
54
- pmQuestion: text.match(/\[ASK_PM:\s*(.*?)\]/s)?.[1],
55
- pmRoute: text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s)?.[1],
56
- workerPrompt: text.match(/WORKER_PROMPT:\s*(.*)/s)?.[1]
57
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
 
 
 
60
  try {
61
  const project = await StateManager.getProject(projectId);
 
62
  StateManager.clearSnapshot(projectId);
63
 
64
- // --- PASS 1: WORKER ---
65
  let stopStatus = startStatusLoop(projectId, 'worker');
66
- let workerPass1 = "";
67
- const result1 = await AIEngine.callWorkerStream(
68
- project.workerHistory, fullInput,
69
- (thought) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); },
70
- (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Worker: Coding..."); workerPass1 += chunk; StateManager.appendStream(projectId, chunk); },
71
- images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  );
73
- stopStatus();
74
 
75
- const tags = extractTags(workerPass1);
76
- if (tags.pmQuestion || tags.pmRoute) {
77
- // Save Pass 1 immediately
78
- await StateManager.addHistory(projectId, 'worker', 'user', fullInput);
79
- await StateManager.addHistory(projectId, 'worker', 'model', workerPass1);
80
- await StateManager.queueCommand(projectId, workerPass1);
81
 
82
- // --- PASS 2: PROJECT MANAGER ---
83
- StateManager.clearSnapshot(projectId);
 
84
  stopStatus = startStatusLoop(projectId, 'pm');
85
- const pmInput = tags.pmRoute ? `[ROUTED TASK]: ${tags.pmRoute}` : `[QUESTION]: ${tags.pmQuestion}`;
86
- let pmText = "";
 
 
87
  await AIEngine.callPMStream(
88
- project.pmHistory, pmInput,
89
- (thought) => { stopStatus(); StateManager.setStatus(projectId, "Manager: Thinking..."); StateManager.appendSnapshotOnly(projectId, thought); },
90
- (chunk) => { stopStatus(); StateManager.setStatus(projectId, "Manager: Architecting..."); pmText += chunk; StateManager.appendSnapshotOnly(projectId, chunk); }
 
 
 
 
 
 
 
 
 
 
91
  );
92
- stopStatus();
 
 
 
 
93
 
94
- await StateManager.addHistory(projectId, 'pm', 'user', pmInput);
95
- await StateManager.addHistory(projectId, 'pm', 'model', pmText);
96
- await StateManager.queueCommand(projectId, pmText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- // Save PM answer into Worker's persistent history
99
- const pmAnswerVisible = `[PM RESPONSE]:\n${pmText}`;
100
- await StateManager.addHistory(projectId, 'worker', 'model', pmAnswerVisible);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- // --- PASS 3: WORKER FINAL PASS ---
103
- const nextTask = extractTags(pmText).workerPrompt || "Proceed based on PM answer.";
104
  StateManager.clearSnapshot(projectId);
105
  stopStatus = startStatusLoop(projectId, 'worker');
106
- let workerPass2 = "";
107
- const currentProj = await StateManager.getProject(projectId); // Refresh for Pass 2 context
108
  await AIEngine.callWorkerStream(
109
- currentProj.workerHistory, nextTask,
110
- (thought) => { stopStatus(); StateManager.appendSnapshotOnly(projectId, thought); },
111
- (chunk) => { stopStatus(); workerPass2 += chunk; StateManager.appendStream(projectId, chunk); }
 
 
 
 
 
 
 
 
 
 
112
  );
113
- stopStatus();
114
- await StateManager.addHistory(projectId, 'worker', 'model', workerPass2);
115
- await StateManager.queueCommand(projectId, workerPass2);
116
- } else {
117
- await StateManager.addHistory(projectId, 'worker', 'user', fullInput);
118
- await StateManager.addHistory(projectId, 'worker', 'model', workerPass1);
119
- await StateManager.queueCommand(projectId, workerPass1);
120
  }
121
 
 
 
 
 
 
 
 
122
  await StateManager.updateProject(projectId, { status: "idle" });
123
- await deductUserCredits(userId, 10);
 
 
 
124
  } catch (err) {
125
- console.error("Feedback Loop Crashed:", err.message);
126
  StateManager.setStatus(projectId, "Error: " + err.message);
127
  await StateManager.updateProject(projectId, { status: "error" });
128
  }
129
  }
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  app.post('/project/feedback', async (req, res) => {
132
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body;
 
133
  try {
134
  const project = await StateManager.getProject(projectId);
135
  if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
 
136
  await checkMinimumCredits(userId, 'basic');
 
137
  await StateManager.updateProject(projectId, { status: "working" });
138
- res.json({ success: true });
139
  const context = formatContext({ hierarchyContext, scriptContext, logContext });
140
- runAsyncFeedback(projectId, userId, `USER: ${prompt || "Auto-Correction Loop"}${context}`, images || []);
141
- } catch (err) { res.status(500).json({ error: err.message }); }
 
 
 
 
 
 
 
 
 
 
142
  });
143
 
144
  app.post('/project/ping', async (req, res) => {
145
  const { projectId, userId, isFrontend } = req.body;
 
 
146
  const project = await StateManager.getProject(projectId);
147
  if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
148
- if (isFrontend) return res.json({ status: StateManager.getStatus(projectId), snapshot: StateManager.getSnapshot(projectId) });
 
 
 
 
 
 
 
149
  const command = await StateManager.popCommand(projectId);
150
- const stream = StateManager.popStream(projectId);
151
- let resData = { action: stream ? "STREAM_APPEND" : "IDLE", stream };
152
- if (command) { resData.action = command.type; resData.target = command.payload; resData.code = command.payload; }
153
- res.json(resData);
 
 
 
 
 
 
 
 
 
 
 
154
  });
155
 
156
- app.post('/onboarding/create', async (req, res) => {
157
- const { userId, description, answers } = req.body;
158
- const projectId = `proj_${Date.now()}_${crypto.randomBytes(2).toString("hex")}`;
159
- const grade = await AIEngine.gradeProject(description, answers);
160
- await supabase.from('projects').insert({ id: projectId, user_id: userId, info: { title: grade.title, status: "idle" } });
161
- res.json({ success: true, projectId, grade });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  });
163
 
164
- app.listen(PORT, () => console.log(`AI Backend live on ${PORT}`));
 
 
 
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 crypto from "crypto";
8
  import dotenv from 'dotenv';
9
 
10
  dotenv.config();
11
+
12
  initDB();
13
  const supabase = StateManager.getSupabaseClient();
14
+
15
  const app = express();
16
  const PORT = process.env.PORT || 7860;
17
+
18
  app.use(cors());
19
  app.use(bodyParser.json({ limit: '50mb' }));
20
 
21
+ const MIN_BASIC_REQUIRED = 50;
22
+ const MIN_DIAMOND_REQUIRED = 50;
23
+
24
+ // --- STATUS PHASES ---
25
+ const WORKER_PHASES = [
26
+ "Worker: Analyzing Request...",
27
+ "Worker: Reading Context...",
28
+ "Worker: Checking Hierarchy...",
29
+ "Worker: Planning Logic...",
30
+ "Worker: Preparing Environment...",
31
+ "Worker: Thinking..."
32
+ ];
33
+
34
+ const PM_PHASES = [
35
+ "Manager: Reviewing Request...",
36
+ "Manager: Analyzing Project Structure...",
37
+ "Manager: Consulting Guidelines...",
38
+ "Manager: Formulating Strategy...",
39
+ "Manager: Delegating Tasks...",
40
+ "Manager: Thinking..."
41
+ ];
42
 
43
  function startStatusLoop(projectId, type = 'worker') {
44
  const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES;
45
  let index = 0;
46
+
47
  StateManager.setStatus(projectId, phases[0]);
48
+
49
  const interval = setInterval(() => {
50
  index++;
51
+ if (index < phases.length) {
52
+ StateManager.setStatus(projectId, phases[index]);
53
+ } else {
54
+ clearInterval(interval);
55
+ }
56
  }, 1500);
57
+
58
  return () => clearInterval(interval);
59
  }
60
 
61
  async function checkMinimumCredits(userId, type = 'basic') {
62
+ if (!supabase) return;
63
+
64
+ const { data, error } = await supabase
65
+ .from('users')
66
+ .select('credits')
67
+ .eq('id', userId)
68
+ .single();
69
+
70
+ if (error || !data) return;
71
+
72
+ const credits = data.credits?.[type] || 0;
73
+ const required = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
74
+
75
+ if (credits < required) {
76
+ throw new Error(`Insufficient ${type} credits. You have ${credits}, need minimum ${required}.`);
77
+ }
78
  }
79
 
80
  async function deductUserCredits(userId, amount, type = 'basic') {
81
+ if (!supabase || !amount || amount <= 0) return;
82
+
83
  try {
84
  const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
85
+ if (!data) return;
86
+
87
+ const currentCredits = data.credits || {};
88
+ const currentVal = currentCredits[type] || 0;
89
+ const newVal = Math.max(0, currentVal - amount);
90
+
91
+ const updatedCredits = { ...currentCredits, [type]: newVal };
92
+
93
+ await supabase
94
+ .from('users')
95
+ .update({ credits: updatedCredits })
96
+ .eq('id', userId);
97
+
98
+ console.log(`[Credits] Deducted ${amount} ${type} from User ${userId}`);
99
+ } catch (err) {
100
+ console.error(`[Credits] Failed to deduct from ${userId}:`, err);
101
+ }
102
  }
103
 
104
+ const validateRequest = (req, res, next) => {
105
+ if (req.path.includes('/admin/cleanup')) return next();
106
+ const { userId } = req.body;
107
+ if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) {
108
+ return res.status(400).json({ error: "Missing userId" });
109
+ }
110
+ next();
111
+ };
112
+
113
+ function extractWorkerPrompt(text) {
114
+ if (!text || typeof text !== 'string') return null;
115
+ const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
116
+ return match ? match[1].trim() : null;
117
+ }
118
+
119
+ function extractPMQuestion(text) {
120
+ if (!text || typeof text !== 'string') return null;
121
+ const match = text.match(/\[ASK_PM:\s*(.*?)\]/s);
122
+ return match ? match[1].trim() : null;
123
+ }
124
+
125
+ function extractImagePrompt(text) {
126
+ if (!text || typeof text !== 'string') return null;
127
+ const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s);
128
+ return match ? match[1].trim() : null;
129
+ }
130
+
131
+ function extractRouteToPM(text) {
132
+ if (!text || typeof text !== 'string') return null;
133
+ const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s);
134
+ return match ? match[1].trim() : null;
135
+ }
136
+
137
+ function formatContext({ hierarchyContext, scriptContext, logContext }) {
138
  let out = "";
139
  if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`;
140
  if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`;
141
  if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`;
142
  return out;
143
+ }
144
 
145
+ async function runBackgroundInitialization(projectId, userId, description) {
146
+ if (StateManager.isLocked(projectId)) {
147
+ console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`);
148
+ return;
149
+ }
150
+
151
+ StateManager.lock(projectId);
152
+ console.log(`[Background] Starting initialization for ${projectId}`);
153
+
154
+ let diamondUsage = 0;
155
+ let basicUsage = 0;
156
+
157
+ try {
158
+ await StateManager.getProject(projectId);
159
+
160
+ await StateManager.updateProject(projectId, {
161
+ status: "WORKING",
162
+ gdd: "",
163
+ failureCount: 0
164
+ });
165
+
166
+ const pmHistory = [];
167
+
168
+ let stopStatus = startStatusLoop(projectId, 'pm');
169
+
170
+ const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`;
171
+ const gddResult = await AIEngine.callPM(pmHistory, gddPrompt);
172
+
173
+ stopStatus();
174
+
175
+ if (!gddResult?.text) throw new Error("PM failed to generate GDD");
176
+
177
+ const gddText = gddResult.text;
178
+ diamondUsage += (gddResult.usage?.totalTokenCount || 0);
179
+
180
+ await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt);
181
+ await StateManager.addHistory(projectId, 'pm', 'model', gddText);
182
+
183
+ pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
184
+ pmHistory.push({ role: 'model', parts: [{ text: gddText }] });
185
+
186
+ stopStatus = startStatusLoop(projectId, 'pm');
187
+
188
+ const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
189
+ const taskResult = await AIEngine.callPM(pmHistory, taskPrompt);
190
+
191
+ stopStatus();
192
+
193
+ if (!taskResult?.text) throw new Error("PM failed to generate Task");
194
+
195
+ const taskText = taskResult.text;
196
+ diamondUsage += (taskResult.usage?.totalTokenCount || 0);
197
+
198
+ await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt);
199
+ await StateManager.addHistory(projectId, 'pm', 'model', taskText);
200
+
201
+ const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`;
202
+ const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
203
+
204
+ stopStatus = startStatusLoop(projectId, 'worker');
205
+
206
+ let workerTextAccumulated = "";
207
+
208
+ await AIEngine.callWorkerStream(
209
+ [],
210
+ initialWorkerPrompt,
211
+ (thought) => {
212
+ stopStatus();
213
+ StateManager.setStatus(projectId, "Worker: Thinking...");
214
+ StateManager.appendSnapshotOnly(projectId, thought);
215
+ },
216
+ (chunk) => {
217
+ stopStatus();
218
+ StateManager.setStatus(projectId, "Worker: Coding...");
219
+ workerTextAccumulated += chunk;
220
+ StateManager.appendStream(projectId, chunk);
221
+ }
222
+ );
223
 
224
+ if (!workerTextAccumulated) throw new Error("Worker failed to initialize");
225
+
226
+ await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt);
227
+ await StateManager.addHistory(projectId, 'worker', 'model', workerTextAccumulated);
228
+
229
+ await StateManager.updateProject(projectId, {
230
+ gdd: gddText,
231
+ status: "IDLE"
232
+ });
233
+
234
+ await processAndQueueResponse(projectId, workerTextAccumulated, userId);
235
+
236
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
237
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
238
+
239
+ console.log(`[Background] Init complete for ${projectId}`);
240
+
241
+ } catch (err) {
242
+ console.error(`[Background] Init Error for ${projectId}:`, err.message);
243
+ await StateManager.updateProject(projectId, { status: "ERROR" });
244
+ } finally {
245
+ StateManager.unlock(projectId);
246
+ }
247
+ }
248
+
249
+ // --- UPDATED: Accepts IMAGES ---
250
  async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
251
+ let diamondUsage = 0;
252
+ let basicUsage = 0;
253
+
254
  try {
255
  const project = await StateManager.getProject(projectId);
256
+
257
  StateManager.clearSnapshot(projectId);
258
 
 
259
  let stopStatus = startStatusLoop(projectId, 'worker');
260
+
261
+ let responseText = "";
262
+ let thoughtText = "";
263
+
264
+ // Pass images to Worker Stream
265
+ await AIEngine.callWorkerStream(
266
+ project.workerHistory,
267
+ fullInput,
268
+ (thought) => {
269
+ stopStatus();
270
+ thoughtText += thought;
271
+ StateManager.setStatus(projectId, "Worker: Thinking...");
272
+ StateManager.appendSnapshotOnly(projectId, thought);
273
+ },
274
+ (chunk) => {
275
+ stopStatus();
276
+ responseText += chunk;
277
+ StateManager.setStatus(projectId, "Worker: Coding...");
278
+ StateManager.appendStream(projectId, chunk);
279
+ },
280
+ images // <--- Passing images here
281
  );
 
282
 
283
+ const routeTask = extractRouteToPM(responseText);
284
+ const pmQuestion = extractPMQuestion(responseText);
 
 
 
 
285
 
286
+ if (routeTask) {
287
+ console.log(`[${projectId}] Routing task to PM...`);
288
+ StateManager.clearSnapshot(projectId);
289
  stopStatus = startStatusLoop(projectId, 'pm');
290
+
291
+ const pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.`;
292
+ let pmResponseText = "";
293
+
294
  await AIEngine.callPMStream(
295
+ project.pmHistory,
296
+ pmPrompt,
297
+ (thought) => {
298
+ stopStatus();
299
+ StateManager.setStatus(projectId, "Manager: Thinking...");
300
+ StateManager.appendSnapshotOnly(projectId, thought);
301
+ },
302
+ (chunk) => {
303
+ stopStatus();
304
+ StateManager.setStatus(projectId, "Manager: Planning...");
305
+ pmResponseText += chunk;
306
+ StateManager.appendSnapshotOnly(projectId, chunk);
307
+ }
308
  );
309
+
310
+ diamondUsage += 0;
311
+
312
+ await StateManager.addHistory(projectId, 'pm', 'user', routeTask);
313
+ await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
314
 
315
+ const nextInstruction = extractWorkerPrompt(pmResponseText);
316
+ if (nextInstruction) {
317
+ StateManager.clearSnapshot(projectId);
318
+ stopStatus = startStatusLoop(projectId, 'worker');
319
+
320
+ let subResponse = "";
321
+ await AIEngine.callWorkerStream(
322
+ project.workerHistory,
323
+ `[PM DELEGATION]: ${nextInstruction}`,
324
+ (thought) => {
325
+ stopStatus();
326
+ StateManager.setStatus(projectId, "Worker: Thinking...");
327
+ StateManager.appendSnapshotOnly(projectId, thought);
328
+ },
329
+ (chunk) => {
330
+ stopStatus();
331
+ StateManager.setStatus(projectId, "Worker: Coding...");
332
+ subResponse += chunk;
333
+ StateManager.appendStream(projectId, chunk);
334
+ }
335
+ );
336
+ responseText = subResponse;
337
+ }
338
+ } else if (pmQuestion) {
339
+ console.log(`[${projectId}] Worker consulting PM...`);
340
+ StateManager.clearSnapshot(projectId);
341
+ stopStatus = startStatusLoop(projectId, 'pm');
342
 
343
+ let pmResponseText = "";
344
+ await AIEngine.callPMStream(
345
+ project.pmHistory,
346
+ pmQuestion,
347
+ (thought) => {
348
+ stopStatus();
349
+ StateManager.setStatus(projectId, "Manager: Thinking...");
350
+ StateManager.appendSnapshotOnly(projectId, thought);
351
+ },
352
+ (chunk) => {
353
+ stopStatus();
354
+ StateManager.setStatus(projectId, "Manager: Advising...");
355
+ pmResponseText += chunk;
356
+ StateManager.appendSnapshotOnly(projectId, chunk);
357
+ }
358
+ );
359
+
360
+ diamondUsage += 0;
361
+
362
+ await StateManager.addHistory(projectId, 'pm', 'user', pmQuestion);
363
+ await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
364
 
 
 
365
  StateManager.clearSnapshot(projectId);
366
  stopStatus = startStatusLoop(projectId, 'worker');
367
+
368
+ let subResponse = "";
369
  await AIEngine.callWorkerStream(
370
+ project.workerHistory,
371
+ `[PM ANSWER]: ${pmResponseText}`,
372
+ (thought) => {
373
+ stopStatus();
374
+ StateManager.setStatus(projectId, "Worker: Thinking...");
375
+ StateManager.appendSnapshotOnly(projectId, thought);
376
+ },
377
+ (chunk) => {
378
+ stopStatus();
379
+ StateManager.setStatus(projectId, "Worker: Coding...");
380
+ subResponse += chunk;
381
+ StateManager.appendStream(projectId, chunk);
382
+ }
383
  );
384
+ responseText = subResponse;
 
 
 
 
 
 
385
  }
386
 
387
+ StateManager.setStatus(projectId, "Finalizing...");
388
+
389
+ await StateManager.addHistory(projectId, 'worker', 'user', fullInput);
390
+ await StateManager.addHistory(projectId, 'worker', 'model', responseText);
391
+
392
+ await processAndQueueResponse(projectId, responseText, userId);
393
+
394
  await StateManager.updateProject(projectId, { status: "idle" });
395
+
396
+ if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
397
+ if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
398
+
399
  } catch (err) {
400
+ console.error("Async Feedback Error:", err);
401
  StateManager.setStatus(projectId, "Error: " + err.message);
402
  await StateManager.updateProject(projectId, { status: "error" });
403
  }
404
  }
405
 
406
+ async function processAndQueueResponse(projectId, rawResponse, userId) {
407
+ if (!rawResponse) return;
408
+
409
+ if (!StateManager.queueCommand) {
410
+ console.error("CRITICAL: StateManager.queueCommand is missing.");
411
+ return;
412
+ }
413
+
414
+ const imgPrompt = extractImagePrompt(rawResponse);
415
+ if (imgPrompt) {
416
+ const imgResult = await AIEngine.generateImage(imgPrompt);
417
+ if (imgResult?.image) {
418
+ if (userId) await deductUserCredits(userId, 1000, 'basic');
419
+ await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
420
+ }
421
+ }
422
+
423
+ await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: rawResponse });
424
+ }
425
+
426
+ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
427
+ const { description, userId } = req.body;
428
+ if (!description) return res.status(400).json({ error: "Description required" });
429
+
430
+ try {
431
+ await checkMinimumCredits(userId, 'basic');
432
+ const result = await AIEngine.generateEntryQuestions(description);
433
+ const usage = result.usage?.totalTokenCount || 0;
434
+ if (usage > 0) await deductUserCredits(userId, usage, 'basic');
435
+
436
+ if (result.status === "REJECTED") {
437
+ return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." });
438
+ }
439
+ res.json({ questions: result.questions });
440
+ } catch (err) {
441
+ res.status(500).json({ error: err.message });
442
+ }
443
+ });
444
+
445
+ app.post('/onboarding/create', validateRequest, async (req, res) => {
446
+ const { userId, description, answers } = req.body;
447
+ let basicTokens = 0;
448
+
449
+ try {
450
+ await checkMinimumCredits(userId, 'basic');
451
+ await checkMinimumCredits(userId, 'diamond');
452
+
453
+ const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
454
+ const projectId = `proj_${Date.now()}_${randomHex(7)}`;
455
+
456
+ const gradingResult = await AIEngine.gradeProject(description, answers);
457
+ basicTokens += (gradingResult.usage?.totalTokenCount || 0);
458
+
459
+ const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F';
460
+
461
+ if (!isFailure) {
462
+ const { error: insertError } = await supabase.from('projects').insert({
463
+ id: projectId,
464
+ user_id: userId,
465
+ info: {
466
+ title: gradingResult.title || "Untitled",
467
+ stats: gradingResult,
468
+ description,
469
+ answers,
470
+ status: "initializing"
471
+ }
472
+ });
473
+
474
+ if (insertError) {
475
+ console.error("Failed to insert project:", insertError.message);
476
+ return res.status(500).json({ error: "Database save failed" });
477
+ }
478
+
479
+ runBackgroundInitialization(projectId, userId, description);
480
+ }
481
+
482
+ if (basicTokens > 0) await deductUserCredits(userId, basicTokens, 'basic');
483
+
484
+ res.json({
485
+ status: 200,
486
+ success: !isFailure,
487
+ projectId,
488
+ stats: gradingResult,
489
+ title: gradingResult.title || "Untitled"
490
+ });
491
+
492
+ } catch (err) {
493
+ console.error("Create Error:", err);
494
+ res.status(500).json({ error: err.message });
495
+ }
496
+ });
497
+
498
  app.post('/project/feedback', async (req, res) => {
499
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body;
500
+
501
  try {
502
  const project = await StateManager.getProject(projectId);
503
  if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
504
+
505
  await checkMinimumCredits(userId, 'basic');
506
+
507
  await StateManager.updateProject(projectId, { status: "working" });
508
+
509
  const context = formatContext({ hierarchyContext, scriptContext, logContext });
510
+ const fullInput = `USER: ${prompt || "Automatic Feedback"}${context}`;
511
+
512
+ // Pass images array to async runner
513
+ runAsyncFeedback(projectId, userId, fullInput, images || []);
514
+
515
+ res.json({ success: true, message: "Processing started" });
516
+
517
+ } catch (err) {
518
+ console.error("Feedback Error:", err);
519
+ await StateManager.updateProject(projectId, { status: "error" });
520
+ res.status(500).json({ error: "Feedback process failed" });
521
+ }
522
  });
523
 
524
  app.post('/project/ping', async (req, res) => {
525
  const { projectId, userId, isFrontend } = req.body;
526
+ if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" });
527
+
528
  const project = await StateManager.getProject(projectId);
529
  if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
530
+
531
+ const currentStatus = StateManager.getStatus(projectId);
532
+
533
+ if (isFrontend) {
534
+ const snapshot = StateManager.getSnapshot(projectId);
535
+ return res.json({ status: currentStatus, snapshot });
536
+ }
537
+
538
  const command = await StateManager.popCommand(projectId);
539
+ const streamData = StateManager.popStream(projectId);
540
+
541
+ let response = { action: "IDLE", stream: streamData || null };
542
+
543
+ if (streamData && streamData.length > 0) {
544
+ response.action = "STREAM_APPEND";
545
+ }
546
+
547
+ if (command) {
548
+ response.action = command.type;
549
+ response.target = command.payload;
550
+ response.code = command.type === 'EXECUTE' ? command.payload : null;
551
+ }
552
+
553
+ res.json(response);
554
  });
555
 
556
+ app.post('/human/override', validateRequest, async (req, res) => {
557
+ const { projectId, instruction, userId } = req.body;
558
+ try {
559
+ await checkMinimumCredits(userId, 'basic');
560
+ const project = await StateManager.getProject(projectId);
561
+
562
+ res.json({ success: true });
563
+
564
+ const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
565
+
566
+ let overrideText = "";
567
+ let stopStatus = startStatusLoop(projectId, 'worker');
568
+
569
+ AIEngine.callWorkerStream(
570
+ project.workerHistory,
571
+ overrideMsg,
572
+ (thought) => {
573
+ stopStatus();
574
+ StateManager.setStatus(projectId, "Worker: Thinking...");
575
+ StateManager.appendSnapshotOnly(projectId, thought);
576
+ },
577
+ (chunk) => {
578
+ stopStatus();
579
+ StateManager.setStatus(projectId, "Worker: Coding...");
580
+ overrideText += chunk;
581
+ StateManager.appendStream(projectId, chunk);
582
+ }
583
+ ).then(async (workerResult) => {
584
+ await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg);
585
+ await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText);
586
+ await processAndQueueResponse(projectId, workerResult.text || overrideText, userId);
587
+
588
+ const usage = workerResult.usage?.totalTokenCount || 0;
589
+ if (usage > 0) await deductUserCredits(userId, usage, 'basic');
590
+ });
591
+
592
+ } catch (err) {
593
+ res.status(500).json({ error: err.message });
594
+ }
595
+ });
596
+
597
+ app.get('/admin/cleanup', async (req, res) => {
598
+ try {
599
+ const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0;
600
+ res.json({ success: true, removedCount: removed });
601
+ } catch (err) {
602
+ res.status(500).json({ error: "Cleanup failed" });
603
+ }
604
  });
605
 
606
+ app.listen(PORT, () => {
607
+ console.log(`AI Backend Running on ${PORT} (Supabase Edition)`);
608
+ });