Spaces:
Running
Running
[BIG UPDATE π] New generation files
Browse files- app/api/ask/route.ts +50 -35
- app/api/me/projects/[namespace]/[repoId]/route.ts +15 -5
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +15 -2
- app/api/me/projects/route.ts +15 -1
- components/editor/ask-ai/context.tsx +124 -0
- components/editor/ask-ai/index.tsx +9 -10
- components/editor/ask-ai/uploader.tsx +33 -3
- components/editor/file-browser/index.tsx +458 -0
- components/editor/header/index.tsx +19 -3
- components/editor/index.tsx +26 -7
- components/editor/preview/index.tsx +252 -47
- components/ui/sheet.tsx +140 -0
- hooks/useAi.ts +136 -86
- hooks/useEditor.ts +13 -0
- lib/inject-badge.ts +50 -0
- lib/prompts.ts +326 -68
- public/deepsite-badge.js +170 -0
app/api/ask/route.ts
CHANGED
|
@@ -10,12 +10,12 @@ import {
|
|
| 10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
| 11 |
INITIAL_SYSTEM_PROMPT,
|
| 12 |
MAX_REQUESTS_PER_IP,
|
| 13 |
-
|
| 14 |
-
|
| 15 |
REPLACE_END,
|
| 16 |
SEARCH_START,
|
| 17 |
-
|
| 18 |
-
|
| 19 |
PROMPT_FOR_PROJECT_NAME,
|
| 20 |
} from "@/lib/prompts";
|
| 21 |
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
|
@@ -27,6 +27,7 @@ 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 |
|
| 31 |
const ipAddresses = new Map();
|
| 32 |
|
|
@@ -192,11 +193,9 @@ export async function POST(request: NextRequest) {
|
|
| 192 |
);
|
| 193 |
}
|
| 194 |
} finally {
|
| 195 |
-
// Ensure the writer is always closed, even if already closed
|
| 196 |
try {
|
| 197 |
await writer?.close();
|
| 198 |
} catch {
|
| 199 |
-
// Ignore errors when closing the writer as it might already be closed
|
| 200 |
}
|
| 201 |
}
|
| 202 |
})();
|
|
@@ -216,7 +215,6 @@ export async function POST(request: NextRequest) {
|
|
| 216 |
}
|
| 217 |
|
| 218 |
export async function PUT(request: NextRequest) {
|
| 219 |
-
console.log("PUT request received");
|
| 220 |
const user = await isAuthenticated();
|
| 221 |
if (user instanceof NextResponse || !user) {
|
| 222 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
|
@@ -225,7 +223,7 @@ export async function PUT(request: NextRequest) {
|
|
| 225 |
const authHeaders = await headers();
|
| 226 |
|
| 227 |
const body = await request.json();
|
| 228 |
-
const { prompt,
|
| 229 |
body;
|
| 230 |
|
| 231 |
let repoId = repoIdFromBody;
|
|
@@ -301,7 +299,6 @@ export async function PUT(request: NextRequest) {
|
|
| 301 |
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
| 302 |
const userContext = "You are modifying the HTML file based on the user's request.";
|
| 303 |
|
| 304 |
-
// Send all pages without filtering
|
| 305 |
const allPages = pages || [];
|
| 306 |
const pagesContext = allPages
|
| 307 |
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
|
@@ -368,18 +365,18 @@ export async function PUT(request: NextRequest) {
|
|
| 368 |
let newHtml = "";
|
| 369 |
const updatedPages = [...(pages || [])];
|
| 370 |
|
| 371 |
-
const
|
| 372 |
-
let
|
| 373 |
|
| 374 |
-
while ((
|
| 375 |
-
const [,
|
| 376 |
|
| 377 |
-
const pageIndex = updatedPages.findIndex(p => p.path ===
|
| 378 |
if (pageIndex !== -1) {
|
| 379 |
let pageHtml = updatedPages[pageIndex].html;
|
| 380 |
|
| 381 |
-
let processedContent =
|
| 382 |
-
const htmlMatch =
|
| 383 |
if (htmlMatch) {
|
| 384 |
processedContent = htmlMatch[1];
|
| 385 |
}
|
|
@@ -438,40 +435,48 @@ export async function PUT(request: NextRequest) {
|
|
| 438 |
|
| 439 |
updatedPages[pageIndex].html = pageHtml;
|
| 440 |
|
| 441 |
-
if (
|
| 442 |
newHtml = pageHtml;
|
| 443 |
}
|
| 444 |
}
|
| 445 |
}
|
| 446 |
|
| 447 |
-
const
|
| 448 |
-
let
|
| 449 |
|
| 450 |
-
while ((
|
| 451 |
-
const [,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
-
let pageHtml = pageContent;
|
| 454 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 455 |
if (htmlMatch) {
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
}
|
| 458 |
|
| 459 |
-
const
|
| 460 |
|
| 461 |
-
if (
|
| 462 |
-
updatedPages[
|
| 463 |
-
path:
|
| 464 |
-
html:
|
| 465 |
};
|
| 466 |
} else {
|
| 467 |
updatedPages.push({
|
| 468 |
-
path:
|
| 469 |
-
html:
|
| 470 |
});
|
| 471 |
}
|
| 472 |
}
|
| 473 |
|
| 474 |
-
if (updatedPages.length === pages?.length && !chunk.includes(
|
| 475 |
let position = 0;
|
| 476 |
let moreBlocks = true;
|
| 477 |
|
|
@@ -525,7 +530,6 @@ export async function PUT(request: NextRequest) {
|
|
| 525 |
position = replaceEndIndex + REPLACE_END.length;
|
| 526 |
}
|
| 527 |
|
| 528 |
-
// Update the main HTML if it's the index page
|
| 529 |
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 530 |
if (mainPageIndex !== -1) {
|
| 531 |
updatedPages[mainPageIndex].html = newHtml;
|
|
@@ -534,12 +538,23 @@ export async function PUT(request: NextRequest) {
|
|
| 534 |
|
| 535 |
const files: File[] = [];
|
| 536 |
updatedPages.forEach((page: Page) => {
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
files.push(file);
|
| 539 |
});
|
| 540 |
|
| 541 |
if (isNew) {
|
| 542 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START
|
| 543 |
const formattedTitle = projectName?.toLowerCase()
|
| 544 |
.replace(/[^a-z0-9]+/g, "-")
|
| 545 |
.split("-")
|
|
|
|
| 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";
|
|
|
|
| 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 |
|
|
|
|
| 193 |
);
|
| 194 |
}
|
| 195 |
} finally {
|
|
|
|
| 196 |
try {
|
| 197 |
await writer?.close();
|
| 198 |
} catch {
|
|
|
|
| 199 |
}
|
| 200 |
}
|
| 201 |
})();
|
|
|
|
| 215 |
}
|
| 216 |
|
| 217 |
export async function PUT(request: NextRequest) {
|
|
|
|
| 218 |
const user = await isAuthenticated();
|
| 219 |
if (user instanceof NextResponse || !user) {
|
| 220 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
|
|
|
| 223 |
const authHeaders = await headers();
|
| 224 |
|
| 225 |
const body = await request.json();
|
| 226 |
+
const { prompt, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew } =
|
| 227 |
body;
|
| 228 |
|
| 229 |
let repoId = repoIdFromBody;
|
|
|
|
| 299 |
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
| 300 |
const userContext = "You are modifying the HTML file based on the user's request.";
|
| 301 |
|
|
|
|
| 302 |
const allPages = pages || [];
|
| 303 |
const pagesContext = allPages
|
| 304 |
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
|
|
|
| 365 |
let newHtml = "";
|
| 366 |
const updatedPages = [...(pages || [])];
|
| 367 |
|
| 368 |
+
const updateFileRegex = new RegExp(`${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`, 'g');
|
| 369 |
+
let updateFileMatch;
|
| 370 |
|
| 371 |
+
while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
|
| 372 |
+
const [, filePath, fileContent] = updateFileMatch;
|
| 373 |
|
| 374 |
+
const pageIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 375 |
if (pageIndex !== -1) {
|
| 376 |
let pageHtml = updatedPages[pageIndex].html;
|
| 377 |
|
| 378 |
+
let processedContent = fileContent;
|
| 379 |
+
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 380 |
if (htmlMatch) {
|
| 381 |
processedContent = htmlMatch[1];
|
| 382 |
}
|
|
|
|
| 435 |
|
| 436 |
updatedPages[pageIndex].html = pageHtml;
|
| 437 |
|
| 438 |
+
if (filePath === '/' || filePath === '/index' || filePath === 'index' || filePath === 'index.html') {
|
| 439 |
newHtml = pageHtml;
|
| 440 |
}
|
| 441 |
}
|
| 442 |
}
|
| 443 |
|
| 444 |
+
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');
|
| 445 |
+
let newFileMatch;
|
| 446 |
|
| 447 |
+
while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
|
| 448 |
+
const [, filePath, fileContent] = newFileMatch;
|
| 449 |
+
|
| 450 |
+
let fileData = fileContent;
|
| 451 |
+
// Try to extract content from code blocks
|
| 452 |
+
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 453 |
+
const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
|
| 454 |
+
const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
|
| 455 |
|
|
|
|
|
|
|
| 456 |
if (htmlMatch) {
|
| 457 |
+
fileData = htmlMatch[1];
|
| 458 |
+
} else if (cssMatch) {
|
| 459 |
+
fileData = cssMatch[1];
|
| 460 |
+
} else if (jsMatch) {
|
| 461 |
+
fileData = jsMatch[1];
|
| 462 |
}
|
| 463 |
|
| 464 |
+
const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 465 |
|
| 466 |
+
if (existingFileIndex !== -1) {
|
| 467 |
+
updatedPages[existingFileIndex] = {
|
| 468 |
+
path: filePath,
|
| 469 |
+
html: fileData.trim()
|
| 470 |
};
|
| 471 |
} else {
|
| 472 |
updatedPages.push({
|
| 473 |
+
path: filePath,
|
| 474 |
+
html: fileData.trim()
|
| 475 |
});
|
| 476 |
}
|
| 477 |
}
|
| 478 |
|
| 479 |
+
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_FILE_START)) {
|
| 480 |
let position = 0;
|
| 481 |
let moreBlocks = true;
|
| 482 |
|
|
|
|
| 530 |
position = replaceEndIndex + REPLACE_END.length;
|
| 531 |
}
|
| 532 |
|
|
|
|
| 533 |
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 534 |
if (mainPageIndex !== -1) {
|
| 535 |
updatedPages[mainPageIndex].html = newHtml;
|
|
|
|
| 538 |
|
| 539 |
const files: File[] = [];
|
| 540 |
updatedPages.forEach((page: Page) => {
|
| 541 |
+
let mimeType = "text/html";
|
| 542 |
+
if (page.path.endsWith(".css")) {
|
| 543 |
+
mimeType = "text/css";
|
| 544 |
+
} else if (page.path.endsWith(".js")) {
|
| 545 |
+
mimeType = "text/javascript";
|
| 546 |
+
} else if (page.path.endsWith(".json")) {
|
| 547 |
+
mimeType = "application/json";
|
| 548 |
+
}
|
| 549 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 550 |
+
? injectDeepSiteBadge(page.html)
|
| 551 |
+
: page.html;
|
| 552 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 553 |
files.push(file);
|
| 554 |
});
|
| 555 |
|
| 556 |
if (isNew) {
|
| 557 |
+
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 558 |
const formattedTitle = projectName?.toLowerCase()
|
| 559 |
.replace(/[^a-z0-9]+/g, "-")
|
| 560 |
.split("-")
|
app/api/me/projects/[namespace]/[repoId]/route.ts
CHANGED
|
@@ -108,7 +108,7 @@ export async function GET(
|
|
| 108 |
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
|
| 109 |
|
| 110 |
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
| 111 |
-
if (fileInfo.path.endsWith(".html")) {
|
| 112 |
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
| 113 |
const html = await blob?.text();
|
| 114 |
if (!html) {
|
|
@@ -126,10 +126,20 @@ export async function GET(
|
|
| 126 |
});
|
| 127 |
}
|
| 128 |
}
|
| 129 |
-
if (fileInfo.type === "directory" && ["videos", "images", "audio"].includes(fileInfo.path)) {
|
| 130 |
-
for await (const
|
| 131 |
-
if (
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
}
|
| 135 |
}
|
|
|
|
| 108 |
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif", "mp4", "webm", "ogg", "avi", "mov", "mp3", "wav", "ogg", "aac", "m4a"];
|
| 109 |
|
| 110 |
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
| 111 |
+
if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js")) {
|
| 112 |
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
| 113 |
const html = await blob?.text();
|
| 114 |
if (!html) {
|
|
|
|
| 126 |
});
|
| 127 |
}
|
| 128 |
}
|
| 129 |
+
if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
|
| 130 |
+
for await (const subFileInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
| 131 |
+
if (subFileInfo.path.includes("components")) {
|
| 132 |
+
const blob = await downloadFile({ repo, accessToken: user.token as string, path: subFileInfo.path, raw: true });
|
| 133 |
+
const html = await blob?.text();
|
| 134 |
+
if (!html) {
|
| 135 |
+
continue;
|
| 136 |
+
}
|
| 137 |
+
htmlFiles.push({
|
| 138 |
+
path: subFileInfo.path,
|
| 139 |
+
html,
|
| 140 |
+
});
|
| 141 |
+
} else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
|
| 142 |
+
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
|
| 143 |
}
|
| 144 |
}
|
| 145 |
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { uploadFiles } from "@huggingface/hub";
|
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
import { Page } from "@/types";
|
|
|
|
| 6 |
|
| 7 |
export async function PUT(
|
| 8 |
req: NextRequest,
|
|
@@ -28,11 +29,23 @@ export async function PUT(
|
|
| 28 |
// Prepare files for upload
|
| 29 |
const files: File[] = [];
|
| 30 |
pages.forEach((page: Page) => {
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
files.push(file);
|
| 33 |
});
|
| 34 |
|
| 35 |
-
// Upload files to HuggingFace Hub
|
| 36 |
const response = await uploadFiles({
|
| 37 |
repo: {
|
| 38 |
type: "space",
|
|
|
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
import { Page } from "@/types";
|
| 6 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 7 |
|
| 8 |
export async function PUT(
|
| 9 |
req: NextRequest,
|
|
|
|
| 29 |
// Prepare files for upload
|
| 30 |
const files: File[] = [];
|
| 31 |
pages.forEach((page: Page) => {
|
| 32 |
+
// Determine MIME type based on file extension
|
| 33 |
+
let mimeType = "text/html";
|
| 34 |
+
if (page.path.endsWith(".css")) {
|
| 35 |
+
mimeType = "text/css";
|
| 36 |
+
} else if (page.path.endsWith(".js")) {
|
| 37 |
+
mimeType = "text/javascript";
|
| 38 |
+
} else if (page.path.endsWith(".json")) {
|
| 39 |
+
mimeType = "application/json";
|
| 40 |
+
}
|
| 41 |
+
// Inject the DeepSite badge script into index pages only (not components or other HTML files)
|
| 42 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 43 |
+
? injectDeepSiteBadge(page.html)
|
| 44 |
+
: page.html;
|
| 45 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 46 |
files.push(file);
|
| 47 |
});
|
| 48 |
|
|
|
|
| 49 |
const response = await uploadFiles({
|
| 50 |
repo: {
|
| 51 |
type: "space",
|
app/api/me/projects/route.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { RepoDesignation, createRepo, listCommits, spaceInfo, uploadFiles } from
|
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
import { Commit, Page } from "@/types";
|
| 6 |
import { COLORS } from "@/lib/utils";
|
|
|
|
| 7 |
|
| 8 |
export async function POST(
|
| 9 |
req: NextRequest,
|
|
@@ -50,7 +51,20 @@ This project was created with [DeepSite](https://deepsite.hf.co).
|
|
| 50 |
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 51 |
files.push(readmeFile);
|
| 52 |
pages.forEach((page: Page) => {
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
files.push(file);
|
| 55 |
});
|
| 56 |
|
|
|
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
import { Commit, Page } from "@/types";
|
| 6 |
import { COLORS } from "@/lib/utils";
|
| 7 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 8 |
|
| 9 |
export async function POST(
|
| 10 |
req: NextRequest,
|
|
|
|
| 51 |
const readmeFile = new File([README], "README.md", { type: "text/markdown" });
|
| 52 |
files.push(readmeFile);
|
| 53 |
pages.forEach((page: Page) => {
|
| 54 |
+
// Determine MIME type based on file extension
|
| 55 |
+
let mimeType = "text/html";
|
| 56 |
+
if (page.path.endsWith(".css")) {
|
| 57 |
+
mimeType = "text/css";
|
| 58 |
+
} else if (page.path.endsWith(".js")) {
|
| 59 |
+
mimeType = "text/javascript";
|
| 60 |
+
} else if (page.path.endsWith(".json")) {
|
| 61 |
+
mimeType = "application/json";
|
| 62 |
+
}
|
| 63 |
+
// Inject the DeepSite badge script into index pages only (not components or other HTML files)
|
| 64 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 65 |
+
? injectDeepSiteBadge(page.html)
|
| 66 |
+
: page.html;
|
| 67 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 68 |
files.push(file);
|
| 69 |
});
|
| 70 |
|
components/editor/ask-ai/context.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useMemo } from "react";
|
| 2 |
+
import { FileCode, FileText, Braces, AtSign } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
import { Button } from "@/components/ui/button";
|
| 5 |
+
import { useEditor } from "@/hooks/useEditor";
|
| 6 |
+
import { useAi } from "@/hooks/useAi";
|
| 7 |
+
import {
|
| 8 |
+
Popover,
|
| 9 |
+
PopoverContent,
|
| 10 |
+
PopoverTrigger,
|
| 11 |
+
} from "@/components/ui/popover";
|
| 12 |
+
import classNames from "classnames";
|
| 13 |
+
|
| 14 |
+
export const Context = () => {
|
| 15 |
+
const { pages, currentPage, globalEditorLoading } = useEditor();
|
| 16 |
+
const { contextFile, setContextFile, globalAiLoading } = useAi();
|
| 17 |
+
const [open, setOpen] = useState(false);
|
| 18 |
+
|
| 19 |
+
const selectedFile = contextFile || null;
|
| 20 |
+
|
| 21 |
+
const getFileIcon = (filePath: string, size = "size-3.5") => {
|
| 22 |
+
if (filePath.endsWith(".css")) {
|
| 23 |
+
return <Braces className={size} />;
|
| 24 |
+
} else if (filePath.endsWith(".js")) {
|
| 25 |
+
return <FileCode className={size} />;
|
| 26 |
+
} else {
|
| 27 |
+
return <FileText className={size} />;
|
| 28 |
+
}
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const buttonContent = useMemo(() => {
|
| 32 |
+
if (selectedFile) {
|
| 33 |
+
return (
|
| 34 |
+
<>
|
| 35 |
+
<span className="truncate max-w-[120px]">{selectedFile}</span>
|
| 36 |
+
</>
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
return <>Add Context</>;
|
| 40 |
+
}, [selectedFile]);
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<Popover open={open} onOpenChange={setOpen}>
|
| 44 |
+
<PopoverTrigger asChild>
|
| 45 |
+
<Button
|
| 46 |
+
size="xs"
|
| 47 |
+
variant={open ? "default" : "outline"}
|
| 48 |
+
className={classNames("!rounded-md", {
|
| 49 |
+
"!bg-blue-500/10 !border-blue-500/30 !text-blue-400":
|
| 50 |
+
selectedFile && selectedFile.endsWith(".css"),
|
| 51 |
+
"!bg-orange-500/10 !border-orange-500/30 !text-orange-400":
|
| 52 |
+
selectedFile && selectedFile.endsWith(".html"),
|
| 53 |
+
"!bg-amber-500/10 !border-amber-500/30 !text-amber-400":
|
| 54 |
+
selectedFile && selectedFile.endsWith(".js"),
|
| 55 |
+
})}
|
| 56 |
+
disabled={
|
| 57 |
+
globalAiLoading || globalEditorLoading || pages.length === 0
|
| 58 |
+
}
|
| 59 |
+
>
|
| 60 |
+
<AtSign className="size-3.5" />
|
| 61 |
+
|
| 62 |
+
{buttonContent}
|
| 63 |
+
</Button>
|
| 64 |
+
</PopoverTrigger>
|
| 65 |
+
<PopoverContent
|
| 66 |
+
align="start"
|
| 67 |
+
className="w-64 !bg-neutral-900 !border-neutral-800 !p-0 !rounded-2xl overflow-hidden"
|
| 68 |
+
>
|
| 69 |
+
<header className="flex items-center justify-center text-xs px-2 py-2.5 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
| 70 |
+
Select a file to send as context
|
| 71 |
+
</header>
|
| 72 |
+
<main className="space-y-1 p-2">
|
| 73 |
+
<div className="max-h-[200px] overflow-y-auto space-y-0.5">
|
| 74 |
+
{pages.length === 0 ? (
|
| 75 |
+
<div className="px-2 py-2 text-xs text-neutral-500">
|
| 76 |
+
No files available
|
| 77 |
+
</div>
|
| 78 |
+
) : (
|
| 79 |
+
<>
|
| 80 |
+
<button
|
| 81 |
+
onClick={() => {
|
| 82 |
+
setContextFile(null);
|
| 83 |
+
setOpen(false);
|
| 84 |
+
}}
|
| 85 |
+
className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors ${
|
| 86 |
+
!selectedFile
|
| 87 |
+
? "bg-neutral-800 text-neutral-200 font-medium"
|
| 88 |
+
: "text-neutral-400 hover:text-neutral-200"
|
| 89 |
+
}`}
|
| 90 |
+
>
|
| 91 |
+
All files (default)
|
| 92 |
+
</button>
|
| 93 |
+
{pages.map((page) => (
|
| 94 |
+
<button
|
| 95 |
+
key={page.path}
|
| 96 |
+
onClick={() => {
|
| 97 |
+
setContextFile(page.path);
|
| 98 |
+
setOpen(false);
|
| 99 |
+
}}
|
| 100 |
+
className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-neutral-800 transition-colors flex items-center gap-1.5 ${
|
| 101 |
+
selectedFile === page.path
|
| 102 |
+
? "bg-neutral-800 text-neutral-200 font-medium"
|
| 103 |
+
: "text-neutral-400 hover:text-neutral-200"
|
| 104 |
+
}`}
|
| 105 |
+
>
|
| 106 |
+
<span className="shrink-0">
|
| 107 |
+
{getFileIcon(page.path, "size-3")}
|
| 108 |
+
</span>
|
| 109 |
+
<span className="truncate flex-1">{page.path}</span>
|
| 110 |
+
{page.path === currentPage && (
|
| 111 |
+
<span className="text-[10px] text-neutral-500 shrink-0">
|
| 112 |
+
(current)
|
| 113 |
+
</span>
|
| 114 |
+
)}
|
| 115 |
+
</button>
|
| 116 |
+
))}
|
| 117 |
+
</>
|
| 118 |
+
)}
|
| 119 |
+
</div>
|
| 120 |
+
</main>
|
| 121 |
+
</PopoverContent>
|
| 122 |
+
</Popover>
|
| 123 |
+
);
|
| 124 |
+
};
|
components/editor/ask-ai/index.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { Uploader } from "@/components/editor/ask-ai/uploader";
|
|
| 15 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
| 16 |
import { Selector } from "@/components/editor/ask-ai/selector";
|
| 17 |
import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
|
|
|
|
| 18 |
import { useUser } from "@/hooks/useUser";
|
| 19 |
import { useLoginModal } from "@/components/contexts/login-context";
|
| 20 |
import { Settings } from "./settings";
|
|
@@ -41,11 +42,8 @@ export const AskAi = ({
|
|
| 41 |
setSelectedFiles,
|
| 42 |
selectedElement,
|
| 43 |
setSelectedElement,
|
| 44 |
-
setIsThinking,
|
| 45 |
callAiNewProject,
|
| 46 |
callAiFollowUp,
|
| 47 |
-
setModel,
|
| 48 |
-
selectedModel,
|
| 49 |
audio: hookAudio,
|
| 50 |
cancelRequest,
|
| 51 |
} = useAi(onScrollToBottom);
|
|
@@ -112,9 +110,6 @@ export const AskAi = ({
|
|
| 112 |
|
| 113 |
if (result?.success) {
|
| 114 |
setPrompt("");
|
| 115 |
-
// if (selectedModel?.isThinker) {
|
| 116 |
-
// setModel(MODELS[0].value);
|
| 117 |
-
// }
|
| 118 |
}
|
| 119 |
}
|
| 120 |
};
|
|
@@ -284,10 +279,14 @@ export const AskAi = ({
|
|
| 284 |
</div>
|
| 285 |
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
| 286 |
<div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
<Settings
|
| 292 |
open={openProvider}
|
| 293 |
error={providerError}
|
|
|
|
| 15 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
| 16 |
import { Selector } from "@/components/editor/ask-ai/selector";
|
| 17 |
import { PromptBuilder } from "@/components/editor/ask-ai/prompt-builder";
|
| 18 |
+
import { Context } from "@/components/editor/ask-ai/context";
|
| 19 |
import { useUser } from "@/hooks/useUser";
|
| 20 |
import { useLoginModal } from "@/components/contexts/login-context";
|
| 21 |
import { Settings } from "./settings";
|
|
|
|
| 42 |
setSelectedFiles,
|
| 43 |
selectedElement,
|
| 44 |
setSelectedElement,
|
|
|
|
| 45 |
callAiNewProject,
|
| 46 |
callAiFollowUp,
|
|
|
|
|
|
|
| 47 |
audio: hookAudio,
|
| 48 |
cancelRequest,
|
| 49 |
} = useAi(onScrollToBottom);
|
|
|
|
| 110 |
|
| 111 |
if (result?.success) {
|
| 112 |
setPrompt("");
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
}
|
| 115 |
};
|
|
|
|
| 279 |
</div>
|
| 280 |
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
| 281 |
<div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
|
| 282 |
+
{isNew ? (
|
| 283 |
+
<PromptBuilder
|
| 284 |
+
enhancedSettings={enhancedSettings!}
|
| 285 |
+
setEnhancedSettings={setEnhancedSettings}
|
| 286 |
+
/>
|
| 287 |
+
) : (
|
| 288 |
+
<Context />
|
| 289 |
+
)}
|
| 290 |
<Settings
|
| 291 |
open={openProvider}
|
| 292 |
error={providerError}
|
components/editor/ask-ai/uploader.tsx
CHANGED
|
@@ -2,13 +2,12 @@ import { useRef, useState } from "react";
|
|
| 2 |
import {
|
| 3 |
CheckCircle,
|
| 4 |
ImageIcon,
|
| 5 |
-
Images,
|
| 6 |
-
Link,
|
| 7 |
Paperclip,
|
| 8 |
Upload,
|
| 9 |
Video,
|
| 10 |
Music,
|
| 11 |
FileVideo,
|
|
|
|
| 12 |
} from "lucide-react";
|
| 13 |
import Image from "next/image";
|
| 14 |
|
|
@@ -24,8 +23,12 @@ import { useUser } from "@/hooks/useUser";
|
|
| 24 |
import { useEditor } from "@/hooks/useEditor";
|
| 25 |
import { useAi } from "@/hooks/useAi";
|
| 26 |
import { useLoginModal } from "@/components/contexts/login-context";
|
|
|
|
| 27 |
|
| 28 |
export const getFileType = (url: string) => {
|
|
|
|
|
|
|
|
|
|
| 29 |
const extension = url.split(".").pop()?.toLowerCase();
|
| 30 |
if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension || "")) {
|
| 31 |
return "image";
|
|
@@ -40,7 +43,13 @@ export const getFileType = (url: string) => {
|
|
| 40 |
export const Uploader = ({ project }: { project: Project | undefined }) => {
|
| 41 |
const { user } = useUser();
|
| 42 |
const { openLoginModal } = useLoginModal();
|
| 43 |
-
const {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
|
| 45 |
|
| 46 |
const [open, setOpen] = useState(false);
|
|
@@ -112,6 +121,27 @@ export const Uploader = ({ project }: { project: Project | undefined }) => {
|
|
| 112 |
</p>
|
| 113 |
</header>
|
| 114 |
<main className="space-y-4 p-5">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
<div>
|
| 116 |
<p className="text-xs text-left text-neutral-700 mb-2">
|
| 117 |
Uploaded Media Files
|
|
|
|
| 2 |
import {
|
| 3 |
CheckCircle,
|
| 4 |
ImageIcon,
|
|
|
|
|
|
|
| 5 |
Paperclip,
|
| 6 |
Upload,
|
| 7 |
Video,
|
| 8 |
Music,
|
| 9 |
FileVideo,
|
| 10 |
+
Lock,
|
| 11 |
} from "lucide-react";
|
| 12 |
import Image from "next/image";
|
| 13 |
|
|
|
|
| 23 |
import { useEditor } from "@/hooks/useEditor";
|
| 24 |
import { useAi } from "@/hooks/useAi";
|
| 25 |
import { useLoginModal } from "@/components/contexts/login-context";
|
| 26 |
+
import Link from "next/link";
|
| 27 |
|
| 28 |
export const getFileType = (url: string) => {
|
| 29 |
+
if (typeof url !== "string") {
|
| 30 |
+
return "unknown";
|
| 31 |
+
}
|
| 32 |
const extension = url.split(".").pop()?.toLowerCase();
|
| 33 |
if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(extension || "")) {
|
| 34 |
return "image";
|
|
|
|
| 43 |
export const Uploader = ({ project }: { project: Project | undefined }) => {
|
| 44 |
const { user } = useUser();
|
| 45 |
const { openLoginModal } = useLoginModal();
|
| 46 |
+
const {
|
| 47 |
+
uploadFiles,
|
| 48 |
+
isUploading,
|
| 49 |
+
files,
|
| 50 |
+
globalEditorLoading,
|
| 51 |
+
project: editorProject,
|
| 52 |
+
} = useEditor();
|
| 53 |
const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
|
| 54 |
|
| 55 |
const [open, setOpen] = useState(false);
|
|
|
|
| 121 |
</p>
|
| 122 |
</header>
|
| 123 |
<main className="space-y-4 p-5">
|
| 124 |
+
{editorProject?.private && (
|
| 125 |
+
<div className="flex items-center justify-center flex-col gap-2 bg-amber-500/10 rounded-md p-3 border border-amber-500/10">
|
| 126 |
+
<Lock className="size-4 text-lg text-amber-700" />
|
| 127 |
+
<p className="text-xs text-amber-700">
|
| 128 |
+
You can upload media files to your private project, but
|
| 129 |
+
probably won't be able to see them in the preview.
|
| 130 |
+
</p>
|
| 131 |
+
<Link
|
| 132 |
+
href={`https://huggingface.co/spaces/${editorProject.space_id}/settings`}
|
| 133 |
+
target="_blank"
|
| 134 |
+
>
|
| 135 |
+
<Button
|
| 136 |
+
variant="black"
|
| 137 |
+
size="xs"
|
| 138 |
+
className="!bg-amber-600 !text-white"
|
| 139 |
+
>
|
| 140 |
+
Make it public
|
| 141 |
+
</Button>
|
| 142 |
+
</Link>
|
| 143 |
+
</div>
|
| 144 |
+
)}
|
| 145 |
<div>
|
| 146 |
<p className="text-xs text-left text-neutral-700 mb-2">
|
| 147 |
Uploaded Media Files
|
components/editor/file-browser/index.tsx
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useMemo } from "react";
|
| 4 |
+
import {
|
| 5 |
+
FolderOpen,
|
| 6 |
+
FileCode2,
|
| 7 |
+
Folder,
|
| 8 |
+
ChevronRight,
|
| 9 |
+
ChevronDown,
|
| 10 |
+
} from "lucide-react";
|
| 11 |
+
import classNames from "classnames";
|
| 12 |
+
|
| 13 |
+
import { Page } from "@/types";
|
| 14 |
+
import { useEditor } from "@/hooks/useEditor";
|
| 15 |
+
import { Button } from "@/components/ui/button";
|
| 16 |
+
import {
|
| 17 |
+
Sheet,
|
| 18 |
+
SheetContent,
|
| 19 |
+
SheetHeader,
|
| 20 |
+
SheetTitle,
|
| 21 |
+
SheetTrigger,
|
| 22 |
+
} from "@/components/ui/sheet";
|
| 23 |
+
import {
|
| 24 |
+
Tooltip,
|
| 25 |
+
TooltipContent,
|
| 26 |
+
TooltipProvider,
|
| 27 |
+
TooltipTrigger,
|
| 28 |
+
} from "@/components/ui/tooltip";
|
| 29 |
+
|
| 30 |
+
interface FileNode {
|
| 31 |
+
name: string;
|
| 32 |
+
path: string;
|
| 33 |
+
type: "file" | "folder";
|
| 34 |
+
children?: FileNode[];
|
| 35 |
+
page?: Page;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function FileBrowser() {
|
| 39 |
+
const { pages, currentPage, setCurrentPage, globalEditorLoading, project } =
|
| 40 |
+
useEditor();
|
| 41 |
+
const [open, setOpen] = useState(false);
|
| 42 |
+
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
|
| 43 |
+
new Set(["/"])
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
const toggleFolder = (path: string) => {
|
| 47 |
+
setExpandedFolders((prev) => {
|
| 48 |
+
const next = new Set(prev);
|
| 49 |
+
if (next.has(path)) {
|
| 50 |
+
next.delete(path);
|
| 51 |
+
} else {
|
| 52 |
+
next.add(path);
|
| 53 |
+
}
|
| 54 |
+
return next;
|
| 55 |
+
});
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
const fileTree = useMemo(() => {
|
| 59 |
+
const root: FileNode = {
|
| 60 |
+
name: "root",
|
| 61 |
+
path: "/",
|
| 62 |
+
type: "folder",
|
| 63 |
+
children: [],
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
pages.forEach((page) => {
|
| 67 |
+
const parts = page.path.split("/").filter(Boolean);
|
| 68 |
+
let currentNode = root;
|
| 69 |
+
|
| 70 |
+
parts.forEach((part, index) => {
|
| 71 |
+
const isFile = index === parts.length - 1;
|
| 72 |
+
const currentPath = "/" + parts.slice(0, index + 1).join("/");
|
| 73 |
+
|
| 74 |
+
if (!currentNode.children) {
|
| 75 |
+
currentNode.children = [];
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
let existingNode = currentNode.children.find((n) => n.name === part);
|
| 79 |
+
|
| 80 |
+
if (!existingNode) {
|
| 81 |
+
existingNode = {
|
| 82 |
+
name: part,
|
| 83 |
+
path: currentPath,
|
| 84 |
+
type: isFile ? "file" : "folder",
|
| 85 |
+
children: isFile ? undefined : [],
|
| 86 |
+
page: isFile ? page : undefined,
|
| 87 |
+
};
|
| 88 |
+
currentNode.children.push(existingNode);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if (!isFile) {
|
| 92 |
+
currentNode = existingNode;
|
| 93 |
+
}
|
| 94 |
+
});
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
// Sort: folders first, then files, both alphabetically
|
| 98 |
+
const sortNodes = (nodes: FileNode[] = []): FileNode[] => {
|
| 99 |
+
return nodes
|
| 100 |
+
.sort((a, b) => {
|
| 101 |
+
if (a.type !== b.type) {
|
| 102 |
+
return a.type === "folder" ? -1 : 1;
|
| 103 |
+
}
|
| 104 |
+
return a.name.localeCompare(b.name);
|
| 105 |
+
})
|
| 106 |
+
.map((node) => ({
|
| 107 |
+
...node,
|
| 108 |
+
children: node.children ? sortNodes(node.children) : undefined,
|
| 109 |
+
}));
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
root.children = sortNodes(root.children);
|
| 113 |
+
return root;
|
| 114 |
+
}, [pages]);
|
| 115 |
+
|
| 116 |
+
const getFileIcon = (path: string) => {
|
| 117 |
+
const extension = path.split(".").pop()?.toLowerCase();
|
| 118 |
+
|
| 119 |
+
switch (extension) {
|
| 120 |
+
case "html":
|
| 121 |
+
return (
|
| 122 |
+
<svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
|
| 123 |
+
<path
|
| 124 |
+
d="M5.902 27.201L3.656 2h24.688l-2.249 25.197L15.985 30 5.902 27.201z"
|
| 125 |
+
fill="#E44D26"
|
| 126 |
+
/>
|
| 127 |
+
<path
|
| 128 |
+
d="M16 27.858l8.17-2.265 1.922-21.532H16v23.797z"
|
| 129 |
+
fill="#F16529"
|
| 130 |
+
/>
|
| 131 |
+
<path
|
| 132 |
+
d="M16 13.407h4.09l.282-3.165H16V7.151h7.75l-.074.829-.759 8.518H16v-3.091z"
|
| 133 |
+
fill="#EBEBEB"
|
| 134 |
+
/>
|
| 135 |
+
<path
|
| 136 |
+
d="M16 21.434l-.014.004-3.442-.929-.22-2.465H9.221l.433 4.852 6.332 1.758.014-.004v-3.216z"
|
| 137 |
+
fill="#EBEBEB"
|
| 138 |
+
/>
|
| 139 |
+
<path
|
| 140 |
+
d="M19.90 16.18l-.372 4.148-3.543.956v3.216l6.336-1.755.047-.522.537-6.043H19.90z"
|
| 141 |
+
fill="#FFF"
|
| 142 |
+
/>
|
| 143 |
+
<path
|
| 144 |
+
d="M16 7.151v3.091h-7.3l-.062-.695-.141-1.567-.074-.829H16zM16 13.407v3.091h-3.399l-.062-.695-.14-1.566-.074-.83H16z"
|
| 145 |
+
fill="#FFF"
|
| 146 |
+
/>
|
| 147 |
+
</svg>
|
| 148 |
+
);
|
| 149 |
+
case "css":
|
| 150 |
+
return (
|
| 151 |
+
<svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
|
| 152 |
+
<path
|
| 153 |
+
d="M5.902 27.201L3.656 2h24.688l-2.249 25.197L15.985 30 5.902 27.201z"
|
| 154 |
+
fill="#1572B6"
|
| 155 |
+
/>
|
| 156 |
+
<path
|
| 157 |
+
d="M16 27.858l8.17-2.265 1.922-21.532H16v23.797z"
|
| 158 |
+
fill="#33A9DC"
|
| 159 |
+
/>
|
| 160 |
+
<path
|
| 161 |
+
d="M16 13.191h4.09l.282-3.165H16V6.935h7.75l-.074.829-.759 8.518H16v-3.091z"
|
| 162 |
+
fill="#FFF"
|
| 163 |
+
/>
|
| 164 |
+
<path
|
| 165 |
+
d="M16.019 21.218l-.014.004-3.442-.929-.22-2.465H9.24l.433 4.852 6.331 1.758.015-.004v-3.216z"
|
| 166 |
+
fill="#EBEBEB"
|
| 167 |
+
/>
|
| 168 |
+
<path
|
| 169 |
+
d="M19.827 16.151l-.372 4.148-3.436.929v3.216l6.336-1.755.047-.522.726-8.016h-7.636v3h4.335z"
|
| 170 |
+
fill="#FFF"
|
| 171 |
+
/>
|
| 172 |
+
<path
|
| 173 |
+
d="M16.011 6.935v3.091h-7.3l-.062-.695-.141-1.567-.074-.829h7.577zM16 13.191v3.091h-3.399l-.062-.695-.14-1.566-.074-.83H16z"
|
| 174 |
+
fill="#EBEBEB"
|
| 175 |
+
/>
|
| 176 |
+
</svg>
|
| 177 |
+
);
|
| 178 |
+
case "js":
|
| 179 |
+
case "jsx":
|
| 180 |
+
return (
|
| 181 |
+
<svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
|
| 182 |
+
<rect width="32" height="32" rx="2" fill="#F7DF1E" />
|
| 183 |
+
<path
|
| 184 |
+
d="M20.63 22.3c.54.88 1.24 1.53 2.48 1.53.98 0 1.6-.48 1.6-1.16 0-.8-.64-1.1-1.72-1.57l-.59-.25c-1.7-.72-2.83-1.63-2.83-3.55 0-1.77 1.35-3.12 3.46-3.12 1.5 0 2.58.52 3.36 1.9l-1.84 1.18c-.4-.72-.84-1-1.51-1-.69 0-1.12.43-1.12 1 0 .7.43 1 1.43 1.43l.59.25c2 .86 3.13 1.73 3.13 3.7 0 2.12-1.66 3.3-3.9 3.3-2.18 0-3.6-1.04-4.3-2.4l1.96-1.12z"
|
| 185 |
+
fill="#000"
|
| 186 |
+
/>
|
| 187 |
+
<path
|
| 188 |
+
d="M11.14 22.56c.35.62.67 1.15 1.44 1.15.74 0 1.2-.29 1.2-1.42V14.7h2.4v7.63c0 2.34-1.37 3.4-3.37 3.4-1.8 0-2.85-.94-3.38-2.06l1.71-1.1z"
|
| 189 |
+
fill="#000"
|
| 190 |
+
/>
|
| 191 |
+
</svg>
|
| 192 |
+
);
|
| 193 |
+
case "json":
|
| 194 |
+
return (
|
| 195 |
+
<svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
|
| 196 |
+
<rect width="32" height="32" rx="2" fill="#F7DF1E" />
|
| 197 |
+
<path
|
| 198 |
+
d="M16 2L4 8v16l12 6 12-6V8L16 2zm8.8 20.4l-8.8 4.4-8.8-4.4V9.6l8.8-4.4 8.8 4.4v12.8z"
|
| 199 |
+
fill="#000"
|
| 200 |
+
opacity="0.15"
|
| 201 |
+
/>
|
| 202 |
+
<text
|
| 203 |
+
x="50%"
|
| 204 |
+
y="50%"
|
| 205 |
+
dominantBaseline="middle"
|
| 206 |
+
textAnchor="middle"
|
| 207 |
+
fill="#000"
|
| 208 |
+
fontSize="14"
|
| 209 |
+
fontWeight="600"
|
| 210 |
+
>
|
| 211 |
+
{}
|
| 212 |
+
</text>
|
| 213 |
+
</svg>
|
| 214 |
+
);
|
| 215 |
+
default:
|
| 216 |
+
return <FileCode2 className="size-4 shrink-0 text-neutral-400" />;
|
| 217 |
+
}
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
+
const getFileExtension = (path: string) => {
|
| 221 |
+
return path.split(".").pop()?.toLowerCase() || "";
|
| 222 |
+
};
|
| 223 |
+
|
| 224 |
+
const getLanguageTag = (path: string) => {
|
| 225 |
+
const extension = path.split(".").pop()?.toLowerCase();
|
| 226 |
+
|
| 227 |
+
switch (extension) {
|
| 228 |
+
case "html":
|
| 229 |
+
return {
|
| 230 |
+
name: "HTML",
|
| 231 |
+
color: "bg-orange-500/20 border-orange-500/30 text-orange-400",
|
| 232 |
+
};
|
| 233 |
+
case "css":
|
| 234 |
+
return {
|
| 235 |
+
name: "CSS",
|
| 236 |
+
color: "bg-blue-500/20 border-blue-500/30 text-blue-400",
|
| 237 |
+
};
|
| 238 |
+
case "js":
|
| 239 |
+
return {
|
| 240 |
+
name: "JS",
|
| 241 |
+
color: "bg-yellow-500/20 border-yellow-500/30 text-yellow-400",
|
| 242 |
+
};
|
| 243 |
+
case "json":
|
| 244 |
+
return {
|
| 245 |
+
name: "JSON",
|
| 246 |
+
color: "bg-yellow-500/20 border-yellow-500/30 text-yellow-400",
|
| 247 |
+
};
|
| 248 |
+
default:
|
| 249 |
+
return {
|
| 250 |
+
name: extension?.toUpperCase() || "FILE",
|
| 251 |
+
color: "bg-neutral-500/20 border-neutral-500/30 text-neutral-400",
|
| 252 |
+
};
|
| 253 |
+
}
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
const currentPageData = pages.find((p) => p.path === currentPage);
|
| 257 |
+
|
| 258 |
+
const renderFileTree = (nodes: FileNode[], depth = 0) => {
|
| 259 |
+
return nodes.map((node) => {
|
| 260 |
+
if (node.type === "folder") {
|
| 261 |
+
const isExpanded = expandedFolders.has(node.path);
|
| 262 |
+
return (
|
| 263 |
+
<div key={node.path}>
|
| 264 |
+
<div
|
| 265 |
+
className="flex items-center gap-2 px-3 py-1 cursor-pointer text-[13px] group transition-colors hover:bg-neutral-800"
|
| 266 |
+
style={{ paddingLeft: `${depth * 12 + 12}px` }}
|
| 267 |
+
onClick={() => toggleFolder(node.path)}
|
| 268 |
+
>
|
| 269 |
+
{isExpanded ? (
|
| 270 |
+
<ChevronDown className="size-3.5 text-neutral-400 shrink-0" />
|
| 271 |
+
) : (
|
| 272 |
+
<ChevronRight className="size-3.5 text-neutral-400 shrink-0" />
|
| 273 |
+
)}
|
| 274 |
+
<Folder className="size-4 text-blue-400 shrink-0" />
|
| 275 |
+
<span className="text-neutral-300 truncate flex-1 font-normal">
|
| 276 |
+
{node.name}
|
| 277 |
+
</span>
|
| 278 |
+
<span className="text-[10px] text-neutral-500">
|
| 279 |
+
{node.children?.length || 0}
|
| 280 |
+
</span>
|
| 281 |
+
</div>
|
| 282 |
+
{isExpanded &&
|
| 283 |
+
node.children &&
|
| 284 |
+
renderFileTree(node.children, depth + 1)}
|
| 285 |
+
</div>
|
| 286 |
+
);
|
| 287 |
+
} else {
|
| 288 |
+
const isActive = currentPage === node.page?.path;
|
| 289 |
+
return (
|
| 290 |
+
<div
|
| 291 |
+
key={node.path}
|
| 292 |
+
className={classNames(
|
| 293 |
+
"flex items-center gap-2.5 px-3 py-1 cursor-pointer text-[13px] group transition-colors relative",
|
| 294 |
+
{
|
| 295 |
+
"bg-neutral-700 text-white": isActive,
|
| 296 |
+
"text-neutral-300 hover:bg-neutral-800": !isActive,
|
| 297 |
+
}
|
| 298 |
+
)}
|
| 299 |
+
style={{ paddingLeft: `${depth * 12 + 12 + 16}px` }}
|
| 300 |
+
onClick={() => {
|
| 301 |
+
if (node.page) {
|
| 302 |
+
setCurrentPage(node.page.path);
|
| 303 |
+
setOpen(false);
|
| 304 |
+
}
|
| 305 |
+
}}
|
| 306 |
+
>
|
| 307 |
+
<div className="w-4 flex justify-center shrink-0">
|
| 308 |
+
{getFileIcon(node.name)}
|
| 309 |
+
</div>
|
| 310 |
+
|
| 311 |
+
<span className="truncate flex-1 font-normal">{node.name}</span>
|
| 312 |
+
|
| 313 |
+
<span
|
| 314 |
+
className={classNames(
|
| 315 |
+
"text-[10px] px-1.5 py-0.5 rounded uppercase font-semibold transition-opacity shrink-0",
|
| 316 |
+
isActive
|
| 317 |
+
? `opacity-100 ${getLanguageTag(node.name).color}`
|
| 318 |
+
: "opacity-0 group-hover:opacity-100 bg-white/5 text-neutral-500"
|
| 319 |
+
)}
|
| 320 |
+
>
|
| 321 |
+
{getFileExtension(node.name)}
|
| 322 |
+
</span>
|
| 323 |
+
|
| 324 |
+
{isActive && (
|
| 325 |
+
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-blue-500" />
|
| 326 |
+
)}
|
| 327 |
+
</div>
|
| 328 |
+
);
|
| 329 |
+
}
|
| 330 |
+
});
|
| 331 |
+
};
|
| 332 |
+
|
| 333 |
+
return (
|
| 334 |
+
<div>
|
| 335 |
+
{/* VS Code-style Tab Bar */}
|
| 336 |
+
<div className="w-full flex items-center bg-neutral-900 min-h-[35px] border-b border-neutral-800">
|
| 337 |
+
<div className="flex items-stretch overflow-x-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-neutral-700">
|
| 338 |
+
{currentPageData && (
|
| 339 |
+
<div className="flex items-center gap-2 px-4 py-2.5 bg-neutral-900 border-r border-neutral-800 text-sm min-w-0 relative group">
|
| 340 |
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
| 341 |
+
{getFileIcon(currentPageData.path)}
|
| 342 |
+
<span className="text-neutral-300 truncate font-normal text-[13px]">
|
| 343 |
+
{currentPageData.path}
|
| 344 |
+
</span>
|
| 345 |
+
<span
|
| 346 |
+
className={classNames(
|
| 347 |
+
"text-[9px] px-1.5 py-0.5 rounded border backdrop-blur-sm font-semibold uppercase tracking-wide",
|
| 348 |
+
getLanguageTag(currentPageData.path).color
|
| 349 |
+
)}
|
| 350 |
+
>
|
| 351 |
+
{getLanguageTag(currentPageData.path).name}
|
| 352 |
+
</span>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
)}
|
| 356 |
+
</div>
|
| 357 |
+
|
| 358 |
+
{/* Open Explorer Button */}
|
| 359 |
+
<TooltipProvider>
|
| 360 |
+
<Tooltip>
|
| 361 |
+
<Sheet open={open} onOpenChange={setOpen} modal={false}>
|
| 362 |
+
<TooltipTrigger asChild>
|
| 363 |
+
<SheetTrigger asChild>
|
| 364 |
+
<Button
|
| 365 |
+
size="sm"
|
| 366 |
+
variant="ghost"
|
| 367 |
+
disabled={pages.length === 0 || globalEditorLoading}
|
| 368 |
+
className="ml-auto mr-2 text-neutral-300 hover:text-white hover:bg-neutral-800 h-7 text-[13px] font-normal gap-1.5"
|
| 369 |
+
>
|
| 370 |
+
<FolderOpen className="size-3.5" />
|
| 371 |
+
<span className="hidden sm:inline">Files</span>
|
| 372 |
+
<span className="text-[11px] px-1.5 py-0.5 rounded bg-neutral-800 text-neutral-500 font-semibold">
|
| 373 |
+
{pages.length}
|
| 374 |
+
</span>
|
| 375 |
+
</Button>
|
| 376 |
+
</SheetTrigger>
|
| 377 |
+
</TooltipTrigger>
|
| 378 |
+
<TooltipContent
|
| 379 |
+
side="bottom"
|
| 380 |
+
className="bg-neutral-800 border-neutral-700 text-neutral-300 text-xs"
|
| 381 |
+
>
|
| 382 |
+
<p>
|
| 383 |
+
Open File Explorer ({pages.length}{" "}
|
| 384 |
+
{pages.length === 1 ? "file" : "files"})
|
| 385 |
+
</p>
|
| 386 |
+
</TooltipContent>
|
| 387 |
+
|
| 388 |
+
<SheetContent
|
| 389 |
+
side="left"
|
| 390 |
+
className="w-[320px] bg-neutral-900 border-neutral-800 p-0"
|
| 391 |
+
>
|
| 392 |
+
<SheetHeader className="px-5 py-2.5 border-b border-neutral-800 space-y-0">
|
| 393 |
+
<SheetTitle className="flex items-center gap-2">
|
| 394 |
+
<FolderOpen className="size-4 text-neutral-300" />
|
| 395 |
+
<span className="text-[11px] uppercase tracking-wider text-neutral-300 font-semibold">
|
| 396 |
+
Explorer
|
| 397 |
+
</span>
|
| 398 |
+
</SheetTitle>
|
| 399 |
+
</SheetHeader>
|
| 400 |
+
|
| 401 |
+
<div className="px-3 py-3 border-b border-neutral-800">
|
| 402 |
+
<div className="flex items-center gap-2 px-2 py-1">
|
| 403 |
+
<svg
|
| 404 |
+
className="size-4 text-neutral-300"
|
| 405 |
+
fill="currentColor"
|
| 406 |
+
viewBox="0 0 16 16"
|
| 407 |
+
>
|
| 408 |
+
<path d="M1.5 1h11l2 2v10l-2 2h-11l-2-2V3l2-2zm0 1l-1 1v10l1 1h11l1-1V3l-1-1h-11z" />
|
| 409 |
+
<path d="M7.5 4.5v3h-3v1h3v3h1v-3h3v-1h-3v-3h-1z" />
|
| 410 |
+
</svg>
|
| 411 |
+
<span className="text-[13px] text-neutral-300 font-normal">
|
| 412 |
+
{project?.space_id || "No space selected"}
|
| 413 |
+
</span>
|
| 414 |
+
<span className="ml-auto text-[11px] text-neutral-500">
|
| 415 |
+
{pages.length || 0}
|
| 416 |
+
</span>
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
|
| 420 |
+
<div
|
| 421 |
+
className="py-1 overflow-y-auto"
|
| 422 |
+
style={{ height: "calc(100vh - 160px)" }}
|
| 423 |
+
>
|
| 424 |
+
{fileTree.children && renderFileTree(fileTree.children)}
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<div className="absolute bottom-0 left-0 right-0 px-5 py-3 border-t border-neutral-800 bg-neutral-900">
|
| 428 |
+
<div className="grid grid-cols-2 gap-2 text-[11px]">
|
| 429 |
+
<div className="flex items-center gap-2 text-neutral-500">
|
| 430 |
+
<div className="size-2 rounded-full bg-orange-600" />
|
| 431 |
+
<span>
|
| 432 |
+
HTML:{" "}
|
| 433 |
+
{pages.filter((p) => p.path.endsWith(".html")).length}
|
| 434 |
+
</span>
|
| 435 |
+
</div>
|
| 436 |
+
<div className="flex items-center gap-2 text-neutral-500">
|
| 437 |
+
<div className="size-2 rounded-full bg-blue-600" />
|
| 438 |
+
<span>
|
| 439 |
+
CSS:{" "}
|
| 440 |
+
{pages.filter((p) => p.path.endsWith(".css")).length}
|
| 441 |
+
</span>
|
| 442 |
+
</div>
|
| 443 |
+
<div className="flex items-center gap-2 text-neutral-500">
|
| 444 |
+
<div className="size-2 rounded-full bg-yellow-400" />
|
| 445 |
+
<span>
|
| 446 |
+
JS: {pages.filter((p) => p.path.endsWith(".js")).length}
|
| 447 |
+
</span>
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
</SheetContent>
|
| 452 |
+
</Sheet>
|
| 453 |
+
</Tooltip>
|
| 454 |
+
</TooltipProvider>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
);
|
| 458 |
+
}
|
components/editor/header/index.tsx
CHANGED
|
@@ -1,4 +1,11 @@
|
|
| 1 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import Image from "next/image";
|
| 3 |
import Link from "next/link";
|
| 4 |
|
|
@@ -82,15 +89,24 @@ export function Header() {
|
|
| 82 |
<div className="flex items-center gap-2">
|
| 83 |
{project?.space_id && (
|
| 84 |
<Link
|
| 85 |
-
href={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
target="_blank"
|
| 87 |
>
|
| 88 |
<Button
|
| 89 |
size="xs"
|
| 90 |
variant="bordered"
|
| 91 |
-
className="flex items-center gap-1 justify-center
|
| 92 |
>
|
|
|
|
| 93 |
See Live Preview
|
|
|
|
| 94 |
</Button>
|
| 95 |
</Link>
|
| 96 |
)}
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
ArrowRight,
|
| 3 |
+
HelpCircle,
|
| 4 |
+
RefreshCcw,
|
| 5 |
+
Lock,
|
| 6 |
+
Eye,
|
| 7 |
+
Sparkles,
|
| 8 |
+
} from "lucide-react";
|
| 9 |
import Image from "next/image";
|
| 10 |
import Link from "next/link";
|
| 11 |
|
|
|
|
| 89 |
<div className="flex items-center gap-2">
|
| 90 |
{project?.space_id && (
|
| 91 |
<Link
|
| 92 |
+
href={
|
| 93 |
+
project?.private
|
| 94 |
+
? `https://huggingface.co/spaces/${project.space_id}`
|
| 95 |
+
: `https://${project.space_id.replaceAll(
|
| 96 |
+
"/",
|
| 97 |
+
"-"
|
| 98 |
+
)}.static.hf.space`
|
| 99 |
+
}
|
| 100 |
target="_blank"
|
| 101 |
>
|
| 102 |
<Button
|
| 103 |
size="xs"
|
| 104 |
variant="bordered"
|
| 105 |
+
className="flex items-center gap-1.5 justify-center bg-gradient-to-r from-emerald-500/20 to-cyan-500/20 hover:from-emerald-500/30 hover:to-cyan-500/30 border-emerald-500/30 text-emerald-400 hover:text-emerald-300 backdrop-blur-sm shadow-lg hover:shadow-emerald-500/20 transition-all duration-300 max-lg:hidden font-medium"
|
| 106 |
>
|
| 107 |
+
<Eye className="size-3.5" />
|
| 108 |
See Live Preview
|
| 109 |
+
<Sparkles className="size-3" />
|
| 110 |
</Button>
|
| 111 |
</Link>
|
| 112 |
)}
|
components/editor/index.tsx
CHANGED
|
@@ -10,9 +10,8 @@ import Editor from "@monaco-editor/react";
|
|
| 10 |
import { useEditor } from "@/hooks/useEditor";
|
| 11 |
import { Header } from "@/components/editor/header";
|
| 12 |
import { useAi } from "@/hooks/useAi";
|
| 13 |
-
import { defaultHTML } from "@/lib/consts";
|
| 14 |
|
| 15 |
-
import {
|
| 16 |
import { AskAi } from "./ask-ai";
|
| 17 |
import { Preview } from "./preview";
|
| 18 |
import { SaveChangesPopup } from "./save-changes-popup";
|
|
@@ -37,6 +36,7 @@ export const AppEditor = ({
|
|
| 37 |
currentCommit,
|
| 38 |
hasUnsavedChanges,
|
| 39 |
saveChanges,
|
|
|
|
| 40 |
pages,
|
| 41 |
} = useEditor(namespace, repoId);
|
| 42 |
const { isAiWorking } = useAi();
|
|
@@ -63,6 +63,22 @@ export const AppEditor = ({
|
|
| 63 |
}
|
| 64 |
}, [hasUnsavedChanges, isAiWorking]);
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
return (
|
| 67 |
<section className="h-screen w-full bg-neutral-950 flex flex-col">
|
| 68 |
<Header />
|
|
@@ -77,16 +93,16 @@ export const AppEditor = ({
|
|
| 77 |
}
|
| 78 |
)}
|
| 79 |
>
|
| 80 |
-
<
|
| 81 |
<CopyIcon
|
| 82 |
className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
|
| 83 |
onClick={() => {
|
| 84 |
copyToClipboard(currentPageData.html);
|
| 85 |
-
toast.success(
|
| 86 |
}}
|
| 87 |
/>
|
| 88 |
<Editor
|
| 89 |
-
|
| 90 |
theme="vs-dark"
|
| 91 |
loading={<Loading overlay={false} />}
|
| 92 |
className="h-full absolute left-0 top-0 lg:min-w-[600px]"
|
|
@@ -99,13 +115,16 @@ export const AppEditor = ({
|
|
| 99 |
horizontal: "hidden",
|
| 100 |
},
|
| 101 |
wordWrap: "on",
|
| 102 |
-
readOnly: !!isAiWorking || !!currentCommit,
|
| 103 |
readOnlyMessage: {
|
| 104 |
-
value:
|
|
|
|
|
|
|
| 105 |
? "You can't edit the code, as this is an old version of the project."
|
| 106 |
: "Wait for DeepSite to finish working...",
|
| 107 |
isTrusted: true,
|
| 108 |
},
|
|
|
|
| 109 |
}}
|
| 110 |
value={currentPageData.html}
|
| 111 |
onChange={(value) => {
|
|
|
|
| 10 |
import { useEditor } from "@/hooks/useEditor";
|
| 11 |
import { Header } from "@/components/editor/header";
|
| 12 |
import { useAi } from "@/hooks/useAi";
|
|
|
|
| 13 |
|
| 14 |
+
import { FileBrowser } from "./file-browser";
|
| 15 |
import { AskAi } from "./ask-ai";
|
| 16 |
import { Preview } from "./preview";
|
| 17 |
import { SaveChangesPopup } from "./save-changes-popup";
|
|
|
|
| 36 |
currentCommit,
|
| 37 |
hasUnsavedChanges,
|
| 38 |
saveChanges,
|
| 39 |
+
globalEditorLoading,
|
| 40 |
pages,
|
| 41 |
} = useEditor(namespace, repoId);
|
| 42 |
const { isAiWorking } = useAi();
|
|
|
|
| 63 |
}
|
| 64 |
}, [hasUnsavedChanges, isAiWorking]);
|
| 65 |
|
| 66 |
+
// Determine the language based on file extension
|
| 67 |
+
const editorLanguage = useMemo(() => {
|
| 68 |
+
const path = currentPageData.path;
|
| 69 |
+
if (path.endsWith(".css")) return "css";
|
| 70 |
+
if (path.endsWith(".js")) return "javascript";
|
| 71 |
+
return "html";
|
| 72 |
+
}, [currentPageData.path]);
|
| 73 |
+
|
| 74 |
+
// Determine the copy message based on file type
|
| 75 |
+
const copyMessage = useMemo(() => {
|
| 76 |
+
if (editorLanguage === "css") return "CSS copied to clipboard!";
|
| 77 |
+
if (editorLanguage === "javascript")
|
| 78 |
+
return "JavaScript copied to clipboard!";
|
| 79 |
+
return "HTML copied to clipboard!";
|
| 80 |
+
}, [editorLanguage]);
|
| 81 |
+
|
| 82 |
return (
|
| 83 |
<section className="h-screen w-full bg-neutral-950 flex flex-col">
|
| 84 |
<Header />
|
|
|
|
| 93 |
}
|
| 94 |
)}
|
| 95 |
>
|
| 96 |
+
<FileBrowser />
|
| 97 |
<CopyIcon
|
| 98 |
className="size-4 absolute top-14 right-5 text-neutral-500 hover:text-neutral-300 z-2 cursor-pointer"
|
| 99 |
onClick={() => {
|
| 100 |
copyToClipboard(currentPageData.html);
|
| 101 |
+
toast.success(copyMessage);
|
| 102 |
}}
|
| 103 |
/>
|
| 104 |
<Editor
|
| 105 |
+
language={editorLanguage}
|
| 106 |
theme="vs-dark"
|
| 107 |
loading={<Loading overlay={false} />}
|
| 108 |
className="h-full absolute left-0 top-0 lg:min-w-[600px]"
|
|
|
|
| 115 |
horizontal: "hidden",
|
| 116 |
},
|
| 117 |
wordWrap: "on",
|
| 118 |
+
readOnly: !!isAiWorking || !!currentCommit || globalEditorLoading,
|
| 119 |
readOnlyMessage: {
|
| 120 |
+
value: globalEditorLoading
|
| 121 |
+
? "Wait for DeepSite loading your project..."
|
| 122 |
+
: currentCommit
|
| 123 |
? "You can't edit the code, as this is an old version of the project."
|
| 124 |
: "Wait for DeepSite to finish working...",
|
| 125 |
isTrusted: true,
|
| 126 |
},
|
| 127 |
+
cursorBlinking: "smooth",
|
| 128 |
}}
|
| 129 |
value={currentPageData.html}
|
| 130 |
onChange={(value) => {
|
components/editor/preview/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import { useRef, useState, useEffect } from "react";
|
| 4 |
import { useUpdateEffect } from "react-use";
|
| 5 |
import classNames from "classnames";
|
| 6 |
|
|
@@ -28,7 +28,8 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 28 |
pages,
|
| 29 |
setPages,
|
| 30 |
setCurrentPage,
|
| 31 |
-
|
|
|
|
| 32 |
} = useEditor();
|
| 33 |
const {
|
| 34 |
isEditableModeEnabled,
|
|
@@ -48,70 +49,213 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 48 |
const [throttledHtml, setThrottledHtml] = useState<string>("");
|
| 49 |
const lastUpdateTimeRef = useRef<number>(0);
|
| 50 |
|
| 51 |
-
// For new projects, throttle HTML updates to every 3 seconds
|
| 52 |
useEffect(() => {
|
| 53 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
const now = Date.now();
|
| 55 |
const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
|
| 56 |
|
| 57 |
-
// If this is the first update or 3 seconds have passed, update immediately
|
| 58 |
if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
|
| 59 |
-
|
|
|
|
| 60 |
lastUpdateTimeRef.current = now;
|
| 61 |
} else {
|
| 62 |
-
// Otherwise, schedule an update for when 3 seconds will have passed
|
| 63 |
const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
|
| 64 |
const timer = setTimeout(() => {
|
| 65 |
-
|
|
|
|
| 66 |
lastUpdateTimeRef.current = Date.now();
|
| 67 |
}, timeUntilNextUpdate);
|
| 68 |
return () => clearTimeout(timer);
|
| 69 |
}
|
| 70 |
}
|
| 71 |
-
}, [isNew,
|
| 72 |
|
| 73 |
useEffect(() => {
|
| 74 |
-
if (!isAiWorking && !globalAiLoading &&
|
| 75 |
-
|
|
|
|
| 76 |
}
|
| 77 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
useEffect(() => {
|
| 80 |
if (
|
| 81 |
-
|
| 82 |
!stableHtml &&
|
| 83 |
!isAiWorking &&
|
| 84 |
!globalAiLoading
|
| 85 |
) {
|
| 86 |
-
|
|
|
|
| 87 |
}
|
| 88 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
const cleanupListeners = () => {
|
| 92 |
if (iframeRef?.current?.contentDocument) {
|
| 93 |
const iframeDocument = iframeRef.current.contentDocument;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
iframeDocument.removeEventListener("mouseover", handleMouseOver);
|
| 95 |
iframeDocument.removeEventListener("mouseout", handleMouseOut);
|
| 96 |
iframeDocument.removeEventListener("click", handleClick);
|
| 97 |
}
|
| 98 |
};
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
if (iframeDocument) {
|
| 103 |
cleanupListeners();
|
| 104 |
-
|
| 105 |
-
if (isEditableModeEnabled) {
|
| 106 |
-
iframeDocument.addEventListener("mouseover", handleMouseOver);
|
| 107 |
-
iframeDocument.addEventListener("mouseout", handleMouseOut);
|
| 108 |
-
iframeDocument.addEventListener("click", handleClick);
|
| 109 |
-
}
|
| 110 |
}
|
| 111 |
-
}
|
| 112 |
|
| 113 |
-
return
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
const promoteVersion = async () => {
|
| 117 |
setIsPromotingVersion(true);
|
|
@@ -124,6 +268,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 124 |
setCurrentCommit(null);
|
| 125 |
setPages(res.data.pages);
|
| 126 |
setCurrentPage(res.data.pages[0].path);
|
|
|
|
| 127 |
toast.success("Version promoted successfully");
|
| 128 |
}
|
| 129 |
})
|
|
@@ -176,6 +321,26 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 176 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 177 |
if (iframeDocument) {
|
| 178 |
const targetElement = event.target as HTMLElement;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
if (targetElement !== iframeDocument.body) {
|
| 180 |
setSelectedElement(targetElement);
|
| 181 |
}
|
|
@@ -187,38 +352,85 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 187 |
if (iframeRef?.current) {
|
| 188 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 189 |
if (iframeDocument) {
|
|
|
|
|
|
|
|
|
|
| 190 |
const findClosestAnchor = (
|
| 191 |
element: HTMLElement
|
| 192 |
): HTMLAnchorElement | null => {
|
| 193 |
-
let current = element;
|
| 194 |
while (current && current !== iframeDocument.body) {
|
| 195 |
if (current.tagName === "A") {
|
| 196 |
return current as HTMLAnchorElement;
|
| 197 |
}
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
return null;
|
| 201 |
};
|
| 202 |
|
| 203 |
-
const anchorElement = findClosestAnchor(
|
| 204 |
if (anchorElement) {
|
| 205 |
let href = anchorElement.getAttribute("href");
|
| 206 |
if (href) {
|
| 207 |
event.stopPropagation();
|
| 208 |
event.preventDefault();
|
| 209 |
|
| 210 |
-
if (href.
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
if (targetElement) {
|
| 213 |
targetElement.scrollIntoView({ behavior: "smooth" });
|
| 214 |
}
|
| 215 |
return;
|
| 216 |
}
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
if (isPageExist) {
|
| 221 |
-
|
| 222 |
}
|
| 223 |
}
|
| 224 |
}
|
|
@@ -244,6 +456,7 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 244 |
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"
|
| 245 |
)}
|
| 246 |
/>
|
|
|
|
| 247 |
{!isAiWorking && hoveredElement && isEditableModeEnabled && (
|
| 248 |
<div
|
| 249 |
className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
|
|
@@ -306,20 +519,12 @@ export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
| 306 |
}
|
| 307 |
);
|
| 308 |
}
|
| 309 |
-
//
|
| 310 |
-
|
| 311 |
-
const links =
|
| 312 |
-
iframeRef.current.contentWindow.document.querySelectorAll(
|
| 313 |
-
"a"
|
| 314 |
-
);
|
| 315 |
-
links.forEach((link) => {
|
| 316 |
-
link.addEventListener("click", handleCustomNavigation);
|
| 317 |
-
});
|
| 318 |
-
}
|
| 319 |
}
|
| 320 |
: undefined
|
| 321 |
}
|
| 322 |
-
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
| 323 |
allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
|
| 324 |
/>
|
| 325 |
{!isNew && (
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
|
| 4 |
import { useUpdateEffect } from "react-use";
|
| 5 |
import classNames from "classnames";
|
| 6 |
|
|
|
|
| 28 |
pages,
|
| 29 |
setPages,
|
| 30 |
setCurrentPage,
|
| 31 |
+
previewPage,
|
| 32 |
+
setPreviewPage,
|
| 33 |
} = useEditor();
|
| 34 |
const {
|
| 35 |
isEditableModeEnabled,
|
|
|
|
| 49 |
const [throttledHtml, setThrottledHtml] = useState<string>("");
|
| 50 |
const lastUpdateTimeRef = useRef<number>(0);
|
| 51 |
|
|
|
|
| 52 |
useEffect(() => {
|
| 53 |
+
if (!previewPage && pages.length > 0) {
|
| 54 |
+
const indexPage = pages.find(
|
| 55 |
+
(p) => p.path === "index.html" || p.path === "index" || p.path === "/"
|
| 56 |
+
);
|
| 57 |
+
const firstHtmlPage = pages.find((p) => p.path.endsWith(".html"));
|
| 58 |
+
setPreviewPage(indexPage?.path || firstHtmlPage?.path || "index.html");
|
| 59 |
+
}
|
| 60 |
+
}, [pages, previewPage]);
|
| 61 |
+
|
| 62 |
+
const previewPageData = useMemo(() => {
|
| 63 |
+
const found = pages.find((p) => {
|
| 64 |
+
const normalizedPagePath = p.path.replace(/^\.?\//, "");
|
| 65 |
+
const normalizedPreviewPage = previewPage.replace(/^\.?\//, "");
|
| 66 |
+
return normalizedPagePath === normalizedPreviewPage;
|
| 67 |
+
});
|
| 68 |
+
return found || currentPageData;
|
| 69 |
+
}, [pages, previewPage, currentPageData]);
|
| 70 |
+
|
| 71 |
+
const injectAssetsIntoHtml = useCallback(
|
| 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 |
+
);
|
| 79 |
+
const jsFiles = pages.filter(
|
| 80 |
+
(p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
|
| 81 |
+
);
|
| 82 |
+
|
| 83 |
+
let modifiedHtml = html;
|
| 84 |
+
|
| 85 |
+
// Inject all CSS files
|
| 86 |
+
if (cssFiles.length > 0) {
|
| 87 |
+
const allCssContent = cssFiles
|
| 88 |
+
.map(
|
| 89 |
+
(file) =>
|
| 90 |
+
`<style data-injected-from="${file.path}">\n${file.html}\n</style>`
|
| 91 |
+
)
|
| 92 |
+
.join("\n");
|
| 93 |
+
|
| 94 |
+
if (modifiedHtml.includes("</head>")) {
|
| 95 |
+
modifiedHtml = modifiedHtml.replace(
|
| 96 |
+
"</head>",
|
| 97 |
+
`${allCssContent}\n</head>`
|
| 98 |
+
);
|
| 99 |
+
} else if (modifiedHtml.includes("<head>")) {
|
| 100 |
+
modifiedHtml = modifiedHtml.replace(
|
| 101 |
+
"<head>",
|
| 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(
|
| 113 |
+
new RegExp(
|
| 114 |
+
`<link\\s+[^>]*href=["'][\\.\/]*${escapedPath}["'][^>]*>`,
|
| 115 |
+
"gi"
|
| 116 |
+
),
|
| 117 |
+
""
|
| 118 |
+
);
|
| 119 |
+
});
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// Inject all JS files
|
| 123 |
+
if (jsFiles.length > 0) {
|
| 124 |
+
const allJsContent = jsFiles
|
| 125 |
+
.map(
|
| 126 |
+
(file) =>
|
| 127 |
+
`<script data-injected-from="${file.path}">\n${file.html}\n</script>`
|
| 128 |
+
)
|
| 129 |
+
.join("\n");
|
| 130 |
+
|
| 131 |
+
if (modifiedHtml.includes("</body>")) {
|
| 132 |
+
modifiedHtml = modifiedHtml.replace(
|
| 133 |
+
"</body>",
|
| 134 |
+
`${allJsContent}\n</body>`
|
| 135 |
+
);
|
| 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(
|
| 147 |
+
new RegExp(
|
| 148 |
+
`<script\\s+[^>]*src=["'][\\.\/]*${escapedPath}["'][^>]*><\\/script>`,
|
| 149 |
+
"gi"
|
| 150 |
+
),
|
| 151 |
+
""
|
| 152 |
+
);
|
| 153 |
+
});
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
return modifiedHtml;
|
| 157 |
+
},
|
| 158 |
+
[pages, previewPageData?.path]
|
| 159 |
+
);
|
| 160 |
+
|
| 161 |
+
useEffect(() => {
|
| 162 |
+
if (isNew && previewPageData?.html) {
|
| 163 |
const now = Date.now();
|
| 164 |
const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
|
| 165 |
|
|
|
|
| 166 |
if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
|
| 167 |
+
const processedHtml = injectAssetsIntoHtml(previewPageData.html);
|
| 168 |
+
setThrottledHtml(processedHtml);
|
| 169 |
lastUpdateTimeRef.current = now;
|
| 170 |
} else {
|
|
|
|
| 171 |
const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
|
| 172 |
const timer = setTimeout(() => {
|
| 173 |
+
const processedHtml = injectAssetsIntoHtml(previewPageData.html);
|
| 174 |
+
setThrottledHtml(processedHtml);
|
| 175 |
lastUpdateTimeRef.current = Date.now();
|
| 176 |
}, timeUntilNextUpdate);
|
| 177 |
return () => clearTimeout(timer);
|
| 178 |
}
|
| 179 |
}
|
| 180 |
+
}, [isNew, previewPageData?.html, injectAssetsIntoHtml]);
|
| 181 |
|
| 182 |
useEffect(() => {
|
| 183 |
+
if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
|
| 184 |
+
const processedHtml = injectAssetsIntoHtml(previewPageData.html);
|
| 185 |
+
setStableHtml(processedHtml);
|
| 186 |
}
|
| 187 |
+
}, [
|
| 188 |
+
isAiWorking,
|
| 189 |
+
globalAiLoading,
|
| 190 |
+
previewPageData?.html,
|
| 191 |
+
injectAssetsIntoHtml,
|
| 192 |
+
previewPage,
|
| 193 |
+
]);
|
| 194 |
|
| 195 |
useEffect(() => {
|
| 196 |
if (
|
| 197 |
+
previewPageData?.html &&
|
| 198 |
!stableHtml &&
|
| 199 |
!isAiWorking &&
|
| 200 |
!globalAiLoading
|
| 201 |
) {
|
| 202 |
+
const processedHtml = injectAssetsIntoHtml(previewPageData.html);
|
| 203 |
+
setStableHtml(processedHtml);
|
| 204 |
}
|
| 205 |
+
}, [
|
| 206 |
+
previewPageData?.html,
|
| 207 |
+
stableHtml,
|
| 208 |
+
isAiWorking,
|
| 209 |
+
globalAiLoading,
|
| 210 |
+
injectAssetsIntoHtml,
|
| 211 |
+
]);
|
| 212 |
+
|
| 213 |
+
const setupIframeListeners = () => {
|
| 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,
|
| 221 |
+
true
|
| 222 |
+
);
|
| 223 |
|
| 224 |
+
if (isEditableModeEnabled) {
|
| 225 |
+
iframeDocument.addEventListener("mouseover", handleMouseOver);
|
| 226 |
+
iframeDocument.addEventListener("mouseout", handleMouseOut);
|
| 227 |
+
iframeDocument.addEventListener("click", handleClick);
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
};
|
| 231 |
+
|
| 232 |
+
useEffect(() => {
|
| 233 |
const cleanupListeners = () => {
|
| 234 |
if (iframeRef?.current?.contentDocument) {
|
| 235 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 236 |
+
iframeDocument.removeEventListener(
|
| 237 |
+
"click",
|
| 238 |
+
handleCustomNavigation as any,
|
| 239 |
+
true
|
| 240 |
+
);
|
| 241 |
iframeDocument.removeEventListener("mouseover", handleMouseOver);
|
| 242 |
iframeDocument.removeEventListener("mouseout", handleMouseOut);
|
| 243 |
iframeDocument.removeEventListener("click", handleClick);
|
| 244 |
}
|
| 245 |
};
|
| 246 |
|
| 247 |
+
const timer = setTimeout(() => {
|
| 248 |
+
if (iframeRef?.current?.contentDocument) {
|
|
|
|
| 249 |
cleanupListeners();
|
| 250 |
+
setupIframeListeners();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
}
|
| 252 |
+
}, 50);
|
| 253 |
|
| 254 |
+
return () => {
|
| 255 |
+
clearTimeout(timer);
|
| 256 |
+
cleanupListeners();
|
| 257 |
+
};
|
| 258 |
+
}, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
|
| 259 |
|
| 260 |
const promoteVersion = async () => {
|
| 261 |
setIsPromotingVersion(true);
|
|
|
|
| 268 |
setCurrentCommit(null);
|
| 269 |
setPages(res.data.pages);
|
| 270 |
setCurrentPage(res.data.pages[0].path);
|
| 271 |
+
setPreviewPage(res.data.pages[0].path);
|
| 272 |
toast.success("Version promoted successfully");
|
| 273 |
}
|
| 274 |
})
|
|
|
|
| 321 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 322 |
if (iframeDocument) {
|
| 323 |
const targetElement = event.target as HTMLElement;
|
| 324 |
+
|
| 325 |
+
const findClosestAnchor = (
|
| 326 |
+
element: HTMLElement
|
| 327 |
+
): HTMLAnchorElement | null => {
|
| 328 |
+
let current = element;
|
| 329 |
+
while (current && current !== iframeDocument.body) {
|
| 330 |
+
if (current.tagName === "A") {
|
| 331 |
+
return current as HTMLAnchorElement;
|
| 332 |
+
}
|
| 333 |
+
current = current.parentElement as HTMLElement;
|
| 334 |
+
}
|
| 335 |
+
return null;
|
| 336 |
+
};
|
| 337 |
+
|
| 338 |
+
const anchorElement = findClosestAnchor(targetElement);
|
| 339 |
+
|
| 340 |
+
if (anchorElement) {
|
| 341 |
+
return;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
if (targetElement !== iframeDocument.body) {
|
| 345 |
setSelectedElement(targetElement);
|
| 346 |
}
|
|
|
|
| 352 |
if (iframeRef?.current) {
|
| 353 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 354 |
if (iframeDocument) {
|
| 355 |
+
const path = event.composedPath();
|
| 356 |
+
const actualTarget = path[0] as HTMLElement;
|
| 357 |
+
|
| 358 |
const findClosestAnchor = (
|
| 359 |
element: HTMLElement
|
| 360 |
): HTMLAnchorElement | null => {
|
| 361 |
+
let current: HTMLElement | null = element;
|
| 362 |
while (current && current !== iframeDocument.body) {
|
| 363 |
if (current.tagName === "A") {
|
| 364 |
return current as HTMLAnchorElement;
|
| 365 |
}
|
| 366 |
+
const parent: Node | null = current.parentNode;
|
| 367 |
+
if (parent instanceof ShadowRoot) {
|
| 368 |
+
current = parent.host as HTMLElement;
|
| 369 |
+
} else if (parent instanceof HTMLElement) {
|
| 370 |
+
current = parent;
|
| 371 |
+
} else {
|
| 372 |
+
break;
|
| 373 |
+
}
|
| 374 |
}
|
| 375 |
return null;
|
| 376 |
};
|
| 377 |
|
| 378 |
+
const anchorElement = findClosestAnchor(actualTarget);
|
| 379 |
if (anchorElement) {
|
| 380 |
let href = anchorElement.getAttribute("href");
|
| 381 |
if (href) {
|
| 382 |
event.stopPropagation();
|
| 383 |
event.preventDefault();
|
| 384 |
|
| 385 |
+
if (href.startsWith("#")) {
|
| 386 |
+
let targetElement = iframeDocument.querySelector(href);
|
| 387 |
+
|
| 388 |
+
if (!targetElement) {
|
| 389 |
+
const searchInShadows = (
|
| 390 |
+
root: Document | ShadowRoot
|
| 391 |
+
): Element | null => {
|
| 392 |
+
const elements = root.querySelectorAll("*");
|
| 393 |
+
for (const el of elements) {
|
| 394 |
+
if (el.shadowRoot) {
|
| 395 |
+
const found = el.shadowRoot.querySelector(href);
|
| 396 |
+
if (found) return found;
|
| 397 |
+
const nested = searchInShadows(el.shadowRoot);
|
| 398 |
+
if (nested) return nested;
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
return null;
|
| 402 |
+
};
|
| 403 |
+
targetElement = searchInShadows(iframeDocument);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
if (targetElement) {
|
| 407 |
targetElement.scrollIntoView({ behavior: "smooth" });
|
| 408 |
}
|
| 409 |
return;
|
| 410 |
}
|
| 411 |
|
| 412 |
+
let normalizedHref = href.replace(/^\.?\//, "");
|
| 413 |
+
|
| 414 |
+
if (normalizedHref === "" || normalizedHref === "/") {
|
| 415 |
+
normalizedHref = "index.html";
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
const hashIndex = normalizedHref.indexOf("#");
|
| 419 |
+
if (hashIndex !== -1) {
|
| 420 |
+
normalizedHref = normalizedHref.substring(0, hashIndex);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
if (!normalizedHref.includes(".")) {
|
| 424 |
+
normalizedHref = normalizedHref + ".html";
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
const isPageExist = pages.some((page) => {
|
| 428 |
+
const pagePath = page.path.replace(/^\.?\//, "");
|
| 429 |
+
return pagePath === normalizedHref;
|
| 430 |
+
});
|
| 431 |
+
|
| 432 |
if (isPageExist) {
|
| 433 |
+
setPreviewPage(normalizedHref);
|
| 434 |
}
|
| 435 |
}
|
| 436 |
}
|
|
|
|
| 456 |
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)] opacity-40"
|
| 457 |
)}
|
| 458 |
/>
|
| 459 |
+
{/* Preview page indicator */}
|
| 460 |
{!isAiWorking && hoveredElement && isEditableModeEnabled && (
|
| 461 |
<div
|
| 462 |
className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
|
|
|
|
| 519 |
}
|
| 520 |
);
|
| 521 |
}
|
| 522 |
+
// Set up event listeners after iframe loads
|
| 523 |
+
setupIframeListeners();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
}
|
| 525 |
: undefined
|
| 526 |
}
|
| 527 |
+
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals allow-forms"
|
| 528 |
allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; clipboard-read; clipboard-write; display-capture; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; serial; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking"
|
| 529 |
/>
|
| 530 |
{!isNew && (
|
components/ui/sheet.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import * as React from "react";
|
| 4 |
+
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
| 5 |
+
import { cva, type VariantProps } from "class-variance-authority";
|
| 6 |
+
import { X } from "lucide-react";
|
| 7 |
+
|
| 8 |
+
import { cn } from "@/lib/utils";
|
| 9 |
+
|
| 10 |
+
const Sheet = SheetPrimitive.Root;
|
| 11 |
+
|
| 12 |
+
const SheetTrigger = SheetPrimitive.Trigger;
|
| 13 |
+
|
| 14 |
+
const SheetClose = SheetPrimitive.Close;
|
| 15 |
+
|
| 16 |
+
const SheetPortal = SheetPrimitive.Portal;
|
| 17 |
+
|
| 18 |
+
const SheetOverlay = React.forwardRef<
|
| 19 |
+
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
| 20 |
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
| 21 |
+
>(({ className, ...props }, ref) => (
|
| 22 |
+
<SheetPrimitive.Overlay
|
| 23 |
+
className={cn(
|
| 24 |
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
| 25 |
+
className
|
| 26 |
+
)}
|
| 27 |
+
{...props}
|
| 28 |
+
ref={ref}
|
| 29 |
+
/>
|
| 30 |
+
));
|
| 31 |
+
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
| 32 |
+
|
| 33 |
+
const sheetVariants = cva(
|
| 34 |
+
"fixed z-50 gap-4 bg-neutral-900 p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
| 35 |
+
{
|
| 36 |
+
variants: {
|
| 37 |
+
side: {
|
| 38 |
+
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
| 39 |
+
bottom:
|
| 40 |
+
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
| 41 |
+
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
| 42 |
+
right:
|
| 43 |
+
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
| 44 |
+
},
|
| 45 |
+
},
|
| 46 |
+
defaultVariants: {
|
| 47 |
+
side: "right",
|
| 48 |
+
},
|
| 49 |
+
}
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
interface SheetContentProps
|
| 53 |
+
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
| 54 |
+
VariantProps<typeof sheetVariants> {}
|
| 55 |
+
|
| 56 |
+
const SheetContent = React.forwardRef<
|
| 57 |
+
React.ElementRef<typeof SheetPrimitive.Content>,
|
| 58 |
+
SheetContentProps
|
| 59 |
+
>(({ side = "right", className, children, ...props }, ref) => (
|
| 60 |
+
<SheetPortal>
|
| 61 |
+
<SheetOverlay />
|
| 62 |
+
<SheetPrimitive.Content
|
| 63 |
+
ref={ref}
|
| 64 |
+
className={cn(sheetVariants({ side }), className)}
|
| 65 |
+
{...props}
|
| 66 |
+
>
|
| 67 |
+
{children}
|
| 68 |
+
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
| 69 |
+
<X className="h-4 w-4" />
|
| 70 |
+
<span className="sr-only">Close</span>
|
| 71 |
+
</SheetPrimitive.Close>
|
| 72 |
+
</SheetPrimitive.Content>
|
| 73 |
+
</SheetPortal>
|
| 74 |
+
));
|
| 75 |
+
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
| 76 |
+
|
| 77 |
+
const SheetHeader = ({
|
| 78 |
+
className,
|
| 79 |
+
...props
|
| 80 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 81 |
+
<div
|
| 82 |
+
className={cn(
|
| 83 |
+
"flex flex-col space-y-2 text-center sm:text-left",
|
| 84 |
+
className
|
| 85 |
+
)}
|
| 86 |
+
{...props}
|
| 87 |
+
/>
|
| 88 |
+
);
|
| 89 |
+
SheetHeader.displayName = "SheetHeader";
|
| 90 |
+
|
| 91 |
+
const SheetFooter = ({
|
| 92 |
+
className,
|
| 93 |
+
...props
|
| 94 |
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
| 95 |
+
<div
|
| 96 |
+
className={cn(
|
| 97 |
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
| 98 |
+
className
|
| 99 |
+
)}
|
| 100 |
+
{...props}
|
| 101 |
+
/>
|
| 102 |
+
);
|
| 103 |
+
SheetFooter.displayName = "SheetFooter";
|
| 104 |
+
|
| 105 |
+
const SheetTitle = React.forwardRef<
|
| 106 |
+
React.ElementRef<typeof SheetPrimitive.Title>,
|
| 107 |
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
| 108 |
+
>(({ className, ...props }, ref) => (
|
| 109 |
+
<SheetPrimitive.Title
|
| 110 |
+
ref={ref}
|
| 111 |
+
className={cn("text-lg font-semibold text-foreground", className)}
|
| 112 |
+
{...props}
|
| 113 |
+
/>
|
| 114 |
+
));
|
| 115 |
+
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
| 116 |
+
|
| 117 |
+
const SheetDescription = React.forwardRef<
|
| 118 |
+
React.ElementRef<typeof SheetPrimitive.Description>,
|
| 119 |
+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
| 120 |
+
>(({ className, ...props }, ref) => (
|
| 121 |
+
<SheetPrimitive.Description
|
| 122 |
+
ref={ref}
|
| 123 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 124 |
+
{...props}
|
| 125 |
+
/>
|
| 126 |
+
));
|
| 127 |
+
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
| 128 |
+
|
| 129 |
+
export {
|
| 130 |
+
Sheet,
|
| 131 |
+
SheetPortal,
|
| 132 |
+
SheetOverlay,
|
| 133 |
+
SheetTrigger,
|
| 134 |
+
SheetClose,
|
| 135 |
+
SheetContent,
|
| 136 |
+
SheetHeader,
|
| 137 |
+
SheetFooter,
|
| 138 |
+
SheetTitle,
|
| 139 |
+
SheetDescription,
|
| 140 |
+
};
|
hooks/useAi.ts
CHANGED
|
@@ -14,12 +14,13 @@ import { isTheSameHtml } from "@/lib/compare-html-diff";
|
|
| 14 |
export const useAi = (onScrollToBottom?: () => void) => {
|
| 15 |
const client = useQueryClient();
|
| 16 |
const audio = useRef<HTMLAudioElement | null>(null);
|
| 17 |
-
const { setPages, setCurrentPage, setPrompts, prompts, pages, project, setProject, commits, setCommits, setLastSavedPages, isSameHtml } = useEditor();
|
| 18 |
const [controller, setController] = useState<AbortController | null>(null);
|
| 19 |
const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
|
| 20 |
const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
|
| 21 |
const router = useRouter();
|
| 22 |
const { projects, setProjects } = useUser();
|
|
|
|
| 23 |
|
| 24 |
const { data: isAiWorking = false } = useQuery({
|
| 25 |
queryKey: ["ai.isAiWorking"],
|
|
@@ -78,6 +79,18 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 78 |
client.setQueryData(["ai.selectedFiles"], newFiles)
|
| 79 |
};
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
const { data: provider } = useQuery({
|
| 82 |
queryKey: ["ai.provider"],
|
| 83 |
queryFn: async () => storageProvider ?? "auto",
|
|
@@ -113,19 +126,25 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 113 |
|
| 114 |
const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean) => {
|
| 115 |
if (isLoggedIn) {
|
| 116 |
-
|
| 117 |
title: projectName,
|
| 118 |
pages: htmlPages,
|
| 119 |
prompt,
|
| 120 |
-
})
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
setIsAiWorking(false);
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
setProjects([...projects, response.data.space]);
|
| 126 |
-
toast.success("AI responded successfully");
|
| 127 |
-
if (audio.current) audio.current.play();
|
| 128 |
-
}
|
| 129 |
} else {
|
| 130 |
setIsAiWorking(false);
|
| 131 |
toast.success("AI responded successfully");
|
|
@@ -138,6 +157,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 138 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 139 |
|
| 140 |
setIsAiWorking(true);
|
|
|
|
| 141 |
|
| 142 |
const abortController = new AbortController();
|
| 143 |
setController(abortController);
|
|
@@ -186,12 +206,11 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 186 |
}
|
| 187 |
}
|
| 188 |
} catch (e) {
|
| 189 |
-
// Not valid JSON, treat as normal content
|
| 190 |
}
|
| 191 |
}
|
| 192 |
|
| 193 |
-
const newPages = formatPages(contentResponse);
|
| 194 |
-
let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START
|
| 195 |
if (!projectName) {
|
| 196 |
projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40);
|
| 197 |
}
|
|
@@ -226,13 +245,11 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 226 |
}
|
| 227 |
}
|
| 228 |
} catch (e) {
|
| 229 |
-
// Not a complete JSON yet, continue reading
|
| 230 |
}
|
| 231 |
}
|
| 232 |
|
| 233 |
-
formatPages(contentResponse);
|
| 234 |
|
| 235 |
-
// Continue reading
|
| 236 |
return read();
|
| 237 |
};
|
| 238 |
|
|
@@ -266,6 +283,10 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 266 |
setController(abortController);
|
| 267 |
|
| 268 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
const request = await fetch("/api/ask", {
|
| 270 |
method: "PUT",
|
| 271 |
body: JSON.stringify({
|
|
@@ -273,7 +294,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 273 |
provider,
|
| 274 |
previousPrompts: prompts,
|
| 275 |
model,
|
| 276 |
-
pages,
|
| 277 |
selectedElementHtml: selectedElement?.outerHTML,
|
| 278 |
files: selectedFiles,
|
| 279 |
repoId: project?.space_id,
|
|
@@ -316,16 +337,28 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 316 |
router.push(`/${res.repoId}`);
|
| 317 |
setIsAiWorking(false);
|
| 318 |
} else {
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
setCommits([res.commit, ...commits]);
|
| 322 |
setPrompts(
|
| 323 |
[...prompts, prompt]
|
| 324 |
)
|
| 325 |
setSelectedElement(null);
|
| 326 |
setSelectedFiles([]);
|
|
|
|
| 327 |
setIsEditableModeEnabled(false);
|
| 328 |
-
setIsAiWorking(false);
|
| 329 |
}
|
| 330 |
|
| 331 |
if (audio.current) audio.current.play();
|
|
@@ -348,35 +381,36 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 348 |
}
|
| 349 |
};
|
| 350 |
|
| 351 |
-
const formatPages = (content: string) => {
|
| 352 |
const pages: Page[] = [];
|
| 353 |
-
if (!content.match(/<<<<<<<
|
| 354 |
return pages;
|
| 355 |
}
|
| 356 |
|
| 357 |
const cleanedContent = content.replace(
|
| 358 |
-
/[\s\S]*?<<<<<<<
|
| 359 |
-
"<<<<<<<
|
| 360 |
);
|
| 361 |
-
const
|
| 362 |
-
/<<<<<<<
|
| 363 |
);
|
| 364 |
const processedChunks = new Set<number>();
|
| 365 |
|
| 366 |
-
|
| 367 |
if (processedChunks.has(index) || !chunk?.trim()) {
|
| 368 |
return;
|
| 369 |
}
|
| 370 |
-
const
|
|
|
|
| 371 |
|
| 372 |
-
if (
|
| 373 |
const page: Page = {
|
| 374 |
-
path:
|
| 375 |
-
html:
|
| 376 |
};
|
| 377 |
pages.push(page);
|
| 378 |
|
| 379 |
-
if (
|
| 380 |
onScrollToBottom?.();
|
| 381 |
}
|
| 382 |
|
|
@@ -386,68 +420,82 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 386 |
});
|
| 387 |
if (pages.length > 0) {
|
| 388 |
setPages(pages);
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
}
|
| 392 |
|
| 393 |
return pages;
|
| 394 |
};
|
| 395 |
|
| 396 |
-
const
|
| 397 |
-
if (!
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
)?.filter(Boolean);
|
| 409 |
-
|
| 410 |
-
const pagePath = htmlChunks[0]?.trim() || "";
|
| 411 |
-
const htmlContent = extractHtmlContent(htmlChunks[1]);
|
| 412 |
-
|
| 413 |
-
if (!pagePath || !htmlContent) {
|
| 414 |
-
return null;
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
const page: Page = {
|
| 418 |
-
path: pagePath,
|
| 419 |
-
html: htmlContent,
|
| 420 |
-
};
|
| 421 |
-
|
| 422 |
-
setPages(prevPages => {
|
| 423 |
-
const existingPageIndex = prevPages.findIndex(p => p.path === currentPagePath || p.path === pagePath);
|
| 424 |
-
|
| 425 |
-
if (existingPageIndex !== -1) {
|
| 426 |
-
const updatedPages = [...prevPages];
|
| 427 |
-
updatedPages[existingPageIndex] = page;
|
| 428 |
-
return updatedPages;
|
| 429 |
} else {
|
| 430 |
-
|
|
|
|
| 431 |
}
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
}
|
| 439 |
-
|
| 440 |
-
return page;
|
| 441 |
-
};
|
| 442 |
-
|
| 443 |
-
const extractHtmlContent = (chunk: string): string => {
|
| 444 |
-
if (!chunk) return "";
|
| 445 |
-
const htmlMatch = chunk.trim().match(/<!DOCTYPE html>[\s\S]*/);
|
| 446 |
-
if (!htmlMatch) return "";
|
| 447 |
-
let htmlContent = htmlMatch[0];
|
| 448 |
-
htmlContent = ensureCompleteHtml(htmlContent);
|
| 449 |
-
htmlContent = htmlContent.replace(/```/g, "");
|
| 450 |
-
return htmlContent;
|
| 451 |
};
|
| 452 |
|
| 453 |
const ensureCompleteHtml = (html: string): string => {
|
|
@@ -488,6 +536,8 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 488 |
setSelectedElement,
|
| 489 |
selectedFiles,
|
| 490 |
setSelectedFiles,
|
|
|
|
|
|
|
| 491 |
isEditableModeEnabled,
|
| 492 |
setIsEditableModeEnabled,
|
| 493 |
globalAiLoading: isThinking || isAiWorking,
|
|
|
|
| 14 |
export const useAi = (onScrollToBottom?: () => void) => {
|
| 15 |
const client = useQueryClient();
|
| 16 |
const audio = useRef<HTMLAudioElement | null>(null);
|
| 17 |
+
const { setPages, setCurrentPage, setPreviewPage, setPrompts, prompts, pages, project, setProject, commits, setCommits, setLastSavedPages, isSameHtml } = useEditor();
|
| 18 |
const [controller, setController] = useState<AbortController | null>(null);
|
| 19 |
const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
|
| 20 |
const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
|
| 21 |
const router = useRouter();
|
| 22 |
const { projects, setProjects } = useUser();
|
| 23 |
+
const streamingPagesRef = useRef<Set<string>>(new Set());
|
| 24 |
|
| 25 |
const { data: isAiWorking = false } = useQuery({
|
| 26 |
queryKey: ["ai.isAiWorking"],
|
|
|
|
| 79 |
client.setQueryData(["ai.selectedFiles"], newFiles)
|
| 80 |
};
|
| 81 |
|
| 82 |
+
const { data: contextFile } = useQuery<string | null>({
|
| 83 |
+
queryKey: ["ai.contextFile"],
|
| 84 |
+
queryFn: async () => null,
|
| 85 |
+
refetchOnWindowFocus: false,
|
| 86 |
+
refetchOnReconnect: false,
|
| 87 |
+
refetchOnMount: false,
|
| 88 |
+
initialData: null
|
| 89 |
+
});
|
| 90 |
+
const setContextFile = (newContextFile: string | null) => {
|
| 91 |
+
client.setQueryData(["ai.contextFile"], newContextFile)
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
const { data: provider } = useQuery({
|
| 95 |
queryKey: ["ai.provider"],
|
| 96 |
queryFn: async () => storageProvider ?? "auto",
|
|
|
|
| 126 |
|
| 127 |
const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean) => {
|
| 128 |
if (isLoggedIn) {
|
| 129 |
+
api.post("/me/projects", {
|
| 130 |
title: projectName,
|
| 131 |
pages: htmlPages,
|
| 132 |
prompt,
|
| 133 |
+
})
|
| 134 |
+
.then((response) => {
|
| 135 |
+
if (response.data.ok) {
|
| 136 |
+
setIsAiWorking(false);
|
| 137 |
+
router.replace(`/${response.data.space.project.space_id}`);
|
| 138 |
+
setProject(response.data.space);
|
| 139 |
+
setProjects([...projects, response.data.space]);
|
| 140 |
+
toast.success("AI responded successfully");
|
| 141 |
+
if (audio.current) audio.current.play();
|
| 142 |
+
}
|
| 143 |
+
})
|
| 144 |
+
.catch((error) => {
|
| 145 |
setIsAiWorking(false);
|
| 146 |
+
toast.error(error?.response?.data?.message || error?.message || "Failed to create project");
|
| 147 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
} else {
|
| 149 |
setIsAiWorking(false);
|
| 150 |
toast.success("AI responded successfully");
|
|
|
|
| 157 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 158 |
|
| 159 |
setIsAiWorking(true);
|
| 160 |
+
streamingPagesRef.current.clear(); // Reset tracking for new generation
|
| 161 |
|
| 162 |
const abortController = new AbortController();
|
| 163 |
setController(abortController);
|
|
|
|
| 206 |
}
|
| 207 |
}
|
| 208 |
} catch (e) {
|
|
|
|
| 209 |
}
|
| 210 |
}
|
| 211 |
|
| 212 |
+
const newPages = formatPages(contentResponse, false);
|
| 213 |
+
let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 214 |
if (!projectName) {
|
| 215 |
projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40);
|
| 216 |
}
|
|
|
|
| 245 |
}
|
| 246 |
}
|
| 247 |
} catch (e) {
|
|
|
|
| 248 |
}
|
| 249 |
}
|
| 250 |
|
| 251 |
+
formatPages(contentResponse, true);
|
| 252 |
|
|
|
|
| 253 |
return read();
|
| 254 |
};
|
| 255 |
|
|
|
|
| 283 |
setController(abortController);
|
| 284 |
|
| 285 |
try {
|
| 286 |
+
const pagesToSend = contextFile
|
| 287 |
+
? pages.filter(page => page.path === contextFile)
|
| 288 |
+
: pages;
|
| 289 |
+
|
| 290 |
const request = await fetch("/api/ask", {
|
| 291 |
method: "PUT",
|
| 292 |
body: JSON.stringify({
|
|
|
|
| 294 |
provider,
|
| 295 |
previousPrompts: prompts,
|
| 296 |
model,
|
| 297 |
+
pages: pagesToSend,
|
| 298 |
selectedElementHtml: selectedElement?.outerHTML,
|
| 299 |
files: selectedFiles,
|
| 300 |
repoId: project?.space_id,
|
|
|
|
| 337 |
router.push(`/${res.repoId}`);
|
| 338 |
setIsAiWorking(false);
|
| 339 |
} else {
|
| 340 |
+
const returnedPages = res.pages as Page[];
|
| 341 |
+
const updatedPagesMap = new Map(returnedPages.map((p: Page) => [p.path, p]));
|
| 342 |
+
const mergedPages: Page[] = pages.map(page =>
|
| 343 |
+
updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
|
| 344 |
+
);
|
| 345 |
+
returnedPages.forEach((page: Page) => {
|
| 346 |
+
if (!pages.find(p => p.path === page.path)) {
|
| 347 |
+
mergedPages.push(page);
|
| 348 |
+
}
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
setPages(mergedPages);
|
| 352 |
+
setLastSavedPages([...mergedPages]);
|
| 353 |
setCommits([res.commit, ...commits]);
|
| 354 |
setPrompts(
|
| 355 |
[...prompts, prompt]
|
| 356 |
)
|
| 357 |
setSelectedElement(null);
|
| 358 |
setSelectedFiles([]);
|
| 359 |
+
// setContextFile(null); not needed yet, keep context for the next request.
|
| 360 |
setIsEditableModeEnabled(false);
|
| 361 |
+
setIsAiWorking(false);
|
| 362 |
}
|
| 363 |
|
| 364 |
if (audio.current) audio.current.play();
|
|
|
|
| 381 |
}
|
| 382 |
};
|
| 383 |
|
| 384 |
+
const formatPages = (content: string, isStreaming: boolean = true) => {
|
| 385 |
const pages: Page[] = [];
|
| 386 |
+
if (!content.match(/<<<<<<< NEW_FILE_START (.*?) >>>>>>> NEW_FILE_END/)) {
|
| 387 |
return pages;
|
| 388 |
}
|
| 389 |
|
| 390 |
const cleanedContent = content.replace(
|
| 391 |
+
/[\s\S]*?<<<<<<< NEW_FILE_START (.*?) >>>>>>> NEW_FILE_END/,
|
| 392 |
+
"<<<<<<< NEW_FILE_START $1 >>>>>>> NEW_FILE_END"
|
| 393 |
);
|
| 394 |
+
const fileChunks = cleanedContent.split(
|
| 395 |
+
/<<<<<<< NEW_FILE_START (.*?) >>>>>>> NEW_FILE_END/
|
| 396 |
);
|
| 397 |
const processedChunks = new Set<number>();
|
| 398 |
|
| 399 |
+
fileChunks.forEach((chunk, index) => {
|
| 400 |
if (processedChunks.has(index) || !chunk?.trim()) {
|
| 401 |
return;
|
| 402 |
}
|
| 403 |
+
const filePath = chunk.trim();
|
| 404 |
+
const fileContent = extractFileContent(fileChunks[index + 1], filePath);
|
| 405 |
|
| 406 |
+
if (fileContent) {
|
| 407 |
const page: Page = {
|
| 408 |
+
path: filePath,
|
| 409 |
+
html: fileContent,
|
| 410 |
};
|
| 411 |
pages.push(page);
|
| 412 |
|
| 413 |
+
if (fileContent.length > 200) {
|
| 414 |
onScrollToBottom?.();
|
| 415 |
}
|
| 416 |
|
|
|
|
| 420 |
});
|
| 421 |
if (pages.length > 0) {
|
| 422 |
setPages(pages);
|
| 423 |
+
if (isStreaming) {
|
| 424 |
+
// Find new pages that haven't been shown yet (HTML, CSS, JS, etc.)
|
| 425 |
+
const newPages = pages.filter(p =>
|
| 426 |
+
!streamingPagesRef.current.has(p.path)
|
| 427 |
+
);
|
| 428 |
+
|
| 429 |
+
if (newPages.length > 0) {
|
| 430 |
+
const newPage = newPages[0];
|
| 431 |
+
setCurrentPage(newPage.path);
|
| 432 |
+
streamingPagesRef.current.add(newPage.path);
|
| 433 |
+
|
| 434 |
+
// Update preview if it's an HTML file not in components folder
|
| 435 |
+
if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
|
| 436 |
+
setPreviewPage(newPage.path);
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
} else {
|
| 440 |
+
streamingPagesRef.current.clear();
|
| 441 |
+
const indexPage = pages.find(p => p.path === 'index.html' || p.path === 'index' || p.path === '/');
|
| 442 |
+
if (indexPage) {
|
| 443 |
+
setCurrentPage(indexPage.path);
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
}
|
| 447 |
|
| 448 |
return pages;
|
| 449 |
};
|
| 450 |
|
| 451 |
+
const extractFileContent = (chunk: string, filePath: string): string => {
|
| 452 |
+
if (!chunk) return "";
|
| 453 |
+
|
| 454 |
+
// Remove backticks first
|
| 455 |
+
let content = chunk.trim();
|
| 456 |
+
|
| 457 |
+
// Handle different file types
|
| 458 |
+
if (filePath.endsWith('.css')) {
|
| 459 |
+
// Try to extract CSS from complete code blocks first
|
| 460 |
+
const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
|
| 461 |
+
if (cssMatch) {
|
| 462 |
+
content = cssMatch[1];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
} else {
|
| 464 |
+
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 465 |
+
content = content.replace(/^```css\s*/i, "");
|
| 466 |
}
|
| 467 |
+
// Remove any remaining backticks
|
| 468 |
+
return content.replace(/```/g, "").trim();
|
| 469 |
+
} else if (filePath.endsWith('.js')) {
|
| 470 |
+
// Try to extract JavaScript from complete code blocks first
|
| 471 |
+
const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
|
| 472 |
+
if (jsMatch) {
|
| 473 |
+
content = jsMatch[1];
|
| 474 |
+
} else {
|
| 475 |
+
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 476 |
+
content = content.replace(/^```(?:javascript|js)\s*/i, "");
|
| 477 |
+
}
|
| 478 |
+
// Remove any remaining backticks
|
| 479 |
+
return content.replace(/```/g, "").trim();
|
| 480 |
+
} else {
|
| 481 |
+
// Handle HTML files
|
| 482 |
+
const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
|
| 483 |
+
if (htmlMatch) {
|
| 484 |
+
content = htmlMatch[1];
|
| 485 |
+
} else {
|
| 486 |
+
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 487 |
+
content = content.replace(/^```html\s*/i, "");
|
| 488 |
+
// Try to find HTML starting with DOCTYPE
|
| 489 |
+
const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
|
| 490 |
+
if (doctypeMatch) {
|
| 491 |
+
content = doctypeMatch[0];
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
let htmlContent = content.replace(/```/g, "");
|
| 496 |
+
htmlContent = ensureCompleteHtml(htmlContent);
|
| 497 |
+
return htmlContent;
|
| 498 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
};
|
| 500 |
|
| 501 |
const ensureCompleteHtml = (html: string): string => {
|
|
|
|
| 536 |
setSelectedElement,
|
| 537 |
selectedFiles,
|
| 538 |
setSelectedFiles,
|
| 539 |
+
contextFile,
|
| 540 |
+
setContextFile,
|
| 541 |
isEditableModeEnabled,
|
| 542 |
setIsEditableModeEnabled,
|
| 543 |
globalAiLoading: isThinking || isAiWorking,
|
hooks/useEditor.ts
CHANGED
|
@@ -98,6 +98,17 @@ export const useEditor = (namespace?: string, repoId?: string) => {
|
|
| 98 |
client.setQueryData(["editor.currentPage"], newCurrentPage);
|
| 99 |
};
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
const { data: prompts = [] } = useQuery({
|
| 102 |
queryKey: ["editor.prompts"],
|
| 103 |
queryFn: async () => [],
|
|
@@ -338,6 +349,8 @@ export const useEditor = (namespace?: string, repoId?: string) => {
|
|
| 338 |
setDevice,
|
| 339 |
currentPage,
|
| 340 |
setCurrentPage,
|
|
|
|
|
|
|
| 341 |
currentPageData,
|
| 342 |
currentTab,
|
| 343 |
setCurrentTab,
|
|
|
|
| 98 |
client.setQueryData(["editor.currentPage"], newCurrentPage);
|
| 99 |
};
|
| 100 |
|
| 101 |
+
const { data: previewPage = "" } = useQuery({
|
| 102 |
+
queryKey: ["editor.previewPage"],
|
| 103 |
+
queryFn: async () => "",
|
| 104 |
+
refetchOnWindowFocus: false,
|
| 105 |
+
refetchOnReconnect: false,
|
| 106 |
+
refetchOnMount: false,
|
| 107 |
+
});
|
| 108 |
+
const setPreviewPage = (newPreviewPage: string) => {
|
| 109 |
+
client.setQueryData(["editor.previewPage"], newPreviewPage);
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
const { data: prompts = [] } = useQuery({
|
| 113 |
queryKey: ["editor.prompts"],
|
| 114 |
queryFn: async () => [],
|
|
|
|
| 349 |
setDevice,
|
| 350 |
currentPage,
|
| 351 |
setCurrentPage,
|
| 352 |
+
previewPage,
|
| 353 |
+
setPreviewPage,
|
| 354 |
currentPageData,
|
| 355 |
currentTab,
|
| 356 |
setCurrentTab,
|
lib/inject-badge.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Injects the DeepSite badge script into HTML content before the closing </body> tag.
|
| 3 |
+
* If the script already exists, it ensures only one instance is present.
|
| 4 |
+
*
|
| 5 |
+
* @param html - The HTML content to inject the script into
|
| 6 |
+
* @returns The HTML content with the badge script injected
|
| 7 |
+
*/
|
| 8 |
+
export function injectDeepSiteBadge(html: string): string {
|
| 9 |
+
const badgeScript = '<script src="https://deepsite.hf.co/deepsite-badge.js"></script>';
|
| 10 |
+
|
| 11 |
+
// Remove any existing badge script to avoid duplicates
|
| 12 |
+
const cleanedHtml = html.replace(
|
| 13 |
+
/<script\s+src=["']https:\/\/deepsite\.hf\.co\/deepsite-badge\.js["']\s*><\/script>\s*/gi,
|
| 14 |
+
''
|
| 15 |
+
);
|
| 16 |
+
|
| 17 |
+
// Check if there's a closing body tag
|
| 18 |
+
const bodyCloseIndex = cleanedHtml.lastIndexOf('</body>');
|
| 19 |
+
|
| 20 |
+
if (bodyCloseIndex !== -1) {
|
| 21 |
+
// Inject the script before the closing </body> tag
|
| 22 |
+
return (
|
| 23 |
+
cleanedHtml.slice(0, bodyCloseIndex) +
|
| 24 |
+
badgeScript +
|
| 25 |
+
'\n' +
|
| 26 |
+
cleanedHtml.slice(bodyCloseIndex)
|
| 27 |
+
);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// If no closing body tag, append the script at the end
|
| 31 |
+
return cleanedHtml + '\n' + badgeScript;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Checks if a page path represents the main index page
|
| 36 |
+
*
|
| 37 |
+
* @param path - The page path to check
|
| 38 |
+
* @returns True if the path represents the main index page
|
| 39 |
+
*/
|
| 40 |
+
export function isIndexPage(path: string): boolean {
|
| 41 |
+
const normalizedPath = path.toLowerCase();
|
| 42 |
+
return (
|
| 43 |
+
normalizedPath === '/' ||
|
| 44 |
+
normalizedPath === 'index' ||
|
| 45 |
+
normalizedPath === '/index' ||
|
| 46 |
+
normalizedPath === 'index.html' ||
|
| 47 |
+
normalizedPath === '/index.html'
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
+
|
lib/prompts.ts
CHANGED
|
@@ -2,20 +2,15 @@ export const SEARCH_START = "<<<<<<< SEARCH";
|
|
| 2 |
export const DIVIDER = "=======";
|
| 3 |
export const REPLACE_END = ">>>>>>> REPLACE";
|
| 4 |
export const MAX_REQUESTS_PER_IP = 4;
|
| 5 |
-
export const
|
| 6 |
-
export const
|
| 7 |
-
export const
|
| 8 |
-
export const
|
| 9 |
-
export const
|
| 10 |
-
export const
|
| 11 |
-
export const PROJECT_NAME_START = "<<<<<<< PROJECT_NAME_START ";
|
| 12 |
-
export const PROJECT_NAME_END = " >>>>>>> PROJECT_NAME_END";
|
| 13 |
export const PROMPT_FOR_REWRITE_PROMPT = "<<<<<<< PROMPT_FOR_REWRITE_PROMPT ";
|
| 14 |
export const PROMPT_FOR_REWRITE_PROMPT_END = " >>>>>>> PROMPT_FOR_REWRITE_PROMPT_END";
|
| 15 |
|
| 16 |
-
// TODO REVIEW LINK. MAYBE GO BACK TO SANDPACK.
|
| 17 |
-
// FIX PREVIEW LINK NOT WORKING ONCE THE SITE IS DEPLOYED.
|
| 18 |
-
|
| 19 |
export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
|
| 20 |
Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).`
|
| 21 |
export const PROMPT_FOR_PROJECT_NAME = `REQUIRED: Generate a name for the project, based on the user's request. Try to be creative and unique. Add a emoji at the end of the name. It should be short, like 6 words. Be fancy, creative and funny. DON'T FORGET IT, IT'S IMPORTANT!`
|
|
@@ -28,24 +23,91 @@ If you want to use ICONS import Feather Icons (Make sure to add <script src="htt
|
|
| 28 |
For interactive animations you can use: Vanta.js (Make sure to add <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> and <script>VANTA.GLOBE({...</script> in the body.).
|
| 29 |
Don't hesitate to use real public API for the datas, you can find good ones here https://github.com/public-apis/public-apis depending on what the user asks for.
|
| 30 |
You can create multiple pages website at once (following the format rules below) or a Single Page Application. But make sure to create multiple pages if the user asks for different pages.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
${PROMPT_FOR_IMAGE_GENERATION}
|
| 32 |
${PROMPT_FOR_PROJECT_NAME}
|
| 33 |
No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
|
| 34 |
-
Return the results
|
| 35 |
1. Start with ${PROJECT_NAME_START}.
|
| 36 |
2. Add the name of the project, right after the start tag.
|
| 37 |
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 38 |
4. The name of the project should be short and concise.
|
| 39 |
-
5.
|
| 40 |
-
6.
|
| 41 |
-
7.
|
| 42 |
-
8.
|
| 43 |
-
9.
|
| 44 |
-
10.
|
| 45 |
-
11.
|
|
|
|
|
|
|
| 46 |
Example Code:
|
| 47 |
-
${PROJECT_NAME_START}Project Name${PROJECT_NAME_END}
|
| 48 |
-
${
|
| 49 |
\`\`\`html
|
| 50 |
<!DOCTYPE html>
|
| 51 |
<html lang="en">
|
|
@@ -54,34 +116,113 @@ ${TITLE_PAGE_START}index.html${TITLE_PAGE_END}
|
|
| 54 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 55 |
<title>Index</title>
|
| 56 |
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
|
|
|
| 57 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 58 |
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 59 |
-
<script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
|
| 60 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 61 |
</head>
|
| 62 |
<body>
|
|
|
|
| 63 |
<h1>Hello World</h1>
|
| 64 |
-
<
|
|
|
|
|
|
|
|
|
|
| 65 |
<script>feather.replace();</script>
|
| 66 |
</body>
|
| 67 |
</html>
|
| 68 |
\`\`\`
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying
|
| 72 |
-
The user wants to apply changes and probably add new features/pages to the website, based on their request.
|
| 73 |
-
You MUST output ONLY the changes required using the following
|
| 74 |
Don't hesitate to use real public API for the datas, you can find good ones here https://github.com/public-apis/public-apis depending on what the user asks for.
|
| 75 |
-
If it's a new page, you MUST
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
${PROMPT_FOR_IMAGE_GENERATION}
|
| 77 |
Do NOT explain the changes or what you did, just return the expected results.
|
| 78 |
Update Format Rules:
|
| 79 |
1. Start with ${PROJECT_NAME_START}.
|
| 80 |
2. Add the name of the project, right after the start tag.
|
| 81 |
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 82 |
-
4. Start with ${
|
| 83 |
-
5. Provide the name of the
|
| 84 |
-
6. Close the start tag with the ${
|
| 85 |
7. Start with ${SEARCH_START}
|
| 86 |
8. Provide the exact lines from the current code that need to be replaced.
|
| 87 |
9. Use ${DIVIDER} to separate the search block from the replacement.
|
|
@@ -94,8 +235,8 @@ Update Format Rules:
|
|
| 94 |
Example Modifying Code:
|
| 95 |
\`\`\`
|
| 96 |
Some explanation...
|
| 97 |
-
${PROJECT_NAME_START}Project Name${PROJECT_NAME_END}
|
| 98 |
-
${
|
| 99 |
${SEARCH_START}
|
| 100 |
<h1>Old Title</h1>
|
| 101 |
${DIVIDER}
|
|
@@ -104,67 +245,184 @@ ${REPLACE_END}
|
|
| 104 |
${SEARCH_START}
|
| 105 |
</body>
|
| 106 |
${DIVIDER}
|
| 107 |
-
<script
|
| 108 |
</body>
|
| 109 |
${REPLACE_END}
|
| 110 |
\`\`\`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
Example Deleting Code:
|
| 112 |
\`\`\`
|
| 113 |
Removing the paragraph...
|
| 114 |
-
${
|
| 115 |
${SEARCH_START}
|
| 116 |
<p>This paragraph will be deleted.</p>
|
| 117 |
${DIVIDER}
|
| 118 |
${REPLACE_END}
|
| 119 |
\`\`\`
|
| 120 |
-
The user can also ask to add a new page, in this case you should return the new
|
| 121 |
-
1. Start with ${
|
| 122 |
-
2. Add the name of the
|
| 123 |
-
3. Close the start tag with the ${
|
| 124 |
-
4. Start the
|
| 125 |
-
5. Insert the
|
| 126 |
6. Close with the triple backticks, like \`\`\`.
|
| 127 |
-
7.
|
| 128 |
-
Example
|
| 129 |
-
${
|
| 130 |
\`\`\`html
|
| 131 |
<!DOCTYPE html>
|
| 132 |
<html lang="en">
|
| 133 |
<head>
|
| 134 |
<meta charset="UTF-8">
|
| 135 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 136 |
-
<title>
|
| 137 |
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
|
|
|
| 138 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 139 |
-
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 140 |
-
<script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script>
|
| 141 |
-
<script src="https://unpkg.com/feather-icons"></script>
|
| 142 |
</head>
|
| 143 |
<body>
|
| 144 |
-
<
|
| 145 |
-
<
|
| 146 |
-
<
|
|
|
|
|
|
|
|
|
|
| 147 |
</body>
|
| 148 |
</html>
|
| 149 |
\`\`\`
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
No need to explain what you did. Just return the expected result.`
|
| 152 |
|
| 153 |
export const PROMPTS_FOR_AI = [
|
| 154 |
-
|
| 155 |
-
"Create a
|
| 156 |
-
"Create a
|
| 157 |
-
"Create a
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
"Create
|
| 161 |
-
"Create
|
| 162 |
-
"Create a
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
"Create a
|
| 166 |
-
"Create a
|
| 167 |
-
"Create a
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
];
|
|
|
|
| 2 |
export const DIVIDER = "=======";
|
| 3 |
export const REPLACE_END = ">>>>>>> REPLACE";
|
| 4 |
export const MAX_REQUESTS_PER_IP = 4;
|
| 5 |
+
export const NEW_FILE_START = "<<<<<<< NEW_FILE_START ";
|
| 6 |
+
export const NEW_FILE_END = " >>>>>>> NEW_FILE_END";
|
| 7 |
+
export const UPDATE_FILE_START = "<<<<<<< UPDATE_FILE_START ";
|
| 8 |
+
export const UPDATE_FILE_END = " >>>>>>> UPDATE_FILE_END";
|
| 9 |
+
export const PROJECT_NAME_START = "<<<<<<< PROJECT_NAME_START";
|
| 10 |
+
export const PROJECT_NAME_END = ">>>>>>> PROJECT_NAME_END";
|
|
|
|
|
|
|
| 11 |
export const PROMPT_FOR_REWRITE_PROMPT = "<<<<<<< PROMPT_FOR_REWRITE_PROMPT ";
|
| 12 |
export const PROMPT_FOR_REWRITE_PROMPT_END = " >>>>>>> PROMPT_FOR_REWRITE_PROMPT_END";
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
|
| 15 |
Examples: http://static.photos/red/320x240/133 (red-themed with seed 133), http://static.photos/640x360 (random category and image), http://static.photos/nature/1200x630/42 (nature-themed with seed 42).`
|
| 16 |
export const PROMPT_FOR_PROJECT_NAME = `REQUIRED: Generate a name for the project, based on the user's request. Try to be creative and unique. Add a emoji at the end of the name. It should be short, like 6 words. Be fancy, creative and funny. DON'T FORGET IT, IT'S IMPORTANT!`
|
|
|
|
| 23 |
For interactive animations you can use: Vanta.js (Make sure to add <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> and <script>VANTA.GLOBE({...</script> in the body.).
|
| 24 |
Don't hesitate to use real public API for the datas, you can find good ones here https://github.com/public-apis/public-apis depending on what the user asks for.
|
| 25 |
You can create multiple pages website at once (following the format rules below) or a Single Page Application. But make sure to create multiple pages if the user asks for different pages.
|
| 26 |
+
IMPORTANT: To avoid duplicate code across pages, you MUST create separate style.css and script.js files for shared CSS and JavaScript code. Each HTML file should link to these files using <link rel="stylesheet" href="style.css"> and <script src="script.js"></script>.
|
| 27 |
+
WEB COMPONENTS: For reusable UI elements like navbars, footers, sidebars, headers, etc., create Native Web Components as separate files in components/ folder:
|
| 28 |
+
- Create each component as a separate .js file in components/ folder (e.g., components/navbar.js, components/footer.js)
|
| 29 |
+
- Each component file defines a class extending HTMLElement and registers it with customElements.define()
|
| 30 |
+
- Use Shadow DOM for style encapsulation
|
| 31 |
+
- Components render using template literals with inline styles
|
| 32 |
+
- Include component files in HTML before using them: <script src="components/navbar.js"></script>
|
| 33 |
+
- Use them in HTML pages with custom element tags (e.g., <custom-navbar></custom-navbar>)
|
| 34 |
+
- If you want to use ICON you can use Feather Icons, as it's already included in the main pages.
|
| 35 |
+
IMPORTANT: NEVER USE ONCLICK FUNCTION TO MAKE A REDIRECT TO NEW PAGE. MAKE SURE TO ALWAYS USE <a href=""/>, OTHERWISE IT WONT WORK WITH SHADOW ROOT AND WEB COMPONENTS.
|
| 36 |
+
Example components/navbar.js:
|
| 37 |
+
class CustomNavbar extends HTMLElement {
|
| 38 |
+
connectedCallback() {
|
| 39 |
+
this.attachShadow({ mode: 'open' });
|
| 40 |
+
this.shadowRoot.innerHTML = \`
|
| 41 |
+
<style>
|
| 42 |
+
nav {
|
| 43 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 44 |
+
padding: 1rem;
|
| 45 |
+
display: flex;
|
| 46 |
+
justify-content: space-between;
|
| 47 |
+
align-items: center;
|
| 48 |
+
}
|
| 49 |
+
.logo { color: white; font-weight: bold; }
|
| 50 |
+
ul { display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0; }
|
| 51 |
+
a { color: white; text-decoration: none; }
|
| 52 |
+
</style>
|
| 53 |
+
<nav>
|
| 54 |
+
<div class="logo">My Website</div>
|
| 55 |
+
<ul>
|
| 56 |
+
<li><a href="/">Home</a></li>
|
| 57 |
+
<li><a href="/about.html">About</a></li>
|
| 58 |
+
</ul>
|
| 59 |
+
</nav>
|
| 60 |
+
\`;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
customElements.define('custom-navbar', CustomNavbar);
|
| 64 |
+
|
| 65 |
+
Example components/footer.js:
|
| 66 |
+
class CustomFooter extends HTMLElement {
|
| 67 |
+
connectedCallback() {
|
| 68 |
+
this.attachShadow({ mode: 'open' });
|
| 69 |
+
this.shadowRoot.innerHTML = \`
|
| 70 |
+
<style>
|
| 71 |
+
footer {
|
| 72 |
+
background: #1a202c;
|
| 73 |
+
color: white;
|
| 74 |
+
padding: 2rem;
|
| 75 |
+
text-align: center;
|
| 76 |
+
}
|
| 77 |
+
</style>
|
| 78 |
+
<footer>
|
| 79 |
+
<p>© 2024 My Website. All rights reserved.</p>
|
| 80 |
+
</footer>
|
| 81 |
+
\`;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
customElements.define('custom-footer', CustomFooter);
|
| 85 |
+
|
| 86 |
+
Then in HTML, include the component scripts and use the tags:
|
| 87 |
+
<script src="components/navbar.js"></script>
|
| 88 |
+
<script src="components/footer.js"></script>
|
| 89 |
+
<custom-navbar></custom-navbar>
|
| 90 |
+
<custom-footer></custom-footer>
|
| 91 |
${PROMPT_FOR_IMAGE_GENERATION}
|
| 92 |
${PROMPT_FOR_PROJECT_NAME}
|
| 93 |
No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
|
| 94 |
+
Return the results following this format:
|
| 95 |
1. Start with ${PROJECT_NAME_START}.
|
| 96 |
2. Add the name of the project, right after the start tag.
|
| 97 |
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 98 |
4. The name of the project should be short and concise.
|
| 99 |
+
5. Generate files in this ORDER: index.html FIRST, then style.css, then script.js, then web components (components/navbar.js, components/footer.js, etc.), then other HTML pages.
|
| 100 |
+
6. For each file, start with ${NEW_FILE_START}.
|
| 101 |
+
7. Add the file name (index.html, style.css, script.js, components/navbar.js, about.html, etc.) right after the start tag.
|
| 102 |
+
8. Close the start tag with the ${NEW_FILE_END}.
|
| 103 |
+
9. Start the file content with the triple backticks and appropriate language marker (\`\`\`html, \`\`\`css, or \`\`\`javascript).
|
| 104 |
+
10. Insert the file content there.
|
| 105 |
+
11. Close with the triple backticks, like \`\`\`.
|
| 106 |
+
12. Repeat for each file.
|
| 107 |
+
13. Web components should be in separate .js files in components/ folder and included via <script> tags before use.
|
| 108 |
Example Code:
|
| 109 |
+
${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
|
| 110 |
+
${NEW_FILE_START}index.html${NEW_FILE_END}
|
| 111 |
\`\`\`html
|
| 112 |
<!DOCTYPE html>
|
| 113 |
<html lang="en">
|
|
|
|
| 116 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 117 |
<title>Index</title>
|
| 118 |
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 119 |
+
<link rel="stylesheet" href="style.css">
|
| 120 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 121 |
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
|
|
|
| 122 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 123 |
</head>
|
| 124 |
<body>
|
| 125 |
+
<custom-navbar></custom-navbar>
|
| 126 |
<h1>Hello World</h1>
|
| 127 |
+
<custom-footer></custom-footer>
|
| 128 |
+
<script src="components/navbar.js"></script>
|
| 129 |
+
<script src="components/footer.js"></script>
|
| 130 |
+
<script src="script.js"></script>
|
| 131 |
<script>feather.replace();</script>
|
| 132 |
</body>
|
| 133 |
</html>
|
| 134 |
\`\`\`
|
| 135 |
+
${NEW_FILE_START}style.css${NEW_FILE_END}
|
| 136 |
+
\`\`\`css
|
| 137 |
+
/* Shared styles across all pages */
|
| 138 |
+
body {
|
| 139 |
+
font-family: 'Inter', sans-serif;
|
| 140 |
+
}
|
| 141 |
+
\`\`\`
|
| 142 |
+
${NEW_FILE_START}script.js${NEW_FILE_END}
|
| 143 |
+
\`\`\`javascript
|
| 144 |
+
// Shared JavaScript across all pages
|
| 145 |
+
console.log('App loaded');
|
| 146 |
+
\`\`\`
|
| 147 |
+
${NEW_FILE_START}components/navbar.js${NEW_FILE_END}
|
| 148 |
+
\`\`\`javascript
|
| 149 |
+
class CustomNavbar extends HTMLElement {
|
| 150 |
+
connectedCallback() {
|
| 151 |
+
this.attachShadow({ mode: 'open' });
|
| 152 |
+
this.shadowRoot.innerHTML = \`
|
| 153 |
+
<style>
|
| 154 |
+
nav {
|
| 155 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 156 |
+
padding: 1rem;
|
| 157 |
+
display: flex;
|
| 158 |
+
justify-content: space-between;
|
| 159 |
+
align-items: center;
|
| 160 |
+
}
|
| 161 |
+
.logo { color: white; font-weight: bold; font-size: 1.25rem; }
|
| 162 |
+
ul { display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0; }
|
| 163 |
+
a { color: white; text-decoration: none; transition: opacity 0.2s; }
|
| 164 |
+
a:hover { opacity: 0.8; }
|
| 165 |
+
</style>
|
| 166 |
+
<nav>
|
| 167 |
+
<div class="logo">My Website</div>
|
| 168 |
+
<ul>
|
| 169 |
+
<li><a href="/">Home</a></li>
|
| 170 |
+
<li><a href="/about.html">About</a></li>
|
| 171 |
+
</ul>
|
| 172 |
+
</nav>
|
| 173 |
+
\`;
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
customElements.define('custom-navbar', CustomNavbar);
|
| 177 |
+
\`\`\`
|
| 178 |
+
${NEW_FILE_START}components/footer.js${NEW_FILE_END}
|
| 179 |
+
\`\`\`javascript
|
| 180 |
+
class CustomFooter extends HTMLElement {
|
| 181 |
+
connectedCallback() {
|
| 182 |
+
this.attachShadow({ mode: 'open' });
|
| 183 |
+
this.shadowRoot.innerHTML = \`
|
| 184 |
+
<style>
|
| 185 |
+
footer {
|
| 186 |
+
background: #1a202c;
|
| 187 |
+
color: white;
|
| 188 |
+
padding: 2rem;
|
| 189 |
+
text-align: center;
|
| 190 |
+
margin-top: auto;
|
| 191 |
+
}
|
| 192 |
+
</style>
|
| 193 |
+
<footer>
|
| 194 |
+
<p>© 2024 My Website. All rights reserved.</p>
|
| 195 |
+
</footer>
|
| 196 |
+
\`;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
customElements.define('custom-footer', CustomFooter);
|
| 200 |
+
\`\`\`
|
| 201 |
+
CRITICAL: The first file MUST always be index.html. Then generate style.css and script.js. If you create web components, place them in components/ folder as separate .js files. All HTML files MUST include <link rel="stylesheet" href="style.css"> and component scripts before using them (e.g., <script src="components/navbar.js"></script>), then <script src="script.js"></script>.`
|
| 202 |
|
| 203 |
+
export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying existing files (HTML, CSS, JavaScript).
|
| 204 |
+
The user wants to apply changes and probably add new features/pages/styles/scripts to the website, based on their request.
|
| 205 |
+
You MUST output ONLY the changes required using the following UPDATE_FILE_START and SEARCH/REPLACE format. Do NOT output the entire file.
|
| 206 |
Don't hesitate to use real public API for the datas, you can find good ones here https://github.com/public-apis/public-apis depending on what the user asks for.
|
| 207 |
+
If it's a new file (HTML page, CSS, JS, or Web Component), you MUST use the NEW_FILE_START and NEW_FILE_END format.
|
| 208 |
+
IMPORTANT: When adding shared CSS or JavaScript code, modify the style.css or script.js files. Make sure all HTML files include <link rel="stylesheet" href="style.css"> and <script src="script.js"></script> tags.
|
| 209 |
+
WEB COMPONENTS: For reusable UI elements like navbars, footers, sidebars, headers, etc., create or update Native Web Components as separate files in components/ folder:
|
| 210 |
+
- Create each component as a separate .js file in components/ folder (e.g., components/navbar.js, components/footer.js)
|
| 211 |
+
- Each component file defines a class extending HTMLElement and registers it with customElements.define()
|
| 212 |
+
- Use Shadow DOM (attachShadow) for style encapsulation
|
| 213 |
+
- Use template literals for HTML/CSS content
|
| 214 |
+
- Include component files in HTML pages where needed: <script src="components/navbar.js"></script>
|
| 215 |
+
- Use custom element tags in HTML (e.g., <custom-navbar></custom-navbar>, <custom-footer></custom-footer>)
|
| 216 |
+
IMPORTANT: NEVER USE ONCLICK FUNCTION TO MAKE A REDIRECT TO NEW PAGE. MAKE SURE TO ALWAYS USE <a href=""/>, OTHERWISE IT WONT WORK WITH SHADOW ROOT AND WEB COMPONENTS.
|
| 217 |
${PROMPT_FOR_IMAGE_GENERATION}
|
| 218 |
Do NOT explain the changes or what you did, just return the expected results.
|
| 219 |
Update Format Rules:
|
| 220 |
1. Start with ${PROJECT_NAME_START}.
|
| 221 |
2. Add the name of the project, right after the start tag.
|
| 222 |
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 223 |
+
4. Start with ${UPDATE_FILE_START}
|
| 224 |
+
5. Provide the name of the file you are modifying (index.html, style.css, script.js, etc.).
|
| 225 |
+
6. Close the start tag with the ${UPDATE_FILE_END}.
|
| 226 |
7. Start with ${SEARCH_START}
|
| 227 |
8. Provide the exact lines from the current code that need to be replaced.
|
| 228 |
9. Use ${DIVIDER} to separate the search block from the replacement.
|
|
|
|
| 235 |
Example Modifying Code:
|
| 236 |
\`\`\`
|
| 237 |
Some explanation...
|
| 238 |
+
${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
|
| 239 |
+
${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
|
| 240 |
${SEARCH_START}
|
| 241 |
<h1>Old Title</h1>
|
| 242 |
${DIVIDER}
|
|
|
|
| 245 |
${SEARCH_START}
|
| 246 |
</body>
|
| 247 |
${DIVIDER}
|
| 248 |
+
<script src="script.js"></script>
|
| 249 |
</body>
|
| 250 |
${REPLACE_END}
|
| 251 |
\`\`\`
|
| 252 |
+
Example Updating CSS:
|
| 253 |
+
\`\`\`
|
| 254 |
+
${UPDATE_FILE_START}style.css${UPDATE_FILE_END}
|
| 255 |
+
${SEARCH_START}
|
| 256 |
+
body {
|
| 257 |
+
background: white;
|
| 258 |
+
}
|
| 259 |
+
${DIVIDER}
|
| 260 |
+
body {
|
| 261 |
+
background: linear-gradient(to right, #667eea, #764ba2);
|
| 262 |
+
}
|
| 263 |
+
${REPLACE_END}
|
| 264 |
+
\`\`\`
|
| 265 |
Example Deleting Code:
|
| 266 |
\`\`\`
|
| 267 |
Removing the paragraph...
|
| 268 |
+
${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
|
| 269 |
${SEARCH_START}
|
| 270 |
<p>This paragraph will be deleted.</p>
|
| 271 |
${DIVIDER}
|
| 272 |
${REPLACE_END}
|
| 273 |
\`\`\`
|
| 274 |
+
The user can also ask to add a new file (HTML page, CSS, JS, or Web Component), in this case you should return the new file in the following format:
|
| 275 |
+
1. Start with ${NEW_FILE_START}.
|
| 276 |
+
2. Add the name of the file (e.g., about.html, style.css, script.js, components/navbar.html), right after the start tag.
|
| 277 |
+
3. Close the start tag with the ${NEW_FILE_END}.
|
| 278 |
+
4. Start the file content with the triple backticks and appropriate language marker (\`\`\`html, \`\`\`css, or \`\`\`javascript).
|
| 279 |
+
5. Insert the file content there.
|
| 280 |
6. Close with the triple backticks, like \`\`\`.
|
| 281 |
+
7. Repeat for additional files.
|
| 282 |
+
Example Creating New HTML Page:
|
| 283 |
+
${NEW_FILE_START}about.html${NEW_FILE_END}
|
| 284 |
\`\`\`html
|
| 285 |
<!DOCTYPE html>
|
| 286 |
<html lang="en">
|
| 287 |
<head>
|
| 288 |
<meta charset="UTF-8">
|
| 289 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 290 |
+
<title>About</title>
|
| 291 |
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 292 |
+
<link rel="stylesheet" href="style.css">
|
| 293 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
|
|
|
|
|
| 294 |
</head>
|
| 295 |
<body>
|
| 296 |
+
<custom-navbar></custom-navbar>
|
| 297 |
+
<h1>About Page</h1>
|
| 298 |
+
<custom-footer></custom-footer>
|
| 299 |
+
<script src="components/navbar.js"></script>
|
| 300 |
+
<script src="components/footer.js"></script>
|
| 301 |
+
<script src="script.js"></script>
|
| 302 |
</body>
|
| 303 |
</html>
|
| 304 |
\`\`\`
|
| 305 |
+
Example Creating New Web Component:
|
| 306 |
+
${NEW_FILE_START}components/sidebar.js${NEW_FILE_END}
|
| 307 |
+
\`\`\`javascript
|
| 308 |
+
class CustomSidebar extends HTMLElement {
|
| 309 |
+
connectedCallback() {
|
| 310 |
+
this.attachShadow({ mode: 'open' });
|
| 311 |
+
this.shadowRoot.innerHTML = \`
|
| 312 |
+
<style>
|
| 313 |
+
aside {
|
| 314 |
+
width: 250px;
|
| 315 |
+
background: #f7fafc;
|
| 316 |
+
padding: 1rem;
|
| 317 |
+
height: 100vh;
|
| 318 |
+
position: fixed;
|
| 319 |
+
left: 0;
|
| 320 |
+
top: 0;
|
| 321 |
+
border-right: 1px solid #e5e7eb;
|
| 322 |
+
}
|
| 323 |
+
h3 { margin: 0 0 1rem 0; }
|
| 324 |
+
ul { list-style: none; padding: 0; margin: 0; }
|
| 325 |
+
li { margin: 0.5rem 0; }
|
| 326 |
+
a { color: #374151; text-decoration: none; }
|
| 327 |
+
a:hover { color: #667eea; }
|
| 328 |
+
</style>
|
| 329 |
+
<aside>
|
| 330 |
+
<h3>Sidebar</h3>
|
| 331 |
+
<ul>
|
| 332 |
+
<li><a href="/">Home</a></li>
|
| 333 |
+
<li><a href="/about.html">About</a></li>
|
| 334 |
+
</ul>
|
| 335 |
+
</aside>
|
| 336 |
+
\`;
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
customElements.define('custom-sidebar', CustomSidebar);
|
| 340 |
+
\`\`\`
|
| 341 |
+
Then UPDATE HTML files to include the component:
|
| 342 |
+
${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
|
| 343 |
+
${SEARCH_START}
|
| 344 |
+
<script src="script.js"></script>
|
| 345 |
+
</body>
|
| 346 |
+
${DIVIDER}
|
| 347 |
+
<script src="components/sidebar.js"></script>
|
| 348 |
+
<script src="script.js"></script>
|
| 349 |
+
</body>
|
| 350 |
+
${REPLACE_END}
|
| 351 |
+
${SEARCH_START}
|
| 352 |
+
<body>
|
| 353 |
+
<custom-navbar></custom-navbar>
|
| 354 |
+
${DIVIDER}
|
| 355 |
+
<body>
|
| 356 |
+
<custom-sidebar></custom-sidebar>
|
| 357 |
+
<custom-navbar></custom-navbar>
|
| 358 |
+
${REPLACE_END}
|
| 359 |
+
IMPORTANT: While creating a new HTML page, UPDATE ALL THE OTHER HTML files (using the UPDATE_FILE_START and SEARCH/REPLACE format) to add or replace the link to the new page, otherwise the user will not be able to navigate to the new page. (Don't use onclick to navigate, only href)
|
| 360 |
+
When creating new CSS/JS files, UPDATE ALL HTML files to include the appropriate <link> or <script> tags.
|
| 361 |
+
When creating new Web Components:
|
| 362 |
+
1. Create a NEW FILE in components/ folder (e.g., components/sidebar.js) with the component definition
|
| 363 |
+
2. UPDATE ALL HTML files that need the component to include <script src="components/componentname.js"></script> before the closing </body> tag
|
| 364 |
+
3. Use the custom element tag (e.g., <custom-componentname></custom-componentname>) in HTML pages where needed
|
| 365 |
No need to explain what you did. Just return the expected result.`
|
| 366 |
|
| 367 |
export const PROMPTS_FOR_AI = [
|
| 368 |
+
// Business & SaaS
|
| 369 |
+
"Create a modern SaaS landing page with a hero section featuring a product demo, benefits section with icons, pricing plans comparison table, customer testimonials with photos, FAQ accordion, and a prominent call-to-action footer.",
|
| 370 |
+
"Create a professional startup landing page with animated hero section, problem-solution showcase, feature highlights with screenshots, team members grid, investor logos, press mentions, and email signup form.",
|
| 371 |
+
"Create a business consulting website with a hero banner, services we offer section with hover effects, case studies carousel, client testimonials, team profiles with LinkedIn links, blog preview, and contact form.",
|
| 372 |
+
|
| 373 |
+
// E-commerce & Retail
|
| 374 |
+
"Create an e-commerce product landing page with hero image carousel, product features grid, size/color selector, customer reviews with star ratings, related products section, add to cart button, and shipping information.",
|
| 375 |
+
"Create an online store homepage with navigation menu, banner slider, featured products grid with hover effects, category cards, special offers section, newsletter signup, and footer with social links.",
|
| 376 |
+
"Create a fashion brand website with a full-screen hero image, new arrivals section, shop by category grid, Instagram feed integration, brand story section, and styling lookbook gallery.",
|
| 377 |
+
|
| 378 |
+
// Food & Restaurant
|
| 379 |
+
"Create a restaurant website with a hero section showing signature dishes, menu with categories and prices, chef's special highlights, reservation form with date picker, location map, opening hours, and customer reviews.",
|
| 380 |
+
"Create a modern coffee shop website with a cozy hero image, menu board with drinks and pastries, about our story section, location finder, online ordering button, and Instagram gallery showing cafΓ© atmosphere.",
|
| 381 |
+
"Create a food delivery landing page with cuisine categories, featured restaurants carousel, how it works steps, delivery zones map, app download buttons, promotional offers banner, and customer testimonials.",
|
| 382 |
+
|
| 383 |
+
// Real Estate & Property
|
| 384 |
+
"Create a real estate agency website with property search filters (location, price, bedrooms), featured listings grid with images, virtual tour options, mortgage calculator, agent profiles, neighborhood guides, and contact form.",
|
| 385 |
+
"Create a luxury property showcase website with full-screen image slider, property details with floor plans, amenities icons, 360Β° virtual tour button, location highlights, similar properties section, and inquiry form.",
|
| 386 |
+
|
| 387 |
+
// Creative & Portfolio
|
| 388 |
+
"Create a professional portfolio website for a photographer with a masonry image gallery, project categories filter, full-screen lightbox viewer, about me section with photo, services offered, client logos, and contact form.",
|
| 389 |
+
"Create a creative agency portfolio with animated hero section, featured projects showcase with case studies, services we provide, team members grid, client testimonials slider, awards section, and get a quote form.",
|
| 390 |
+
"Create a UX/UI designer portfolio with hero section showcasing best work, projects grid with filter tags, detailed case studies with before/after, design process timeline, skills and tools, testimonials, and hire me button.",
|
| 391 |
+
|
| 392 |
+
// Personal & Blog
|
| 393 |
+
"Create a personal brand website with an engaging hero section, about me with professional photo, skills and expertise cards, featured blog posts, speaking engagements, social media links, and newsletter signup.",
|
| 394 |
+
"Create a modern blog website with featured post hero, article cards grid with thumbnails, categories sidebar, search functionality, author bio section, related posts, social sharing buttons, and comment section.",
|
| 395 |
+
"Create a travel blog with full-width destination photos, travel stories grid, interactive world map showing visited places, travel tips section, gear recommendations, and subscription form.",
|
| 396 |
+
|
| 397 |
+
// Health & Fitness
|
| 398 |
+
"Create a fitness gym website with motivational hero video, class schedule timetable, trainer profiles with specializations, membership pricing comparison, transformation gallery, facilities photos, and trial class signup form.",
|
| 399 |
+
"Create a yoga studio website with calming hero section, class types with descriptions, instructor bios with photos, weekly schedule calendar, pricing packages, meditation tips blog, studio location map, and booking form.",
|
| 400 |
+
"Create a health & wellness landing page with hero section, service offerings, nutritionist/trainer profiles, success stories before/after, health blog articles, free consultation booking, and testimonials slider.",
|
| 401 |
+
|
| 402 |
+
// Education & Learning
|
| 403 |
+
"Create an online course landing page with course overview, curriculum breakdown with expandable modules, instructor credentials, student testimonials with videos, pricing and enrollment options, FAQ section, and money-back guarantee badge.",
|
| 404 |
+
"Create a university/school website with hero carousel, academic programs grid, campus life photo gallery, upcoming events calendar, faculty directory, admissions process timeline, virtual campus tour, and application form.",
|
| 405 |
+
"Create a tutoring service website with subjects offered, tutor profiles with qualifications, pricing plans, scheduling calendar, student success stories, free trial lesson signup, learning resources, and parent testimonials.",
|
| 406 |
+
|
| 407 |
+
// Events & Entertainment
|
| 408 |
+
"Create an event conference website with hero countdown timer, speaker lineup with bios, schedule/agenda tabs, venue information with map, ticket tiers and pricing, sponsors logos grid, past event highlights, and registration form.",
|
| 409 |
+
"Create a music festival landing page with artist/band lineup, stage schedule, venue map, ticket options with early bird pricing, photo gallery from previous years, camping information, FAQ, and buy tickets button.",
|
| 410 |
+
"Create a wedding website with couple's story, event timeline, venue details with directions, RSVP form, photo gallery, gift registry links, accommodation suggestions, and message board for guests.",
|
| 411 |
+
|
| 412 |
+
// Professional Services
|
| 413 |
+
"Create a law firm website with practice areas grid, attorney profiles with expertise, case results/wins, legal resources blog, testimonials, office locations, consultation booking form, and trust badges.",
|
| 414 |
+
"Create a dental clinic website with services offered, meet the dentist section with credentials, patient testimonials, before/after smile gallery, insurance accepted, appointment booking system, emergency contact, and dental tips blog.",
|
| 415 |
+
"Create an architecture firm website with portfolio of completed projects with large images, services overview, design process timeline, team members, awards and recognition, sustainable design approach, and project inquiry form.",
|
| 416 |
+
|
| 417 |
+
// Technology & Apps
|
| 418 |
+
"Create an app landing page with hero section showing app screenshots, key features with icons, how it works steps, pricing plans, user testimonials, app store download buttons, video demo, and early access signup.",
|
| 419 |
+
"Create a software product page with hero demo video, features comparison table, integration logos, API documentation link, use cases with examples, security certifications, customer stories, and free trial signup.",
|
| 420 |
+
|
| 421 |
+
// Non-profit & Community
|
| 422 |
+
"Create a non-profit organization website with mission statement hero, our impact statistics, current campaigns, donation form with amounts, volunteer opportunities, success stories, upcoming events, and newsletter signup.",
|
| 423 |
+
"Create a community organization website with welcome hero, about our mission, programs and services offered, event calendar, member spotlights, resources library, donation/support options, and get involved form.",
|
| 424 |
+
|
| 425 |
+
// Misc & Utility (keeping a few interactive examples)
|
| 426 |
+
"Create an interactive weather dashboard with current conditions, 5-day forecast cards, hourly temperature graph, air quality index, UV index, sunrise/sunset times, and location search with autocomplete.",
|
| 427 |
+
"Create a modern calculator with basic operations, scientific mode toggle, calculation history log, memory functions, keyboard support, light/dark theme switch, and copy result button.",
|
| 428 |
];
|
public/deepsite-badge.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
'use strict';
|
| 3 |
+
|
| 4 |
+
// Check if badge already exists
|
| 5 |
+
if (document.getElementById('deepsite-badge-wrapper')) {
|
| 6 |
+
return;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
// Inject keyframes for gradient rotation
|
| 10 |
+
const style = document.createElement('style');
|
| 11 |
+
style.textContent = `
|
| 12 |
+
@keyframes deepsite-spin {
|
| 13 |
+
0% {
|
| 14 |
+
transform: translate(-50%, -50%) rotate(0deg);
|
| 15 |
+
}
|
| 16 |
+
100% {
|
| 17 |
+
transform: translate(-50%, -50%) rotate(360deg);
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
#deepsite-badge-wrapper i {
|
| 22 |
+
pointer-events: none;
|
| 23 |
+
position: absolute;
|
| 24 |
+
top: 0;
|
| 25 |
+
right: 0;
|
| 26 |
+
bottom: 0;
|
| 27 |
+
left: 0;
|
| 28 |
+
z-index: -1;
|
| 29 |
+
padding: 1.5px;
|
| 30 |
+
transition-property: all;
|
| 31 |
+
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
|
| 32 |
+
transition-duration: .2s;
|
| 33 |
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
| 34 |
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
| 35 |
+
-webkit-mask-composite: xor;
|
| 36 |
+
mask-composite: exclude;
|
| 37 |
+
border-radius: inherit;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
#deepsite-badge-wrapper i::before {
|
| 41 |
+
content: "";
|
| 42 |
+
position: absolute;
|
| 43 |
+
left: 50%;
|
| 44 |
+
top: 50%;
|
| 45 |
+
display: block;
|
| 46 |
+
border-radius: 9999px;
|
| 47 |
+
opacity: 0;
|
| 48 |
+
background: conic-gradient(from 0deg at 50% 50%, #ec4899, #fbbf24, #3b82f6, #ec4899);
|
| 49 |
+
width: calc(100% * 2);
|
| 50 |
+
padding-bottom: calc(100% * 2);
|
| 51 |
+
transform: translate(-50%, -50%);
|
| 52 |
+
z-index: -1;
|
| 53 |
+
will-change: transform;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
#deepsite-badge-wrapper:hover i::before {
|
| 57 |
+
opacity: 1;
|
| 58 |
+
animation: deepsite-spin 3s linear infinite;
|
| 59 |
+
}
|
| 60 |
+
`;
|
| 61 |
+
document.head.appendChild(style);
|
| 62 |
+
|
| 63 |
+
// Create badge wrapper (like the button element)
|
| 64 |
+
const badgeWrapper = document.createElement('div');
|
| 65 |
+
badgeWrapper.id = 'deepsite-badge-wrapper';
|
| 66 |
+
|
| 67 |
+
// Create inner badge (like the span element)
|
| 68 |
+
const badgeInner = document.createElement('span');
|
| 69 |
+
badgeInner.id = 'deepsite-badge-inner';
|
| 70 |
+
|
| 71 |
+
// Create mask element (the i element)
|
| 72 |
+
const borderMask = document.createElement('i');
|
| 73 |
+
|
| 74 |
+
// Create link
|
| 75 |
+
const link = document.createElement('a');
|
| 76 |
+
link.href = 'https://deepsite.hf.co';
|
| 77 |
+
link.target = '_blank';
|
| 78 |
+
link.rel = 'noopener noreferrer';
|
| 79 |
+
|
| 80 |
+
// Create icon placeholder
|
| 81 |
+
const icon = document.createElement('img');
|
| 82 |
+
icon.src = 'https://deepsite.hf.co/logo.svg';
|
| 83 |
+
icon.alt = 'DeepSite';
|
| 84 |
+
icon.style.marginRight = '8px';
|
| 85 |
+
icon.style.width = '20px';
|
| 86 |
+
icon.style.height = '20px';
|
| 87 |
+
icon.style.filter = 'brightness(0) invert(1)';
|
| 88 |
+
|
| 89 |
+
// Create text
|
| 90 |
+
const text = document.createTextNode('Made with DeepSite');
|
| 91 |
+
|
| 92 |
+
// Apply styles to wrapper (like button element)
|
| 93 |
+
Object.assign(badgeWrapper.style, {
|
| 94 |
+
position: 'fixed',
|
| 95 |
+
bottom: '20px',
|
| 96 |
+
left: '20px',
|
| 97 |
+
zIndex: '999999',
|
| 98 |
+
color: '#ffffff',
|
| 99 |
+
borderRadius: '9999px',
|
| 100 |
+
background: 'rgba(0, 0, 0, 0.4)',
|
| 101 |
+
fontSize: '14px',
|
| 102 |
+
fontWeight: '500',
|
| 103 |
+
display: 'inline-block',
|
| 104 |
+
cursor: 'pointer',
|
| 105 |
+
padding: '1.5px',
|
| 106 |
+
overflow: 'hidden',
|
| 107 |
+
backdropFilter: 'blur(16px) saturate(180%)',
|
| 108 |
+
WebkitBackdropFilter: 'blur(16px) saturate(180%)',
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
// Apply styles to inner badge (like span element)
|
| 112 |
+
Object.assign(badgeInner.style, {
|
| 113 |
+
background: 'rgba(0, 0, 0, 0.6)',
|
| 114 |
+
padding: '10px 20px',
|
| 115 |
+
display: 'flex',
|
| 116 |
+
alignItems: 'center',
|
| 117 |
+
borderRadius: '9999px',
|
| 118 |
+
boxShadow: '0 8px 32px 0 rgba(0, 0, 0, 0.5)',
|
| 119 |
+
transition: 'all 0.3s ease',
|
| 120 |
+
border: '1px solid rgba(255, 255, 255, 0.1)'
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
// Apply styles to link
|
| 124 |
+
Object.assign(link.style, {
|
| 125 |
+
color: '#ffffff',
|
| 126 |
+
textDecoration: 'none',
|
| 127 |
+
fontWeight: '500',
|
| 128 |
+
display: 'flex',
|
| 129 |
+
alignItems: 'center',
|
| 130 |
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
| 131 |
+
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)'
|
| 132 |
+
});
|
| 133 |
+
|
| 134 |
+
// Add hover effect
|
| 135 |
+
badgeWrapper.addEventListener('mouseenter', function() {
|
| 136 |
+
badgeInner.style.background = 'rgba(0, 0, 0, 0.75)';
|
| 137 |
+
badgeInner.style.boxShadow = '0 8px 32px 0 rgba(0, 0, 0, 0.7)';
|
| 138 |
+
});
|
| 139 |
+
|
| 140 |
+
badgeWrapper.addEventListener('mouseleave', function() {
|
| 141 |
+
badgeInner.style.background = 'rgba(0, 0, 0, 0.6)';
|
| 142 |
+
badgeInner.style.boxShadow = '0 8px 32px 0 rgba(0, 0, 0, 0.5)';
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
// Append elements
|
| 146 |
+
link.appendChild(icon);
|
| 147 |
+
link.appendChild(text);
|
| 148 |
+
badgeInner.appendChild(link);
|
| 149 |
+
badgeWrapper.appendChild(badgeInner);
|
| 150 |
+
badgeWrapper.appendChild(borderMask);
|
| 151 |
+
|
| 152 |
+
// Wait for DOM to be ready
|
| 153 |
+
function init() {
|
| 154 |
+
if (document.body) {
|
| 155 |
+
document.body.appendChild(badgeWrapper);
|
| 156 |
+
} else {
|
| 157 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 158 |
+
document.body.appendChild(badgeWrapper);
|
| 159 |
+
});
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Initialize
|
| 164 |
+
if (document.readyState === 'loading') {
|
| 165 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 166 |
+
} else {
|
| 167 |
+
init();
|
| 168 |
+
}
|
| 169 |
+
})();
|
| 170 |
+
|