Leon4gr45 commited on
Commit
2f1535a
Β·
verified Β·
1 Parent(s): f23b3c9

Upload folder using huggingface_hub

Browse files
app/api/generate-ai-code-stream/route.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { streamText } from 'ai';
3
  import type { SandboxState } from '@/types/sandbox';
4
  import { selectFilesForEdit, getFileContents, formatFilesForAI } from '@/lib/context-selector';
5
  import { executeSearchPlan, formatSearchResultsForAI, selectTargetFile } from '@/lib/file-search-executor';
@@ -1188,579 +1188,690 @@ MORPH FAST APPLY MODE (EDIT-ONLY):
1188
  console.log(`[generate-ai-code-stream] Using provider for model: ${actualModel}`);
1189
  console.log(`[generate-ai-code-stream] Model string: ${model}`);
1190
 
1191
- // Make streaming API call with appropriate provider
1192
- const streamOptions: any = {
1193
- model: modelProvider(actualModel),
1194
- messages: [
1195
- {
1196
- role: 'system',
1197
- content: systemPrompt + `
1198
-
1199
- 🚨 CRITICAL CODE GENERATION RULES - VIOLATION = FAILURE 🚨:
1200
- 1. NEVER truncate ANY code - ALWAYS write COMPLETE files
1201
- 2. NEVER use "..." anywhere in your code - this causes syntax errors
1202
- 3. NEVER cut off strings mid-sentence - COMPLETE every string
1203
- 4. NEVER leave incomplete class names or attributes
1204
- 5. ALWAYS close ALL tags, quotes, brackets, and parentheses
1205
- 6. If you run out of space, prioritize completing the current file
1206
-
1207
- CRITICAL STRING RULES TO PREVENT SYNTAX ERRORS:
1208
- - NEVER write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobr...
1209
- - ALWAYS write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobrut-shadow"
1210
- - COMPLETE every className attribute
1211
- - COMPLETE every string literal
1212
- - NO ellipsis (...) ANYWHERE in code
1213
-
1214
- PACKAGE RULES:
1215
- - For INITIAL generation: Use ONLY React, no external packages
1216
- - For EDITS: You may use packages, specify them with <package> tags
1217
- - NEVER install packages like @mendable/firecrawl-js unless explicitly requested
1218
-
1219
- Examples of SYNTAX ERRORS (NEVER DO THIS):
1220
- ❌ className="px-4 py-2 bg-blue-600 hover:bg-blue-7...
1221
- ❌ <button className="btn btn-primary btn-...
1222
- ❌ const title = "Welcome to our...
1223
- ❌ import { useState, useEffect, ... } from 'react'
1224
-
1225
- Examples of CORRECT CODE (ALWAYS DO THIS):
1226
- βœ… className="px-4 py-2 bg-blue-600 hover:bg-blue-700"
1227
- βœ… <button className="btn btn-primary btn-large">
1228
- βœ… const title = "Welcome to our application"
1229
- βœ… import { useState, useEffect, useCallback } from 'react'
1230
-
1231
- REMEMBER: It's better to generate fewer COMPLETE files than many INCOMPLETE files.`
1232
- },
1233
- {
1234
- role: 'user',
1235
- content: fullPrompt + `
1236
-
1237
- CRITICAL: You MUST complete EVERY file you start. If you write:
1238
- <file path="src/components/Hero.jsx">
1239
-
1240
- You MUST include the closing </file> tag and ALL the code in between.
1241
-
1242
- NEVER write partial code like:
1243
- <h1>Build and deploy on the AI Cloud.</h1>
1244
- <p>Some text...</p> ❌ WRONG
1245
-
1246
- ALWAYS write complete code:
1247
- <h1>Build and deploy on the AI Cloud.</h1>
1248
- <p>Some text here with full content</p> βœ… CORRECT
1249
-
1250
- If you're running out of space, generate FEWER files but make them COMPLETE.
1251
- It's better to have 3 complete files than 10 incomplete files.`
 
 
 
 
 
 
 
 
 
 
 
 
1252
  }
1253
- ],
1254
- maxTokens: 8192, // Reduce to ensure completion
1255
- stopSequences: [] // Don't stop early
1256
- // Note: Neither Groq nor Anthropic models support tool/function calling in this context
1257
- // We use XML tags for package detection instead
1258
- };
1259
-
1260
- // Add temperature for non-reasoning models
1261
- if (!model.startsWith('openai/gpt-5')) {
1262
- streamOptions.temperature = 0.7;
1263
- }
1264
-
1265
- // Add reasoning effort for GPT-5 models
1266
- if (isOpenAI) {
1267
- streamOptions.experimental_providerMetadata = {
1268
- openai: {
1269
- reasoningEffort: 'high'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270
  }
1271
- };
1272
- }
1273
-
1274
- let result;
1275
- let retryCount = 0;
1276
- const maxRetries = 2;
1277
-
1278
- while (retryCount <= maxRetries) {
1279
- try {
1280
- result = await streamText(streamOptions);
1281
- break; // Success, exit retry loop
1282
- } catch (streamError: any) {
1283
- console.error(`[generate-ai-code-stream] Error calling streamText (attempt ${retryCount + 1}/${maxRetries + 1}):`, streamError);
1284
 
1285
- const isRetryableError = streamError.message?.includes('Service unavailable') ||
1286
- streamError.message?.includes('rate limit') ||
1287
- streamError.message?.includes('timeout');
 
 
 
 
 
1288
 
1289
- if (retryCount < maxRetries && isRetryableError) {
1290
- retryCount++;
1291
- console.log(`[generate-ai-code-stream] Retrying in ${retryCount * 2} seconds...`);
 
 
 
 
 
1292
 
1293
- // Send progress update about retry
1294
- await sendProgress({
1295
- type: 'info',
1296
- message: `Service temporarily unavailable, retrying (attempt ${retryCount + 1}/${maxRetries + 1})...`
1297
- });
1298
 
1299
- // Wait before retry with exponential backoff
1300
- await new Promise(resolve => setTimeout(resolve, retryCount * 2000));
1301
 
1302
- } else {
1303
- // Final error, send to user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1304
  await sendProgress({
1305
- type: 'error',
1306
- message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'AI'} streaming: ${streamError.message}`
 
1307
  });
1308
 
1309
- // If this is a Google model error, provide helpful info
1310
- if (isGoogle) {
1311
- await sendProgress({
1312
- type: 'info',
1313
- message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
1314
- });
1315
  }
1316
 
1317
- throw streamError;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1318
  }
1319
- }
1320
- }
1321
-
1322
- // Stream the response and parse in real-time
1323
- let generatedCode = '';
1324
- let currentFile = '';
1325
- let currentFilePath = '';
1326
- let componentCount = 0;
1327
- let isInFile = false;
1328
- let isInTag = false;
1329
- let conversationalBuffer = '';
1330
-
1331
- // Buffer for incomplete tags
1332
- let tagBuffer = '';
1333
-
1334
- // Stream the response and parse for packages in real-time
1335
- for await (const textPart of result?.textStream || []) {
1336
- const text = textPart || '';
1337
- generatedCode += text;
1338
- currentFile += text;
1339
-
1340
- // Combine with buffer for tag detection
1341
- const searchText = tagBuffer + text;
1342
-
1343
- // Log streaming chunks to console
1344
- process.stdout.write(text);
1345
-
1346
- // Check if we're entering or leaving a tag
1347
- const hasOpenTag = /<(file|package|packages|explanation|command|structure|template)\b/.test(text);
1348
- const hasCloseTag = /<\/(file|package|packages|explanation|command|structure|template)>/.test(text);
1349
-
1350
- if (hasOpenTag) {
1351
- // Send any buffered conversational text before the tag
1352
- if (conversationalBuffer.trim() && !isInTag) {
1353
  await sendProgress({
1354
  type: 'conversation',
1355
  text: conversationalBuffer.trim()
1356
  });
1357
- conversationalBuffer = '';
1358
  }
1359
- isInTag = true;
1360
- }
1361
-
1362
- if (hasCloseTag) {
1363
- isInTag = false;
1364
- }
1365
-
1366
- // If we're not in a tag, buffer as conversational text
1367
- if (!isInTag && !hasOpenTag) {
1368
- conversationalBuffer += text;
1369
- }
1370
-
1371
- // Stream the raw text for live preview
1372
- await sendProgress({
1373
- type: 'stream',
1374
- text: text,
1375
- raw: true
1376
- });
1377
-
1378
- // Debug: Log every 100 characters streamed
1379
- if (generatedCode.length % 100 < text.length) {
1380
- console.log(`[generate-ai-code-stream] Streamed ${generatedCode.length} chars`);
1381
- }
1382
-
1383
- // Check for package tags in buffered text (ONLY for edits, not initial generation)
1384
- let lastIndex = 0;
1385
- if (isEdit) {
1386
- const packageRegex = /<package>([^<]+)<\/package>/g;
1387
- let packageMatch;
1388
 
1389
- while ((packageMatch = packageRegex.exec(searchText)) !== null) {
1390
- const packageName = packageMatch[1].trim();
1391
- if (packageName && !packagesToInstall.includes(packageName)) {
1392
- packagesToInstall.push(packageName);
1393
- console.log(`[generate-ai-code-stream] Package detected: ${packageName}`);
1394
- await sendProgress({
1395
- type: 'package',
1396
- name: packageName,
1397
- message: `Package detected: ${packageName}`
1398
- });
 
 
 
 
 
 
 
 
 
 
 
1399
  }
1400
- lastIndex = packageMatch.index + packageMatch[0].length;
1401
- }
1402
- }
1403
-
1404
- // Keep unmatched portion in buffer for next iteration
1405
- tagBuffer = searchText.substring(Math.max(0, lastIndex - 50)); // Keep last 50 chars
1406
-
1407
- // Check for file boundaries
1408
- if (text.includes('<file path="')) {
1409
- const pathMatch = text.match(/<file path="([^"]+)"/);
1410
- if (pathMatch) {
1411
- currentFilePath = pathMatch[1];
1412
- isInFile = true;
1413
- currentFile = text;
1414
  }
1415
- }
1416
-
1417
- // Check for file end
1418
- if (isInFile && currentFile.includes('</file>')) {
1419
- isInFile = false;
1420
 
1421
- // Send component progress update
1422
- if (currentFilePath.includes('components/')) {
1423
- componentCount++;
1424
- const componentName = currentFilePath.split('/').pop()?.replace('.jsx', '') || 'Component';
1425
- await sendProgress({
1426
- type: 'component',
1427
- name: componentName,
1428
- path: currentFilePath,
1429
- index: componentCount
1430
- });
1431
- } else if (currentFilePath.includes('App.jsx')) {
1432
- await sendProgress({
1433
- type: 'app',
1434
- message: 'Generated main App.jsx',
1435
- path: currentFilePath
1436
- });
 
 
 
 
 
 
 
 
 
1437
  }
1438
 
1439
- currentFile = '';
1440
- currentFilePath = '';
1441
- }
1442
- }
1443
-
1444
- console.log('\n\n[generate-ai-code-stream] Streaming complete.');
1445
-
1446
- // Send any remaining conversational text
1447
- if (conversationalBuffer.trim()) {
1448
- await sendProgress({
1449
- type: 'conversation',
1450
- text: conversationalBuffer.trim()
1451
- });
1452
- }
1453
-
1454
- // Also parse <packages> tag for multiple packages - ONLY for edits
1455
- if (isEdit) {
1456
- const packagesRegex = /<packages>([\s\S]*?)<\/packages>/g;
1457
- let packagesMatch;
1458
- while ((packagesMatch = packagesRegex.exec(generatedCode)) !== null) {
1459
- const packagesContent = packagesMatch[1].trim();
1460
- const packagesList = packagesContent.split(/[\n,]+/)
1461
- .map(pkg => pkg.trim())
1462
- .filter(pkg => pkg.length > 0);
1463
 
1464
- for (const packageName of packagesList) {
1465
- if (!packagesToInstall.includes(packageName)) {
1466
- packagesToInstall.push(packageName);
1467
- console.log(`[generate-ai-code-stream] Package from <packages> tag: ${packageName}`);
1468
- await sendProgress({
1469
- type: 'package',
1470
- name: packageName,
1471
- message: `Package detected: ${packageName}`
1472
- });
1473
- }
1474
- }
1475
- }
1476
- }
1477
-
1478
- // Function to extract packages from import statements
1479
- function extractPackagesFromCode(content: string): string[] {
1480
- const packages: string[] = [];
1481
- // Match ES6 imports
1482
- const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
1483
- let importMatch;
1484
-
1485
- while ((importMatch = importRegex.exec(content)) !== null) {
1486
- const importPath = importMatch[1];
1487
- // Skip relative imports and built-in React
1488
- if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
1489
- importPath !== 'react' && importPath !== 'react-dom' &&
1490
- !importPath.startsWith('@/')) {
1491
- // Extract package name (handle scoped packages like @heroicons/react)
1492
- const packageName = importPath.startsWith('@')
1493
- ? importPath.split('/').slice(0, 2).join('/')
1494
- : importPath.split('/')[0];
1495
 
1496
- if (!packages.includes(packageName)) {
1497
- packages.push(packageName);
 
 
 
 
 
 
 
 
 
 
 
 
1498
  }
1499
- }
1500
- }
1501
-
1502
- return packages;
1503
- }
1504
-
1505
- // Parse files and send progress for each
1506
- const fileRegex = /<file path="([^"]+)">([\s\S]*?)<\/file>/g;
1507
- const files = [];
1508
- let match;
1509
-
1510
- while ((match = fileRegex.exec(generatedCode)) !== null) {
1511
- const filePath = match[1];
1512
- const content = match[2].trim();
1513
- files.push({ path: filePath, content });
1514
-
1515
- // Extract packages from file content - ONLY for edits
1516
- if (isEdit) {
1517
- const filePackages = extractPackagesFromCode(content);
1518
- for (const pkg of filePackages) {
1519
- if (!packagesToInstall.includes(pkg)) {
1520
- packagesToInstall.push(pkg);
1521
- console.log(`[generate-ai-code-stream] Package detected from imports: ${pkg}`);
1522
  await sendProgress({
1523
- type: 'package',
1524
- name: pkg,
1525
- message: `Package detected from imports: ${pkg}`
1526
  });
1527
  }
1528
  }
1529
- }
1530
-
1531
- // Send progress for each file (reusing componentCount from streaming)
1532
- if (filePath.includes('components/')) {
1533
- const componentName = filePath.split('/').pop()?.replace('.jsx', '') || 'Component';
1534
- await sendProgress({
1535
- type: 'component',
1536
- name: componentName,
1537
- path: filePath,
1538
- index: componentCount
1539
- });
1540
- } else if (filePath.includes('App.jsx')) {
1541
- await sendProgress({
1542
- type: 'app',
1543
- message: 'Generated main App.jsx',
1544
- path: filePath
1545
- });
1546
- }
1547
- }
1548
-
1549
- // Extract explanation
1550
- const explanationMatch = generatedCode.match(/<explanation>([\s\S]*?)<\/explanation>/);
1551
- const explanation = explanationMatch ? explanationMatch[1].trim() : 'Code generated successfully!';
1552
-
1553
- // Validate generated code for truncation issues
1554
- const truncationWarnings: string[] = [];
1555
-
1556
- // Skip ellipsis checking entirely - too many false positives with spread operators, loading text, etc.
1557
-
1558
- // Check for unclosed file tags
1559
- const fileOpenCount = (generatedCode.match(/<file path="/g) || []).length;
1560
- const fileCloseCount = (generatedCode.match(/<\/file>/g) || []).length;
1561
- if (fileOpenCount !== fileCloseCount) {
1562
- truncationWarnings.push(`Unclosed file tags detected: ${fileOpenCount} open, ${fileCloseCount} closed`);
1563
- }
1564
-
1565
- // Check for files that seem truncated (very short or ending abruptly)
1566
- const truncationCheckRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
1567
- let truncationMatch;
1568
- while ((truncationMatch = truncationCheckRegex.exec(generatedCode)) !== null) {
1569
- const filePath = truncationMatch[1];
1570
- const content = truncationMatch[2];
1571
-
1572
- // Only check for really obvious HTML truncation - file ends with opening tag
1573
- if (content.trim().endsWith('<') || content.trim().endsWith('</')) {
1574
- truncationWarnings.push(`File ${filePath} appears to have incomplete HTML tags`);
1575
- }
1576
-
1577
- // Skip "..." check - too many false positives with loading text, etc.
1578
-
1579
- // Only check for SEVERE truncation issues
1580
- if (filePath.match(/\.(jsx?|tsx?)$/)) {
1581
- // Only check for severely unmatched brackets (more than 3 difference)
1582
- const openBraces = (content.match(/{/g) || []).length;
1583
- const closeBraces = (content.match(/}/g) || []).length;
1584
- const braceDiff = Math.abs(openBraces - closeBraces);
1585
- if (braceDiff > 3) { // Only flag severe mismatches
1586
- truncationWarnings.push(`File ${filePath} has severely unmatched braces (${openBraces} open, ${closeBraces} closed)`);
1587
- }
1588
 
1589
- // Check if file is extremely short and looks incomplete
1590
- if (content.length < 20 && content.includes('function') && !content.includes('}')) {
1591
- truncationWarnings.push(`File ${filePath} appears severely truncated`);
1592
- }
1593
- }
1594
- }
1595
-
1596
- // Handle truncation with automatic retry (if enabled in config)
1597
- if (truncationWarnings.length > 0 && appConfig.codeApplication.enableTruncationRecovery) {
1598
- console.warn('[generate-ai-code-stream] Truncation detected, attempting to fix:', truncationWarnings);
1599
-
1600
- await sendProgress({
1601
- type: 'warning',
1602
- message: 'Detected incomplete code generation. Attempting to complete...',
1603
- warnings: truncationWarnings
1604
- });
1605
-
1606
- // Try to fix truncated files automatically
1607
- const truncatedFiles: string[] = [];
1608
- const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
1609
- let match;
1610
-
1611
- while ((match = fileRegex.exec(generatedCode)) !== null) {
1612
- const filePath = match[1];
1613
- const content = match[2];
1614
 
1615
- // Check if this file appears truncated - be more selective
1616
- const hasEllipsis = content.includes('...') &&
1617
- !content.includes('...rest') &&
1618
- !content.includes('...props') &&
1619
- !content.includes('spread');
1620
-
1621
- const endsAbruptly = content.trim().endsWith('...') ||
1622
- content.trim().endsWith(',') ||
1623
- content.trim().endsWith('(');
1624
-
1625
- const hasUnclosedTags = content.includes('</') &&
1626
- !content.match(/<\/[a-zA-Z0-9]+>/) &&
1627
- content.includes('<');
1628
-
1629
- const tooShort = content.length < 50 && filePath.match(/\.(jsx?|tsx?)$/);
1630
 
1631
- // Check for unmatched braces specifically
1632
- const openBraceCount = (content.match(/{/g) || []).length;
1633
- const closeBraceCount = (content.match(/}/g) || []).length;
1634
- const hasUnmatchedBraces = Math.abs(openBraceCount - closeBraceCount) > 1;
1635
 
1636
- const isTruncated = (hasEllipsis && endsAbruptly) ||
1637
- hasUnclosedTags ||
1638
- (tooShort && !content.includes('export')) ||
1639
- hasUnmatchedBraces;
 
 
1640
 
1641
- if (isTruncated) {
1642
- truncatedFiles.push(filePath);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1643
  }
1644
- }
1645
-
1646
- // If we have truncated files, try to regenerate them
1647
- if (truncatedFiles.length > 0) {
1648
- console.log('[generate-ai-code-stream] Attempting to regenerate truncated files:', truncatedFiles);
1649
 
1650
- for (const filePath of truncatedFiles) {
 
 
 
1651
  await sendProgress({
1652
- type: 'info',
1653
- message: `Completing ${filePath}...`
 
1654
  });
1655
 
1656
- try {
1657
- // Create a focused prompt to complete just this file
1658
- const completionPrompt = `Complete the following file that was truncated. Provide the FULL file content.
1659
-
1660
- File: ${filePath}
1661
- Original request: ${prompt}
 
 
1662
 
1663
- Provide the complete file content without any truncation. Include all necessary imports, complete all functions, and close all tags properly.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1664
 
1665
- // Make a focused API call to complete this specific file
1666
- const { client: completionClient, actualModel: completionModelName } = getProviderForModel(model);
 
 
1667
 
1668
- const completionResult = await streamText({
1669
- model: completionClient(completionModelName),
1670
- messages: [
1671
- {
1672
- role: 'system',
1673
- content: 'You are completing a truncated file. Provide the complete, working file content.'
1674
- },
1675
- { role: 'user', content: completionPrompt }
1676
- ],
1677
- temperature: model.startsWith('openai/gpt-5') ? undefined : appConfig.ai.defaultTemperature
1678
- });
1679
 
1680
- // Get the full text from the stream
1681
- let completedContent = '';
1682
- for await (const chunk of completionResult.textStream) {
1683
- completedContent += chunk;
1684
  }
 
 
 
 
 
1685
 
1686
- // Replace the truncated file in the generatedCode
1687
- const filePattern = new RegExp(
1688
- `<file path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}">[\\s\\S]*?(?:</file>|$)`,
1689
- 'g'
1690
- );
1691
-
1692
- // Extract just the code content (remove any markdown or explanation)
1693
- let cleanContent = completedContent;
1694
- if (cleanContent.includes('```')) {
1695
- const codeMatch = cleanContent.match(/```[\w]*\n([\s\S]*?)```/);
1696
- if (codeMatch) {
1697
- cleanContent = codeMatch[1];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1698
  }
1699
  }
1700
 
1701
- generatedCode = generatedCode.replace(
1702
- filePattern,
1703
- `<file path="${filePath}">\n${cleanContent}\n</file>`
1704
- );
1705
-
1706
- console.log(`[generate-ai-code-stream] Successfully completed ${filePath}`);
1707
-
1708
- } catch (completionError) {
1709
- console.error(`[generate-ai-code-stream] Failed to complete ${filePath}:`, completionError);
1710
  await sendProgress({
1711
- type: 'warning',
1712
- message: `Could not auto-complete ${filePath}. Manual review may be needed.`
1713
  });
1714
  }
1715
  }
1716
 
1717
- // Clear the warnings after attempting fixes
1718
- truncationWarnings.length = 0;
1719
- await sendProgress({
1720
- type: 'info',
1721
- message: 'Truncation recovery complete'
 
 
 
 
 
1722
  });
1723
- }
1724
- }
1725
-
1726
- // Send completion with packages info
1727
- await sendProgress({
1728
- type: 'complete',
1729
- generatedCode,
1730
- explanation,
1731
- files: files.length,
1732
- components: componentCount,
1733
- model,
1734
- packagesToInstall: packagesToInstall.length > 0 ? packagesToInstall : undefined,
1735
- warnings: truncationWarnings.length > 0 ? truncationWarnings : undefined
1736
- });
1737
-
1738
- // Track edit in conversation history
1739
- if (isEdit && editContext && global.conversationState) {
1740
- const editRecord: ConversationEdit = {
1741
- timestamp: Date.now(),
1742
- userRequest: prompt,
1743
- editType: editContext.editIntent.type,
1744
- targetFiles: editContext.primaryFiles,
1745
- confidence: editContext.editIntent.confidence,
1746
- outcome: 'success' // Assuming success if we got here
1747
- };
1748
-
1749
- global.conversationState.context.edits.push(editRecord);
1750
-
1751
- // Track major changes
1752
- if (editContext.editIntent.type === 'ADD_FEATURE' || files.length > 3) {
1753
- global.conversationState.context.projectEvolution.majorChanges.push({
1754
- timestamp: Date.now(),
1755
- description: editContext.editIntent.description,
1756
- filesAffected: editContext.primaryFiles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1757
  });
1758
- }
1759
-
1760
- // Update last updated timestamp
1761
- global.conversationState.lastUpdated = Date.now();
1762
-
1763
- console.log('[generate-ai-code-stream] Updated conversation history with edit:', editRecord);
1764
  }
