Spaces:
Paused
Paused
Update app.js
Browse files
app.js
CHANGED
|
@@ -17,13 +17,13 @@ const app = express();
|
|
| 17 |
const PORT = process.env.PORT || 7860;
|
| 18 |
|
| 19 |
app.use(cors());
|
| 20 |
-
app.use(express.json({ limit: '50mb' }));
|
| 21 |
|
| 22 |
// --- BILLING CONSTANTS ---
|
| 23 |
-
const MIN_BASIC_REQUIRED =
|
| 24 |
-
const MIN_DIAMOND_REQUIRED =
|
| 25 |
|
| 26 |
-
// --- STATUS PHASES
|
| 27 |
const WORKER_PHASES = [
|
| 28 |
"Worker: Sorting...",
|
| 29 |
"Worker: Analyzing Request...",
|
|
@@ -53,14 +53,13 @@ function startStatusLoop(projectId, type = 'worker') {
|
|
| 53 |
StateManager.setStatus(projectId, phases[0]);
|
| 54 |
|
| 55 |
const interval = setInterval(() => {
|
| 56 |
-
index = (index + 1) % phases.length;
|
| 57 |
StateManager.setStatus(projectId, phases[index]);
|
| 58 |
-
},
|
| 59 |
|
| 60 |
return () => clearInterval(interval);
|
| 61 |
}
|
| 62 |
|
| 63 |
-
// Restored robust regex helpers
|
| 64 |
function extractWorkerPrompt(text) {
|
| 65 |
if (!text || typeof text !== 'string') return null;
|
| 66 |
const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
|
|
@@ -102,26 +101,36 @@ const validateRequest = (req, res, next) => {
|
|
| 102 |
next();
|
| 103 |
};
|
| 104 |
|
| 105 |
-
// --- BILLING LOGIC
|
| 106 |
|
| 107 |
async function checkMinimumCredits(userId, type = 'basic') {
|
|
|
|
| 108 |
const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
|
| 109 |
-
if (!data) return;
|
|
|
|
| 110 |
const credits = data.credits?.[type] || 0;
|
| 111 |
const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
|
| 115 |
async function deductUserCredits(userId, amount, type = 'basic') {
|
| 116 |
const deduction = Math.floor(parseInt(amount, 10));
|
| 117 |
-
if (!deduction || deduction <= 0) return;
|
| 118 |
|
| 119 |
try {
|
| 120 |
const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
|
| 121 |
if (!data) return;
|
|
|
|
| 122 |
const currentCredits = data.credits || { basic: 0, diamond: 0 };
|
| 123 |
-
const
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`);
|
| 126 |
} catch (err) {
|
| 127 |
console.error("Deduction failed", err);
|
|
@@ -192,7 +201,6 @@ async function runBackgroundInitialization(projectId, userId, description) {
|
|
| 192 |
|
| 193 |
let workerTextAccumulated = "";
|
| 194 |
|
| 195 |
-
// We use stream here to populate UI immediately if user is watching
|
| 196 |
const workerResult = await AIEngine.callWorkerStream(
|
| 197 |
[],
|
| 198 |
initialWorkerPrompt,
|
|
@@ -208,6 +216,7 @@ async function runBackgroundInitialization(projectId, userId, description) {
|
|
| 208 |
StateManager.appendStream(projectId, chunk);
|
| 209 |
}
|
| 210 |
);
|
|
|
|
| 211 |
|
| 212 |
basicUsage += (workerResult.usage?.totalTokenCount || 0);
|
| 213 |
|
|
@@ -230,7 +239,6 @@ async function runBackgroundInitialization(projectId, userId, description) {
|
|
| 230 |
await StateManager.updateProject(projectId, { status: "error" });
|
| 231 |
} finally {
|
| 232 |
StateManager.unlock(projectId);
|
| 233 |
-
// Deduct collected usage
|
| 234 |
if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
|
| 235 |
if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
|
| 236 |
}
|
|
@@ -245,7 +253,8 @@ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
|
|
| 245 |
try {
|
| 246 |
console.log(`[${projectId}] Feedback received. Persisting state immediately.`);
|
| 247 |
|
| 248 |
-
// 1. Instant Persistence
|
|
|
|
| 249 |
await Promise.all([
|
| 250 |
StateManager.updateProject(projectId, { status: "working" }),
|
| 251 |
StateManager.addHistory(projectId, 'worker', 'user', fullInput)
|
|
@@ -259,6 +268,8 @@ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
|
|
| 259 |
let firstTurnResponse = "";
|
| 260 |
let thoughtText = "";
|
| 261 |
|
|
|
|
|
|
|
| 262 |
const currentWorkerHistory = [...(project.workerHistory || [])];
|
| 263 |
|
| 264 |
// 2. First Turn (Worker Execution)
|
|
@@ -281,16 +292,17 @@ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
|
|
| 281 |
);
|
| 282 |
stopStatus();
|
| 283 |
basicUsage += (workerResult.usage?.totalTokenCount || 0);
|
|
|
|
| 284 |
|
| 285 |
// 3. Analyze Output (Delegation/Questions)
|
| 286 |
-
const routeTask = extractRouteToPM(
|
| 287 |
-
const pmQuestion = extractPMQuestion(
|
| 288 |
-
let finalResponseToSave =
|
| 289 |
|
| 290 |
if (routeTask || pmQuestion) {
|
| 291 |
-
// Save preliminary response
|
| 292 |
-
await StateManager.addHistory(projectId, 'worker', 'model',
|
| 293 |
-
currentWorkerHistory.push({ role: 'model', parts: [{ text:
|
| 294 |
|
| 295 |
let pmPrompt = "";
|
| 296 |
let pmContextPrefix = "";
|
|
@@ -328,17 +340,20 @@ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
|
|
| 328 |
);
|
| 329 |
stopStatus();
|
| 330 |
diamondUsage += (pmResult.usage?.totalTokenCount || 0);
|
|
|
|
| 331 |
|
| 332 |
await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt);
|
| 333 |
await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
|
| 334 |
|
| 335 |
-
//
|
|
|
|
| 336 |
await processAndQueueResponse(projectId, pmResponseText, userId);
|
| 337 |
|
| 338 |
// 5. Run Worker Continuation
|
| 339 |
const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText;
|
| 340 |
const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`;
|
| 341 |
|
|
|
|
| 342 |
await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt);
|
| 343 |
currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] });
|
| 344 |
|
|
@@ -363,27 +378,33 @@ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
|
|
| 363 |
}
|
| 364 |
);
|
| 365 |
stopStatus();
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
| 368 |
|
| 369 |
await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse);
|
| 370 |
finalResponseToSave = secondTurnResponse;
|
| 371 |
|
| 372 |
} else {
|
| 373 |
-
// No delegation, save the
|
| 374 |
await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
|
| 375 |
}
|
| 376 |
|
| 377 |
StateManager.setStatus(projectId, "Idle");
|
| 378 |
-
|
| 379 |
-
|
| 380 |
// 6. Final Execution & Cleanup
|
| 381 |
await processAndQueueResponse(projectId, finalResponseToSave, userId);
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
} catch (err) {
|
| 384 |
console.error("Async Feedback Error:", err);
|
|
|
|
| 385 |
await StateManager.updateProject(projectId, { status: "error" });
|
| 386 |
} finally {
|
|
|
|
| 387 |
if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
|
| 388 |
if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
|
| 389 |
}
|
|
@@ -392,13 +413,13 @@ async function runAsyncFeedback(projectId, userId, fullInput, images = []) {
|
|
| 392 |
async function processAndQueueResponse(projectId, rawResponse, userId) {
|
| 393 |
if (!rawResponse) return;
|
| 394 |
|
| 395 |
-
// Image Generation
|
| 396 |
const imgPrompt = extractImagePrompt(rawResponse);
|
| 397 |
if (imgPrompt) {
|
| 398 |
try {
|
| 399 |
const imgResult = await AIEngine.generateImage(imgPrompt);
|
| 400 |
if (imgResult?.image) {
|
| 401 |
-
if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee
|
| 402 |
await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
|
| 403 |
}
|
| 404 |
} catch (e) {
|
|
@@ -407,12 +428,16 @@ async function processAndQueueResponse(projectId, rawResponse, userId) {
|
|
| 407 |
}
|
| 408 |
|
| 409 |
// Code Execution
|
| 410 |
-
|
|
|
|
|
|
|
| 411 |
if (codeMatch) {
|
| 412 |
await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() });
|
| 413 |
} else {
|
| 414 |
-
//
|
| 415 |
-
|
|
|
|
|
|
|
| 416 |
}
|
| 417 |
}
|
| 418 |
|
|
@@ -423,8 +448,14 @@ app.post('/onboarding/analyze', validateRequest, async (req, res) => {
|
|
| 423 |
if (!description) return res.status(400).json({ error: "Description required" });
|
| 424 |
|
| 425 |
try {
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
if (result.usage?.totalTokenCount > 0) {
|
| 430 |
await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
|
|
@@ -444,15 +475,19 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
|
|
| 444 |
const { userId, description, answers } = req.body;
|
| 445 |
|
| 446 |
try {
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
|
| 451 |
const projectId = `proj_${Date.now()}_${randomHex(7)}`;
|
| 452 |
|
| 453 |
const gradingResult = await AIEngine.gradeProject(description, answers);
|
| 454 |
|
| 455 |
-
// Deduct grading cost immediately
|
| 456 |
if (gradingResult.usage?.totalTokenCount) {
|
| 457 |
await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic');
|
| 458 |
}
|
|
@@ -477,7 +512,6 @@ app.post('/onboarding/create', validateRequest, async (req, res) => {
|
|
| 477 |
return res.status(500).json({ error: "Database save failed" });
|
| 478 |
}
|
| 479 |
|
| 480 |
-
// Kick off background initialization
|
| 481 |
runBackgroundInitialization(projectId, userId, description);
|
| 482 |
}
|
| 483 |
|
|
@@ -502,12 +536,18 @@ app.post('/project/feedback', async (req, res) => {
|
|
| 502 |
const project = await StateManager.getProject(projectId);
|
| 503 |
if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
|
| 504 |
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
|
| 507 |
const context = formatContext({ hierarchyContext, scriptContext, logContext });
|
| 508 |
const fullInput = `USER: ${prompt || "Automatic Feedback"}${context}`;
|
| 509 |
|
| 510 |
-
// Trigger Async
|
| 511 |
runAsyncFeedback(projectId, userId, fullInput, images || []);
|
| 512 |
|
| 513 |
res.json({ success: true, message: "Processing started" });
|
|
@@ -527,14 +567,12 @@ app.post('/project/ping', async (req, res) => {
|
|
| 527 |
if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
|
| 528 |
|
| 529 |
if (isFrontend) {
|
| 530 |
-
// Return Snapshot for streaming UI
|
| 531 |
return res.json({
|
| 532 |
status: StateManager.getStatus(projectId),
|
| 533 |
snapshot: StateManager.getSnapshot(projectId)
|
| 534 |
});
|
| 535 |
}
|
| 536 |
|
| 537 |
-
// Backend Polling for Commands
|
| 538 |
const command = await StateManager.popCommand(projectId);
|
| 539 |
const streamData = StateManager.popStream(projectId);
|
| 540 |
|
|
@@ -559,14 +597,13 @@ app.post('/human/override', validateRequest, async (req, res) => {
|
|
| 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 |
-
// Async execution of override
|
| 570 |
const workerResult = await AIEngine.callWorkerStream(
|
| 571 |
project.workerHistory,
|
| 572 |
overrideMsg,
|
|
@@ -582,7 +619,6 @@ app.post('/human/override', validateRequest, async (req, res) => {
|
|
| 582 |
StateManager.appendStream(projectId, chunk);
|
| 583 |
}
|
| 584 |
);
|
| 585 |
-
|
| 586 |
stopStatus();
|
| 587 |
|
| 588 |
await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg);
|
|
@@ -593,7 +629,6 @@ app.post('/human/override', validateRequest, async (req, res) => {
|
|
| 593 |
if (usage > 0) await deductUserCredits(userId, usage, 'basic');
|
| 594 |
|
| 595 |
} catch (err) {
|
| 596 |
-
// Since we already responded with json success, we just log here
|
| 597 |
console.error("Override failed", err);
|
| 598 |
}
|
| 599 |
});
|
|
|
|
| 17 |
const PORT = process.env.PORT || 7860;
|
| 18 |
|
| 19 |
app.use(cors());
|
| 20 |
+
app.use(express.json({ limit: '50mb' }));
|
| 21 |
|
| 22 |
// --- BILLING CONSTANTS ---
|
| 23 |
+
const MIN_BASIC_REQUIRED = 100;
|
| 24 |
+
const MIN_DIAMOND_REQUIRED = 100;
|
| 25 |
|
| 26 |
+
// --- STATUS PHASES ---
|
| 27 |
const WORKER_PHASES = [
|
| 28 |
"Worker: Sorting...",
|
| 29 |
"Worker: Analyzing Request...",
|
|
|
|
| 53 |
StateManager.setStatus(projectId, phases[0]);
|
| 54 |
|
| 55 |
const interval = setInterval(() => {
|
| 56 |
+
index = (index + 1) % phases.length;
|
| 57 |
StateManager.setStatus(projectId, phases[index]);
|
| 58 |
+
}, 2000); // Slightly faster updates for better UI feedback
|
| 59 |
|
| 60 |
return () => clearInterval(interval);
|
| 61 |
}
|
| 62 |
|
|
|
|
| 63 |
function extractWorkerPrompt(text) {
|
| 64 |
if (!text || typeof text !== 'string') return null;
|
| 65 |
const match = text.match(/WORKER_PROMPT:\s*(.*)/s);
|
|
|
|
| 101 |
next();
|
| 102 |
};
|
| 103 |
|
| 104 |
+
// --- BILLING LOGIC ---
|
| 105 |
|
| 106 |
async function checkMinimumCredits(userId, type = 'basic') {
|
| 107 |
+
if (!supabase) return;
|
| 108 |
const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
|
| 109 |
+
if (!data) return;
|
| 110 |
+
|
| 111 |
const credits = data.credits?.[type] || 0;
|
| 112 |
const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED;
|
| 113 |
+
|
| 114 |
+
if (credits < req) {
|
| 115 |
+
throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`);
|
| 116 |
+
}
|
| 117 |
}
|
| 118 |
|
| 119 |
async function deductUserCredits(userId, amount, type = 'basic') {
|
| 120 |
const deduction = Math.floor(parseInt(amount, 10));
|
| 121 |
+
if (!supabase || !deduction || deduction <= 0) return;
|
| 122 |
|
| 123 |
try {
|
| 124 |
const { data } = await supabase.from('users').select('credits').eq('id', userId).single();
|
| 125 |
if (!data) return;
|
| 126 |
+
|
| 127 |
const currentCredits = data.credits || { basic: 0, diamond: 0 };
|
| 128 |
+
const currentVal = currentCredits[type] || 0;
|
| 129 |
+
const newVal = Math.max(0, currentVal - deduction);
|
| 130 |
+
|
| 131 |
+
const updatedCredits = { ...currentCredits, [type]: newVal };
|
| 132 |
+
|
| 133 |
+
await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId);
|
| 134 |
console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`);
|
| 135 |
} catch (err) {
|
| 136 |
console.error("Deduction failed", err);
|
|
|
|
| 201 |
|
| 202 |
let workerTextAccumulated = "";
|
| 203 |
|
|
|
|
| 204 |
const workerResult = await AIEngine.callWorkerStream(
|
| 205 |
[],
|
| 206 |
initialWorkerPrompt,
|
|
|
|
| 216 |
StateManager.appendStream(projectId, chunk);
|
| 217 |
}
|
| 218 |
);
|
| 219 |
+
stopStatus();
|
| 220 |
|
| 221 |
basicUsage += (workerResult.usage?.totalTokenCount || 0);
|
| 222 |
|
|
|
|
| 239 |
await StateManager.updateProject(projectId, { status: "error" });
|
| 240 |
} finally {
|
| 241 |
StateManager.unlock(projectId);
|
|
|
|
| 242 |
if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
|
| 243 |
if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
|
| 244 |
}
|
|
|
|
| 253 |
try {
|
| 254 |
console.log(`[${projectId}] Feedback received. Persisting state immediately.`);
|
| 255 |
|
| 256 |
+
// 1. Instant Persistence: Saves status and message BEFORE processing
|
| 257 |
+
// This ensures if user reloads, the state is preserved
|
| 258 |
await Promise.all([
|
| 259 |
StateManager.updateProject(projectId, { status: "working" }),
|
| 260 |
StateManager.addHistory(projectId, 'worker', 'user', fullInput)
|
|
|
|
| 268 |
let firstTurnResponse = "";
|
| 269 |
let thoughtText = "";
|
| 270 |
|
| 271 |
+
// Clone history to prevent mutation issues
|
| 272 |
+
// Note: project.workerHistory includes the message we just saved above
|
| 273 |
const currentWorkerHistory = [...(project.workerHistory || [])];
|
| 274 |
|
| 275 |
// 2. First Turn (Worker Execution)
|
|
|
|
| 292 |
);
|
| 293 |
stopStatus();
|
| 294 |
basicUsage += (workerResult.usage?.totalTokenCount || 0);
|
| 295 |
+
firstTurnResponse = workerResult.text || firstTurnResponse;
|
| 296 |
|
| 297 |
// 3. Analyze Output (Delegation/Questions)
|
| 298 |
+
const routeTask = extractRouteToPM(firstTurnResponse);
|
| 299 |
+
const pmQuestion = extractPMQuestion(firstTurnResponse);
|
| 300 |
+
let finalResponseToSave = firstTurnResponse;
|
| 301 |
|
| 302 |
if (routeTask || pmQuestion) {
|
| 303 |
+
// Save preliminary response to DB so it doesn't get lost
|
| 304 |
+
await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
|
| 305 |
+
currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] });
|
| 306 |
|
| 307 |
let pmPrompt = "";
|
| 308 |
let pmContextPrefix = "";
|
|
|
|
| 340 |
);
|
| 341 |
stopStatus();
|
| 342 |
diamondUsage += (pmResult.usage?.totalTokenCount || 0);
|
| 343 |
+
pmResponseText = pmResult.text || pmResponseText;
|
| 344 |
|
| 345 |
await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt);
|
| 346 |
await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText);
|
| 347 |
|
| 348 |
+
// --- EXECUTE PM CODE ---
|
| 349 |
+
// If the PM wrote code (e.g. ServerScriptService logic), execute it immediately
|
| 350 |
await processAndQueueResponse(projectId, pmResponseText, userId);
|
| 351 |
|
| 352 |
// 5. Run Worker Continuation
|
| 353 |
const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText;
|
| 354 |
const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`;
|
| 355 |
|
| 356 |
+
// Add system prompt to history
|
| 357 |
await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt);
|
| 358 |
currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] });
|
| 359 |
|
|
|
|
| 378 |
}
|
| 379 |
);
|
| 380 |
stopStatus();
|
| 381 |
+
|
| 382 |
+
// Deduct usage for second turn
|
| 383 |
+
basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0);
|
| 384 |
+
secondTurnResponse = secondWorkerResult.text || secondTurnResponse;
|
| 385 |
|
| 386 |
await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse);
|
| 387 |
finalResponseToSave = secondTurnResponse;
|
| 388 |
|
| 389 |
} else {
|
| 390 |
+
// No delegation, just save the first response
|
| 391 |
await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse);
|
| 392 |
}
|
| 393 |
|
| 394 |
StateManager.setStatus(projectId, "Idle");
|
| 395 |
+
|
|
|
|
| 396 |
// 6. Final Execution & Cleanup
|
| 397 |
await processAndQueueResponse(projectId, finalResponseToSave, userId);
|
| 398 |
+
|
| 399 |
+
// Ensure status is idle (this also updates timestamps)
|
| 400 |
+
await StateManager.updateProject(projectId, { status: "idle" });
|
| 401 |
|
| 402 |
} catch (err) {
|
| 403 |
console.error("Async Feedback Error:", err);
|
| 404 |
+
StateManager.setStatus(projectId, "Error: " + err.message);
|
| 405 |
await StateManager.updateProject(projectId, { status: "error" });
|
| 406 |
} finally {
|
| 407 |
+
// Billing
|
| 408 |
if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond');
|
| 409 |
if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic');
|
| 410 |
}
|
|
|
|
| 413 |
async function processAndQueueResponse(projectId, rawResponse, userId) {
|
| 414 |
if (!rawResponse) return;
|
| 415 |
|
| 416 |
+
// Image Generation
|
| 417 |
const imgPrompt = extractImagePrompt(rawResponse);
|
| 418 |
if (imgPrompt) {
|
| 419 |
try {
|
| 420 |
const imgResult = await AIEngine.generateImage(imgPrompt);
|
| 421 |
if (imgResult?.image) {
|
| 422 |
+
if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee
|
| 423 |
await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image });
|
| 424 |
}
|
| 425 |
} catch (e) {
|
|
|
|
| 428 |
}
|
| 429 |
|
| 430 |
// Code Execution
|
| 431 |
+
// Regex looks for lua/luau code blocks to execute
|
| 432 |
+
const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i);
|
| 433 |
+
|
| 434 |
if (codeMatch) {
|
| 435 |
await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() });
|
| 436 |
} else {
|
| 437 |
+
// If there's no code block but response is non-empty, maybe check if it's pure code?
|
| 438 |
+
// For safety, we usually only execute if wrapped in blocks.
|
| 439 |
+
// But if you want to support raw text execution:
|
| 440 |
+
// await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: rawResponse });
|
| 441 |
}
|
| 442 |
}
|
| 443 |
|
|
|
|
| 448 |
if (!description) return res.status(400).json({ error: "Description required" });
|
| 449 |
|
| 450 |
try {
|
| 451 |
+
// await checkMinimumCredits(userId, 'basic');
|
| 452 |
+
try {
|
| 453 |
+
await checkMinimumCredits(userId, 'basic');
|
| 454 |
+
} catch (creditErr) {
|
| 455 |
+
return res.json({ success: false, insufficient: true, error: creditErr.message });
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
const result = await AIEngine.generateEntryQuestions(description);
|
| 459 |
|
| 460 |
if (result.usage?.totalTokenCount > 0) {
|
| 461 |
await deductUserCredits(userId, result.usage.totalTokenCount, 'basic');
|
|
|
|
| 475 |
const { userId, description, answers } = req.body;
|
| 476 |
|
| 477 |
try {
|
| 478 |
+
|
| 479 |
+
try {
|
| 480 |
+
await checkMinimumCredits(userId, 'basic');
|
| 481 |
+
await checkMinimumCredits(userId, 'diamond');
|
| 482 |
+
} catch (creditErr) {
|
| 483 |
+
return res.json({ success: false, insufficient: true, error: creditErr.message });
|
| 484 |
+
}
|
| 485 |
|
| 486 |
const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n);
|
| 487 |
const projectId = `proj_${Date.now()}_${randomHex(7)}`;
|
| 488 |
|
| 489 |
const gradingResult = await AIEngine.gradeProject(description, answers);
|
| 490 |
|
|
|
|
| 491 |
if (gradingResult.usage?.totalTokenCount) {
|
| 492 |
await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic');
|
| 493 |
}
|
|
|
|
| 512 |
return res.status(500).json({ error: "Database save failed" });
|
| 513 |
}
|
| 514 |
|
|
|
|
| 515 |
runBackgroundInitialization(projectId, userId, description);
|
| 516 |
}
|
| 517 |
|
|
|
|
| 536 |
const project = await StateManager.getProject(projectId);
|
| 537 |
if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" });
|
| 538 |
|
| 539 |
+
// --- FIXED CREDIT CHECK ---
|
| 540 |
+
// Catch credit errors here to return a soft 200 OK so frontend banner shows
|
| 541 |
+
try {
|
| 542 |
+
await checkMinimumCredits(userId, 'basic');
|
| 543 |
+
} catch (creditErr) {
|
| 544 |
+
return res.json({ success: false, insufficient: true, error: creditErr.message });
|
| 545 |
+
}
|
| 546 |
|
| 547 |
const context = formatContext({ hierarchyContext, scriptContext, logContext });
|
| 548 |
const fullInput = `USER: ${prompt || "Automatic Feedback"}${context}`;
|
| 549 |
|
| 550 |
+
// Trigger Async
|
| 551 |
runAsyncFeedback(projectId, userId, fullInput, images || []);
|
| 552 |
|
| 553 |
res.json({ success: true, message: "Processing started" });
|
|
|
|
| 567 |
if (!project || project.userId !== userId) return res.json({ action: "IDLE" });
|
| 568 |
|
| 569 |
if (isFrontend) {
|
|
|
|
| 570 |
return res.json({
|
| 571 |
status: StateManager.getStatus(projectId),
|
| 572 |
snapshot: StateManager.getSnapshot(projectId)
|
| 573 |
});
|
| 574 |
}
|
| 575 |
|
|
|
|
| 576 |
const command = await StateManager.popCommand(projectId);
|
| 577 |
const streamData = StateManager.popStream(projectId);
|
| 578 |
|
|
|
|
| 597 |
await checkMinimumCredits(userId, 'basic');
|
| 598 |
const project = await StateManager.getProject(projectId);
|
| 599 |
|
| 600 |
+
res.json({ success: true });
|
| 601 |
|
| 602 |
const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`;
|
| 603 |
|
| 604 |
let overrideText = "";
|
| 605 |
let stopStatus = startStatusLoop(projectId, 'worker');
|
| 606 |
|
|
|
|
| 607 |
const workerResult = await AIEngine.callWorkerStream(
|
| 608 |
project.workerHistory,
|
| 609 |
overrideMsg,
|
|
|
|
| 619 |
StateManager.appendStream(projectId, chunk);
|
| 620 |
}
|
| 621 |
);
|
|
|
|
| 622 |
stopStatus();
|
| 623 |
|
| 624 |
await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg);
|
|
|
|
| 629 |
if (usage > 0) await deductUserCredits(userId, usage, 'basic');
|
| 630 |
|
| 631 |
} catch (err) {
|
|
|
|
| 632 |
console.error("Override failed", err);
|
| 633 |
}
|
| 634 |
});
|