Spaces:
Build error
Build error
Upload folder using huggingface_hub
Browse files- app/api/generate-ai-code-stream/route.ts +628 -517
- upload_to_hf.py +23 -0
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 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
-
|
| 1210 |
-
-
|
| 1211 |
-
- COMPLETE every
|
| 1212 |
-
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
- For
|
| 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 |
-
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 |
-
|
| 1286 |
-
|
| 1287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1288 |
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1292 |
|
| 1293 |
-
//
|
| 1294 |
-
|
| 1295 |
-
type: 'info',
|
| 1296 |
-
message: `Service temporarily unavailable, retrying (attempt ${retryCount + 1}/${maxRetries + 1})...`
|
| 1297 |
-
});
|
| 1298 |
|
| 1299 |
-
//
|
| 1300 |
-
|
| 1301 |
|
| 1302 |
-
|
| 1303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1304 |
await sendProgress({
|
| 1305 |
-
type: '
|
| 1306 |
-
|
|
|
|
| 1307 |
});
|
| 1308 |
|
| 1309 |
-
//
|
| 1310 |
-
if (
|
| 1311 |
-
|
| 1312 |
-
type: 'info',
|
| 1313 |
-
message: 'Tip: Make sure your GEMINI_API_KEY is set correctly and has proper permissions.'
|
| 1314 |
-
});
|
| 1315 |
}
|
| 1316 |
|
| 1317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1318 |
}
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 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 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 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 |
-
//
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1437 |
}
|
| 1438 |
|
| 1439 |
-
|
| 1440 |
-
|
| 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 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 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 |
-
|
| 1497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1498 |
}
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
|
| 1505 |
-
|
| 1506 |
-
|
| 1507 |
-
|
| 1508 |
-
|
| 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: '
|
| 1524 |
-
|
| 1525 |
-
|
| 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 |
-
//
|
| 1590 |
-
|
| 1591 |
-
|
| 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 |
-
//
|
| 1616 |
-
const
|
| 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 |
-
//
|
| 1632 |
-
const openBraceCount = (content.match(/{/g) || []).length;
|
| 1633 |
-
const closeBraceCount = (content.match(/}/g) || []).length;
|
| 1634 |
-
const hasUnmatchedBraces = Math.abs(openBraceCount - closeBraceCount) > 1;
|
| 1635 |
|
| 1636 |
-
|
| 1637 |
-
|
| 1638 |
-
|
| 1639 |
-
|
|
|
|
|
|
|
| 1640 |
|
| 1641 |
-
|
| 1642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 1651 |
await sendProgress({
|
| 1652 |
-
type: '
|
| 1653 |
-
message:
|
|
|
|
| 1654 |
});
|
| 1655 |
|
| 1656 |
-
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
-
|
| 1660 |
-
|
| 1661 |
-
|
|
|
|
|
|
|
| 1662 |
|
| 1663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1664 |
|
| 1665 |
-
//
|
| 1666 |
-
const
|
|
|
|
|
|
|
| 1667 |
|
| 1668 |
-
const
|
| 1669 |
-
|
| 1670 |
-
|
| 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 |
-
|
| 1681 |
-
|
| 1682 |
-
for await (const chunk of completionResult.textStream) {
|
| 1683 |
-
completedContent += chunk;
|
| 1684 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1685 |
|
| 1686 |
-
|
| 1687 |
-
|
| 1688 |
-
|
| 1689 |
-
|
| 1690 |
-
|
| 1691 |
-
|
| 1692 |
-
|
| 1693 |
-
|
| 1694 |
-
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1698 |
}
|
| 1699 |
}
|
| 1700 |
|
| 1701 |
-
|
| 1702 |
-
|
| 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: '
|
| 1712 |
-
message:
|
| 1713 |
});
|
| 1714 |
}
|
| 1715 |
}
|
| 1716 |
|
| 1717 |
-
//
|
| 1718 |
-
|
| 1719 |
-
|
| 1720 |
-
|
| 1721 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1722 |
});
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
| 1732 |
-
|
| 1733 |
-
|
| 1734 |
-
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
|
| 1746 |
-
|
| 1747 |
-
|
| 1748 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.")
|