1765
 
1766
  } catch (error) {
 
1
  import { NextRequest, NextResponse } from 'next/server';
2
+ import { streamText, generateText } from 'ai';
3
  import type { SandboxState } from '@/types/sandbox';
4
  import { selectFilesForEdit, getFileContents, formatFilesForAI } from '@/lib/context-selector';
5
  import { executeSearchPlan, formatSearchResultsForAI, selectTargetFile } from '@/lib/file-search-executor';
 
1188
  console.log(`[generate-ai-code-stream] Using provider for model: ${actualModel}`);
1189
  console.log(`[generate-ai-code-stream] Model string: ${model}`);
1190
 
1191
+ if (isEdit) {
1192
+ // Make streaming API call with appropriate provider
1193
+ const streamOptions: any = {
1194
+ model: modelProvider(actualModel),
1195
+ messages: [
1196
+ {
1197
+ role: 'system',
1198
+ content: systemPrompt + `
1199
+
1200
+ 🚨 CRITICAL CODE GENERATION RULES - VIOLATION = FAILURE 🚨:
1201
+ 1. NEVER truncate ANY code - ALWAYS write COMPLETE files
1202
+ 2. NEVER use "..." anywhere in your code - this causes syntax errors
1203
+ 3. NEVER cut off strings mid-sentence - COMPLETE every string
1204
+ 4. NEVER leave incomplete class names or attributes
1205
+ 5. ALWAYS close ALL tags, quotes, brackets, and parentheses
1206
+ 6. If you run out of space, prioritize completing the current file
1207
+
1208
+ CRITICAL STRING RULES TO PREVENT SYNTAX ERRORS:
1209
+ - NEVER write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobr...
1210
+ - ALWAYS write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobrut-shadow"
1211
+ - COMPLETE every className attribute
1212
+ - COMPLETE every string literal
1213
+ - NO ellipsis (...) ANYWHERE in code
1214
+
1215
+ PACKAGE RULES:
1216
+ - For INITIAL generation: Use ONLY React, no external packages
1217
+ - For EDITS: You may use packages, specify them with <package> tags
1218
+ - NEVER install packages like @mendable/firecrawl-js unless explicitly requested
1219
+
1220
+ Examples of SYNTAX ERRORS (NEVER DO THIS):
1221
+ ❌ className="px-4 py-2 bg-blue-600 hover:bg-blue-7...
1222
+ ❌ <button className="btn btn-primary btn-...
1223
+ ❌ const title = "Welcome to our...
1224
+ ❌ import { useState, useEffect, ... } from 'react'
1225
+
1226
+ Examples of CORRECT CODE (ALWAYS DO THIS):
1227
+ βœ… className="px-4 py-2 bg-blue-600 hover:bg-blue-700"
1228
+ βœ… <button className="btn btn-primary btn-large">
1229
+ βœ… const title = "Welcome to our application"
1230
+ βœ… import { useState, useEffect, useCallback } from 'react'
1231
+
1232
+ REMEMBER: It's better to generate fewer COMPLETE files than many INCOMPLETE files.`
1233
+ },
1234
+ {
1235
+ role: 'user',
1236
+ content: fullPrompt + `
1237
+
1238
+ CRITICAL: You MUST complete EVERY file you start. If you write:
1239
+ <file path="src/components/Hero.jsx">
1240
+
1241
+ You MUST include the closing </file> tag and ALL the code in between.
1242
+
1243
+ NEVER write partial code like:
1244
+ <h1>Build and deploy on the AI Cloud.</h1>
1245
+ <p>Some text...</p> ❌ WRONG
1246
+
1247
+ ALWAYS write complete code:
1248
+ <h1>Build and deploy on the AI Cloud.</h1>
1249
+ <p>Some text here with full content</p> βœ… CORRECT
1250
+
1251
+ If you're running out of space, generate FEWER files but make them COMPLETE.
1252
+ It's better to have 3 complete files than 10 incomplete files.`
1253
+ }
1254
+ ],
1255
+ maxTokens: 8192, // Reduce to ensure completion
1256
+ stopSequences: [] // Don't stop early
1257
+ // Note: Neither Groq nor Anthropic models support tool/function calling in this context
1258
+ // We use XML tags for package detection instead
1259
+ };
1260
+
1261
+ // Add temperature for non-reasoning models
1262
+ if (!model.startsWith('openai/gpt-5')) {
1263
+ streamOptions.temperature = 0.7;
1264
  }
1265
+
1266
+ // Add reasoning effort for GPT-5 models
1267
+ if (isOpenAI) {
1268
+ streamOptions.experimental_providerMetadata = {
1269
+ openai: {
1270
+ reasoningEffort: 'high'
1271
+ }
1272
+ };
1273
+ }
1274
+
1275
+ let result;
1276
+ let retryCount = 0;
1277
+ const maxRetries = 2;
1278
+
1279
+ while (retryCount <= maxRetries) {
1280
+ try {
1281
+ result = await streamText(streamOptions);
1282
+ break; // Success, exit retry loop
1283
+ } catch (streamError: any) {
1284
+ console.error(`[generate-ai-code-stream] Error calling streamText (attempt ${retryCount + 1}/${maxRetries + 1}):`, streamError);
1285
+
1286
+ const isRetryableError = streamError.message?.includes('Service unavailable') ||
1287
+ streamError.message?.includes('rate limit') ||
1288
+ streamError.message?.includes('timeout');
1289
+
1290
+ if (retryCount < maxRetries && isRetryableError) {
1291
+ retryCount++;
1292
+ console.log(`[generate-ai-code-stream] Retrying in ${retryCount * 2} seconds...`);
1293
+
1294
+ // Send progress update about retry
1295
+ await sendProgress({
1296
+ type: 'info',
1297
+ message: `Service temporarily unavailable, retrying (attempt ${retryCount + 1}/${maxRetries + 1})...`
1298
+ });
1299
+
1300
+ // Wait before retry with exponential backoff
1301
+ await new Promise(resolve => setTimeout(resolve, retryCount * 2000));
1302
+
1303
+ } else {
1304
+ // Final error, send to user
1305
+ await sendProgress({
1306
+ type: 'error',
1307
+ message: `Failed to initialize ${isGoogle ? 'Gemini' : isAnthropic ? 'Claude' : isOpenAI ? 'GPT-5' : 'AI'} streaming: ${streamError.message}`
1308
+ });
1309
+
1310
+ // If this is a Google model error, provide helpful info
1311
+ if (isGoogle) {
1312
+ await sendProgress({
1313
+ type: 'info',
1314
+ message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
1315
+ });
1316
+ }
1317
+
1318
+ throw streamError;
1319
+ }
1320
+ }
1321
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1322
 
1323
+ // Stream the response and parse in real-time
1324
+ let generatedCode = '';
1325
+ let currentFile = '';
1326
+ let currentFilePath = '';
1327
+ let componentCount = 0;
1328
+ let isInFile = false;
1329
+ let isInTag = false;
1330
+ let conversationalBuffer = '';
1331
 
1332
+ // Buffer for incomplete tags
1333
+ let tagBuffer = '';
1334
+
1335
+ // Stream the response and parse for packages in real-time
1336
+ for await (const textPart of result?.textStream || []) {
1337
+ const text = textPart || '';
1338
+ generatedCode += text;
1339
+ currentFile += text;
1340
 
1341
+ // Combine with buffer for tag detection
1342
+ const searchText = tagBuffer + text;
 
 
 
1343
 
1344
+ // Log streaming chunks to console
1345
+ process.stdout.write(text);
1346
 
1347
+ // Check if we're entering or leaving a tag
1348
+ const hasOpenTag = /<(file|package|packages|explanation|command|structure|template)\b/.test(text);
1349
+ const hasCloseTag = /<\/(file|package|packages|explanation|command|structure|template)>/.test(text);
1350
+
1351
+ if (hasOpenTag) {
1352
+ // Send any buffered conversational text before the tag
1353
+ if (conversationalBuffer.trim() && !isInTag) {
1354
+ await sendProgress({
1355
+ type: 'conversation',
1356
+ text: conversationalBuffer.trim()
1357
+ });
1358
+ conversationalBuffer = '';
1359
+ }
1360
+ isInTag = true;
1361
+ }
1362
+
1363
+ if (hasCloseTag) {
1364
+ isInTag = false;
1365
+ }
1366
+
1367
+ // If we're not in a tag, buffer as conversational text
1368
+ if (!isInTag && !hasOpenTag) {
1369
+ conversationalBuffer += text;
1370
+ }
1371
+
1372
+ // Stream the raw text for live preview
1373
  await sendProgress({
1374
+ type: 'stream',
1375
+ text: text,
1376
+ raw: true
1377
  });
1378
 
1379
+ // Debug: Log every 100 characters streamed
1380
+ if (generatedCode.length % 100 < text.length) {
1381
+ console.log(`[generate-ai-code-stream] Streamed ${generatedCode.length} chars`);
 
 
 
1382
  }
1383
 
1384
+ // Check for package tags in buffered text (ONLY for edits, not initial generation)
1385
+ let lastIndex = 0;
1386
+ if (isEdit) {
1387
+ const packageRegex = /<package>([^<]+)<\/package>/g;
1388
+ let packageMatch;
1389
+
1390
+ while ((packageMatch = packageRegex.exec(searchText)) !== null) {
1391
+ const packageName = packageMatch[1].trim();
1392
+ if (packageName && !packagesToInstall.includes(packageName)) {
1393
+ packagesToInstall.push(packageName);
1394
+ console.log(`[generate-ai-code-stream] Package detected: ${packageName}`);
1395
+ await sendProgress({
1396
+ type: 'package',
1397
+ name: packageName,
1398
+ message: `Package detected: ${packageName}`
1399
+ });
1400
+ }
1401
+ lastIndex = packageMatch.index + packageMatch[0].length;
1402
+ }
1403
+ }
1404
+
1405
+ // Keep unmatched portion in buffer for next iteration
1406
+ tagBuffer = searchText.substring(Math.max(0, lastIndex - 50)); // Keep last 50 chars
1407
+
1408
+ // Check for file boundaries
1409
+ if (text.includes('<file path="')) {
1410
+ const pathMatch = text.match(/<file path="([^"]+)"/);
1411
+ if (pathMatch) {
1412
+ currentFilePath = pathMatch[1];
1413
+ isInFile = true;
1414
+ currentFile = text;
1415
+ }
1416
+ }
1417
+
1418
+ // Check for file end
1419
+ if (isInFile && currentFile.includes('</file>')) {
1420
+ isInFile = false;
1421
+
1422
+ // Send component progress update
1423
+ if (currentFilePath.includes('components/')) {
1424
+ componentCount++;
1425
+ const componentName = currentFilePath.split('/').pop()?.replace('.jsx', '') || 'Component';
1426
+ await sendProgress({
1427
+ type: 'component',
1428
+ name: componentName,
1429
+ path: currentFilePath,
1430
+ index: componentCount
1431
+ });
1432
+ } else if (currentFilePath.includes('App.jsx')) {
1433
+ await sendProgress({
1434
+ type: 'app',
1435
+ message: 'Generated main App.jsx',
1436
+ path: currentFilePath
1437
+ });
1438
+ }
1439
+
1440
+ currentFile = '';
1441
+ currentFilePath = '';
1442
+ }
1443
  }
1444
+
1445
+ console.log('\n\n[generate-ai-code-stream] Streaming complete.');
1446
+
1447
+ // Send any remaining conversational text
1448
+ if (conversationalBuffer.trim()) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1449
  await sendProgress({
1450
  type: 'conversation',
1451
  text: conversationalBuffer.trim()
1452
  });
 
1453
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1454
 
1455
+ // Also parse <packages> tag for multiple packages - ONLY for edits
1456
+ if (isEdit) {
1457
+ const packagesRegex = /<packages>([\s\S]*?)<\/packages>/g;
1458
+ let packagesMatch;
1459
+ while ((packagesMatch = packagesRegex.exec(generatedCode)) !== null) {
1460
+ const packagesContent = packagesMatch[1].trim();
1461
+ const packagesList = packagesContent.split(/[\n,]+/)
1462
+ .map(pkg => pkg.trim())
1463
+ .filter(pkg => pkg.length > 0);
1464
+
1465
+ for (const packageName of packagesList) {
1466
+ if (!packagesToInstall.includes(packageName)) {
1467
+ packagesToInstall.push(packageName);
1468
+ console.log(`[generate-ai-code-stream] Package from <packages> tag: ${packageName}`);
1469
+ await sendProgress({
1470
+ type: 'package',
1471
+ name: packageName,
1472
+ message: `Package detected: ${packageName}`
1473
+ });
1474
+ }
1475
+ }
1476
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1477
  }
 
 
 
 
 
1478
 
1479
+ // Function to extract packages from import statements
1480
+ function extractPackagesFromCode(content: string): string[] {
1481
+ const packages: string[] = [];
1482
+ // Match ES6 imports
1483
+ const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
1484
+ let importMatch;
1485
+
1486
+ while ((importMatch = importRegex.exec(content)) !== null) {
1487
+ const importPath = importMatch[1];
1488
+ // Skip relative imports and built-in React
1489
+ if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
1490
+ importPath !== 'react' && importPath !== 'react-dom' &&
1491
+ !importPath.startsWith('@/')) {
1492
+ // Extract package name (handle scoped packages like @heroicons/react)
1493
+ const packageName = importPath.startsWith('@')
1494
+ ? importPath.split('/').slice(0, 2).join('/')
1495
+ : importPath.split('/')[0];
1496
+
1497
+ if (!packages.includes(packageName)) {
1498
+ packages.push(packageName);
1499
+ }
1500
+ }
1501
+ }
1502
+
1503
+ return packages;
1504
  }
1505
 
1506
+ // Parse files and send progress for each
1507
+ const fileRegex = /<file path="([^"]+)">([\s\S]*?)<\/file>/g;
1508
+ const files = [];
1509
+ let match;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1510
 
