Spaces:
Build error
Build error
File size: 83,484 Bytes
d83e271 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 |
import { NextRequest, NextResponse } from 'next/server';
import { createGroq } from '@ai-sdk/groq';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { streamText } from 'ai';
import type { SandboxState } from '@/types/sandbox';
import { selectFilesForEdit, getFileContents, formatFilesForAI } from '@/lib/context-selector';
import { executeSearchPlan, formatSearchResultsForAI, selectTargetFile } from '@/lib/file-search-executor';
import { FileManifest } from '@/types/file-manifest';
import type { ConversationState, ConversationMessage, ConversationEdit } from '@/types/conversation';
import { appConfig } from '@/config/app.config';
const groq = createGroq({
apiKey: process.env.GROQ_API_KEY,
});
const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com/v1',
});
const googleGenerativeAI = createGoogleGenerativeAI({
apiKey: process.env.GEMINI_API_KEY,
});
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// Helper function to analyze user preferences from conversation history
function analyzeUserPreferences(messages: ConversationMessage[]): {
commonPatterns: string[];
preferredEditStyle: 'targeted' | 'comprehensive';
} {
const userMessages = messages.filter(m => m.role === 'user');
const patterns: string[] = [];
// Count edit-related keywords
let targetedEditCount = 0;
let comprehensiveEditCount = 0;
userMessages.forEach(msg => {
const content = msg.content.toLowerCase();
// Check for targeted edit patterns
if (content.match(/\b(update|change|fix|modify|edit|remove|delete)\s+(\w+\s+)?(\w+)\b/)) {
targetedEditCount++;
}
// Check for comprehensive edit patterns
if (content.match(/\b(rebuild|recreate|redesign|overhaul|refactor)\b/)) {
comprehensiveEditCount++;
}
// Extract common request patterns
if (content.includes('hero')) patterns.push('hero section edits');
if (content.includes('header')) patterns.push('header modifications');
if (content.includes('color') || content.includes('style')) patterns.push('styling changes');
if (content.includes('button')) patterns.push('button updates');
if (content.includes('animation')) patterns.push('animation requests');
});
return {
commonPatterns: [...new Set(patterns)].slice(0, 3), // Top 3 unique patterns
preferredEditStyle: targetedEditCount > comprehensiveEditCount ? 'targeted' : 'comprehensive'
};
}
declare global {
var sandboxState: SandboxState;
var conversationState: ConversationState | null;
}
export async function POST(request: NextRequest) {
try {
const { prompt, model = 'openai/gpt-oss-20b', context, isEdit = false } = await request.json();
console.log('[generate-ai-code-stream] Received request:');
console.log('[generate-ai-code-stream] - prompt:', prompt);
console.log('[generate-ai-code-stream] - isEdit:', isEdit);
console.log('[generate-ai-code-stream] - context.sandboxId:', context?.sandboxId);
console.log('[generate-ai-code-stream] - context.currentFiles:', context?.currentFiles ? Object.keys(context.currentFiles) : 'none');
console.log('[generate-ai-code-stream] - currentFiles count:', context?.currentFiles ? Object.keys(context.currentFiles).length : 0);
// Initialize conversation state if not exists
if (!global.conversationState) {
global.conversationState = {
conversationId: `conv-${Date.now()}`,
startedAt: Date.now(),
lastUpdated: Date.now(),
context: {
messages: [],
edits: [],
projectEvolution: { majorChanges: [] },
userPreferences: {}
}
};
}
// Add user message to conversation history
const userMessage: ConversationMessage = {
id: `msg-${Date.now()}`,
role: 'user',
content: prompt,
timestamp: Date.now(),
metadata: {
sandboxId: context?.sandboxId
}
};
global.conversationState.context.messages.push(userMessage);
// Clean up old messages to prevent unbounded growth
if (global.conversationState.context.messages.length > 20) {
// Keep only the last 15 messages
global.conversationState.context.messages = global.conversationState.context.messages.slice(-15);
console.log('[generate-ai-code-stream] Trimmed conversation history to prevent context overflow');
}
// Clean up old edits
if (global.conversationState.context.edits.length > 10) {
global.conversationState.context.edits = global.conversationState.context.edits.slice(-8);
}
// Debug: Show a sample of actual file content
if (context?.currentFiles && Object.keys(context.currentFiles).length > 0) {
const firstFile = Object.entries(context.currentFiles)[0];
console.log('[generate-ai-code-stream] - sample file:', firstFile[0]);
console.log('[generate-ai-code-stream] - sample content preview:',
typeof firstFile[1] === 'string' ? firstFile[1].substring(0, 100) + '...' : 'not a string');
}
if (!prompt) {
return NextResponse.json({
success: false,
error: 'Prompt is required'
}, { status: 400 });
}
// Create a stream for real-time updates
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// Function to send progress updates
const sendProgress = async (data: any) => {
const message = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(message));
};
// Start processing in background
(async () => {
try {
// Send initial status
await sendProgress({ type: 'status', message: 'Initializing AI...' });
// No keep-alive needed - sandbox provisioned for 10 minutes
// Check if we have a file manifest for edit mode
let editContext = null;
let enhancedSystemPrompt = '';
if (isEdit) {
console.log('[generate-ai-code-stream] Edit mode detected - starting agentic search workflow');
console.log('[generate-ai-code-stream] Has fileCache:', !!global.sandboxState?.fileCache);
console.log('[generate-ai-code-stream] Has manifest:', !!global.sandboxState?.fileCache?.manifest);
const manifest: FileManifest | undefined = global.sandboxState?.fileCache?.manifest;
if (manifest) {
await sendProgress({ type: 'status', message: 'π Creating search plan...' });
const fileContents = global.sandboxState.fileCache.files;
console.log('[generate-ai-code-stream] Files available for search:', Object.keys(fileContents).length);
// STEP 1: Get search plan from AI
try {
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, manifest, model })
});
if (intentResponse.ok) {
const { searchPlan } = await intentResponse.json();
console.log('[generate-ai-code-stream] Search plan received:', searchPlan);
await sendProgress({
type: 'status',
message: `π Searching for: "${searchPlan.searchTerms.join('", "')}"`
});
// STEP 2: Execute the search plan
const searchExecution = executeSearchPlan(searchPlan,
Object.fromEntries(
Object.entries(fileContents).map(([path, data]) => [
path.startsWith('/') ? path : `/home/user/app/${path}`,
data.content
])
)
);
console.log('[generate-ai-code-stream] Search execution:', {
success: searchExecution.success,
resultsCount: searchExecution.results.length,
filesSearched: searchExecution.filesSearched,
time: searchExecution.executionTime + 'ms'
});
if (searchExecution.success && searchExecution.results.length > 0) {
// STEP 3: Select the best target file
const target = selectTargetFile(searchExecution.results, searchPlan.editType);
if (target) {
await sendProgress({
type: 'status',
message: `β
Found code in ${target.filePath.split('/').pop()} at line ${target.lineNumber}`
});
console.log('[generate-ai-code-stream] Target selected:', target);
// Create surgical edit context with exact location
const normalizedPath = target.filePath.replace('/home/user/app/', '');
const fileContent = fileContents[normalizedPath]?.content || '';
// Build enhanced context with search results
enhancedSystemPrompt = `
${formatSearchResultsForAI(searchExecution.results)}
SURGICAL EDIT INSTRUCTIONS:
You have been given the EXACT location of the code to edit.
- File: ${target.filePath}
- Line: ${target.lineNumber}
- Reason: ${target.reason}
Make ONLY the change requested by the user. Do not modify any other code.
User request: "${prompt}"`;
// Set up edit context with just this one file
editContext = {
primaryFiles: [target.filePath],
contextFiles: [],
systemPrompt: enhancedSystemPrompt,
editIntent: {
type: searchPlan.editType,
description: searchPlan.reasoning,
targetFiles: [target.filePath],
confidence: 0.95, // High confidence since we found exact location
searchTerms: searchPlan.searchTerms
}
};
console.log('[generate-ai-code-stream] Surgical edit context created');
}
} else {
// Search failed - fall back to old behavior but inform user
console.warn('[generate-ai-code-stream] Search found no results, falling back to broader context');
await sendProgress({
type: 'status',
message: 'β οΈ Could not find exact match, using broader search...'
});
}
} else {
console.error('[generate-ai-code-stream] Failed to get search plan');
}
} catch (error) {
console.error('[generate-ai-code-stream] Error in agentic search workflow:', error);
await sendProgress({
type: 'status',
message: 'β οΈ Search workflow error, falling back to keyword method...'
});
// Fall back to old method on any error if we have a manifest
if (manifest) {
editContext = selectFilesForEdit(prompt, manifest);
}
}
} else {
// Fall back to old method if AI analysis fails
console.warn('[generate-ai-code-stream] AI intent analysis failed, falling back to keyword method');
if (manifest) {
editContext = selectFilesForEdit(prompt, manifest);
} else {
console.log('[generate-ai-code-stream] No manifest available for fallback');
await sendProgress({
type: 'status',
message: 'β οΈ No file manifest available, will use broad context'
});
}
}
// If we got an edit context from any method, use its system prompt
if (editContext) {
enhancedSystemPrompt = editContext.systemPrompt;
await sendProgress({
type: 'status',
message: `Identified edit type: ${editContext.editIntent?.description || 'Code modification'}`
});
} else if (!manifest) {
console.log('[generate-ai-code-stream] WARNING: No manifest available for edit mode!');
// Try to fetch files from sandbox if we have one
if (global.activeSandbox) {
await sendProgress({ type: 'status', message: 'Fetching current files from sandbox...' });
try {
// Fetch files directly from sandbox
const filesResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/get-sandbox-files`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (filesResponse.ok) {
const filesData = await filesResponse.json();
if (filesData.success && filesData.manifest) {
console.log('[generate-ai-code-stream] Successfully fetched manifest from sandbox');
const manifest = filesData.manifest;
// Now try to analyze edit intent with the fetched manifest
try {
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, manifest, model })
});
if (intentResponse.ok) {
const { searchPlan } = await intentResponse.json();
console.log('[generate-ai-code-stream] Search plan received (after fetch):', searchPlan);
// For now, fall back to keyword search since we don't have file contents for search execution
// This path happens when no manifest was initially available
let targetFiles = [];
if (!searchPlan || searchPlan.searchTerms.length === 0) {
console.warn('[generate-ai-code-stream] No target files after fetch, searching for relevant files');
const promptLower = prompt.toLowerCase();
const allFilePaths = Object.keys(manifest.files);
// Look for component names mentioned in the prompt
if (promptLower.includes('hero')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('hero'));
} else if (promptLower.includes('header')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('header'));
} else if (promptLower.includes('footer')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('footer'));
} else if (promptLower.includes('nav')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('nav'));
} else if (promptLower.includes('button')) {
targetFiles = allFilePaths.filter(p => p.toLowerCase().includes('button'));
}
if (targetFiles.length > 0) {
console.log('[generate-ai-code-stream] Found target files by keyword search after fetch:', targetFiles);
}
}
const allFiles = Object.keys(manifest.files)
.filter(path => !targetFiles.includes(path));
editContext = {
primaryFiles: targetFiles,
contextFiles: allFiles,
systemPrompt: `
You are an expert senior software engineer performing a surgical, context-aware code modification. Your primary directive is **precision and preservation**.
Think of yourself as a surgeon making a precise incision, not a construction worker demolishing a wall.
## Search-Based Edit
Search Terms: ${searchPlan?.searchTerms?.join(', ') || 'keyword-based'}
Edit Type: ${searchPlan?.editType || 'UPDATE_COMPONENT'}
Reasoning: ${searchPlan?.reasoning || 'Modifying based on user request'}
Files to Edit: ${targetFiles.join(', ') || 'To be determined'}
User Request: "${prompt}"
## Your Mandatory Thought Process (Execute Internally):
Before writing ANY code, you MUST follow these steps:
1. **Understand Intent:**
- What is the user's core goal? (adding feature, fixing bug, changing style?)
- Does the conversation history provide extra clues?
2. **Locate the Code:**
- First examine the Primary Files provided
- Check the "ALL PROJECT FILES" list to find the EXACT file name
- "nav" might be Navigation.tsx, NavBar.tsx, Nav.tsx, or Header.tsx
- DO NOT create a new file if a similar one exists!
3. **Plan the Changes (Mental Diff):**
- What is the *minimal* set of changes required?
- Which exact lines need to be added, modified, or deleted?
- Will this require new packages?
4. **Verify Preservation:**
- What existing code, props, state, and logic must NOT be touched?
- How can I make my change without disrupting surrounding code?
5. **Construct the Final Code:**
- Only after completing steps above, generate the final code
- Provide the ENTIRE file content with modifications integrated
## Critical Rules & Constraints:
**PRESERVATION IS KEY:** You MUST NOT rewrite entire components or files. Integrate your changes into the existing code. Preserve all existing logic, props, state, and comments not directly related to the user's request.
**MINIMALISM:** Only output files you have actually changed. If a file doesn't need modification, don't include it.
**COMPLETENESS:** Each file must be COMPLETE from first line to last:
- NEVER TRUNCATE - Include EVERY line
- NO ellipsis (...) to skip content
- ALL imports, functions, JSX, and closing tags must be present
- The file MUST be runnable
**SURGICAL PRECISION:**
- Change ONLY what's explicitly requested
- If user says "change background to green", change ONLY the background class
- 99% of the original code should remain untouched
- NO refactoring, reformatting, or "improvements" unless requested
**NO CONVERSATION:** Your output must contain ONLY the code. No explanations or apologies.
## EXAMPLES:
### CORRECT APPROACH for "change hero background to blue":
<thinking>
I need to change the background color of the Hero component. Looking at the file, I see the main div has 'bg-gray-900'. I will change ONLY this to 'bg-blue-500' and leave everything else exactly as is.
</thinking>
Then return the EXACT same file with only 'bg-gray-900' changed to 'bg-blue-500'.
### WRONG APPROACH (DO NOT DO THIS):
- Rewriting the Hero component from scratch
- Changing the structure or reorganizing imports
- Adding or removing unrelated code
- Reformatting or "cleaning up" the code
Remember: You are a SURGEON making a precise incision, not an artist repainting the canvas!`,
editIntent: {
type: searchPlan?.editType || 'UPDATE_COMPONENT',
targetFiles: targetFiles,
confidence: searchPlan ? 0.85 : 0.6,
description: searchPlan?.reasoning || 'Keyword-based file selection',
suggestedContext: []
}
};
enhancedSystemPrompt = editContext.systemPrompt;
await sendProgress({
type: 'status',
message: `Identified edit type: ${editContext.editIntent.description}`
});
}
} catch (error) {
console.error('[generate-ai-code-stream] Error analyzing intent after fetch:', error);
}
} else {
console.error('[generate-ai-code-stream] Failed to get manifest from sandbox files');
}
} else {
console.error('[generate-ai-code-stream] Failed to fetch sandbox files:', filesResponse.status);
}
} catch (error) {
console.error('[generate-ai-code-stream] Error fetching sandbox files:', error);
await sendProgress({
type: 'warning',
message: 'Could not analyze existing files for targeted edits. Proceeding with general edit mode.'
});
}
} else {
console.log('[generate-ai-code-stream] No active sandbox to fetch files from');
await sendProgress({
type: 'warning',
message: 'No existing files found. Consider generating initial code first.'
});
}
}
}
// Build conversation context for system prompt
let conversationContext = '';
if (global.conversationState && global.conversationState.context.messages.length > 1) {
console.log('[generate-ai-code-stream] Building conversation context');
console.log('[generate-ai-code-stream] Total messages:', global.conversationState.context.messages.length);
console.log('[generate-ai-code-stream] Total edits:', global.conversationState.context.edits.length);
conversationContext = `\n\n## Conversation History (Recent)\n`;
// Include only the last 3 edits to save context
const recentEdits = global.conversationState.context.edits.slice(-3);
if (recentEdits.length > 0) {
console.log('[generate-ai-code-stream] Including', recentEdits.length, 'recent edits in context');
conversationContext += `\n### Recent Edits:\n`;
recentEdits.forEach(edit => {
conversationContext += `- "${edit.userRequest}" β ${edit.editType} (${edit.targetFiles.map(f => f.split('/').pop()).join(', ')})\n`;
});
}
// Include recently created files - CRITICAL for preventing duplicates
const recentMsgs = global.conversationState.context.messages.slice(-5);
const recentlyCreatedFiles: string[] = [];
recentMsgs.forEach(msg => {
if (msg.metadata?.editedFiles) {
recentlyCreatedFiles.push(...msg.metadata.editedFiles);
}
});
if (recentlyCreatedFiles.length > 0) {
const uniqueFiles = [...new Set(recentlyCreatedFiles)];
conversationContext += `\n### π¨ RECENTLY CREATED/EDITED FILES (DO NOT RECREATE THESE):\n`;
uniqueFiles.forEach(file => {
conversationContext += `- ${file}\n`;
});
conversationContext += `\nIf the user mentions any of these components, UPDATE the existing file!\n`;
}
// Include only last 5 messages for context (reduced from 10)
const recentMessages = recentMsgs;
if (recentMessages.length > 2) { // More than just current message
conversationContext += `\n### Recent Messages:\n`;
recentMessages.slice(0, -1).forEach(msg => { // Exclude current message
if (msg.role === 'user') {
const truncatedContent = msg.content.length > 100 ? msg.content.substring(0, 100) + '...' : msg.content;
conversationContext += `- "${truncatedContent}"\n`;
}
});
}
// Include only last 2 major changes
const majorChanges = global.conversationState.context.projectEvolution.majorChanges.slice(-2);
if (majorChanges.length > 0) {
conversationContext += `\n### Recent Changes:\n`;
majorChanges.forEach(change => {
conversationContext += `- ${change.description}\n`;
});
}
// Keep user preferences - they're concise
const userPrefs = analyzeUserPreferences(global.conversationState.context.messages);
if (userPrefs.commonPatterns.length > 0) {
conversationContext += `\n### User Preferences:\n`;
conversationContext += `- Edit style: ${userPrefs.preferredEditStyle}\n`;
}
// Limit total conversation context length
if (conversationContext.length > 2000) {
conversationContext = conversationContext.substring(0, 2000) + '\n[Context truncated to prevent length errors]';
}
}
// Build system prompt with conversation awareness
const systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications.
${conversationContext}
π¨ CRITICAL RULES - YOUR MOST IMPORTANT INSTRUCTIONS:
1. **DO EXACTLY WHAT IS ASKED - NOTHING MORE, NOTHING LESS**
- Don't add features not requested
- Don't fix unrelated issues
- Don't improve things not mentioned
2. **CHECK App.jsx FIRST** - ALWAYS see what components exist before creating new ones
3. **NAVIGATION LIVES IN Header.jsx** - Don't create Nav.jsx if Header exists with nav
4. **USE STANDARD TAILWIND CLASSES ONLY**:
- β
CORRECT: bg-white, text-black, bg-blue-500, bg-gray-100, text-gray-900
- β WRONG: bg-background, text-foreground, bg-primary, bg-muted, text-secondary
- Use ONLY classes from the official Tailwind CSS documentation
5. **FILE COUNT LIMITS**:
- Simple style/text change = 1 file ONLY
- New component = 2 files MAX (component + parent)
- If >3 files, YOU'RE DOING TOO MUCH
COMPONENT RELATIONSHIPS (CHECK THESE FIRST):
- Navigation usually lives INSIDE Header.jsx, not separate Nav.jsx
- Logo is typically in Header, not standalone
- Footer often contains nav links already
- Menu/Hamburger is part of Header, not separate
PACKAGE USAGE RULES:
- DO NOT use react-router-dom unless user explicitly asks for routing
- For simple nav links in a single-page app, use scroll-to-section or href="#"
- Only add routing if building a multi-page application
- Common packages are auto-installed from your imports
WEBSITE CLONING REQUIREMENTS:
When recreating/cloning a website, you MUST include:
1. **Header with Navigation** - Usually Header.jsx containing nav
2. **Hero Section** - The main landing area (Hero.jsx)
3. **Main Content Sections** - Features, Services, About, etc.
4. **Footer** - Contact info, links, copyright (Footer.jsx)
5. **App.jsx** - Main app component that imports and uses all components
${isEdit ? `CRITICAL: THIS IS AN EDIT TO AN EXISTING APPLICATION
YOU MUST FOLLOW THESE EDIT RULES:
0. NEVER create tailwind.config.js, vite.config.js, package.json, or any other config files - they already exist!
1. DO NOT regenerate the entire application
2. DO NOT create files that already exist (like App.jsx, index.css, tailwind.config.js)
3. ONLY edit the EXACT files needed for the requested change - NO MORE, NO LESS
4. If the user says "update the header", ONLY edit the Header component - DO NOT touch Footer, Hero, or any other components
5. If the user says "change the color", ONLY edit the relevant style or component file - DO NOT "improve" other parts
6. If you're unsure which file to edit, choose the SINGLE most specific one related to the request
7. IMPORTANT: When adding new components or libraries:
- Create the new component file
- UPDATE ONLY the parent component that will use it
- Example: Adding a Newsletter component means:
* Create Newsletter.jsx
* Update ONLY the file that will use it (e.g., Footer.jsx OR App.jsx) - NOT both
8. When adding npm packages:
- Import them ONLY in the files where they're actually used
- The system will auto-install missing packages
CRITICAL FILE MODIFICATION RULES - VIOLATION = FAILURE:
- **NEVER TRUNCATE FILES** - Always return COMPLETE files with ALL content
- **NO ELLIPSIS (...)** - Include every single line of code, no skipping
- Files MUST be complete and runnable - include ALL imports, functions, JSX, and closing tags
- Count the files you're about to generate
- If the user asked to change ONE thing, you should generate ONE file (or at most two if adding a new component)
- DO NOT "fix" or "improve" files that weren't mentioned in the request
- DO NOT update multiple components when only one was requested
- DO NOT add features the user didn't ask for
- RESIST the urge to be "helpful" by updating related files
CRITICAL: DO NOT REDESIGN OR REIMAGINE COMPONENTS
- "update" means make a small change, NOT redesign the entire component
- "change X to Y" means ONLY change X to Y, nothing else
- "fix" means repair what's broken, NOT rewrite everything
- "remove X" means delete X from the existing file, NOT create a new file
- "delete X" means remove X from where it currently exists
- Preserve ALL existing functionality and design unless explicitly asked to change it
NEVER CREATE NEW FILES WHEN THE USER ASKS TO REMOVE/DELETE SOMETHING
If the user says "remove X", you must:
1. Find which existing file contains X
2. Edit that file to remove X
3. DO NOT create any new files
${editContext ? `
TARGETED EDIT MODE ACTIVE
- Edit Type: ${editContext.editIntent.type}
- Confidence: ${editContext.editIntent.confidence}
- Files to Edit: ${editContext.primaryFiles.join(', ')}
π¨ CRITICAL RULE - VIOLATION WILL RESULT IN FAILURE π¨
YOU MUST ***ONLY*** GENERATE THE FILES LISTED ABOVE!
ABSOLUTE REQUIREMENTS:
1. COUNT the files in "Files to Edit" - that's EXACTLY how many files you must generate
2. If "Files to Edit" shows ONE file, generate ONLY that ONE file
3. DO NOT generate App.jsx unless it's EXPLICITLY listed in "Files to Edit"
4. DO NOT generate ANY components that aren't listed in "Files to Edit"
5. DO NOT "helpfully" update related files
6. DO NOT fix unrelated issues you notice
7. DO NOT improve code quality in files not being edited
8. DO NOT add bonus features
EXAMPLE VIOLATIONS (THESE ARE FAILURES):
β User says "update the hero" β You update Hero, Header, Footer, and App.jsx
β User says "change header color" β You redesign the entire header
β User says "fix the button" β You update multiple components
β Files to Edit shows "Hero.jsx" β You also generate App.jsx "to integrate it"
β Files to Edit shows "Header.jsx" β You also update Footer.jsx "for consistency"
CORRECT BEHAVIOR (THIS IS SUCCESS):
β
User says "update the hero" β You ONLY edit Hero.jsx with the requested change
β
User says "change header color" β You ONLY change the color in Header.jsx
β
User says "fix the button" β You ONLY fix the specific button issue
β
Files to Edit shows "Hero.jsx" β You generate ONLY Hero.jsx
β
Files to Edit shows "Header.jsx, Nav.jsx" β You generate EXACTLY 2 files: Header.jsx and Nav.jsx
THE AI INTENT ANALYZER HAS ALREADY DETERMINED THE FILES.
DO NOT SECOND-GUESS IT.
DO NOT ADD MORE FILES.
ONLY OUTPUT THE EXACT FILES LISTED IN "Files to Edit".
` : ''}
VIOLATION OF THESE RULES WILL RESULT IN FAILURE!
` : ''}
CRITICAL INCREMENTAL UPDATE RULES:
- When the user asks for additions or modifications (like "add a videos page", "create a new component", "update the header"):
- DO NOT regenerate the entire application
- DO NOT recreate files that already exist unless explicitly asked
- ONLY create/modify the specific files needed for the requested change
- Preserve all existing functionality and files
- If adding a new page/route, integrate it with the existing routing system
- Reference existing components and styles rather than duplicating them
- NEVER recreate config files (tailwind.config.js, vite.config.js, package.json, etc.)
IMPORTANT: When the user asks for edits or modifications:
- You have access to the current file contents in the context
- Make targeted changes to existing files rather than regenerating everything
- Preserve the existing structure and only modify what's requested
- If you need to see a specific file that's not in context, mention it
IMPORTANT: You have access to the full conversation context including:
- Previously scraped websites and their content
- Components already generated and applied
- The current project being worked on
- Recent conversation history
- Any Vite errors that need to be resolved
When the user references "the app", "the website", or "the site" without specifics, refer to:
1. The most recently scraped website in the context
2. The current project name in the context
3. The files currently in the sandbox
If you see scraped websites in the context, you're working on a clone/recreation of that site.
CRITICAL UI/UX RULES:
- NEVER use emojis in any code, text, console logs, or UI elements
- ALWAYS ensure responsive design using proper Tailwind classes (sm:, md:, lg:, xl:)
- ALWAYS use proper mobile-first responsive design patterns
- NEVER hardcode pixel widths - use relative units and responsive classes
- ALWAYS test that the layout works on mobile devices (320px and up)
- ALWAYS make sections full-width by default - avoid max-w-7xl or similar constraints
- For full-width layouts: use className="w-full" or no width constraint at all
- Only add max-width constraints when explicitly needed for readability (like blog posts)
- Prefer system fonts and clean typography
- Ensure all interactive elements have proper hover/focus states
- Use proper semantic HTML elements for accessibility
CRITICAL STYLING RULES - MUST FOLLOW:
- NEVER use inline styles with style={{ }} in JSX
- NEVER use <style jsx> tags or any CSS-in-JS solutions
- NEVER create App.css, Component.css, or any component-specific CSS files
- NEVER import './App.css' or any CSS files except index.css
- ALWAYS use Tailwind CSS classes for ALL styling
- ONLY create src/index.css with the @tailwind directives
- The ONLY CSS file should be src/index.css with:
@tailwind base;
@tailwind components;
@tailwind utilities;
- Use Tailwind's full utility set: spacing, colors, typography, flexbox, grid, animations, etc.
- ALWAYS add smooth transitions and animations where appropriate:
- Use transition-all, transition-colors, transition-opacity for hover states
- Use animate-fade-in, animate-pulse, animate-bounce for engaging UI elements
- Add hover:scale-105 or hover:scale-110 for interactive elements
- Use transform and transition utilities for smooth interactions
- For complex layouts, combine Tailwind utilities rather than writing custom CSS
- NEVER use non-standard Tailwind classes like "border-border", "bg-background", "text-foreground", etc.
- Use standard Tailwind classes only:
- For borders: use "border-gray-200", "border-gray-300", etc. NOT "border-border"
- For backgrounds: use "bg-white", "bg-gray-100", etc. NOT "bg-background"
- For text: use "text-gray-900", "text-black", etc. NOT "text-foreground"
- Examples of good Tailwind usage:
- Buttons: className="px-4 py-2 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 hover:shadow-lg transform hover:scale-105 transition-all duration-200"
- Cards: className="bg-white rounded-lg shadow-md p-6 border border-gray-200 hover:shadow-xl transition-shadow duration-300"
- Full-width sections: className="w-full px-4 sm:px-6 lg:px-8"
- Constrained content (only when needed): className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
- Dark backgrounds: className="min-h-screen bg-gray-900 text-white"
- Hero sections: className="animate-fade-in-up"
- Feature cards: className="transform hover:scale-105 transition-transform duration-300"
- CTAs: className="animate-pulse hover:animate-none"
CRITICAL STRING AND SYNTAX RULES:
- ALWAYS escape apostrophes in strings: use \' instead of ' or use double quotes
- ALWAYS escape quotes properly in JSX attributes
- NEVER use curly quotes or smart quotes ('' "" '' "") - only straight quotes (' ")
- ALWAYS convert smart/curly quotes to straight quotes:
- ' and ' β '
- " and " β "
- Any other Unicode quotes β straight quotes
- When strings contain apostrophes, either:
1. Use double quotes: "you're" instead of 'you're'
2. Escape the apostrophe: 'you\'re'
- When working with scraped content, ALWAYS sanitize quotes first
- Replace all smart quotes with straight quotes before using in code
- Be extra careful with user-generated content or scraped text
- Always validate that JSX syntax is correct before generating
CRITICAL CODE SNIPPET DISPLAY RULES:
- When displaying code examples in JSX, NEVER put raw curly braces { } in text
- ALWAYS wrap code snippets in template literals with backticks
- For code examples in components, use one of these patterns:
1. Template literals: <div>{\`const example = { key: 'value' }\`}</div>
2. Pre/code blocks: <pre><code>{\`your code here\`}</code></pre>
3. Escape braces: <div>{'{'}key: value{'}'}</div>
- NEVER do this: <div>const example = { key: 'value' }</div> (causes parse errors)
- For multi-line code snippets, always use:
<pre className="bg-gray-900 text-gray-100 p-4 rounded">
<code>{\`
// Your code here
const example = {
key: 'value'
}
\`}</code>
</pre>
CRITICAL: When asked to create a React app or components:
- ALWAYS CREATE ALL FILES IN FULL - never provide partial implementations
- ALWAYS CREATE EVERY COMPONENT that you import - no placeholders
- ALWAYS IMPLEMENT COMPLETE FUNCTIONALITY - don't leave TODOs unless explicitly asked
- If you're recreating a website, implement ALL sections and features completely
- NEVER create tailwind.config.js - it's already configured in the template
- ALWAYS include a Navigation/Header component (Nav.jsx or Header.jsx) - websites need navigation!
REQUIRED COMPONENTS for website clones:
1. Nav.jsx or Header.jsx - Navigation bar with links (NEVER SKIP THIS!)
2. Hero.jsx - Main landing section
3. Features/Services/Products sections - Based on the site content
4. Footer.jsx - Footer with links and info
5. App.jsx - Main component that imports and arranges all components
- NEVER create vite.config.js - it's already configured in the template
- NEVER create package.json - it's already configured in the template
WHEN WORKING WITH SCRAPED CONTENT:
- ALWAYS sanitize all text content before using in code
- Convert ALL smart quotes to straight quotes
- Example transformations:
- "Firecrawl's API" β "Firecrawl's API" or "Firecrawl\\'s API"
- 'It's amazing' β "It's amazing" or 'It\\'s amazing'
- "Best tool ever" β "Best tool ever"
- When in doubt, use double quotes for strings containing apostrophes
- For testimonials or quotes from scraped content, ALWAYS clean the text:
- Bad: content: 'Moved our internal agent's web scraping...'
- Good: content: "Moved our internal agent's web scraping..."
- Also good: content: 'Moved our internal agent\\'s web scraping...'
When generating code, FOLLOW THIS PROCESS:
1. ALWAYS generate src/index.css FIRST - this establishes the styling foundation
2. List ALL components you plan to import in App.jsx
3. Count them - if there are 10 imports, you MUST create 10 component files
4. Generate src/index.css first (with proper CSS reset and base styles)
5. Generate App.jsx second
6. Then generate EVERY SINGLE component file you imported
7. Do NOT stop until all imports are satisfied
Use this XML format for React components only (DO NOT create tailwind.config.js - it already exists):
<file path="src/index.css">
@tailwind base;
@tailwind components;
@tailwind utilities;
</file>
<file path="src/App.jsx">
// Main App component that imports and uses other components
// Use Tailwind classes: className="min-h-screen bg-gray-50"
</file>
<file path="src/components/Example.jsx">
// Your React component code here
// Use Tailwind classes for ALL styling
</file>
CRITICAL COMPLETION RULES:
1. NEVER say "I'll continue with the remaining components"
2. NEVER say "Would you like me to proceed?"
3. NEVER use <continue> tags
4. Generate ALL components in ONE response
5. If App.jsx imports 10 components, generate ALL 10
6. Complete EVERYTHING before ending your response
With 16,000 tokens available, you have plenty of space to generate a complete application. Use it!
UNDERSTANDING USER INTENT FOR INCREMENTAL VS FULL GENERATION:
- "add/create/make a [specific feature]" β Add ONLY that feature to existing app
- "add a videos page" β Create ONLY Videos.jsx and update routing
- "update the header" β Modify ONLY header component
- "fix the styling" β Update ONLY the affected components
- "change X to Y" β Find the file containing X and modify it
- "make the header black" β Find Header component and change its color
- "rebuild/recreate/start over" β Full regeneration
- Default to incremental updates when working on an existing app
SURGICAL EDIT RULES (CRITICAL FOR PERFORMANCE):
- **PREFER TARGETED CHANGES**: Don't regenerate entire components for small edits
- For color/style changes: Edit ONLY the specific className or style prop
- For text changes: Change ONLY the text content, keep everything else
- For adding elements: INSERT into existing JSX, don't rewrite the whole return
- **PRESERVE EXISTING CODE**: Keep all imports, functions, and unrelated code exactly as-is
- Maximum files to edit:
- Style change = 1 file ONLY
- Text change = 1 file ONLY
- New feature = 2 files MAX (feature + parent)
- If you're editing >3 files for a simple request, STOP - you're doing too much
EXAMPLES OF CORRECT SURGICAL EDITS:
β
"change header to black" β Find className="..." in Header.jsx, change ONLY color classes
β
"update hero text" β Find the <h1> or <p> in Hero.jsx, change ONLY the text inside
β
"add a button to hero" β Find the return statement, ADD button, keep everything else
β WRONG: Regenerating entire Header.jsx to change one color
β WRONG: Rewriting Hero.jsx to add one button
NAVIGATION/HEADER INTELLIGENCE:
- ALWAYS check App.jsx imports first
- Navigation is usually INSIDE Header.jsx, not separate
- If user says "nav", check Header.jsx FIRST
- Only create Nav.jsx if no navigation exists anywhere
- Logo, menu, hamburger = all typically in Header
CRITICAL: When files are provided in the context:
1. The user is asking you to MODIFY the existing app, not create a new one
2. Find the relevant file(s) from the provided context
3. Generate ONLY the files that need changes
4. Do NOT ask to see files - they are already provided in the context above
5. Make the requested change immediately`;
// Build full prompt with context
let fullPrompt = prompt;
if (context) {
const contextParts = [];
if (context.sandboxId) {
contextParts.push(`Current sandbox ID: ${context.sandboxId}`);
}
if (context.structure) {
contextParts.push(`Current file structure:\n${context.structure}`);
}
// Use backend file cache instead of frontend-provided files
let backendFiles = global.sandboxState?.fileCache?.files || {};
let hasBackendFiles = Object.keys(backendFiles).length > 0;
console.log('[generate-ai-code-stream] Backend file cache status:');
console.log('[generate-ai-code-stream] - Has sandboxState:', !!global.sandboxState);
console.log('[generate-ai-code-stream] - Has fileCache:', !!global.sandboxState?.fileCache);
console.log('[generate-ai-code-stream] - File count:', Object.keys(backendFiles).length);
console.log('[generate-ai-code-stream] - Has manifest:', !!global.sandboxState?.fileCache?.manifest);
// If no backend files and we're in edit mode, try to fetch from sandbox
if (!hasBackendFiles && isEdit && (global.activeSandbox || context?.sandboxId)) {
console.log('[generate-ai-code-stream] No backend files, attempting to fetch from sandbox...');
try {
const filesResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/get-sandbox-files`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (filesResponse.ok) {
const filesData = await filesResponse.json();
if (filesData.success && filesData.files) {
console.log('[generate-ai-code-stream] Successfully fetched', Object.keys(filesData.files).length, 'files from sandbox');
// Initialize sandboxState if needed
if (!global.sandboxState) {
global.sandboxState = {
fileCache: {
files: {},
lastSync: Date.now(),
sandboxId: context?.sandboxId || 'unknown'
}
} as any;
} else if (!global.sandboxState.fileCache) {
global.sandboxState.fileCache = {
files: {},
lastSync: Date.now(),
sandboxId: context?.sandboxId || 'unknown'
};
}
// Store files in cache
for (const [path, content] of Object.entries(filesData.files)) {
const normalizedPath = path.replace('/home/user/app/', '');
global.sandboxState.fileCache.files[normalizedPath] = {
content: content as string,
lastModified: Date.now()
};
}
if (filesData.manifest) {
global.sandboxState.fileCache.manifest = filesData.manifest;
// Now try to analyze edit intent with the fetched manifest
if (!editContext) {
console.log('[generate-ai-code-stream] Analyzing edit intent with fetched manifest');
try {
const intentResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/analyze-edit-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, manifest: filesData.manifest, model })
});
if (intentResponse.ok) {
const { searchPlan } = await intentResponse.json();
console.log('[generate-ai-code-stream] Search plan received:', searchPlan);
// Create edit context from AI analysis
// Note: We can't execute search here without file contents, so fall back to keyword method
const fileContext = selectFilesForEdit(prompt, filesData.manifest);
editContext = fileContext;
enhancedSystemPrompt = fileContext.systemPrompt;
console.log('[generate-ai-code-stream] Edit context created with', editContext.primaryFiles.length, 'primary files');
}
} catch (error) {
console.error('[generate-ai-code-stream] Failed to analyze edit intent:', error);
}
}
}
// Update variables
backendFiles = global.sandboxState.fileCache.files;
hasBackendFiles = Object.keys(backendFiles).length > 0;
console.log('[generate-ai-code-stream] Updated backend cache with fetched files');
}
}
} catch (error) {
console.error('[generate-ai-code-stream] Failed to fetch sandbox files:', error);
}
}
// Include current file contents from backend cache
if (hasBackendFiles) {
// If we have edit context, use intelligent file selection
if (editContext && editContext.primaryFiles.length > 0) {
contextParts.push('\nEXISTING APPLICATION - TARGETED EDIT MODE');
contextParts.push(`\n${editContext.systemPrompt || enhancedSystemPrompt}\n`);
// Get contents of primary and context files
const primaryFileContents = await getFileContents(editContext.primaryFiles, global.sandboxState!.fileCache!.manifest!);
const contextFileContents = await getFileContents(editContext.contextFiles, global.sandboxState!.fileCache!.manifest!);
// Format files for AI
const formattedFiles = formatFilesForAI(primaryFileContents, contextFileContents);
contextParts.push(formattedFiles);
contextParts.push('\nIMPORTANT: Only modify the files listed under "Files to Edit". The context files are provided for reference only.');
} else {
// Fallback to showing all files if no edit context
console.log('[generate-ai-code-stream] WARNING: Using fallback mode - no edit context available');
contextParts.push('\nEXISTING APPLICATION - TARGETED EDIT REQUIRED');
contextParts.push('\nYou MUST analyze the user request and determine which specific file(s) to edit.');
contextParts.push('\nCurrent project files (DO NOT regenerate all of these):');
const fileEntries = Object.entries(backendFiles);
console.log(`[generate-ai-code-stream] Using backend cache: ${fileEntries.length} files`);
// Show file list first for reference
contextParts.push('\n### File List:');
for (const [path] of fileEntries) {
contextParts.push(`- ${path}`);
}
// Include ALL files as context in fallback mode
contextParts.push('\n### File Contents (ALL FILES FOR CONTEXT):');
for (const [path, fileData] of fileEntries) {
const content = fileData.content;
if (typeof content === 'string') {
contextParts.push(`\n<file path="${path}">\n${content}\n</file>`);
}
}
contextParts.push('\nπ¨ CRITICAL INSTRUCTIONS - VIOLATION = FAILURE π¨');
contextParts.push('1. Analyze the user request: "' + prompt + '"');
contextParts.push('2. Identify the MINIMUM number of files that need editing (usually just ONE)');
contextParts.push('3. PRESERVE ALL EXISTING CONTENT in those files');
contextParts.push('4. ONLY ADD/MODIFY the specific part requested');
contextParts.push('5. DO NOT regenerate entire components from scratch');
contextParts.push('6. DO NOT change unrelated parts of any file');
contextParts.push('7. Generate ONLY the files that MUST be changed - NO EXTRAS');
contextParts.push('\nβ οΈ FILE COUNT RULE:');
contextParts.push('- Simple change (color, text, spacing) = 1 file ONLY');
contextParts.push('- Adding new component = 2 files MAX (new component + parent that imports it)');
contextParts.push('- DO NOT exceed these limits unless absolutely necessary');
contextParts.push('\nEXAMPLES OF CORRECT BEHAVIOR:');
contextParts.push('β
"add a chart to the hero" β Edit ONLY Hero.jsx, ADD the chart, KEEP everything else');
contextParts.push('β
"change header to black" β Edit ONLY Header.jsx, change ONLY the color');
contextParts.push('β
"fix spacing in footer" β Edit ONLY Footer.jsx, adjust ONLY spacing');
contextParts.push('\nEXAMPLES OF FAILURES:');
contextParts.push('β "change header color" β You edit Header, Footer, and App "for consistency"');
contextParts.push('β "add chart to hero" β You regenerate the entire Hero component');
contextParts.push('β "fix button" β You update 5 different component files');
contextParts.push('\nβ οΈ FINAL WARNING:');
contextParts.push('If you generate MORE files than necessary, you have FAILED');
contextParts.push('If you DELETE or REWRITE existing functionality, you have FAILED');
contextParts.push('ONLY change what was EXPLICITLY requested - NOTHING MORE');
}
} else if (context.currentFiles && Object.keys(context.currentFiles).length > 0) {
// Fallback to frontend-provided files if backend cache is empty
console.log('[generate-ai-code-stream] Warning: Backend cache empty, using frontend files');
contextParts.push('\nEXISTING APPLICATION - DO NOT REGENERATE FROM SCRATCH');
contextParts.push('Current project files (modify these, do not recreate):');
const fileEntries = Object.entries(context.currentFiles);
for (const [path, content] of fileEntries) {
if (typeof content === 'string') {
contextParts.push(`\n<file path="${path}">\n${content}\n</file>`);
}
}
contextParts.push('\nThe above files already exist. When the user asks to modify something (like "change the header color to black"), find the relevant file above and generate ONLY that file with the requested changes.');
}
// Add explicit edit mode indicator
if (isEdit) {
contextParts.push('\nEDIT MODE ACTIVE');
contextParts.push('This is an incremental update to an existing application.');
contextParts.push('DO NOT regenerate App.jsx, index.css, or other core files unless explicitly requested.');
contextParts.push('ONLY create or modify the specific files needed for the user\'s request.');
contextParts.push('\nβ οΈ CRITICAL FILE OUTPUT FORMAT - VIOLATION = FAILURE:');
contextParts.push('YOU MUST OUTPUT EVERY FILE IN THIS EXACT XML FORMAT:');
contextParts.push('<file path="src/components/ComponentName.jsx">');
contextParts.push('// Complete file content here');
contextParts.push('</file>');
contextParts.push('<file path="src/index.css">');
contextParts.push('/* CSS content here */');
contextParts.push('</file>');
contextParts.push('\nβ NEVER OUTPUT: "Generated Files: index.css, App.jsx"');
contextParts.push('β NEVER LIST FILE NAMES WITHOUT CONTENT');
contextParts.push('β
ALWAYS: One <file> tag per file with COMPLETE content');
contextParts.push('β
ALWAYS: Include EVERY file you modified');
} else if (!hasBackendFiles) {
// First generation mode - make it beautiful!
contextParts.push('\nπ¨ FIRST GENERATION MODE - CREATE SOMETHING BEAUTIFUL!');
contextParts.push('\nThis is the user\'s FIRST experience. Make it impressive:');
contextParts.push('1. **USE TAILWIND PROPERLY** - Use standard Tailwind color classes');
contextParts.push('2. **NO PLACEHOLDERS** - Use real content, not lorem ipsum');
contextParts.push('3. **COMPLETE COMPONENTS** - Header, Hero, Features, Footer minimum');
contextParts.push('4. **VISUAL POLISH** - Shadows, hover states, transitions');
contextParts.push('5. **STANDARD CLASSES** - bg-white, text-gray-900, bg-blue-500, NOT bg-background');
contextParts.push('\nCreate a polished, professional application that works perfectly on first load.');
contextParts.push('\nβ οΈ OUTPUT FORMAT:');
contextParts.push('Use <file path="...">content</file> tags for EVERY file');
contextParts.push('NEVER output "Generated Files:" as plain text');
}
// Add conversation context (scraped websites, etc)
if (context.conversationContext) {
if (context.conversationContext.scrapedWebsites?.length > 0) {
contextParts.push('\nScraped Websites in Context:');
context.conversationContext.scrapedWebsites.forEach((site: any) => {
contextParts.push(`\nURL: ${site.url}`);
contextParts.push(`Scraped: ${new Date(site.timestamp).toLocaleString()}`);
if (site.content) {
// Include a summary of the scraped content
const contentPreview = typeof site.content === 'string'
? site.content.substring(0, 1000)
: JSON.stringify(site.content).substring(0, 1000);
contextParts.push(`Content Preview: ${contentPreview}...`);
}
});
}
if (context.conversationContext.currentProject) {
contextParts.push(`\nCurrent Project: ${context.conversationContext.currentProject}`);
}
}
if (contextParts.length > 0) {
fullPrompt = `CONTEXT:\n${contextParts.join('\n')}\n\nUSER REQUEST:\n${prompt}`;
}
}
await sendProgress({ type: 'status', message: 'Planning application structure...' });
console.log('\n[generate-ai-code-stream] Starting streaming response...\n');
// Track packages that need to be installed
const packagesToInstall: string[] = [];
// Determine which provider to use based on model
const isAnthropic = model.startsWith('anthropic/');
const isGoogle = model.startsWith('google/');
const isOpenAI = model.startsWith('openai/gpt-5');
const modelProvider = isAnthropic ? anthropic : (isOpenAI ? openai : (isGoogle ? googleGenerativeAI : groq));
const actualModel = isAnthropic ? model.replace('anthropic/', '') :
(model === 'openai/gpt-5') ? 'gpt-5' :
(isGoogle ? model.replace('google/', '') : model);
// Make streaming API call with appropriate provider
const streamOptions: any = {
model: modelProvider(actualModel),
messages: [
{
role: 'system',
content: systemPrompt + `
π¨ CRITICAL CODE GENERATION RULES - VIOLATION = FAILURE π¨:
1. NEVER truncate ANY code - ALWAYS write COMPLETE files
2. NEVER use "..." anywhere in your code - this causes syntax errors
3. NEVER cut off strings mid-sentence - COMPLETE every string
4. NEVER leave incomplete class names or attributes
5. ALWAYS close ALL tags, quotes, brackets, and parentheses
6. If you run out of space, prioritize completing the current file
CRITICAL STRING RULES TO PREVENT SYNTAX ERRORS:
- NEVER write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobr...
- ALWAYS write: className="px-8 py-4 bg-black text-white font-bold neobrut-border neobrut-shadow"
- COMPLETE every className attribute
- COMPLETE every string literal
- NO ellipsis (...) ANYWHERE in code
PACKAGE RULES:
- For INITIAL generation: Use ONLY React, no external packages
- For EDITS: You may use packages, specify them with <package> tags
- NEVER install packages like @mendable/firecrawl-js unless explicitly requested
Examples of SYNTAX ERRORS (NEVER DO THIS):
β className="px-4 py-2 bg-blue-600 hover:bg-blue-7...
β <button className="btn btn-primary btn-...
β const title = "Welcome to our...
β import { useState, useEffect, ... } from 'react'
Examples of CORRECT CODE (ALWAYS DO THIS):
β
className="px-4 py-2 bg-blue-600 hover:bg-blue-700"
β
<button className="btn btn-primary btn-large">
β
const title = "Welcome to our application"
β
import { useState, useEffect, useCallback } from 'react'
REMEMBER: It's better to generate fewer COMPLETE files than many INCOMPLETE files.`
},
{
role: 'user',
content: fullPrompt + `
CRITICAL: You MUST complete EVERY file you start. If you write:
<file path="src/components/Hero.jsx">
You MUST include the closing </file> tag and ALL the code in between.
NEVER write partial code like:
<h1>Build and deploy on the AI Cloud.</h1>
<p>Some text...</p> β WRONG
ALWAYS write complete code:
<h1>Build and deploy on the AI Cloud.</h1>
<p>Some text here with full content</p> β
CORRECT
If you're running out of space, generate FEWER files but make them COMPLETE.
It's better to have 3 complete files than 10 incomplete files.`
}
],
maxTokens: 8192, // Reduce to ensure completion
stopSequences: [] // Don't stop early
// Note: Neither Groq nor Anthropic models support tool/function calling in this context
// We use XML tags for package detection instead
};
// Add temperature for non-reasoning models
if (!model.startsWith('openai/gpt-5')) {
streamOptions.temperature = 0.7;
}
// Add reasoning effort for GPT-5 models
if (isOpenAI) {
streamOptions.experimental_providerMetadata = {
openai: {
reasoningEffort: 'high'
}
};
}
const result = await streamText(streamOptions);
// Stream the response and parse in real-time
let generatedCode = '';
let currentFile = '';
let currentFilePath = '';
let componentCount = 0;
let isInFile = false;
let isInTag = false;
let conversationalBuffer = '';
// Buffer for incomplete tags
let tagBuffer = '';
// Stream the response and parse for packages in real-time
for await (const textPart of result.textStream) {
const text = textPart || '';
generatedCode += text;
currentFile += text;
// Combine with buffer for tag detection
const searchText = tagBuffer + text;
// Log streaming chunks to console
process.stdout.write(text);
// Check if we're entering or leaving a tag
const hasOpenTag = /<(file|package|packages|explanation|command|structure|template)\b/.test(text);
const hasCloseTag = /<\/(file|package|packages|explanation|command|structure|template)>/.test(text);
if (hasOpenTag) {
// Send any buffered conversational text before the tag
if (conversationalBuffer.trim() && !isInTag) {
await sendProgress({
type: 'conversation',
text: conversationalBuffer.trim()
});
conversationalBuffer = '';
}
isInTag = true;
}
if (hasCloseTag) {
isInTag = false;
}
// If we're not in a tag, buffer as conversational text
if (!isInTag && !hasOpenTag) {
conversationalBuffer += text;
}
// Stream the raw text for live preview
await sendProgress({
type: 'stream',
text: text,
raw: true
});
// Check for package tags in buffered text (ONLY for edits, not initial generation)
let lastIndex = 0;
if (isEdit) {
const packageRegex = /<package>([^<]+)<\/package>/g;
let packageMatch;
while ((packageMatch = packageRegex.exec(searchText)) !== null) {
const packageName = packageMatch[1].trim();
if (packageName && !packagesToInstall.includes(packageName)) {
packagesToInstall.push(packageName);
console.log(`[generate-ai-code-stream] Package detected: ${packageName}`);
await sendProgress({
type: 'package',
name: packageName,
message: `Package detected: ${packageName}`
});
}
lastIndex = packageMatch.index + packageMatch[0].length;
}
}
// Keep unmatched portion in buffer for next iteration
tagBuffer = searchText.substring(Math.max(0, lastIndex - 50)); // Keep last 50 chars
// Check for file boundaries
if (text.includes('<file path="')) {
const pathMatch = text.match(/<file path="([^"]+)"/);
if (pathMatch) {
currentFilePath = pathMatch[1];
isInFile = true;
currentFile = text;
}
}
// Check for file end
if (isInFile && currentFile.includes('</file>')) {
isInFile = false;
// Send component progress update
if (currentFilePath.includes('components/')) {
componentCount++;
const componentName = currentFilePath.split('/').pop()?.replace('.jsx', '') || 'Component';
await sendProgress({
type: 'component',
name: componentName,
path: currentFilePath,
index: componentCount
});
} else if (currentFilePath.includes('App.jsx')) {
await sendProgress({
type: 'app',
message: 'Generated main App.jsx',
path: currentFilePath
});
}
currentFile = '';
currentFilePath = '';
}
}
console.log('\n\n[generate-ai-code-stream] Streaming complete.');
// Send any remaining conversational text
if (conversationalBuffer.trim()) {
await sendProgress({
type: 'conversation',
text: conversationalBuffer.trim()
});
}
// Also parse <packages> tag for multiple packages - ONLY for edits
if (isEdit) {
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/g;
let packagesMatch;
while ((packagesMatch = packagesRegex.exec(generatedCode)) !== null) {
const packagesContent = packagesMatch[1].trim();
const packagesList = packagesContent.split(/[\n,]+/)
.map(pkg => pkg.trim())
.filter(pkg => pkg.length > 0);
for (const packageName of packagesList) {
if (!packagesToInstall.includes(packageName)) {
packagesToInstall.push(packageName);
console.log(`[generate-ai-code-stream] Package from <packages> tag: ${packageName}`);
await sendProgress({
type: 'package',
name: packageName,
message: `Package detected: ${packageName}`
});
}
}
}
}
// Function to extract packages from import statements
function extractPackagesFromCode(content: string): string[] {
const packages: string[] = [];
// Match ES6 imports
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
let importMatch;
while ((importMatch = importRegex.exec(content)) !== null) {
const importPath = importMatch[1];
// Skip relative imports and built-in React
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
importPath !== 'react' && importPath !== 'react-dom' &&
!importPath.startsWith('@/')) {
// Extract package name (handle scoped packages like @heroicons/react)
const packageName = importPath.startsWith('@')
? importPath.split('/').slice(0, 2).join('/')
: importPath.split('/')[0];
if (!packages.includes(packageName)) {
packages.push(packageName);
}
}
}
return packages;
}
// Parse files and send progress for each
const fileRegex = /<file path="([^"]+)">([\s\S]*?)<\/file>/g;
const files = [];
let match;
while ((match = fileRegex.exec(generatedCode)) !== null) {
const filePath = match[1];
const content = match[2].trim();
files.push({ path: filePath, content });
// Extract packages from file content - ONLY for edits
if (isEdit) {
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
if (!packagesToInstall.includes(pkg)) {
packagesToInstall.push(pkg);
console.log(`[generate-ai-code-stream] Package detected from imports: ${pkg}`);
await sendProgress({
type: 'package',
name: pkg,
message: `Package detected from imports: ${pkg}`
});
}
}
}
// Send progress for each file (reusing componentCount from streaming)
if (filePath.includes('components/')) {
const componentName = filePath.split('/').pop()?.replace('.jsx', '') || 'Component';
await sendProgress({
type: 'component',
name: componentName,
path: filePath,
index: componentCount
});
} else if (filePath.includes('App.jsx')) {
await sendProgress({
type: 'app',
message: 'Generated main App.jsx',
path: filePath
});
}
}
// Extract explanation
const explanationMatch = generatedCode.match(/<explanation>([\s\S]*?)<\/explanation>/);
const explanation = explanationMatch ? explanationMatch[1].trim() : 'Code generated successfully!';
// Validate generated code for truncation issues
const truncationWarnings: string[] = [];
// Skip ellipsis checking entirely - too many false positives with spread operators, loading text, etc.
// Check for unclosed file tags
const fileOpenCount = (generatedCode.match(/<file path="/g) || []).length;
const fileCloseCount = (generatedCode.match(/<\/file>/g) || []).length;
if (fileOpenCount !== fileCloseCount) {
truncationWarnings.push(`Unclosed file tags detected: ${fileOpenCount} open, ${fileCloseCount} closed`);
}
// Check for files that seem truncated (very short or ending abruptly)
const truncationCheckRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
let truncationMatch;
while ((truncationMatch = truncationCheckRegex.exec(generatedCode)) !== null) {
const filePath = truncationMatch[1];
const content = truncationMatch[2];
// Only check for really obvious HTML truncation - file ends with opening tag
if (content.trim().endsWith('<') || content.trim().endsWith('</')) {
truncationWarnings.push(`File ${filePath} appears to have incomplete HTML tags`);
}
// Skip "..." check - too many false positives with loading text, etc.
// Only check for SEVERE truncation issues
if (filePath.match(/\.(jsx?|tsx?)$/)) {
// Only check for severely unmatched brackets (more than 3 difference)
const openBraces = (content.match(/{/g) || []).length;
const closeBraces = (content.match(/}/g) || []).length;
const braceDiff = Math.abs(openBraces - closeBraces);
if (braceDiff > 3) { // Only flag severe mismatches
truncationWarnings.push(`File ${filePath} has severely unmatched braces (${openBraces} open, ${closeBraces} closed)`);
}
// Check if file is extremely short and looks incomplete
if (content.length < 20 && content.includes('function') && !content.includes('}')) {
truncationWarnings.push(`File ${filePath} appears severely truncated`);
}
}
}
// Handle truncation with automatic retry (if enabled in config)
if (truncationWarnings.length > 0 && appConfig.codeApplication.enableTruncationRecovery) {
console.warn('[generate-ai-code-stream] Truncation detected, attempting to fix:', truncationWarnings);
await sendProgress({
type: 'warning',
message: 'Detected incomplete code generation. Attempting to complete...',
warnings: truncationWarnings
});
// Try to fix truncated files automatically
const truncatedFiles: string[] = [];
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
let match;
while ((match = fileRegex.exec(generatedCode)) !== null) {
const filePath = match[1];
const content = match[2];
// Check if this file appears truncated - be more selective
const hasEllipsis = content.includes('...') &&
!content.includes('...rest') &&
!content.includes('...props') &&
!content.includes('spread');
const endsAbruptly = content.trim().endsWith('...') ||
content.trim().endsWith(',') ||
content.trim().endsWith('(');
const hasUnclosedTags = content.includes('</') &&
!content.match(/<\/[a-zA-Z0-9]+>/) &&
content.includes('<');
const tooShort = content.length < 50 && filePath.match(/\.(jsx?|tsx?)$/);
// Check for unmatched braces specifically
const openBraceCount = (content.match(/{/g) || []).length;
const closeBraceCount = (content.match(/}/g) || []).length;
const hasUnmatchedBraces = Math.abs(openBraceCount - closeBraceCount) > 1;
const isTruncated = (hasEllipsis && endsAbruptly) ||
hasUnclosedTags ||
(tooShort && !content.includes('export')) ||
hasUnmatchedBraces;
if (isTruncated) {
truncatedFiles.push(filePath);
}
}
// If we have truncated files, try to regenerate them
if (truncatedFiles.length > 0) {
console.log('[generate-ai-code-stream] Attempting to regenerate truncated files:', truncatedFiles);
for (const filePath of truncatedFiles) {
await sendProgress({
type: 'info',
message: `Completing ${filePath}...`
});
try {
// Create a focused prompt to complete just this file
const completionPrompt = `Complete the following file that was truncated. Provide the FULL file content.
File: ${filePath}
Original request: ${prompt}
Provide the complete file content without any truncation. Include all necessary imports, complete all functions, and close all tags properly.`;
// Make a focused API call to complete this specific file
// Create a new client for the completion based on the provider
let completionClient;
if (model.includes('gpt') || model.includes('openai')) {
completionClient = openai;
} else if (model.includes('claude')) {
completionClient = anthropic;
} else {
completionClient = groq;
}
const completionResult = await streamText({
model: completionClient(modelMapping[model] || model),
messages: [
{
role: 'system',
content: 'You are completing a truncated file. Provide the complete, working file content.'
},
{ role: 'user', content: completionPrompt }
],
temperature: isGPT5 ? undefined : appConfig.ai.defaultTemperature,
maxTokens: appConfig.ai.truncationRecoveryMaxTokens
});
// Get the full text from the stream
let completedContent = '';
for await (const chunk of completionResult.textStream) {
completedContent += chunk;
}
// Replace the truncated file in the generatedCode
const filePattern = new RegExp(
`<file path="${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}">[\\s\\S]*?(?:</file>|$)`,
'g'
);
// Extract just the code content (remove any markdown or explanation)
let cleanContent = completedContent;
if (cleanContent.includes('```')) {
const codeMatch = cleanContent.match(/```[\w]*\n([\s\S]*?)```/);
if (codeMatch) {
cleanContent = codeMatch[1];
}
}
generatedCode = generatedCode.replace(
filePattern,
`<file path="${filePath}">\n${cleanContent}\n</file>`
);
console.log(`[generate-ai-code-stream] Successfully completed ${filePath}`);
} catch (completionError) {
console.error(`[generate-ai-code-stream] Failed to complete ${filePath}:`, completionError);
await sendProgress({
type: 'warning',
message: `Could not auto-complete ${filePath}. Manual review may be needed.`
});
}
}
// Clear the warnings after attempting fixes
truncationWarnings.length = 0;
await sendProgress({
type: 'info',
message: 'Truncation recovery complete'
});
}
}
// Send completion with packages info
await sendProgress({
type: 'complete',
generatedCode,
explanation,
files: files.length,
components: componentCount,
model,
packagesToInstall: packagesToInstall.length > 0 ? packagesToInstall : undefined,
warnings: truncationWarnings.length > 0 ? truncationWarnings : undefined
});
// Track edit in conversation history
if (isEdit && editContext && global.conversationState) {
const editRecord: ConversationEdit = {
timestamp: Date.now(),
userRequest: prompt,
editType: editContext.editIntent.type,
targetFiles: editContext.primaryFiles,
confidence: editContext.editIntent.confidence,
outcome: 'success' // Assuming success if we got here
};
global.conversationState.context.edits.push(editRecord);
// Track major changes
if (editContext.editIntent.type === 'ADD_FEATURE' || files.length > 3) {
global.conversationState.context.projectEvolution.majorChanges.push({
timestamp: Date.now(),
description: editContext.editIntent.description,
filesAffected: editContext.primaryFiles
});
}
// Update last updated timestamp
global.conversationState.lastUpdated = Date.now();
console.log('[generate-ai-code-stream] Updated conversation history with edit:', editRecord);
}
} catch (error) {
console.error('[generate-ai-code-stream] Stream processing error:', error);
// Check if it's a tool validation error
if ((error as any).message?.includes('tool call validation failed')) {
console.error('[generate-ai-code-stream] Tool call validation error - this may be due to the AI model sending incorrect parameters');
await sendProgress({
type: 'warning',
message: 'Package installation tool encountered an issue. Packages will be detected from imports instead.'
});
// Continue processing - packages can still be detected from the code
} else {
await sendProgress({
type: 'error',
error: (error as Error).message
});
}
} finally {
await writer.close();
}
})();
// Return the stream
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error) {
console.error('[generate-ai-code-stream] Error:', error);
return NextResponse.json({
success: false,
error: (error as Error).message
}, { status: 500 });
}
} |