Spaces:
Running
Running
stream PUT request to avoid timeout from cloudfront
Browse files- app/api/ask/route.ts +115 -486
- app/api/me/projects/[namespace]/[repoId]/update/route.ts +141 -0
- components/editor/ask-ai/index.tsx +2 -1
- components/editor/preview/index.tsx +2 -50
- hooks/useAi.ts +173 -132
- lib/format-ai-response.ts +255 -0
app/api/ask/route.ts
CHANGED
|
@@ -6,35 +6,19 @@ import { InferenceClient } from "@huggingface/inference";
|
|
| 6 |
|
| 7 |
import { MODELS } from "@/lib/providers";
|
| 8 |
import {
|
| 9 |
-
DIVIDER,
|
| 10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
| 11 |
INITIAL_SYSTEM_PROMPT,
|
| 12 |
MAX_REQUESTS_PER_IP,
|
| 13 |
-
NEW_FILE_END,
|
| 14 |
-
NEW_FILE_START,
|
| 15 |
-
REPLACE_END,
|
| 16 |
-
SEARCH_START,
|
| 17 |
-
UPDATE_FILE_START,
|
| 18 |
-
UPDATE_FILE_END,
|
| 19 |
PROMPT_FOR_PROJECT_NAME,
|
| 20 |
} from "@/lib/prompts";
|
| 21 |
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
| 22 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 23 |
import { Page } from "@/types";
|
| 24 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 25 |
import { isAuthenticated } from "@/lib/auth";
|
| 26 |
import { getBestProvider } from "@/lib/best-provider";
|
| 27 |
-
// import { rewritePrompt } from "@/lib/rewrite-prompt";
|
| 28 |
-
import { COLORS } from "@/lib/utils";
|
| 29 |
-
import { templates } from "@/lib/templates";
|
| 30 |
-
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 31 |
|
| 32 |
const ipAddresses = new Map();
|
| 33 |
|
| 34 |
-
const STREAMING_TIMEOUT = 180000;
|
| 35 |
-
const REQUEST_TIMEOUT = 240000;
|
| 36 |
-
export const maxDuration = 300;
|
| 37 |
-
|
| 38 |
export async function POST(request: NextRequest) {
|
| 39 |
const authHeaders = await headers();
|
| 40 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
|
@@ -117,9 +101,6 @@ export async function POST(request: NextRequest) {
|
|
| 117 |
|
| 118 |
(async () => {
|
| 119 |
// let completeResponse = "";
|
| 120 |
-
let timeoutId: NodeJS.Timeout | null = null;
|
| 121 |
-
let isTimedOut = false;
|
| 122 |
-
|
| 123 |
try {
|
| 124 |
const client = new InferenceClient(token);
|
| 125 |
|
|
@@ -151,51 +132,21 @@ export async function POST(request: NextRequest) {
|
|
| 151 |
billTo ? { billTo } : {}
|
| 152 |
);
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
while (true) {
|
| 166 |
-
const { done, value } = await chatCompletion.next()
|
| 167 |
-
if (done) {
|
| 168 |
-
break;
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
const chunk = value.choices[0]?.delta?.content;
|
| 172 |
-
if (chunk) {
|
| 173 |
-
await writer.write(encoder.encode(chunk));
|
| 174 |
-
}
|
| 175 |
-
}
|
| 176 |
-
})(),
|
| 177 |
-
timeoutPromise
|
| 178 |
-
]);
|
| 179 |
-
|
| 180 |
-
// Clear timeout if successful
|
| 181 |
-
if (timeoutId) clearTimeout(timeoutId);
|
| 182 |
|
| 183 |
-
// Explicitly close the writer after successful completion
|
| 184 |
await writer.close();
|
| 185 |
} catch (error: any) {
|
| 186 |
-
|
| 187 |
-
if (timeoutId) clearTimeout(timeoutId);
|
| 188 |
-
|
| 189 |
-
if (isTimedOut || error.message?.includes('timeout') || error.message?.includes('Request timeout')) {
|
| 190 |
-
await writer.write(
|
| 191 |
-
encoder.encode(
|
| 192 |
-
JSON.stringify({
|
| 193 |
-
ok: false,
|
| 194 |
-
message: "Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.",
|
| 195 |
-
})
|
| 196 |
-
)
|
| 197 |
-
);
|
| 198 |
-
} else if (error.message?.includes("exceeded your monthly included credits")) {
|
| 199 |
await writer.write(
|
| 200 |
encoder.encode(
|
| 201 |
JSON.stringify({
|
|
@@ -259,11 +210,9 @@ export async function PUT(request: NextRequest) {
|
|
| 259 |
const authHeaders = await headers();
|
| 260 |
|
| 261 |
const body = await request.json();
|
| 262 |
-
const { prompt, provider, selectedElementHtml, model, pages, files, repoId
|
| 263 |
body;
|
| 264 |
|
| 265 |
-
let repoId = repoIdFromBody;
|
| 266 |
-
|
| 267 |
if (!prompt || pages.length === 0) {
|
| 268 |
return NextResponse.json(
|
| 269 |
{ ok: false, error: "Missing required fields" },
|
|
@@ -314,453 +263,133 @@ export async function PUT(request: NextRequest) {
|
|
| 314 |
billTo = "huggingface";
|
| 315 |
}
|
| 316 |
|
| 317 |
-
const
|
| 318 |
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
|
|
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
};
|
| 331 |
|
| 332 |
-
|
|
|
|
|
|
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
const userContext = "You are modifying the HTML file based on the user's request.";
|
| 337 |
-
|
| 338 |
-
const allPages = pages || [];
|
| 339 |
-
const pagesContext = allPages
|
| 340 |
-
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
| 341 |
-
.join("\n\n");
|
| 342 |
-
|
| 343 |
-
const assistantContext = `${
|
| 344 |
-
selectedElementHtml
|
| 345 |
-
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
|
| 346 |
-
: ""
|
| 347 |
-
}. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
| 348 |
-
|
| 349 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
| 350 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
| 351 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 352 |
-
|
| 353 |
-
const chatCompletion = client.chatCompletionStream(
|
| 354 |
-
{
|
| 355 |
-
model: selectedModel.value,
|
| 356 |
-
provider: selectedProvider.provider,
|
| 357 |
-
messages: [
|
| 358 |
-
{
|
| 359 |
-
role: "system",
|
| 360 |
-
content: systemPrompt,
|
| 361 |
-
},
|
| 362 |
-
{
|
| 363 |
-
role: "user",
|
| 364 |
-
content: userContext,
|
| 365 |
-
},
|
| 366 |
-
{
|
| 367 |
-
role: "assistant",
|
| 368 |
-
content: assistantContext,
|
| 369 |
-
},
|
| 370 |
-
{
|
| 371 |
-
role: "user",
|
| 372 |
-
content: prompt,
|
| 373 |
-
},
|
| 374 |
-
],
|
| 375 |
-
...providerConfig,
|
| 376 |
-
},
|
| 377 |
-
billTo ? { billTo } : {}
|
| 378 |
-
);
|
| 379 |
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
}, REQUEST_TIMEOUT);
|
| 390 |
-
});
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
(
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
if (done) {
|
| 398 |
-
break;
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
const deltaContent = value.choices[0]?.delta?.content;
|
| 402 |
-
if (deltaContent) {
|
| 403 |
-
chunk += deltaContent;
|
| 404 |
-
}
|
| 405 |
-
}
|
| 406 |
-
})(),
|
| 407 |
-
timeoutPromise
|
| 408 |
-
]);
|
| 409 |
-
|
| 410 |
-
// Clear timeout if successful
|
| 411 |
-
if (timeoutId) clearTimeout(timeoutId);
|
| 412 |
-
} catch (timeoutError: any) {
|
| 413 |
-
console.error("++TIMEOUT ERROR++", timeoutError);
|
| 414 |
-
console.error("++TIMEOUT ERROR MESSAGE++", timeoutError.message);
|
| 415 |
-
// Clear timeout on error
|
| 416 |
-
if (timeoutId) clearTimeout(timeoutId);
|
| 417 |
-
|
| 418 |
-
if (isTimedOut || timeoutError.message?.includes('timeout') || timeoutError.message?.includes('Request timeout')) {
|
| 419 |
-
return NextResponse.json(
|
| 420 |
{
|
| 421 |
-
|
| 422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
},
|
| 424 |
-
{
|
| 425 |
);
|
| 426 |
-
}
|
| 427 |
-
throw timeoutError;
|
| 428 |
-
}
|
| 429 |
-
if (!chunk) {
|
| 430 |
-
return NextResponse.json(
|
| 431 |
-
{ ok: false, message: "No content returned from the model" },
|
| 432 |
-
{ status: 400 }
|
| 433 |
-
);
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
if (chunk) {
|
| 437 |
-
const updatedLines: number[][] = [];
|
| 438 |
-
let newHtml = "";
|
| 439 |
-
const updatedPages = [...(pages || [])];
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
const pageIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 448 |
-
if (pageIndex !== -1) {
|
| 449 |
-
let pageHtml = updatedPages[pageIndex].html;
|
| 450 |
-
|
| 451 |
-
let processedContent = fileContent;
|
| 452 |
-
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 453 |
-
if (htmlMatch) {
|
| 454 |
-
processedContent = htmlMatch[1];
|
| 455 |
-
}
|
| 456 |
-
let position = 0;
|
| 457 |
-
let moreBlocks = true;
|
| 458 |
-
|
| 459 |
-
while (moreBlocks) {
|
| 460 |
-
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
| 461 |
-
if (searchStartIndex === -1) {
|
| 462 |
-
moreBlocks = false;
|
| 463 |
-
continue;
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
| 467 |
-
if (dividerIndex === -1) {
|
| 468 |
-
moreBlocks = false;
|
| 469 |
-
continue;
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
| 473 |
-
if (replaceEndIndex === -1) {
|
| 474 |
-
moreBlocks = false;
|
| 475 |
-
continue;
|
| 476 |
-
}
|
| 477 |
-
|
| 478 |
-
const searchBlock = processedContent.substring(
|
| 479 |
-
searchStartIndex + SEARCH_START.length,
|
| 480 |
-
dividerIndex
|
| 481 |
-
);
|
| 482 |
-
const replaceBlock = processedContent.substring(
|
| 483 |
-
dividerIndex + DIVIDER.length,
|
| 484 |
-
replaceEndIndex
|
| 485 |
-
);
|
| 486 |
-
|
| 487 |
-
if (searchBlock.trim() === "") {
|
| 488 |
-
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
| 489 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 490 |
-
} else {
|
| 491 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 492 |
-
const match = regex.exec(pageHtml);
|
| 493 |
-
|
| 494 |
-
if (match) {
|
| 495 |
-
const matchedText = match[0];
|
| 496 |
-
const beforeText = pageHtml.substring(0, match.index);
|
| 497 |
-
const startLineNumber = beforeText.split("\n").length;
|
| 498 |
-
const replaceLines = replaceBlock.split("\n").length;
|
| 499 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 500 |
-
|
| 501 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
| 502 |
-
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
| 503 |
-
}
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
position = replaceEndIndex + REPLACE_END.length;
|
| 507 |
}
|
| 508 |
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
newHtml = pageHtml;
|
| 513 |
}
|
| 514 |
}
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
const newFileRegex = new RegExp(`${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
|
| 518 |
-
let newFileMatch;
|
| 519 |
-
|
| 520 |
-
while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
|
| 521 |
-
const [, filePath, fileContent] = newFileMatch;
|
| 522 |
-
|
| 523 |
-
let fileData = fileContent;
|
| 524 |
-
// Try to extract content from code blocks
|
| 525 |
-
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 526 |
-
const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
|
| 527 |
-
const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
|
| 528 |
-
|
| 529 |
-
if (htmlMatch) {
|
| 530 |
-
fileData = htmlMatch[1];
|
| 531 |
-
} else if (cssMatch) {
|
| 532 |
-
fileData = cssMatch[1];
|
| 533 |
-
} else if (jsMatch) {
|
| 534 |
-
fileData = jsMatch[1];
|
| 535 |
-
}
|
| 536 |
-
|
| 537 |
-
const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 538 |
-
|
| 539 |
-
if (existingFileIndex !== -1) {
|
| 540 |
-
updatedPages[existingFileIndex] = {
|
| 541 |
-
path: filePath,
|
| 542 |
-
html: fileData.trim()
|
| 543 |
-
};
|
| 544 |
-
} else {
|
| 545 |
-
updatedPages.push({
|
| 546 |
-
path: filePath,
|
| 547 |
-
html: fileData.trim()
|
| 548 |
-
});
|
| 549 |
-
}
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_FILE_START)) {
|
| 553 |
-
let position = 0;
|
| 554 |
-
let moreBlocks = true;
|
| 555 |
-
|
| 556 |
-
while (moreBlocks) {
|
| 557 |
-
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 558 |
-
if (searchStartIndex === -1) {
|
| 559 |
-
moreBlocks = false;
|
| 560 |
-
continue;
|
| 561 |
-
}
|
| 562 |
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
| 578 |
);
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
);
|
| 583 |
-
|
| 584 |
-
if (searchBlock.trim() === "") {
|
| 585 |
-
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 586 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 587 |
-
} else {
|
| 588 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 589 |
-
const match = regex.exec(newHtml);
|
| 590 |
-
|
| 591 |
-
if (match) {
|
| 592 |
-
const matchedText = match[0];
|
| 593 |
-
const beforeText = newHtml.substring(0, match.index);
|
| 594 |
-
const startLineNumber = beforeText.split("\n").length;
|
| 595 |
-
const replaceLines = replaceBlock.split("\n").length;
|
| 596 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 597 |
-
|
| 598 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
| 599 |
-
newHtml = newHtml.replace(matchedText, replaceBlock);
|
| 600 |
-
}
|
| 601 |
-
}
|
| 602 |
-
|
| 603 |
-
position = replaceEndIndex + REPLACE_END.length;
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 607 |
-
if (mainPageIndex !== -1) {
|
| 608 |
-
updatedPages[mainPageIndex].html = newHtml;
|
| 609 |
-
}
|
| 610 |
-
}
|
| 611 |
-
|
| 612 |
-
const files: File[] = [];
|
| 613 |
-
updatedPages.forEach((page: Page) => {
|
| 614 |
-
let mimeType = "text/html";
|
| 615 |
-
if (page.path.endsWith(".css")) {
|
| 616 |
-
mimeType = "text/css";
|
| 617 |
-
} else if (page.path.endsWith(".js")) {
|
| 618 |
-
mimeType = "text/javascript";
|
| 619 |
-
} else if (page.path.endsWith(".json")) {
|
| 620 |
-
mimeType = "application/json";
|
| 621 |
}
|
| 622 |
-
|
| 623 |
-
? injectDeepSiteBadge(page.html)
|
| 624 |
-
: page.html;
|
| 625 |
-
const file = new File([content], page.path, { type: mimeType });
|
| 626 |
-
files.push(file);
|
| 627 |
-
});
|
| 628 |
-
|
| 629 |
-
if (isNew) {
|
| 630 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 631 |
-
const formattedTitle = projectName?.toLowerCase()
|
| 632 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 633 |
-
.split("-")
|
| 634 |
-
.filter(Boolean)
|
| 635 |
-
.join("-")
|
| 636 |
-
.slice(0, 96);
|
| 637 |
-
const repo: RepoDesignation = {
|
| 638 |
-
type: "space",
|
| 639 |
-
name: `${user.name}/${formattedTitle}`,
|
| 640 |
-
};
|
| 641 |
-
|
| 642 |
try {
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
});
|
| 647 |
-
repoId = repoUrl.split("/").slice(-2).join("/");
|
| 648 |
-
} catch (createRepoError: any) {
|
| 649 |
-
console.error("++CREATE REPO ERROR++", createRepoError);
|
| 650 |
-
throw new Error(`Failed to create repository: ${createRepoError.message || 'Unknown error'}`);
|
| 651 |
}
|
| 652 |
-
|
| 653 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 654 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 655 |
-
const README = `---
|
| 656 |
-
title: ${projectName}
|
| 657 |
-
colorFrom: ${colorFrom}
|
| 658 |
-
colorTo: ${colorTo}
|
| 659 |
-
emoji: 🐳
|
| 660 |
-
sdk: static
|
| 661 |
-
pinned: false
|
| 662 |
-
tags:
|
| 663 |
-
- deepsite-v3
|
| 664 |
-
---
|
| 665 |
-
|
| 666 |
-
# Welcome to your new DeepSite project!
|
| 667 |
-
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 668 |
-
`;
|
| 669 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 670 |
-
}
|
| 671 |
-
|
| 672 |
-
let response;
|
| 673 |
-
try {
|
| 674 |
-
// Add a timeout wrapper for the upload
|
| 675 |
-
const uploadPromise = uploadFiles({
|
| 676 |
-
repo: {
|
| 677 |
-
type: "space",
|
| 678 |
-
name: repoId,
|
| 679 |
-
},
|
| 680 |
-
files,
|
| 681 |
-
commitTitle: prompt,
|
| 682 |
-
accessToken: user.token as string,
|
| 683 |
-
});
|
| 684 |
-
|
| 685 |
-
const uploadTimeout = new Promise<never>((_, reject) => {
|
| 686 |
-
setTimeout(() => {
|
| 687 |
-
reject(new Error('Upload operation timed out'));
|
| 688 |
-
}, 180000); // 3 minutes timeout for upload
|
| 689 |
-
});
|
| 690 |
-
|
| 691 |
-
response = await Promise.race([uploadPromise, uploadTimeout]);
|
| 692 |
-
} catch (uploadError: any) {
|
| 693 |
-
console.error("++UPLOAD FILES ERROR++", uploadError);
|
| 694 |
-
console.error("++UPLOAD FILES ERROR MESSAGE++", uploadError.message);
|
| 695 |
-
console.error("++REPO ID++", repoId);
|
| 696 |
-
|
| 697 |
-
// If it's a timeout, files might have been uploaded but we didn't get response
|
| 698 |
-
if (uploadError.message?.includes('timed out') || uploadError.message?.includes('timeout')) {
|
| 699 |
-
console.warn("++UPLOAD TIMEOUT - Files may have been uploaded++");
|
| 700 |
-
// Return a partial success response
|
| 701 |
-
return NextResponse.json({
|
| 702 |
-
ok: true,
|
| 703 |
-
updatedLines,
|
| 704 |
-
pages: updatedPages,
|
| 705 |
-
repoId,
|
| 706 |
-
commit: {
|
| 707 |
-
title: prompt,
|
| 708 |
-
oid: 'timeout',
|
| 709 |
-
timedOut: true,
|
| 710 |
-
}
|
| 711 |
-
});
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
throw new Error(`Failed to upload files to repository: ${uploadError.message || 'Unknown error'}`);
|
| 715 |
-
}
|
| 716 |
-
const responseData: any = {
|
| 717 |
-
ok: true,
|
| 718 |
-
updatedLines,
|
| 719 |
-
pages: updatedPages,
|
| 720 |
-
repoId,
|
| 721 |
-
};
|
| 722 |
-
|
| 723 |
-
if (response && response.commit) {
|
| 724 |
-
responseData.commit = {
|
| 725 |
-
...response.commit,
|
| 726 |
-
title: prompt,
|
| 727 |
-
};
|
| 728 |
-
} else {
|
| 729 |
-
responseData.commit = {
|
| 730 |
-
title: prompt,
|
| 731 |
-
oid: 'unknown',
|
| 732 |
-
};
|
| 733 |
}
|
|
|
|
| 734 |
|
| 735 |
-
|
| 736 |
-
} else {
|
| 737 |
-
return NextResponse.json(
|
| 738 |
-
{ ok: false, message: "No content returned from the model" },
|
| 739 |
-
{ status: 400 }
|
| 740 |
-
);
|
| 741 |
-
}
|
| 742 |
} catch (error: any) {
|
| 743 |
-
console.error("++ERROR++", error);
|
| 744 |
-
console.error("++ERROR MESSAGE++", error.message);
|
| 745 |
-
if (error.message?.includes('timeout') || error.message?.includes('Request timeout')) {
|
| 746 |
-
return NextResponse.json(
|
| 747 |
-
{
|
| 748 |
-
ok: false,
|
| 749 |
-
message: "Request timeout: The operation took too long to complete. Please try again with a simpler request or try a different model.",
|
| 750 |
-
},
|
| 751 |
-
{ status: 504 }
|
| 752 |
-
);
|
| 753 |
-
}
|
| 754 |
-
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 755 |
-
return NextResponse.json(
|
| 756 |
-
{
|
| 757 |
-
ok: false,
|
| 758 |
-
openProModal: true,
|
| 759 |
-
message: error.message,
|
| 760 |
-
},
|
| 761 |
-
{ status: 402 }
|
| 762 |
-
);
|
| 763 |
-
}
|
| 764 |
return NextResponse.json(
|
| 765 |
{
|
| 766 |
ok: false,
|
|
|
|
| 6 |
|
| 7 |
import { MODELS } from "@/lib/providers";
|
| 8 |
import {
|
|
|
|
| 9 |
FOLLOW_UP_SYSTEM_PROMPT,
|
| 10 |
INITIAL_SYSTEM_PROMPT,
|
| 11 |
MAX_REQUESTS_PER_IP,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
PROMPT_FOR_PROJECT_NAME,
|
| 13 |
} from "@/lib/prompts";
|
| 14 |
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
| 15 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 16 |
import { Page } from "@/types";
|
|
|
|
| 17 |
import { isAuthenticated } from "@/lib/auth";
|
| 18 |
import { getBestProvider } from "@/lib/best-provider";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
const ipAddresses = new Map();
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
export async function POST(request: NextRequest) {
|
| 23 |
const authHeaders = await headers();
|
| 24 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
|
|
|
| 101 |
|
| 102 |
(async () => {
|
| 103 |
// let completeResponse = "";
|
|
|
|
|
|
|
|
|
|
| 104 |
try {
|
| 105 |
const client = new InferenceClient(token);
|
| 106 |
|
|
|
|
| 132 |
billTo ? { billTo } : {}
|
| 133 |
);
|
| 134 |
|
| 135 |
+
while (true) {
|
| 136 |
+
const { done, value } = await chatCompletion.next()
|
| 137 |
+
if (done) {
|
| 138 |
+
break;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const chunk = value.choices[0]?.delta?.content;
|
| 142 |
+
if (chunk) {
|
| 143 |
+
await writer.write(encoder.encode(chunk));
|
| 144 |
+
}
|
| 145 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
|
|
|
| 147 |
await writer.close();
|
| 148 |
} catch (error: any) {
|
| 149 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
await writer.write(
|
| 151 |
encoder.encode(
|
| 152 |
JSON.stringify({
|
|
|
|
| 210 |
const authHeaders = await headers();
|
| 211 |
|
| 212 |
const body = await request.json();
|
| 213 |
+
const { prompt, provider, selectedElementHtml, model, pages, files, repoId, isNew } =
|
| 214 |
body;
|
| 215 |
|
|
|
|
|
|
|
| 216 |
if (!prompt || pages.length === 0) {
|
| 217 |
return NextResponse.json(
|
| 218 |
{ ok: false, error: "Missing required fields" },
|
|
|
|
| 263 |
billTo = "huggingface";
|
| 264 |
}
|
| 265 |
|
| 266 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider);
|
| 267 |
|
| 268 |
+
try {
|
| 269 |
+
const encoder = new TextEncoder();
|
| 270 |
+
const stream = new TransformStream();
|
| 271 |
+
const writer = stream.writable.getWriter();
|
| 272 |
|
| 273 |
+
const response = new NextResponse(stream.readable, {
|
| 274 |
+
headers: {
|
| 275 |
+
"Content-Type": "text/plain; charset=utf-8",
|
| 276 |
+
"Cache-Control": "no-cache",
|
| 277 |
+
Connection: "keep-alive",
|
| 278 |
+
},
|
| 279 |
+
});
|
|
|
|
| 280 |
|
| 281 |
+
(async () => {
|
| 282 |
+
try {
|
| 283 |
+
const client = new InferenceClient(token);
|
| 284 |
|
| 285 |
+
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
| 286 |
+
const userContext = "You are modifying the HTML file based on the user's request.";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
+
const allPages = pages || [];
|
| 289 |
+
const pagesContext = allPages
|
| 290 |
+
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
| 291 |
+
.join("\n\n");
|
| 292 |
|
| 293 |
+
const assistantContext = `${selectedElementHtml
|
| 294 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
|
| 295 |
+
: ""
|
| 296 |
+
}. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
|
|
|
|
|
|
| 297 |
|
| 298 |
+
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
| 299 |
+
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
| 300 |
+
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 301 |
+
|
| 302 |
+
const chatCompletion = client.chatCompletionStream(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
{
|
| 304 |
+
model: selectedModel.value,
|
| 305 |
+
provider: selectedProvider.provider,
|
| 306 |
+
messages: [
|
| 307 |
+
{
|
| 308 |
+
role: "system",
|
| 309 |
+
content: systemPrompt,
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
role: "user",
|
| 313 |
+
content: userContext,
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
role: "assistant",
|
| 317 |
+
content: assistantContext,
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
role: "user",
|
| 321 |
+
content: prompt,
|
| 322 |
+
},
|
| 323 |
+
],
|
| 324 |
+
...providerConfig,
|
| 325 |
},
|
| 326 |
+
billTo ? { billTo } : {}
|
| 327 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
+
// Stream the response chunks to the client
|
| 330 |
+
while (true) {
|
| 331 |
+
const { done, value } = await chatCompletion.next();
|
| 332 |
+
if (done) {
|
| 333 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
}
|
| 335 |
|
| 336 |
+
const chunk = value.choices[0]?.delta?.content;
|
| 337 |
+
if (chunk) {
|
| 338 |
+
await writer.write(encoder.encode(chunk));
|
|
|
|
| 339 |
}
|
| 340 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
+
await writer.write(encoder.encode(`\n___METADATA_START___\n${JSON.stringify({
|
| 343 |
+
repoId,
|
| 344 |
+
isNew,
|
| 345 |
+
userName: user.name,
|
| 346 |
+
})}\n___METADATA_END___\n`));
|
| 347 |
|
| 348 |
+
await writer.close();
|
| 349 |
+
} catch (error: any) {
|
| 350 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 351 |
+
await writer.write(
|
| 352 |
+
encoder.encode(
|
| 353 |
+
JSON.stringify({
|
| 354 |
+
ok: false,
|
| 355 |
+
openProModal: true,
|
| 356 |
+
message: error.message,
|
| 357 |
+
})
|
| 358 |
+
)
|
| 359 |
);
|
| 360 |
+
} else if (error?.message?.includes("inference provider information")) {
|
| 361 |
+
await writer.write(
|
| 362 |
+
encoder.encode(
|
| 363 |
+
JSON.stringify({
|
| 364 |
+
ok: false,
|
| 365 |
+
openSelectProvider: true,
|
| 366 |
+
message: error.message,
|
| 367 |
+
})
|
| 368 |
+
)
|
| 369 |
+
);
|
| 370 |
+
} else {
|
| 371 |
+
await writer.write(
|
| 372 |
+
encoder.encode(
|
| 373 |
+
JSON.stringify({
|
| 374 |
+
ok: false,
|
| 375 |
+
message:
|
| 376 |
+
error.message ||
|
| 377 |
+
"An error occurred while processing your request.",
|
| 378 |
+
})
|
| 379 |
+
)
|
| 380 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
+
} finally {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
try {
|
| 384 |
+
await writer?.close();
|
| 385 |
+
} catch {
|
| 386 |
+
// ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
}
|
| 389 |
+
})();
|
| 390 |
|
| 391 |
+
return response;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
} catch (error: any) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
return NextResponse.json(
|
| 394 |
{
|
| 395 |
ok: false,
|
app/api/me/projects/[namespace]/[repoId]/update/route.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
+
|
| 4 |
+
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
+
import { Page } from "@/types";
|
| 6 |
+
import { COLORS } from "@/lib/utils";
|
| 7 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 8 |
+
import { pagesToFiles } from "@/lib/format-ai-response";
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* UPDATE route - for updating existing projects or creating new ones after AI streaming
|
| 12 |
+
* This route handles the HuggingFace upload after client-side AI response processing
|
| 13 |
+
*/
|
| 14 |
+
export async function PUT(
|
| 15 |
+
req: NextRequest,
|
| 16 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 17 |
+
) {
|
| 18 |
+
const user = await isAuthenticated();
|
| 19 |
+
if (user instanceof NextResponse || !user) {
|
| 20 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const param = await params;
|
| 24 |
+
let { namespace, repoId } = param;
|
| 25 |
+
const { pages, commitTitle = "AI-generated changes", isNew, projectName } = await req.json();
|
| 26 |
+
|
| 27 |
+
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 28 |
+
return NextResponse.json(
|
| 29 |
+
{ ok: false, error: "Pages are required" },
|
| 30 |
+
{ status: 400 }
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
let files: File[];
|
| 36 |
+
|
| 37 |
+
if (isNew) {
|
| 38 |
+
// Creating a new project
|
| 39 |
+
const title = projectName || "DeepSite Project";
|
| 40 |
+
const formattedTitle = title
|
| 41 |
+
.toLowerCase()
|
| 42 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 43 |
+
.split("-")
|
| 44 |
+
.filter(Boolean)
|
| 45 |
+
.join("-")
|
| 46 |
+
.slice(0, 96);
|
| 47 |
+
|
| 48 |
+
const repo: RepoDesignation = {
|
| 49 |
+
type: "space",
|
| 50 |
+
name: `${user.name}/${formattedTitle}`,
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
try {
|
| 54 |
+
const { repoUrl } = await createRepo({
|
| 55 |
+
repo,
|
| 56 |
+
accessToken: user.token as string,
|
| 57 |
+
});
|
| 58 |
+
namespace = user.name;
|
| 59 |
+
repoId = repoUrl.split("/").slice(-2).join("/").split("/")[1];
|
| 60 |
+
} catch (createRepoError: any) {
|
| 61 |
+
return NextResponse.json(
|
| 62 |
+
{
|
| 63 |
+
ok: false,
|
| 64 |
+
error: `Failed to create repository: ${createRepoError.message || 'Unknown error'}`,
|
| 65 |
+
},
|
| 66 |
+
{ status: 500 }
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Prepare files with badge injection for new projects
|
| 71 |
+
files = [];
|
| 72 |
+
pages.forEach((page: Page) => {
|
| 73 |
+
let mimeType = "text/html";
|
| 74 |
+
if (page.path.endsWith(".css")) {
|
| 75 |
+
mimeType = "text/css";
|
| 76 |
+
} else if (page.path.endsWith(".js")) {
|
| 77 |
+
mimeType = "text/javascript";
|
| 78 |
+
} else if (page.path.endsWith(".json")) {
|
| 79 |
+
mimeType = "application/json";
|
| 80 |
+
}
|
| 81 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 82 |
+
? injectDeepSiteBadge(page.html)
|
| 83 |
+
: page.html;
|
| 84 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 85 |
+
files.push(file);
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
// Add README.md for new projects
|
| 89 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 90 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 91 |
+
const README = `---
|
| 92 |
+
title: ${title}
|
| 93 |
+
colorFrom: ${colorFrom}
|
| 94 |
+
colorTo: ${colorTo}
|
| 95 |
+
emoji: 🐳
|
| 96 |
+
sdk: static
|
| 97 |
+
pinned: false
|
| 98 |
+
tags:
|
| 99 |
+
- deepsite-v3
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
# Welcome to your new DeepSite project!
|
| 103 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 104 |
+
`;
|
| 105 |
+
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 106 |
+
} else {
|
| 107 |
+
// Updating existing project - no badge injection
|
| 108 |
+
files = pagesToFiles(pages);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
const response = await uploadFiles({
|
| 112 |
+
repo: {
|
| 113 |
+
type: "space",
|
| 114 |
+
name: `${namespace}/${repoId}`,
|
| 115 |
+
},
|
| 116 |
+
files,
|
| 117 |
+
commitTitle,
|
| 118 |
+
accessToken: user.token as string,
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
return NextResponse.json({
|
| 122 |
+
ok: true,
|
| 123 |
+
pages,
|
| 124 |
+
repoId: `${namespace}/${repoId}`,
|
| 125 |
+
commit: {
|
| 126 |
+
...response.commit,
|
| 127 |
+
title: commitTitle,
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
} catch (error: any) {
|
| 131 |
+
console.error("Error updating project:", error);
|
| 132 |
+
return NextResponse.json(
|
| 133 |
+
{
|
| 134 |
+
ok: false,
|
| 135 |
+
error: error.message || "Failed to update project",
|
| 136 |
+
},
|
| 137 |
+
{ status: 500 }
|
| 138 |
+
);
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
components/editor/ask-ai/index.tsx
CHANGED
|
@@ -100,7 +100,8 @@ export const AskAi = ({
|
|
| 100 |
prompt,
|
| 101 |
enhancedSettings,
|
| 102 |
redesignMarkdown,
|
| 103 |
-
!!user
|
|
|
|
| 104 |
);
|
| 105 |
|
| 106 |
if (result?.error) {
|
|
|
|
| 100 |
prompt,
|
| 101 |
enhancedSettings,
|
| 102 |
redesignMarkdown,
|
| 103 |
+
!!user,
|
| 104 |
+
user?.name
|
| 105 |
);
|
| 106 |
|
| 107 |
if (result?.error) {
|
components/editor/preview/index.tsx
CHANGED
|
@@ -72,7 +72,6 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 72 |
(html: string): string => {
|
| 73 |
if (!html) return html;
|
| 74 |
|
| 75 |
-
// Find all CSS and JS files (including those in subdirectories)
|
| 76 |
const cssFiles = pages.filter(
|
| 77 |
(p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
|
| 78 |
);
|
|
@@ -102,11 +101,9 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 102 |
`<head>\n${allCssContent}`
|
| 103 |
);
|
| 104 |
} else {
|
| 105 |
-
// If no head tag, prepend to document
|
| 106 |
modifiedHtml = allCssContent + "\n" + modifiedHtml;
|
| 107 |
}
|
| 108 |
|
| 109 |
-
// Remove all link tags that reference CSS files we're injecting
|
| 110 |
cssFiles.forEach((file) => {
|
| 111 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 112 |
modifiedHtml = modifiedHtml.replace(
|
|
@@ -119,7 +116,6 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 119 |
});
|
| 120 |
}
|
| 121 |
|
| 122 |
-
// Inject all JS files
|
| 123 |
if (jsFiles.length > 0) {
|
| 124 |
const allJsContent = jsFiles
|
| 125 |
.map(
|
|
@@ -136,11 +132,9 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 136 |
} else if (modifiedHtml.includes("<body>")) {
|
| 137 |
modifiedHtml = modifiedHtml + allJsContent;
|
| 138 |
} else {
|
| 139 |
-
// If no body tag, append to document
|
| 140 |
modifiedHtml = modifiedHtml + "\n" + allJsContent;
|
| 141 |
}
|
| 142 |
|
| 143 |
-
// Remove all script tags that reference JS files we're injecting
|
| 144 |
jsFiles.forEach((file) => {
|
| 145 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 146 |
modifiedHtml = modifiedHtml.replace(
|
|
@@ -214,7 +208,6 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 214 |
if (iframeRef?.current?.contentDocument) {
|
| 215 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 216 |
|
| 217 |
-
// Use event delegation to catch clicks on anchors in both light and shadow DOM
|
| 218 |
iframeDocument.addEventListener(
|
| 219 |
"click",
|
| 220 |
handleCustomNavigation as any,
|
|
@@ -323,18 +316,11 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 323 |
const path = event.composedPath();
|
| 324 |
const targetElement = path[0] as HTMLElement;
|
| 325 |
|
| 326 |
-
console.log(
|
| 327 |
-
"[handleClick] Target element:",
|
| 328 |
-
targetElement.tagName,
|
| 329 |
-
targetElement
|
| 330 |
-
);
|
| 331 |
-
|
| 332 |
const findClosestAnchor = (
|
| 333 |
element: HTMLElement
|
| 334 |
): HTMLAnchorElement | null => {
|
| 335 |
let current: HTMLElement | null = element;
|
| 336 |
while (current) {
|
| 337 |
-
console.log("[handleClick] Checking element:", current.tagName);
|
| 338 |
if (current.tagName?.toUpperCase() === "A") {
|
| 339 |
return current as HTMLAnchorElement;
|
| 340 |
}
|
|
@@ -342,13 +328,9 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 342 |
break;
|
| 343 |
}
|
| 344 |
const parent: Node | null = current.parentNode;
|
| 345 |
-
// Use nodeType to check - works across iframe boundaries
|
| 346 |
-
// nodeType 1 = Element, nodeType 11 = DocumentFragment (including ShadowRoot)
|
| 347 |
if (parent && parent.nodeType === 11) {
|
| 348 |
-
// ShadowRoot
|
| 349 |
current = (parent as ShadowRoot).host as HTMLElement;
|
| 350 |
} else if (parent && parent.nodeType === 1) {
|
| 351 |
-
// Element node
|
| 352 |
current = parent as HTMLElement;
|
| 353 |
} else {
|
| 354 |
break;
|
|
@@ -359,8 +341,6 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 359 |
|
| 360 |
const anchorElement = findClosestAnchor(targetElement);
|
| 361 |
|
| 362 |
-
console.log("[handleClick] Found anchor:", anchorElement);
|
| 363 |
-
|
| 364 |
if (anchorElement) {
|
| 365 |
return;
|
| 366 |
}
|
|
@@ -379,49 +359,23 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 379 |
const path = event.composedPath();
|
| 380 |
const actualTarget = path[0] as HTMLElement;
|
| 381 |
|
| 382 |
-
console.log(
|
| 383 |
-
"[handleCustomNavigation] Click detected in iframe:",
|
| 384 |
-
actualTarget.tagName,
|
| 385 |
-
actualTarget
|
| 386 |
-
);
|
| 387 |
-
|
| 388 |
const findClosestAnchor = (
|
| 389 |
element: HTMLElement
|
| 390 |
): HTMLAnchorElement | null => {
|
| 391 |
let current: HTMLElement | null = element;
|
| 392 |
while (current) {
|
| 393 |
-
console.log(
|
| 394 |
-
"[handleCustomNavigation] Checking element:",
|
| 395 |
-
current.tagName,
|
| 396 |
-
current
|
| 397 |
-
);
|
| 398 |
if (current.tagName?.toUpperCase() === "A") {
|
| 399 |
-
console.log("[handleCustomNavigation] Found anchor!", current);
|
| 400 |
return current as HTMLAnchorElement;
|
| 401 |
}
|
| 402 |
if (current === iframeDocument.body) {
|
| 403 |
-
console.log("[handleCustomNavigation] Reached body, stopping");
|
| 404 |
break;
|
| 405 |
}
|
| 406 |
const parent: Node | null = current.parentNode;
|
| 407 |
-
console.log(
|
| 408 |
-
"[handleCustomNavigation] Parent node:",
|
| 409 |
-
parent,
|
| 410 |
-
"nodeType:",
|
| 411 |
-
parent?.nodeType
|
| 412 |
-
);
|
| 413 |
-
// Use nodeType to check - works across iframe boundaries
|
| 414 |
-
// nodeType 1 = Element, nodeType 11 = DocumentFragment (including ShadowRoot)
|
| 415 |
if (parent && parent.nodeType === 11) {
|
| 416 |
-
// ShadowRoot
|
| 417 |
current = (parent as ShadowRoot).host as HTMLElement;
|
| 418 |
} else if (parent && parent.nodeType === 1) {
|
| 419 |
-
// Element node
|
| 420 |
current = parent as HTMLElement;
|
| 421 |
} else {
|
| 422 |
-
console.log(
|
| 423 |
-
"[handleCustomNavigation] Parent is not an element node, breaking"
|
| 424 |
-
);
|
| 425 |
break;
|
| 426 |
}
|
| 427 |
}
|
|
@@ -429,7 +383,6 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 429 |
};
|
| 430 |
|
| 431 |
const anchorElement = findClosestAnchor(actualTarget);
|
| 432 |
-
console.log("[handleCustomNavigation] Anchor element:", anchorElement);
|
| 433 |
if (anchorElement) {
|
| 434 |
let href = anchorElement.getAttribute("href");
|
| 435 |
if (href) {
|
|
@@ -573,7 +526,6 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 573 |
}
|
| 574 |
);
|
| 575 |
}
|
| 576 |
-
// Set up event listeners after iframe loads
|
| 577 |
setupIframeListeners();
|
| 578 |
}
|
| 579 |
: undefined
|
|
@@ -583,7 +535,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 583 |
/>
|
| 584 |
{!isNew && (
|
| 585 |
<>
|
| 586 |
-
<div
|
| 587 |
className={classNames(
|
| 588 |
"w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",
|
| 589 |
{
|
|
@@ -601,7 +553,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 601 |
<AnimatedBlobs />
|
| 602 |
<AnimatedBlobs />
|
| 603 |
</div>
|
| 604 |
-
</div>
|
| 605 |
<HistoryNotification
|
| 606 |
isVisible={!!currentCommit}
|
| 607 |
isPromotingVersion={isPromotingVersion}
|
|
|
|
| 72 |
(html: string): string => {
|
| 73 |
if (!html) return html;
|
| 74 |
|
|
|
|
| 75 |
const cssFiles = pages.filter(
|
| 76 |
(p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
|
| 77 |
);
|
|
|
|
| 101 |
`<head>\n${allCssContent}`
|
| 102 |
);
|
| 103 |
} else {
|
|
|
|
| 104 |
modifiedHtml = allCssContent + "\n" + modifiedHtml;
|
| 105 |
}
|
| 106 |
|
|
|
|
| 107 |
cssFiles.forEach((file) => {
|
| 108 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 109 |
modifiedHtml = modifiedHtml.replace(
|
|
|
|
| 116 |
});
|
| 117 |
}
|
| 118 |
|
|
|
|
| 119 |
if (jsFiles.length > 0) {
|
| 120 |
const allJsContent = jsFiles
|
| 121 |
.map(
|
|
|
|
| 132 |
} else if (modifiedHtml.includes("<body>")) {
|
| 133 |
modifiedHtml = modifiedHtml + allJsContent;
|
| 134 |
} else {
|
|
|
|
| 135 |
modifiedHtml = modifiedHtml + "\n" + allJsContent;
|
| 136 |
}
|
| 137 |
|
|
|
|
| 138 |
jsFiles.forEach((file) => {
|
| 139 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 140 |
modifiedHtml = modifiedHtml.replace(
|
|
|
|
| 208 |
if (iframeRef?.current?.contentDocument) {
|
| 209 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 210 |
|
|
|
|
| 211 |
iframeDocument.addEventListener(
|
| 212 |
"click",
|
| 213 |
handleCustomNavigation as any,
|
|
|
|
| 316 |
const path = event.composedPath();
|
| 317 |
const targetElement = path[0] as HTMLElement;
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
const findClosestAnchor = (
|
| 320 |
element: HTMLElement
|
| 321 |
): HTMLAnchorElement | null => {
|
| 322 |
let current: HTMLElement | null = element;
|
| 323 |
while (current) {
|
|
|
|
| 324 |
if (current.tagName?.toUpperCase() === "A") {
|
| 325 |
return current as HTMLAnchorElement;
|
| 326 |
}
|
|
|
|
| 328 |
break;
|
| 329 |
}
|
| 330 |
const parent: Node | null = current.parentNode;
|
|
|
|
|
|
|
| 331 |
if (parent && parent.nodeType === 11) {
|
|
|
|
| 332 |
current = (parent as ShadowRoot).host as HTMLElement;
|
| 333 |
} else if (parent && parent.nodeType === 1) {
|
|
|
|
| 334 |
current = parent as HTMLElement;
|
| 335 |
} else {
|
| 336 |
break;
|
|
|
|
| 341 |
|
| 342 |
const anchorElement = findClosestAnchor(targetElement);
|
| 343 |
|
|
|
|
|
|
|
| 344 |
if (anchorElement) {
|
| 345 |
return;
|
| 346 |
}
|
|
|
|
| 359 |
const path = event.composedPath();
|
| 360 |
const actualTarget = path[0] as HTMLElement;
|
| 361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
const findClosestAnchor = (
|
| 363 |
element: HTMLElement
|
| 364 |
): HTMLAnchorElement | null => {
|
| 365 |
let current: HTMLElement | null = element;
|
| 366 |
while (current) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
if (current.tagName?.toUpperCase() === "A") {
|
|
|
|
| 368 |
return current as HTMLAnchorElement;
|
| 369 |
}
|
| 370 |
if (current === iframeDocument.body) {
|
|
|
|
| 371 |
break;
|
| 372 |
}
|
| 373 |
const parent: Node | null = current.parentNode;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
if (parent && parent.nodeType === 11) {
|
|
|
|
| 375 |
current = (parent as ShadowRoot).host as HTMLElement;
|
| 376 |
} else if (parent && parent.nodeType === 1) {
|
|
|
|
| 377 |
current = parent as HTMLElement;
|
| 378 |
} else {
|
|
|
|
|
|
|
|
|
|
| 379 |
break;
|
| 380 |
}
|
| 381 |
}
|
|
|
|
| 383 |
};
|
| 384 |
|
| 385 |
const anchorElement = findClosestAnchor(actualTarget);
|
|
|
|
| 386 |
if (anchorElement) {
|
| 387 |
let href = anchorElement.getAttribute("href");
|
| 388 |
if (href) {
|
|
|
|
| 526 |
}
|
| 527 |
);
|
| 528 |
}
|
|
|
|
| 529 |
setupIframeListeners();
|
| 530 |
}
|
| 531 |
: undefined
|
|
|
|
| 535 |
/>
|
| 536 |
{!isNew && (
|
| 537 |
<>
|
| 538 |
+
{/* <div
|
| 539 |
className={classNames(
|
| 540 |
"w-full h-full flex items-center justify-center absolute left-0 top-0 bg-black/40 backdrop-blur-lg transition-all duration-200",
|
| 541 |
{
|
|
|
|
| 553 |
<AnimatedBlobs />
|
| 554 |
<AnimatedBlobs />
|
| 555 |
</div>
|
| 556 |
+
</div> */}
|
| 557 |
<HistoryNotification
|
| 558 |
isVisible={!!currentCommit}
|
| 559 |
isPromotingVersion={isPromotingVersion}
|
hooks/useAi.ts
CHANGED
|
@@ -124,27 +124,37 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 124 |
client.setQueryData(["ai.model"], newModel);
|
| 125 |
};
|
| 126 |
|
| 127 |
-
const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean) => {
|
| 128 |
-
if (isLoggedIn) {
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
-
|
| 144 |
-
.catch((error) => {
|
| 145 |
setIsAiWorking(false);
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
} else {
|
| 149 |
setIsAiWorking(false);
|
| 150 |
toast.success("AI responded successfully");
|
|
@@ -152,7 +162,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 152 |
}
|
| 153 |
}
|
| 154 |
|
| 155 |
-
const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean) => {
|
| 156 |
if (isAiWorking) return;
|
| 157 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 158 |
|
|
@@ -218,7 +228,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 218 |
setPages(newPages);
|
| 219 |
setLastSavedPages([...newPages]);
|
| 220 |
if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
|
| 221 |
-
createNewProject(prompt, newPages, projectName, isLoggedIn);
|
| 222 |
}
|
| 223 |
setPrompts([...prompts, prompt]);
|
| 224 |
|
|
@@ -311,118 +321,162 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 311 |
});
|
| 312 |
|
| 313 |
if (request && request.body) {
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
if (isNew) {
|
| 322 |
-
toast.error("The request timed out. Your project may have been created. Please check your HuggingFace spaces.");
|
| 323 |
-
return { error: "gateway_timeout", message: "Request timed out after upload" };
|
| 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 |
-
} catch (textError) {
|
| 358 |
-
}
|
| 359 |
-
setIsAiWorking(false);
|
| 360 |
-
toast.error("Server returned invalid response. Check console for details.");
|
| 361 |
-
return { error: "invalid_response", message: "Server returned non-JSON response" };
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
if (!request.ok) {
|
| 365 |
-
if (res.openLogin) {
|
| 366 |
-
setIsAiWorking(false);
|
| 367 |
-
return { error: "login_required" };
|
| 368 |
-
} else if (res.openSelectProvider) {
|
| 369 |
-
setIsAiWorking(false);
|
| 370 |
-
return { error: "provider_required", message: res.message };
|
| 371 |
-
} else if (res.openProModal) {
|
| 372 |
-
setIsAiWorking(false);
|
| 373 |
-
return { error: "pro_required" };
|
| 374 |
-
} else {
|
| 375 |
-
setIsAiWorking(false);
|
| 376 |
-
return { error: "api_error", message: res.message };
|
| 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 |
-
setCommits([res.commit, ...commits]);
|
| 403 |
-
setPrompts(
|
| 404 |
-
[...prompts, prompt]
|
| 405 |
-
)
|
| 406 |
-
setSelectedElement(null);
|
| 407 |
-
setSelectedFiles([]);
|
| 408 |
-
// setContextFile(null); not needed yet, keep context for the next request.
|
| 409 |
-
setIsEditableModeEnabled(false);
|
| 410 |
-
setIsAiWorking(false);
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
if (audio.current) audio.current.play();
|
| 414 |
-
if (iframe) {
|
| 415 |
-
setTimeout(() => {
|
| 416 |
-
iframe.src = iframe.src;
|
| 417 |
-
}, 500);
|
| 418 |
-
}
|
| 419 |
|
| 420 |
-
return
|
| 421 |
}
|
| 422 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 423 |
} catch (error: any) {
|
| 424 |
setIsAiWorking(false);
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
if (error.openLogin) {
|
| 427 |
return { error: "login_required" };
|
| 428 |
}
|
|
@@ -470,7 +524,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 470 |
if (pages.length > 0) {
|
| 471 |
setPages(pages);
|
| 472 |
if (isStreaming) {
|
| 473 |
-
// Find new pages that haven't been shown yet (HTML, CSS, JS, etc.)
|
| 474 |
const newPages = pages.filter(p =>
|
| 475 |
!streamingPagesRef.current.has(p.path)
|
| 476 |
);
|
|
@@ -480,7 +533,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 480 |
setCurrentPage(newPage.path);
|
| 481 |
streamingPagesRef.current.add(newPage.path);
|
| 482 |
|
| 483 |
-
// Update preview if it's an HTML file not in components folder
|
| 484 |
if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
|
| 485 |
setPreviewPage(newPage.path);
|
| 486 |
}
|
|
@@ -500,41 +552,30 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 500 |
const extractFileContent = (chunk: string, filePath: string): string => {
|
| 501 |
if (!chunk) return "";
|
| 502 |
|
| 503 |
-
// Remove backticks first
|
| 504 |
let content = chunk.trim();
|
| 505 |
|
| 506 |
-
// Handle different file types
|
| 507 |
if (filePath.endsWith('.css')) {
|
| 508 |
-
// Try to extract CSS from complete code blocks first
|
| 509 |
const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
|
| 510 |
if (cssMatch) {
|
| 511 |
content = cssMatch[1];
|
| 512 |
} else {
|
| 513 |
-
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 514 |
content = content.replace(/^```css\s*/i, "");
|
| 515 |
}
|
| 516 |
-
// Remove any remaining backticks
|
| 517 |
return content.replace(/```/g, "").trim();
|
| 518 |
} else if (filePath.endsWith('.js')) {
|
| 519 |
-
// Try to extract JavaScript from complete code blocks first
|
| 520 |
const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
|
| 521 |
if (jsMatch) {
|
| 522 |
content = jsMatch[1];
|
| 523 |
} else {
|
| 524 |
-
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 525 |
content = content.replace(/^```(?:javascript|js)\s*/i, "");
|
| 526 |
}
|
| 527 |
-
// Remove any remaining backticks
|
| 528 |
return content.replace(/```/g, "").trim();
|
| 529 |
} else {
|
| 530 |
-
// Handle HTML files
|
| 531 |
const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
|
| 532 |
if (htmlMatch) {
|
| 533 |
content = htmlMatch[1];
|
| 534 |
} else {
|
| 535 |
-
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 536 |
content = content.replace(/^```html\s*/i, "");
|
| 537 |
-
// Try to find HTML starting with DOCTYPE
|
| 538 |
const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
|
| 539 |
if (doctypeMatch) {
|
| 540 |
content = doctypeMatch[0];
|
|
|
|
| 124 |
client.setQueryData(["ai.model"], newModel);
|
| 125 |
};
|
| 126 |
|
| 127 |
+
const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean, userName?: string) => {
|
| 128 |
+
if (isLoggedIn && userName) {
|
| 129 |
+
try {
|
| 130 |
+
const uploadRequest = await fetch(`/deepsite/api/me/projects/${userName}/new/update`, {
|
| 131 |
+
method: "PUT",
|
| 132 |
+
body: JSON.stringify({
|
| 133 |
+
pages: htmlPages,
|
| 134 |
+
commitTitle: prompt,
|
| 135 |
+
isNew: true,
|
| 136 |
+
projectName,
|
| 137 |
+
}),
|
| 138 |
+
headers: {
|
| 139 |
+
"Content-Type": "application/json",
|
| 140 |
+
"Authorization": `Bearer ${token}`,
|
| 141 |
+
},
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
const uploadRes = await uploadRequest.json();
|
| 145 |
+
|
| 146 |
+
if (!uploadRequest.ok || !uploadRes.ok) {
|
| 147 |
+
throw new Error(uploadRes.error || "Failed to create project");
|
| 148 |
}
|
| 149 |
+
|
|
|
|
| 150 |
setIsAiWorking(false);
|
| 151 |
+
router.replace(`/${uploadRes.repoId}`);
|
| 152 |
+
toast.success("AI responded successfully");
|
| 153 |
+
if (audio.current) audio.current.play();
|
| 154 |
+
} catch (error: any) {
|
| 155 |
+
setIsAiWorking(false);
|
| 156 |
+
toast.error(error?.message || "Failed to create project");
|
| 157 |
+
}
|
| 158 |
} else {
|
| 159 |
setIsAiWorking(false);
|
| 160 |
toast.success("AI responded successfully");
|
|
|
|
| 162 |
}
|
| 163 |
}
|
| 164 |
|
| 165 |
+
const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean, userName?: string) => {
|
| 166 |
if (isAiWorking) return;
|
| 167 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 168 |
|
|
|
|
| 228 |
setPages(newPages);
|
| 229 |
setLastSavedPages([...newPages]);
|
| 230 |
if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
|
| 231 |
+
createNewProject(prompt, newPages, projectName, isLoggedIn, userName);
|
| 232 |
}
|
| 233 |
setPrompts([...prompts, prompt]);
|
| 234 |
|
|
|
|
| 321 |
});
|
| 322 |
|
| 323 |
if (request && request.body) {
|
| 324 |
+
const reader = request.body.getReader();
|
| 325 |
+
const decoder = new TextDecoder("utf-8");
|
| 326 |
+
let contentResponse = "";
|
| 327 |
+
let metadata: any = null;
|
| 328 |
+
|
| 329 |
+
const read = async (): Promise<any> => {
|
| 330 |
+
const { done, value } = await reader.read();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
+
if (done) {
|
| 333 |
+
const metadataMatch = contentResponse.match(/___METADATA_START___([\s\S]*?)___METADATA_END___/);
|
| 334 |
+
if (metadataMatch) {
|
| 335 |
+
try {
|
| 336 |
+
metadata = JSON.parse(metadataMatch[1]);
|
| 337 |
+
contentResponse = contentResponse.replace(/___METADATA_START___[\s\S]*?___METADATA_END___/, '').trim();
|
| 338 |
+
} catch (e) {
|
| 339 |
+
console.error("Failed to parse metadata", e);
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
const trimmedResponse = contentResponse.trim();
|
| 344 |
+
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 345 |
+
try {
|
| 346 |
+
const jsonResponse = JSON.parse(trimmedResponse);
|
| 347 |
+
if (jsonResponse && !jsonResponse.ok) {
|
| 348 |
+
setIsAiWorking(false);
|
| 349 |
+
if (jsonResponse.openLogin) {
|
| 350 |
+
return { error: "login_required" };
|
| 351 |
+
} else if (jsonResponse.openSelectProvider) {
|
| 352 |
+
return { error: "provider_required", message: jsonResponse.message };
|
| 353 |
+
} else if (jsonResponse.openProModal) {
|
| 354 |
+
return { error: "pro_required" };
|
| 355 |
+
} else {
|
| 356 |
+
toast.error(jsonResponse.message);
|
| 357 |
+
return { error: "api_error", message: jsonResponse.message };
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
} catch (e) {
|
| 361 |
+
// Not JSON, continue with normal processing
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
|
| 365 |
+
const { processAiResponse, extractProjectName } = await import("@/lib/format-ai-response");
|
| 366 |
+
const { updatedPages, updatedLines } = processAiResponse(contentResponse, pagesToSend);
|
| 367 |
+
|
| 368 |
+
const updatedPagesMap = new Map(updatedPages.map((p: Page) => [p.path, p]));
|
| 369 |
+
const mergedPages: Page[] = pages.map(page =>
|
| 370 |
+
updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
|
| 371 |
+
);
|
| 372 |
+
updatedPages.forEach((page: Page) => {
|
| 373 |
+
if (!pages.find(p => p.path === page.path)) {
|
| 374 |
+
mergedPages.push(page);
|
| 375 |
+
}
|
| 376 |
+
});
|
| 377 |
+
|
| 378 |
+
let projectName = null;
|
| 379 |
+
if (isNew) {
|
| 380 |
+
projectName = extractProjectName(contentResponse);
|
| 381 |
+
if (!projectName) {
|
| 382 |
+
projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40) + "-" + Math.random().toString(36).substring(2, 15);
|
| 383 |
}
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
try {
|
| 387 |
+
const uploadRequest = await fetch(`/deepsite/api/me/projects/${metadata?.userName || 'unknown'}/${isNew ? 'new' : (project?.space_id?.split('/')[1] || 'unknown')}/update`, {
|
| 388 |
+
method: "PUT",
|
| 389 |
+
body: JSON.stringify({
|
| 390 |
+
pages: mergedPages,
|
| 391 |
+
commitTitle: prompt,
|
| 392 |
+
isNew,
|
| 393 |
+
projectName,
|
| 394 |
+
}),
|
| 395 |
+
headers: {
|
| 396 |
+
"Content-Type": "application/json",
|
| 397 |
+
"Authorization": `Bearer ${token}`,
|
| 398 |
+
},
|
| 399 |
+
});
|
| 400 |
+
|
| 401 |
+
const uploadRes = await uploadRequest.json();
|
| 402 |
|
| 403 |
+
if (!uploadRequest.ok || !uploadRes.ok) {
|
| 404 |
+
throw new Error(uploadRes.error || "Failed to upload to HuggingFace");
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
toast.success("AI responded successfully");
|
| 408 |
+
const iframe = document.getElementById("preview-iframe") as HTMLIFrameElement;
|
| 409 |
+
|
| 410 |
+
if (isNew && uploadRes.repoId) {
|
| 411 |
+
router.push(`/${uploadRes.repoId}`);
|
| 412 |
+
setIsAiWorking(false);
|
| 413 |
+
} else {
|
| 414 |
+
setPages(mergedPages);
|
| 415 |
+
setLastSavedPages([...mergedPages]);
|
| 416 |
+
setCommits([uploadRes.commit, ...commits]);
|
| 417 |
+
setPrompts([...prompts, prompt]);
|
| 418 |
+
setSelectedElement(null);
|
| 419 |
+
setSelectedFiles([]);
|
| 420 |
+
setIsEditableModeEnabled(false);
|
| 421 |
+
setIsAiWorking(false);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
if (audio.current) audio.current.play();
|
| 425 |
+
if (iframe) {
|
| 426 |
+
setTimeout(() => {
|
| 427 |
+
iframe.src = iframe.src;
|
| 428 |
+
}, 500);
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
return { success: true, updatedLines };
|
| 432 |
+
} catch (uploadError: any) {
|
| 433 |
+
setIsAiWorking(false);
|
| 434 |
+
toast.error(uploadError.message || "Failed to save changes");
|
| 435 |
+
return { error: "upload_error", message: uploadError.message };
|
| 436 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
}
|
|
|
|
| 438 |
|
| 439 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 440 |
+
contentResponse += chunk;
|
| 441 |
+
|
| 442 |
+
// Check for error responses during streaming
|
| 443 |
+
const trimmedResponse = contentResponse.trim();
|
| 444 |
+
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 445 |
+
try {
|
| 446 |
+
const jsonResponse = JSON.parse(trimmedResponse);
|
| 447 |
+
if (jsonResponse && !jsonResponse.ok) {
|
| 448 |
+
setIsAiWorking(false);
|
| 449 |
+
if (jsonResponse.openLogin) {
|
| 450 |
+
return { error: "login_required" };
|
| 451 |
+
} else if (jsonResponse.openSelectProvider) {
|
| 452 |
+
return { error: "provider_required", message: jsonResponse.message };
|
| 453 |
+
} else if (jsonResponse.openProModal) {
|
| 454 |
+
return { error: "pro_required" };
|
| 455 |
+
} else {
|
| 456 |
+
toast.error(jsonResponse.message);
|
| 457 |
+
return { error: "api_error", message: jsonResponse.message };
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
} catch (e) {
|
| 461 |
+
// Not complete JSON yet, continue
|
| 462 |
}
|
| 463 |
+
}
|
| 464 |
|
| 465 |
+
return read();
|
| 466 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
+
return await read();
|
| 469 |
}
|
| 470 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 471 |
} catch (error: any) {
|
| 472 |
setIsAiWorking(false);
|
| 473 |
+
setIsThinking(false);
|
| 474 |
+
setController(null);
|
| 475 |
+
|
| 476 |
+
if (!abortController.signal.aborted) {
|
| 477 |
+
toast.error(error.message || "Network error occurred");
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
if (error.openLogin) {
|
| 481 |
return { error: "login_required" };
|
| 482 |
}
|
|
|
|
| 524 |
if (pages.length > 0) {
|
| 525 |
setPages(pages);
|
| 526 |
if (isStreaming) {
|
|
|
|
| 527 |
const newPages = pages.filter(p =>
|
| 528 |
!streamingPagesRef.current.has(p.path)
|
| 529 |
);
|
|
|
|
| 533 |
setCurrentPage(newPage.path);
|
| 534 |
streamingPagesRef.current.add(newPage.path);
|
| 535 |
|
|
|
|
| 536 |
if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
|
| 537 |
setPreviewPage(newPage.path);
|
| 538 |
}
|
|
|
|
| 552 |
const extractFileContent = (chunk: string, filePath: string): string => {
|
| 553 |
if (!chunk) return "";
|
| 554 |
|
|
|
|
| 555 |
let content = chunk.trim();
|
| 556 |
|
|
|
|
| 557 |
if (filePath.endsWith('.css')) {
|
|
|
|
| 558 |
const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
|
| 559 |
if (cssMatch) {
|
| 560 |
content = cssMatch[1];
|
| 561 |
} else {
|
|
|
|
| 562 |
content = content.replace(/^```css\s*/i, "");
|
| 563 |
}
|
|
|
|
| 564 |
return content.replace(/```/g, "").trim();
|
| 565 |
} else if (filePath.endsWith('.js')) {
|
|
|
|
| 566 |
const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
|
| 567 |
if (jsMatch) {
|
| 568 |
content = jsMatch[1];
|
| 569 |
} else {
|
|
|
|
| 570 |
content = content.replace(/^```(?:javascript|js)\s*/i, "");
|
| 571 |
}
|
|
|
|
| 572 |
return content.replace(/```/g, "").trim();
|
| 573 |
} else {
|
|
|
|
| 574 |
const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
|
| 575 |
if (htmlMatch) {
|
| 576 |
content = htmlMatch[1];
|
| 577 |
} else {
|
|
|
|
| 578 |
content = content.replace(/^```html\s*/i, "");
|
|
|
|
| 579 |
const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
|
| 580 |
if (doctypeMatch) {
|
| 581 |
content = doctypeMatch[0];
|
lib/format-ai-response.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
+
import { Page } from "@/types";
|
| 3 |
+
import {
|
| 4 |
+
DIVIDER,
|
| 5 |
+
NEW_FILE_END,
|
| 6 |
+
NEW_FILE_START,
|
| 7 |
+
REPLACE_END,
|
| 8 |
+
SEARCH_START,
|
| 9 |
+
UPDATE_FILE_END,
|
| 10 |
+
UPDATE_FILE_START,
|
| 11 |
+
} from "./prompts";
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Escape special regex characters in a string
|
| 15 |
+
*/
|
| 16 |
+
export const escapeRegExp = (string: string): string => {
|
| 17 |
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Create a flexible HTML regex that accounts for whitespace variations
|
| 22 |
+
*/
|
| 23 |
+
export const createFlexibleHtmlRegex = (searchBlock: string): RegExp => {
|
| 24 |
+
let searchRegex = escapeRegExp(searchBlock)
|
| 25 |
+
.replace(/\s+/g, '\\s*')
|
| 26 |
+
.replace(/>\s*</g, '>\\s*<')
|
| 27 |
+
.replace(/\s*>/g, '\\s*>');
|
| 28 |
+
|
| 29 |
+
return new RegExp(searchRegex, 'g');
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Process AI response chunk and apply updates to pages
|
| 34 |
+
* Returns updated pages and updated line numbers
|
| 35 |
+
*/
|
| 36 |
+
export interface ProcessAiResponseResult {
|
| 37 |
+
updatedPages: Page[];
|
| 38 |
+
updatedLines: number[][];
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export const processAiResponse = (
|
| 42 |
+
chunk: string,
|
| 43 |
+
pages: Page[]
|
| 44 |
+
): ProcessAiResponseResult => {
|
| 45 |
+
const updatedLines: number[][] = [];
|
| 46 |
+
const updatedPages = [...pages];
|
| 47 |
+
|
| 48 |
+
// Process UPDATE_FILE blocks
|
| 49 |
+
const updateFileRegex = new RegExp(
|
| 50 |
+
`${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`,
|
| 51 |
+
'g'
|
| 52 |
+
);
|
| 53 |
+
let updateFileMatch;
|
| 54 |
+
|
| 55 |
+
while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
|
| 56 |
+
const [, filePath, fileContent] = updateFileMatch;
|
| 57 |
+
|
| 58 |
+
const pageIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 59 |
+
if (pageIndex !== -1) {
|
| 60 |
+
let pageHtml = updatedPages[pageIndex].html;
|
| 61 |
+
|
| 62 |
+
let processedContent = fileContent;
|
| 63 |
+
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 64 |
+
if (htmlMatch) {
|
| 65 |
+
processedContent = htmlMatch[1];
|
| 66 |
+
}
|
| 67 |
+
let position = 0;
|
| 68 |
+
let moreBlocks = true;
|
| 69 |
+
|
| 70 |
+
while (moreBlocks) {
|
| 71 |
+
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
| 72 |
+
if (searchStartIndex === -1) {
|
| 73 |
+
moreBlocks = false;
|
| 74 |
+
continue;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
| 78 |
+
if (dividerIndex === -1) {
|
| 79 |
+
moreBlocks = false;
|
| 80 |
+
continue;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
| 84 |
+
if (replaceEndIndex === -1) {
|
| 85 |
+
moreBlocks = false;
|
| 86 |
+
continue;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const searchBlock = processedContent.substring(
|
| 90 |
+
searchStartIndex + SEARCH_START.length,
|
| 91 |
+
dividerIndex
|
| 92 |
+
);
|
| 93 |
+
const replaceBlock = processedContent.substring(
|
| 94 |
+
dividerIndex + DIVIDER.length,
|
| 95 |
+
replaceEndIndex
|
| 96 |
+
);
|
| 97 |
+
|
| 98 |
+
if (searchBlock.trim() === "") {
|
| 99 |
+
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
| 100 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 101 |
+
} else {
|
| 102 |
+
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 103 |
+
const match = regex.exec(pageHtml);
|
| 104 |
+
|
| 105 |
+
if (match) {
|
| 106 |
+
const matchedText = match[0];
|
| 107 |
+
const beforeText = pageHtml.substring(0, match.index);
|
| 108 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 109 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 110 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 111 |
+
|
| 112 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 113 |
+
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
updatedPages[pageIndex].html = pageHtml;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// Process NEW_FILE blocks
|
| 125 |
+
const newFileRegex = new RegExp(
|
| 126 |
+
`${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`,
|
| 127 |
+
'g'
|
| 128 |
+
);
|
| 129 |
+
let newFileMatch;
|
| 130 |
+
|
| 131 |
+
while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
|
| 132 |
+
const [, filePath, fileContent] = newFileMatch;
|
| 133 |
+
|
| 134 |
+
let fileData = fileContent;
|
| 135 |
+
// Try to extract content from code blocks
|
| 136 |
+
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 137 |
+
const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
|
| 138 |
+
const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
|
| 139 |
+
|
| 140 |
+
if (htmlMatch) {
|
| 141 |
+
fileData = htmlMatch[1];
|
| 142 |
+
} else if (cssMatch) {
|
| 143 |
+
fileData = cssMatch[1];
|
| 144 |
+
} else if (jsMatch) {
|
| 145 |
+
fileData = jsMatch[1];
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 149 |
+
|
| 150 |
+
if (existingFileIndex !== -1) {
|
| 151 |
+
updatedPages[existingFileIndex] = {
|
| 152 |
+
path: filePath,
|
| 153 |
+
html: fileData.trim()
|
| 154 |
+
};
|
| 155 |
+
} else {
|
| 156 |
+
updatedPages.push({
|
| 157 |
+
path: filePath,
|
| 158 |
+
html: fileData.trim()
|
| 159 |
+
});
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Fallback: process SEARCH/REPLACE blocks without UPDATE_FILE wrapper (backward compatibility)
|
| 164 |
+
if (updatedPages.length === pages.length && !chunk.includes(UPDATE_FILE_START)) {
|
| 165 |
+
let position = 0;
|
| 166 |
+
let moreBlocks = true;
|
| 167 |
+
let newHtml = updatedPages[0]?.html || "";
|
| 168 |
+
|
| 169 |
+
while (moreBlocks) {
|
| 170 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 171 |
+
if (searchStartIndex === -1) {
|
| 172 |
+
moreBlocks = false;
|
| 173 |
+
continue;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
| 177 |
+
if (dividerIndex === -1) {
|
| 178 |
+
moreBlocks = false;
|
| 179 |
+
continue;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
| 183 |
+
if (replaceEndIndex === -1) {
|
| 184 |
+
moreBlocks = false;
|
| 185 |
+
continue;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
const searchBlock = chunk.substring(
|
| 189 |
+
searchStartIndex + SEARCH_START.length,
|
| 190 |
+
dividerIndex
|
| 191 |
+
);
|
| 192 |
+
const replaceBlock = chunk.substring(
|
| 193 |
+
dividerIndex + DIVIDER.length,
|
| 194 |
+
replaceEndIndex
|
| 195 |
+
);
|
| 196 |
+
|
| 197 |
+
if (searchBlock.trim() === "") {
|
| 198 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 199 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 200 |
+
} else {
|
| 201 |
+
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 202 |
+
const match = regex.exec(newHtml);
|
| 203 |
+
|
| 204 |
+
if (match) {
|
| 205 |
+
const matchedText = match[0];
|
| 206 |
+
const beforeText = newHtml.substring(0, match.index);
|
| 207 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 208 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 209 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 210 |
+
|
| 211 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 212 |
+
newHtml = newHtml.replace(matchedText, replaceBlock);
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 220 |
+
if (mainPageIndex !== -1) {
|
| 221 |
+
updatedPages[mainPageIndex].html = newHtml;
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
return { updatedPages, updatedLines };
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
/**
|
| 229 |
+
* Convert pages to File objects for upload to HuggingFace
|
| 230 |
+
*/
|
| 231 |
+
export const pagesToFiles = (pages: Page[]): File[] => {
|
| 232 |
+
const files: File[] = [];
|
| 233 |
+
pages.forEach((page: Page) => {
|
| 234 |
+
let mimeType = "text/html";
|
| 235 |
+
if (page.path.endsWith(".css")) {
|
| 236 |
+
mimeType = "text/css";
|
| 237 |
+
} else if (page.path.endsWith(".js")) {
|
| 238 |
+
mimeType = "text/javascript";
|
| 239 |
+
} else if (page.path.endsWith(".json")) {
|
| 240 |
+
mimeType = "application/json";
|
| 241 |
+
}
|
| 242 |
+
const file = new File([page.html], page.path, { type: mimeType });
|
| 243 |
+
files.push(file);
|
| 244 |
+
});
|
| 245 |
+
return files;
|
| 246 |
+
};
|
| 247 |
+
|
| 248 |
+
/**
|
| 249 |
+
* Extract project name from AI response
|
| 250 |
+
*/
|
| 251 |
+
export const extractProjectName = (chunk: string): string | null => {
|
| 252 |
+
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 253 |
+
return projectName || null;
|
| 254 |
+
};
|
| 255 |
+
|