1511
+ while ((match = fileRegex.exec(generatedCode)) !== null) {
1512
+ const filePath = match[1];
1513
+ const content = match[2].trim();
1514
+ files.push({ path: filePath, content });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1515
 
1516
+ // Extract packages from file content - ONLY for edits
1517
+ if (isEdit) {
1518
+ const filePackages = extractPackagesFromCode(content);
1519
+ for (const pkg of filePackages) {
1520
+ if (!packagesToInstall.includes(pkg)) {
1521
+ packagesToInstall.push(pkg);
1522
+ console.log(`[generate-ai-code-stream] Package detected from imports: ${pkg}`);
1523
+ await sendProgress({
1524
+ type: 'package',
1525
+ name: pkg,
1526
+ message: `Package detected from imports: ${pkg}`
1527
+ });
1528
+ }
1529
+ }
1530
  }
1531
+
1532
+ // Send progress for each file (reusing componentCount from streaming)
1533
+ if (filePath.includes('components/')) {
1534
+ const componentName = filePath.split('/').pop()?.replace('.jsx', '') || 'Component';
1535
+ await sendProgress({
1536
+ type: 'component',
1537
+ name: componentName,
1538
+ path: filePath,
1539
+ index: componentCount
1540
+ });
1541
+ } else if (filePath.includes('App.jsx')) {
 
 
 
 
 
 
 
 
 
 
 
 
1542
  await sendProgress({
1543
+ type: 'app',
1544
+ message: 'Generated main App.jsx',
1545
+ path: filePath
1546
  });
1547
  }
1548
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1549
 
1550
+ // Extract explanation
1551
+ const explanationMatch = generatedCode.match(/<explanation>([\s\S]*?)<\/explanation>/);
1552
+ const explanation = explanationMatch ? explanationMatch[1].trim() : 'Code generated successfully!';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1553
 
1554
+ // Validate generated code for truncation issues
1555
+ const truncationWarnings: string[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
1556
 
1557
+ // Skip ellipsis checking entirely - too many false positives with spread operators, loading text, etc.
 
 
 
1558
 
1559
+ // Check for unclosed file tags
1560
+ const fileOpenCount = (generatedCode.match(/<file path="/g) || []).length;
1561
+ const fileCloseCount = (generatedCode.match(/<\/file>/g) || []).length;
1562
+ if (fileOpenCount !== fileCloseCount) {
1563
+ truncationWarnings.push(`Unclosed file tags detected: ${fileOpenCount} open, ${fileCloseCount} closed`);
1564
+ }
1565
 
1566
+ // Check for files that seem truncated (very short or ending abruptly)
1567
+ const truncationCheckRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
1568
+ let truncationMatch;
1569
+ while ((truncationMatch = truncationCheckRegex.exec(generatedCode)) !== null) {
1570
+ const filePath = truncationMatch[1];
1571
+ const content = truncationMatch[2];
1572
+
1573
+ // Only check for really obvious HTML truncation - file ends with opening tag
1574
+ if (content.trim().endsWith('<') || content.trim().endsWith('</')) {
1575
+ truncationWarnings.push(`File ${filePath} appears to have incomplete HTML tags`);
1576
+ }
1577
+
1578
+ // Skip "..." check - too many false positives with loading text, etc.
1579
+
1580
+ // Only check for SEVERE truncation issues
1581
+ if (filePath.match(/\.(jsx?|tsx?)$/)) {
1582
+ // Only check for severely unmatched brackets (more than 3 difference)
1583
+ const openBraces = (content.match(/{/g) || []).length;
1584
+ const closeBraces = (content.match(/}/g) || []).length;
1585
+ const braceDiff = Math.abs(openBraces - closeBraces);
1586
+ if (braceDiff > 3) { // Only flag severe mismatches
1587
+ truncationWarnings.push(`File ${filePath} has severely unmatched braces (${openBraces} open, ${closeBraces} closed)`);
1588
+ }
1589
+
1590
+ // Check if file is extremely short and looks incomplete
1591
+ if (content.length < 20 && content.includes('function') && !content.includes('}')) {
1592
+ truncationWarnings.push(`File ${filePath} appears severely truncated`);
1593
+ }
1594
+ }
1595
  }
 
 
 
 
 
1596
 
1597
+ // Handle truncation with automatic retry (if enabled in config)
1598
+ if (truncationWarnings.length > 0 && appConfig.codeApplication.enableTruncationRecovery) {
1599
+ console.warn('[generate-ai-code-stream] Truncation detected, attempting to fix:', truncationWarnings);
1600
+
1601
  await sendProgress({
1602
+ type: 'warning',
1603
+ message: 'Detected incomplete code generation. Attempting to complete...',
1604
+ warnings: truncationWarnings
1605
  });
1606
 
1607
+ // Try to fix truncated files automatically
1608
+ const truncatedFiles: string[] = [];
1609
+ const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
1610
+ let match;
1611
+
1612
+ while ((match = fileRegex.exec(generatedCode)) !== null) {
1613
+ const filePath = match[1];
1614
+ const content = match[2];
1615
 
1616
+ // Check if this file appears truncated - be more selective
1617
+ const hasEllipsis = content.includes('...') &&
1618
+ !content.includes('...rest') &&
1619
+ !content.includes('...props') &&
1620
+ !content.includes('spread');
1621
+
1622
+ const endsAbruptly = content.trim().endsWith('...') ||
1623
+ content.trim().endsWith(',') ||
1624
+ content.trim().endsWith('(');
1625
+
1626
+ const hasUnclosedTags = content.includes('</') &&
1627
+ !content.match(/<\/[a-zA-Z0-9]+>/) &&
1628
+ content.includes('<');
1629
+
1630
+ const tooShort = content.length < 50 && filePath.match(/\.(jsx?|tsx?)$/);
1631
 
1632
+ // Check for unmatched braces specifically
1633
+ const openBraceCount = (content.match(/{/g) || []).length;
1634
+ const closeBraceCount = (content.match(/}/g) || []).length;
1635
+ const hasUnmatchedBraces = Math.abs(openBraceCount - closeBraceCount) > 1;
1636
 
1637
+ const isTruncated = (hasEllipsis && endsAbruptly) ||
1638
+ hasUnclosedTags ||
1639
+ (tooShort && !content.includes('export')) ||
1640
+ hasUnmatchedBraces;
 
 
 
 
 
 
 
1641
 
1642
+ if (isTruncated) {
1643
+ truncatedFiles.push(filePath);
 
 
1644
  }
1645
+ }
1646
+
1647
+ // If we have truncated files, try to regenerate them
1648
+ if (truncatedFiles.length > 0) {
1649
+ console.log('[generate-ai-code-stream] Attempting to regenerate truncated files:', truncatedFiles);
1650
 
1651
+ for (const filePath of truncatedFiles) {
1652
+ await sendProgress({
1653
+ type: 'info',
1654
+ message: `Completing ${filePath}...`
1655
+ });
1656
+
1657
+ try {
1658
+ // Create a focused prompt to complete just this file
1659
+ const completionPrompt = `Complete the following file that was truncated. Provide the FULL file content.
1660
+
1661
+ File: ${filePath}
1662
+ Original request: ${prompt}
1663
+
1664
+ Provide the complete file content without any truncation. Include all necessary imports, complete all functions, and close all tags properly.`;
1665
+
1666
+ // Make a focused API call to complete this specific file
1667
+ const { client: completionClient, actualModel: completionModelName } = getProviderForModel(model);
1668
+
1669
+ const completionResult = await streamText({
1670
+ model: completionClient(completionModelName),
1671
+ messages: [
1672
+ {
1673
+ role: 'system',
1674
+ content: 'You are completing a truncated file. Provide the complete, working file content.'
1675
+ },
1676
+ { role: 'user', content: completionPrompt }
1677
+ ],
1678
+ temperature: model.startsWith('openai/gpt-5') ? undefined : appConfig.ai.defaultTemperature
1679
+ });
1680
+
1681
+ // Get the full text from the stream
1682
+ let completedContent = '';
1683
+ for await (const chunk of completionResult.textStream) {
1684
+ completedContent += chunk;
1685
+ }
1686
+
1687
+ // Replace the truncated file in the generatedCode
1688
+ const filePattern = new RegExp(
1689
+ `<file path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}">[\\s\\S]*?(?:</file>|$)`,
1690
+ 'g'
1691
+ );
1692
+
1693
+ // Extract just the code content (remove any markdown or explanation)
1694
+ let cleanContent = completedContent;
1695
+ if (cleanContent.includes('```')) {
1696
+ const codeMatch = cleanContent.match(/```[\w]*\n([\s\S]*?)```/);
1697
+ if (codeMatch) {
1698
+ cleanContent = codeMatch[1];
1699
+ }
1700
+ }
1701
+
1702
+ generatedCode = generatedCode.replace(
1703
+ filePattern,
1704
+ `<file path="${filePath}">\n${cleanContent}\n</file>`
1705
+ );
1706
+
1707
+ console.log(`[generate-ai-code-stream] Successfully completed ${filePath}`);
1708
+
1709
+ } catch (completionError) {
1710
+ console.error(`[generate-ai-code-stream] Failed to complete ${filePath}:`, completionError);
1711
+ await sendProgress({
1712
+ type: 'warning',
1713
+ message: `Could not auto-complete ${filePath}. Manual review may be needed.`
1714
+ });
1715
  }
1716
  }
1717
 
1718
+ // Clear the warnings after attempting fixes
1719
+ truncationWarnings.length = 0;
 
 
 
 
 
 
 
1720
  await sendProgress({
1721
+ type: 'info',
1722
+ message: 'Truncation recovery complete'
1723
  });
1724
  }
1725
  }
1726
 
1727
+ // Send completion with packages info
1728
+ await sendProgress({
1729
+ type: 'complete',
1730
+ generatedCode,
1731
+ explanation,
1732
+ files: files.length,
1733
+ components: componentCount,
1734
+ model,
1735
+ packagesToInstall: packagesToInstall.length > 0 ? packagesToInstall : undefined,
1736
+ warnings: truncationWarnings.length > 0 ? truncationWarnings : undefined
1737
  });
1738
+
1739
+ // Track edit in conversation history
1740
+ if (isEdit && editContext && global.conversationState) {
1741
+ const editRecord: ConversationEdit = {
1742
+ timestamp: Date.now(),
1743
+ userRequest: prompt,
1744
+ editType: editContext.editIntent.type,
1745
+ targetFiles: editContext.primaryFiles,
1746
+ confidence: editContext.editIntent.confidence,
1747
+ outcome: 'success' // Assuming success if we got here
1748
+ };
1749
+
1750
+ global.conversationState.context.edits.push(editRecord);
1751
+
1752
+ // Track major changes
1753
+ if (editContext.editIntent.type === 'ADD_FEATURE' || files.length > 3) {
1754
+ global.conversationState.context.projectEvolution.majorChanges.push({
1755
+ timestamp: Date.now(),
1756
+ description: editContext.editIntent.description,
1757
+ filesAffected: editContext.primaryFiles
1758
+ });
1759
+ }
1760
+
1761
+ // Update last updated timestamp
1762
+ global.conversationState.lastUpdated = Date.now();
1763
+
1764
+ console.log('[generate-ai-code-stream] Updated conversation history with edit:', editRecord);
1765
+ }
1766
+ } else {
1767
+ // New logic for initial generation (non-edit mode)
1768
+ await sendProgress({ type: 'status', message: 'Creating file generation plan...' });
1769
+ const planPrompt = `Based on the user's request for a new web application, provide a list of files to create.
1770
+ User Request: "${prompt}"
1771
+
1772
+ Respond ONLY with a JSON array of strings, where each string is a file path.
1773
+ Example: ["src/index.css", "src/App.jsx", "src/components/Header.jsx", "src/components/Hero.jsx", "src/components/Footer.jsx"]`;
1774
+
1775
+ const { text: filePlanJson } = await generateText({
1776
+ model: modelProvider(actualModel),
1777
+ system: "You are a senior software architect. Your task is to plan the file structure for a new React application based on a user's request. You only respond with a JSON array of file paths.",
1778
+ prompt: planPrompt,
1779
+ temperature: 0.2, // Low temp for planning
1780
+ });
1781
+
1782
+ let filePlan: string[];
1783
+ try {
1784
+ // Attempt to parse the JSON. Handle cases where the AI might return markdown
1785
+ const cleanedJson = filePlanJson.replace(/```json\n|```/g, '').trim();
1786
+ filePlan = JSON.parse(cleanedJson);
1787
+ console.log('[generate-ai-code-stream] Parsed file plan:', filePlan);
1788
+ } catch (e) {
1789
+ console.error("Failed to parse file plan:", filePlanJson);
1790
+ await sendProgress({ type: 'error', message: 'Failed to create a file generation plan. The AI returned an invalid format.' });
1791
+ throw new Error("Invalid file plan format");
1792
+ }
1793
+
1794
+ await sendProgress({ type: 'plan', files: filePlan });
1795
+
1796
+ let generatedCode = '';
1797
+ let componentCount = 0;
1798
+ const generatedFilesContent: { [key: string]: string } = {};
1799
+
1800
+ for (const filePath of filePlan) {
1801
+ await sendProgress({ type: 'status', message: `Generating ${filePath}...` });
1802
+
1803
+ // Accumulate context from previously generated files
1804
+ let accumulatedContext = '';
1805
+ if (Object.keys(generatedFilesContent).length > 0) {
1806
+ accumulatedContext += "\n\nPreviously generated files for context:\n";
1807
+ for (const [path, content] of Object.entries(generatedFilesContent)) {
1808
+ accumulatedContext += `<file path="${path}">\n${content}\n</file>\n`;
1809
+ }
1810
+ }
1811
+
1812
+ const fileGenPrompt = `The overall user request is to build a new web application: "${prompt}".
1813
+ The full planned application file structure is: ${JSON.stringify(filePlan)}.
1814
+ ${accumulatedContext}
1815
+ Your current task is to generate the complete, production-ready code for the following file ONLY:
1816
+ File: ${filePath}
1817
+
1818
+ CRITICAL INSTRUCTIONS:
1819
+ 1. Generate ONLY the code for the specified file path.
1820
+ 2. The file must be complete, with all necessary imports and code.
1821
+ 3. Do NOT include any explanations, markdown, or XML tags. Your entire response will be the content of this single file.
1822
+ 4. Adhere to all the rules specified in the system prompt (Tailwind usage, no inline styles, etc.).
1823
+ 5. Make sure you are generating code that aligns with the other files in the plan (e.g., if App.jsx imports Header.jsx, the Header.jsx you generate should be a valid React component).`;
1824
+
1825
+ let fileContent = '';
1826
+ const fileResult = await streamText({
1827
+ model: modelProvider(actualModel),
1828
+ messages: [
1829
+ { role: 'system', content: systemPrompt },
1830
+ { role: 'user', content: fileGenPrompt }
1831
+ ],
1832
+ maxTokens: 4096, // Smaller token limit per file
1833
+ temperature: 0.7
1834
+ });
1835
+
1836
+ const fileTagStart = `<file path="${filePath}">`;
1837
+ generatedCode += fileTagStart;
1838
+ await sendProgress({ type: 'stream', text: fileTagStart, raw: true });
1839
+
1840
+ for await (const textPart of fileResult.textStream) {
1841
+ fileContent += textPart;
1842
+ generatedCode += textPart;
1843
+ await sendProgress({ type: 'stream', text: textPart, raw: true });
1844
+ }
1845
+ generatedFilesContent[filePath] = fileContent;
1846
+
1847
+ const fileTagEnd = `</file>`;
1848
+ generatedCode += fileTagEnd;
1849
+ await sendProgress({ type: 'stream', text: fileTagEnd, raw: true });
1850
+
1851
+
1852
+ if (filePath.includes('components/')) {
1853
+ componentCount++;
1854
+ const componentName = filePath.split('/').pop()?.replace('.jsx', '') || 'Component';
1855
+ await sendProgress({
1856
+ type: 'component',
1857
+ name: componentName,
1858
+ path: filePath,
1859
+ index: componentCount
1860
+ });
1861
+ }
1862
+ }
1863
+
1864
+ // Finalize
1865
+ await sendProgress({
1866
+ type: 'complete',
1867
+ generatedCode,
1868
+ explanation: 'Application generated successfully.',
1869
+ files: filePlan.length,
1870
+ components: componentCount,
1871
+ model,
1872
+ packagesToInstall: undefined,
1873
+ warnings: undefined
1874
  });
 
 
 
 
 
 
1875
  }
1876
 
1877
  } catch (error) {
upload_to_hf.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import HfApi
2
+ import os
3
+
4
+ # Get the Hugging Face token from the environment variable
5
+ hf_token = os.environ.get("HF_TOKEN")
6
+ if not hf_token:
7
+ raise ValueError("HF_TOKEN environment variable not set")
8
+
9
+ repo_id = "Leon4gr45/openoperator"
10
+ repo_type = "space"
11
+
12
+ # Initialize the HfApi client
13
+ api = HfApi(token=hf_token)
14
+
15
+ # Upload the entire directory
16
+ api.upload_folder(
17
+ folder_path=".",
18
+ repo_id=repo_id,
19
+ repo_type=repo_type,
20
+ ignore_patterns=[".git/*", "node_modules/*", ".next/*", "upload_to_hf.py"] # ignore some files
21
+ )
22
+
23
+ print("Files uploaded successfully.")