everydaycats commited on
Commit
93c3eaa
·
verified ·
1 Parent(s): 1de2c2c

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +48 -62
app.js CHANGED
@@ -17,7 +17,6 @@ try {
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
19
  // We assume the project ID is available in the service account to construct the bucket name
20
- // Or you can hardcode your bucket: "your-project-id.firebasestorage.app"
21
  const bucketName = `shago-web.firebasestorage.app`;
22
 
23
  if (admin.apps.length === 0) {
@@ -119,7 +118,6 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
119
  app.post('/onboarding/create', validateRequest, async (req, res) => {
120
  const { userId, description, answers } = req.body;
121
 
122
- // helper: random hex of N chars (N can be 6 or 7)
123
  const randomHex = (n = 6) => {
124
  const bytes = Math.ceil(n / 2);
125
  return crypto.randomBytes(bytes).toString("hex").slice(0, n);
@@ -144,12 +142,10 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
144
  thumbnailBase64 = await AIEngine.generateImage(imagePrompt);
145
  }
146
 
147
- // --- UPLOAD TO STORAGE & PREPARE URL ---
148
  let thumbnailUrl = null;
149
  if (thumbnailBase64 && storage) {
150
  try {
151
  console.log(`[Onboarding] 📤 Uploading Thumbnail to Storage...`);
152
- // Remove header if present to get pure buffer
153
  const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
154
  const buffer = Buffer.from(base64Data, 'base64');
155
  const bucket = storage.bucket();
@@ -159,22 +155,17 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
159
  metadata: { contentType: 'image/png' }
160
  });
161
 
162
- // Make public and get URL
163
  await file.makePublic();
164
- // Construct the public URL manually or use file.publicUrl()
165
  thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
166
  console.log(`[Onboarding] 🖼️ Thumbnail URL: ${thumbnailUrl}`);
167
  } catch (uploadErr) {
168
  console.error("Storage Upload Failed:", uploadErr);
169
- // Fallback: thumbnailUrl remains null, logic continues without crashing
170
  }
171
  }
172
 
173
  const timestamp = Date.now();
174
  const status = isFailure ? "rejected" : "Idle";
175
 
176
- // 1. Prepare Memory Object
177
- // Note: storing URL instead of raw Base64 now
178
  const memoryObject = {
179
  id: projectId,
180
  userId,
@@ -191,7 +182,6 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
191
  failureCount: 0
192
  };
193
 
194
- // 2. FIRESTORE WRITE (Project Indexing)
195
  if (firestore && !isFailure) {
196
  try {
197
  await firestore.collection('projects').doc(projectId).set({
@@ -206,11 +196,8 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
206
  }
207
  }
208
 
209
- // 3. REALTIME DB WRITE (Deep State)
210
  if (db && !isFailure) {
211
  const updates = {};
212
-
213
- // Bucket 1: Info (Lightweight)
214
  updates[`projects/${projectId}/info`] = {
215
  id: projectId,
216
  userId,
@@ -222,12 +209,10 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
222
  status
223
  };
224
 
225
- // Bucket 2: Thumbnail (Store URL object, not Blob)
226
  if (thumbnailUrl) {
227
  updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
228
  }
229
 
230
- // Bucket 3: State (History)
231
  updates[`projects/${projectId}/state`] = {
232
  workerHistory: [],
233
  pmHistory: [],
@@ -238,7 +223,6 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
238
  await db.ref().update(updates);
239
  }
240
 
241
- // 4. Update Memory
242
  if (!isFailure) {
243
  await StateManager.updateProject(projectId, memoryObject);
244
  }
@@ -248,7 +232,7 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
248
  projectId,
249
  stats: grading,
250
  title: grading.title || "Untitled Project",
251
- thumbnail: thumbnailBase64 // thumbnailUrl // Return URL to frontend
252
  });
253
 
254
  } catch (err) {
@@ -259,26 +243,28 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
259
 
260
  // --- CORE ENDPOINTS ---
261
 
262
- app.post('/new/project', validateRequest, async (req, res) => {
263
- const { userId, projectId, description } = req.body;
 
264
  try {
265
  const pmHistory = [];
266
- await db.ref(`projects/${projectId}/info/status`).set("working");
267
-
268
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
269
  const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
270
 
271
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
272
  pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
273
 
 
274
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
275
  const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
276
 
277
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
278
  pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
279
 
 
280
  const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
281
-
282
  const workerHistory = [];
283
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
284
  const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
@@ -286,6 +272,7 @@ app.post('/new/project', validateRequest, async (req, res) => {
286
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
287
  workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
288
 
 
289
  await StateManager.updateProject(projectId, {
290
  userId,
291
  pmHistory,
@@ -295,28 +282,48 @@ app.post('/new/project', validateRequest, async (req, res) => {
295
  });
296
 
297
  await processAndQueueResponse(projectId, workerResponse);
298
- await db.ref(`projects/${projectId}/info/status`).set("working");
299
-
300
- res.json({ success: true, message: "Workspace Initialized", gddPreview: gddResponse.substring(0, 200) });
 
 
 
301
  } catch (err) {
302
- console.error("Init Error:", err);
303
- res.status(500).json({ error: "Initialization Failed" });
304
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  });
306
 
307
  app.post('/project/feedback', async (req, res) => {
308
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
309
 
310
- // This now handles Hydration automatically
311
  const project = await StateManager.getProject(projectId);
312
 
313
  if (!project) return res.status(404).json({ error: "Project not found." });
314
 
315
- if (project.userId !== userId) {
316
  console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
317
  return res.status(403).json({ error: "Unauthorized: You do not own this project." });
318
- }
319
- await db.ref(`projects/${projectId}/info/status`).set("working");
 
 
320
 
321
  if (taskComplete) {
322
  console.log(`[${projectId}] ✅ TASK COMPLETE.`);
@@ -329,6 +336,7 @@ app.post('/project/feedback', async (req, res) => {
329
  const nextInstruction = extractWorkerPrompt(pmResponse);
330
  if (!nextInstruction) {
331
  await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
 
332
  return res.json({ success: true, message: "No further tasks. Project Idle." });
333
  }
334
 
@@ -347,6 +355,7 @@ app.post('/project/feedback', async (req, res) => {
347
 
348
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
349
  await processAndQueueResponse(projectId, workerResponse);
 
350
  return res.json({ success: true, message: "Next Task Assigned" });
351
  }
352
 
@@ -420,9 +429,13 @@ app.post('/project/feedback', async (req, res) => {
420
  });
421
 
422
  await processAndQueueResponse(projectId, response);
423
- await db.ref(`projects/${projectId}/info/status`).set("working");
 
 
 
 
424
 
425
- res.json({ success: true });
426
  } catch (err) {
427
  console.error("AI Error:", err);
428
  res.status(500).json({ error: "AI Failed" });
@@ -430,31 +443,23 @@ app.post('/project/feedback', async (req, res) => {
430
  });
431
 
432
  app.post('/project/ping', async (req, res) => {
433
- // 1. Accept userId along with projectId
434
  const { projectId, userId } = req.body;
435
- // console.log("1");
436
 
437
  if (!projectId || !userId) {
438
  return res.status(400).json({ error: "Missing ID fields" });
439
  }
440
- // console.log("2");
441
- // 2. Retrieve Project State (Hydrates from DB if not in memory)
442
  const project = await StateManager.getProject(projectId);
443
 
444
  if (!project) {
445
- // If project doesn't exist in Memory or DB
446
- console.log("project not found in db, id: ", projectId);
447
  return res.status(404).json({ action: "IDLE", error: "Project not found" });
448
  }
449
- // console.log("3");
450
 
451
- // 3. SECURITY CHECK: Ensure the user matches the project owner
452
  if (project.userId !== userId) {
453
  console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
454
  return res.status(403).json({ error: "Unauthorized: You do not own this project." });
455
  }
456
- // console.log("4");
457
- // 4. Retrieve Command (Only if authorized)
458
  const command = await StateManager.popCommand(projectId);
459
 
460
  if (command) {
@@ -467,29 +472,10 @@ app.post('/project/ping', async (req, res) => {
467
  code: command.type === 'EXECUTE' ? command.payload : null
468
  });
469
  }
470
- } else {
471
- // console.log("5");
472
- res.json({ action: "IDLE" });
473
- }
474
- });
475
-
476
- /*
477
- app.post('/project/ping', async (req, res) => {
478
- const { projectId } = req.body;
479
- // This will hydrate from DB if missing
480
- const command = await StateManager.popCommand(projectId);
481
-
482
- if (command) {
483
- if (command.payload === "CLEAR_CONSOLE") {
484
- res.json({ action: "CLEAR_LOGS" });
485
- } else {
486
- res.json({ action: command.type, target: command.payload, code: command.type === 'EXECUTE' ? command.payload : null });
487
- }
488
  } else {
489
  res.json({ action: "IDLE" });
490
  }
491
  });
492
- */
493
 
494
  app.post('/human/override', validateRequest, async (req, res) => {
495
  const { projectId, instruction, pruneHistory } = req.body;
 
17
  const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
18
 
19
  // We assume the project ID is available in the service account to construct the bucket name
 
20
  const bucketName = `shago-web.firebasestorage.app`;
21
 
22
  if (admin.apps.length === 0) {
 
118
  app.post('/onboarding/create', validateRequest, async (req, res) => {
119
  const { userId, description, answers } = req.body;
120
 
 
121
  const randomHex = (n = 6) => {
122
  const bytes = Math.ceil(n / 2);
123
  return crypto.randomBytes(bytes).toString("hex").slice(0, n);
 
142
  thumbnailBase64 = await AIEngine.generateImage(imagePrompt);
143
  }
144
 
 
145
  let thumbnailUrl = null;
146
  if (thumbnailBase64 && storage) {
147
  try {
148
  console.log(`[Onboarding] 📤 Uploading Thumbnail to Storage...`);
 
149
  const base64Data = thumbnailBase64.replace(/^data:image\/\w+;base64,/, "");
150
  const buffer = Buffer.from(base64Data, 'base64');
151
  const bucket = storage.bucket();
 
155
  metadata: { contentType: 'image/png' }
156
  });
157
 
 
158
  await file.makePublic();
 
159
  thumbnailUrl = `https://storage.googleapis.com/${bucket.name}/${projectId}/thumbnail.png`;
160
  console.log(`[Onboarding] 🖼️ Thumbnail URL: ${thumbnailUrl}`);
161
  } catch (uploadErr) {
162
  console.error("Storage Upload Failed:", uploadErr);
 
163
  }
164
  }
165
 
166
  const timestamp = Date.now();
167
  const status = isFailure ? "rejected" : "Idle";
168
 
 
 
169
  const memoryObject = {
170
  id: projectId,
171
  userId,
 
182
  failureCount: 0
183
  };
184
 
 
185
  if (firestore && !isFailure) {
186
  try {
187
  await firestore.collection('projects').doc(projectId).set({
 
196
  }
197
  }
198
 
 
199
  if (db && !isFailure) {
200
  const updates = {};
 
 
201
  updates[`projects/${projectId}/info`] = {
202
  id: projectId,
203
  userId,
 
209
  status
210
  };
211
 
 
212
  if (thumbnailUrl) {
213
  updates[`projects/${projectId}/thumbnail`] = { url: thumbnailUrl };
214
  }
215
 
 
216
  updates[`projects/${projectId}/state`] = {
217
  workerHistory: [],
218
  pmHistory: [],
 
223
  await db.ref().update(updates);
224
  }
225
 
 
226
  if (!isFailure) {
227
  await StateManager.updateProject(projectId, memoryObject);
228
  }
 
232
  projectId,
233
  stats: grading,
234
  title: grading.title || "Untitled Project",
235
+ thumbnail: thumbnailBase64
236
  });
237
 
238
  } catch (err) {
 
243
 
244
  // --- CORE ENDPOINTS ---
245
 
246
+ // NEW: Helper function to run initialization in background
247
+ async function runBackgroundInitialization(projectId, userId, description) {
248
+ console.log(`[Background] Starting initialization for ${projectId}`);
249
  try {
250
  const pmHistory = [];
251
+
252
+ // 1. Generate GDD
253
  const gddPrompt = `Create a comprehensive GDD for: ${description}`;
254
  const gddResponse = await AIEngine.callPM(pmHistory, gddPrompt);
255
 
256
  pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] });
257
  pmHistory.push({ role: 'model', parts: [{ text: gddResponse }] });
258
 
259
+ // 2. Generate First Task
260
  const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>";
261
  const taskResponse = await AIEngine.callPM(pmHistory, taskPrompt);
262
 
263
  pmHistory.push({ role: 'user', parts: [{ text: taskPrompt }] });
264
  pmHistory.push({ role: 'model', parts: [{ text: taskResponse }] });
265
 
266
+ // 3. Initialize Worker
267
  const initialWorkerInstruction = extractWorkerPrompt(taskResponse) || `Initialize structure for: ${description}`;
 
268
  const workerHistory = [];
269
  const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`;
270
  const workerResponse = await AIEngine.callWorker(workerHistory, initialWorkerPrompt, []);
 
272
  workerHistory.push({ role: 'user', parts: [{ text: initialWorkerPrompt }] });
273
  workerHistory.push({ role: 'model', parts: [{ text: workerResponse }] });
274
 
275
+ // 4. Update Memory & DB
276
  await StateManager.updateProject(projectId, {
277
  userId,
278
  pmHistory,
 
282
  });
283
 
284
  await processAndQueueResponse(projectId, workerResponse);
285
+
286
+ // 5. Update Status to IDLE (Ready for user input)
287
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("IDLE");
288
+
289
+ console.log(`[Background] Initialization complete for ${projectId}`);
290
+
291
  } catch (err) {
292
+ console.error(`[Background] Init Error for ${projectId}:`, err);
293
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("error");
294
  }
295
+ }
296
+
297
+ app.post('/new/project', validateRequest, (req, res) => {
298
+ const { userId, projectId, description } = req.body;
299
+
300
+ // 1. Immediately acknowledge request
301
+ // We set status to 'working' or 'initializing' immediately so frontend shows a loader
302
+ if(db) db.ref(`projects/${projectId}/info/status`).set("initializing");
303
+
304
+ res.json({
305
+ success: true,
306
+ message: "Project initialization started in background."
307
+ });
308
+
309
+ // 2. Trigger background process (no await)
310
+ runBackgroundInitialization(projectId, userId, description);
311
  });
312
 
313
  app.post('/project/feedback', async (req, res) => {
314
  const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, taskComplete, images } = req.body;
315
 
 
316
  const project = await StateManager.getProject(projectId);
317
 
318
  if (!project) return res.status(404).json({ error: "Project not found." });
319
 
320
+ if (project.userId !== userId) {
321
  console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
322
  return res.status(403).json({ error: "Unauthorized: You do not own this project." });
323
+ }
324
+
325
+ // Update status to working immediately
326
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
327
 
328
  if (taskComplete) {
329
  console.log(`[${projectId}] ✅ TASK COMPLETE.`);
 
336
  const nextInstruction = extractWorkerPrompt(pmResponse);
337
  if (!nextInstruction) {
338
  await StateManager.updateProject(projectId, { pmHistory: project.pmHistory, status: "IDLE" });
339
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("IDLE");
340
  return res.json({ success: true, message: "No further tasks. Project Idle." });
341
  }
342
 
 
355
 
356
  StateManager.queueCommand(projectId, { type: "EXECUTE", payload: "warn('Starting Next Task...')" });
357
  await processAndQueueResponse(projectId, workerResponse);
358
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("working"); // Keep working while plugin picks it up? Or IDLE to wait for plugin?
359
  return res.json({ success: true, message: "Next Task Assigned" });
360
  }
361
 
 
429
  });
430
 
431
  await processAndQueueResponse(projectId, response);
432
+ // Ensure status goes back to working or IDLE depending on your loop preference.
433
+ // Usually, if we just gave a command, we are 'working' until the plugin polls and finishes.
434
+ // But for UI feedback, 'working' usually means "AI is generating".
435
+ // Once generated (here), we might want to set it to 'IDLE' or 'waiting_for_plugin'.
436
+ if(db) await db.ref(`projects/${projectId}/info/status`).set("working");
437
 
438
+ res.json({ success: true });
439
  } catch (err) {
440
  console.error("AI Error:", err);
441
  res.status(500).json({ error: "AI Failed" });
 
443
  });
444
 
445
  app.post('/project/ping', async (req, res) => {
 
446
  const { projectId, userId } = req.body;
 
447
 
448
  if (!projectId || !userId) {
449
  return res.status(400).json({ error: "Missing ID fields" });
450
  }
451
+
 
452
  const project = await StateManager.getProject(projectId);
453
 
454
  if (!project) {
 
 
455
  return res.status(404).json({ action: "IDLE", error: "Project not found" });
456
  }
 
457
 
 
458
  if (project.userId !== userId) {
459
  console.warn(`[Security] Unauthorized ping for ${projectId}. Owner: ${project.userId}, Request: ${userId}`);
460
  return res.status(403).json({ error: "Unauthorized: You do not own this project." });
461
  }
462
+
 
463
  const command = await StateManager.popCommand(projectId);
464
 
465
  if (command) {
 
472
  code: command.type === 'EXECUTE' ? command.payload : null
473
  });
474
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  } else {
476
  res.json({ action: "IDLE" });
477
  }
478
  });
 
479
 
480
  app.post('/human/override', validateRequest, async (req, res) => {
481
  const { projectId, instruction, pruneHistory } = req.body;