Spaces:
Running
Running
pull
#502
by
wolfaiOM
- opened
- README.md +4 -3
- app/api/ask/route.ts +455 -134
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +22 -62
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts +0 -102
- app/api/me/projects/[namespace]/[repoId]/download/route.ts +0 -105
- app/api/me/projects/[namespace]/[repoId]/route.ts +1 -1
- app/api/me/projects/[namespace]/[repoId]/update/route.ts +0 -141
- app/layout.tsx +5 -20
- assets/globals.css +0 -9
- assets/minimax.svg +0 -1
- components/discord-promo-modal/index.tsx +0 -225
- components/domain-redirect/index.tsx +23 -0
- components/editor/ask-ai/context.tsx +0 -4
- components/editor/ask-ai/index.tsx +11 -43
- components/editor/ask-ai/prompt-builder/content-modal.tsx +1 -1
- components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +1 -1
- components/editor/ask-ai/re-imagine.tsx +2 -8
- components/editor/ask-ai/selected-redesign-url.tsx +0 -37
- components/editor/ask-ai/settings.tsx +35 -81
- components/editor/file-browser/index.tsx +22 -10
- components/editor/header/index.tsx +18 -36
- components/editor/index.tsx +3 -7
- components/editor/preview/index.tsx +54 -365
- components/icons/discord.tsx +0 -27
- components/my-projects/project-card.tsx +1 -54
- components/public/navigation/index.tsx +2 -15
- hooks/useAi.ts +92 -254
- lib/best-provider.ts +8 -3
- lib/format-ai-response.ts +0 -255
- lib/prompts.ts +126 -146
- lib/providers.ts +22 -39
- middleware.ts +1 -2
- next.config.ts +0 -1
- package-lock.json +7 -113
- package.json +0 -1
README.md
CHANGED
|
@@ -7,7 +7,6 @@ sdk: docker
|
|
| 7 |
pinned: true
|
| 8 |
app_port: 3000
|
| 9 |
license: mit
|
| 10 |
-
failure_strategy: rollback
|
| 11 |
short_description: Generate any application by Vibe Coding
|
| 12 |
models:
|
| 13 |
- deepseek-ai/DeepSeek-V3-0324
|
|
@@ -19,10 +18,12 @@ models:
|
|
| 19 |
- moonshotai/Kimi-K2-Instruct
|
| 20 |
- moonshotai/Kimi-K2-Instruct-0905
|
| 21 |
- zai-org/GLM-4.6
|
| 22 |
-
- MiniMaxAI/MiniMax-M2
|
| 23 |
-
- moonshotai/Kimi-K2-Thinking
|
| 24 |
---
|
| 25 |
|
| 26 |
# DeepSite 🐳
|
| 27 |
|
| 28 |
DeepSite is a Vibe Coding Platform designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pinned: true
|
| 8 |
app_port: 3000
|
| 9 |
license: mit
|
|
|
|
| 10 |
short_description: Generate any application by Vibe Coding
|
| 11 |
models:
|
| 12 |
- deepseek-ai/DeepSeek-V3-0324
|
|
|
|
| 18 |
- moonshotai/Kimi-K2-Instruct
|
| 19 |
- moonshotai/Kimi-K2-Instruct-0905
|
| 20 |
- zai-org/GLM-4.6
|
|
|
|
|
|
|
| 21 |
---
|
| 22 |
|
| 23 |
# DeepSite 🐳
|
| 24 |
|
| 25 |
DeepSite is a Vibe Coding Platform designed to make coding smarter and more efficient. Tailored for developers, data scientists, and AI engineers, it integrates generative AI into your coding projects to enhance creativity and productivity.
|
| 26 |
+
|
| 27 |
+
## How to use it locally
|
| 28 |
+
|
| 29 |
+
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
app/api/ask/route.ts
CHANGED
|
@@ -6,24 +6,38 @@ import { InferenceClient } from "@huggingface/inference";
|
|
| 6 |
|
| 7 |
import { MODELS } from "@/lib/providers";
|
| 8 |
import {
|
|
|
|
| 9 |
FOLLOW_UP_SYSTEM_PROMPT,
|
| 10 |
-
FOLLOW_UP_SYSTEM_PROMPT_LIGHT,
|
| 11 |
INITIAL_SYSTEM_PROMPT,
|
| 12 |
-
INITIAL_SYSTEM_PROMPT_LIGHT,
|
| 13 |
MAX_REQUESTS_PER_IP,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
PROMPT_FOR_PROJECT_NAME,
|
| 15 |
} from "@/lib/prompts";
|
|
|
|
| 16 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 17 |
import { Page } from "@/types";
|
|
|
|
| 18 |
import { isAuthenticated } from "@/lib/auth";
|
| 19 |
import { getBestProvider } from "@/lib/best-provider";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
const ipAddresses = new Map();
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
export async function POST(request: NextRequest) {
|
| 24 |
const authHeaders = await headers();
|
| 25 |
-
const
|
| 26 |
-
const userToken = tokenInHeaders ? tokenInHeaders.replace("Bearer ", "") : request.cookies.get(MY_TOKEN_KEY())?.value;
|
| 27 |
|
| 28 |
const body = await request.json();
|
| 29 |
const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
|
|
@@ -63,7 +77,7 @@ export async function POST(request: NextRequest) {
|
|
| 63 |
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 64 |
: authHeaders.get("x-forwarded-for");
|
| 65 |
|
| 66 |
-
if (!token
|
| 67 |
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 68 |
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 69 |
return NextResponse.json(
|
|
@@ -80,6 +94,14 @@ export async function POST(request: NextRequest) {
|
|
| 80 |
billTo = "huggingface";
|
| 81 |
}
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
try {
|
| 84 |
const encoder = new TextEncoder();
|
| 85 |
const stream = new TransformStream();
|
|
@@ -94,27 +116,29 @@ export async function POST(request: NextRequest) {
|
|
| 94 |
});
|
| 95 |
|
| 96 |
(async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
try {
|
| 98 |
const client = new InferenceClient(token);
|
| 99 |
|
| 100 |
-
const systemPrompt =
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
const userPrompt = prompt;
|
| 105 |
-
|
| 106 |
const chatCompletion = client.chatCompletionStream(
|
| 107 |
{
|
| 108 |
-
model: selectedModel.value
|
|
|
|
| 109 |
messages: [
|
| 110 |
{
|
| 111 |
role: "system",
|
| 112 |
content: systemPrompt,
|
| 113 |
},
|
| 114 |
-
...(redesignMarkdown ? [{
|
| 115 |
-
role: "assistant",
|
| 116 |
-
content: `User will ask you to redesign the site based on this markdown. Use the same images as the site, but you can improve the content and the design. Here is the markdown: ${redesignMarkdown}`
|
| 117 |
-
}] : []),
|
| 118 |
{
|
| 119 |
role: "user",
|
| 120 |
content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
|
|
@@ -122,28 +146,56 @@ export async function POST(request: NextRequest) {
|
|
| 122 |
3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
|
| 123 |
},
|
| 124 |
],
|
| 125 |
-
...
|
| 126 |
-
...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
|
| 127 |
-
...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
|
| 128 |
},
|
| 129 |
billTo ? { billTo } : {}
|
| 130 |
);
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
|
|
|
| 144 |
await writer.close();
|
| 145 |
} catch (error: any) {
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
await writer.write(
|
| 148 |
encoder.encode(
|
| 149 |
JSON.stringify({
|
|
@@ -207,9 +259,11 @@ export async function PUT(request: NextRequest) {
|
|
| 207 |
const authHeaders = await headers();
|
| 208 |
|
| 209 |
const body = await request.json();
|
| 210 |
-
const { prompt, provider, selectedElementHtml, model, pages, files, repoId, isNew } =
|
| 211 |
body;
|
| 212 |
|
|
|
|
|
|
|
| 213 |
if (!prompt || pages.length === 0) {
|
| 214 |
return NextResponse.json(
|
| 215 |
{ ok: false, error: "Missing required fields" },
|
|
@@ -243,7 +297,7 @@ export async function PUT(request: NextRequest) {
|
|
| 243 |
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 244 |
: authHeaders.get("x-forwarded-for");
|
| 245 |
|
| 246 |
-
if (!token
|
| 247 |
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 248 |
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 249 |
return NextResponse.json(
|
|
@@ -260,131 +314,398 @@ export async function PUT(request: NextRequest) {
|
|
| 260 |
billTo = "huggingface";
|
| 261 |
}
|
| 262 |
|
| 263 |
-
|
| 264 |
-
const encoder = new TextEncoder();
|
| 265 |
-
const stream = new TransformStream();
|
| 266 |
-
const writer = stream.writable.getWriter();
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
"Cache-Control": "no-cache",
|
| 272 |
-
Connection: "keep-alive",
|
| 273 |
-
},
|
| 274 |
-
});
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
|
|
|
|
|
|
| 295 |
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
{
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
{
|
| 301 |
-
role: "system",
|
| 302 |
-
content: systemPrompt,
|
| 303 |
-
},
|
| 304 |
-
{
|
| 305 |
-
role: "user",
|
| 306 |
-
content: userContext,
|
| 307 |
-
},
|
| 308 |
-
{
|
| 309 |
-
role: "assistant",
|
| 310 |
-
content: assistantContext,
|
| 311 |
-
},
|
| 312 |
-
{
|
| 313 |
-
role: "user",
|
| 314 |
-
content: prompt,
|
| 315 |
-
},
|
| 316 |
-
],
|
| 317 |
-
...(selectedModel.top_k ? { top_k: selectedModel.top_k } : {}),
|
| 318 |
-
...(selectedModel.temperature ? { temperature: selectedModel.temperature } : {}),
|
| 319 |
-
...(selectedModel.top_p ? { top_p: selectedModel.top_p } : {}),
|
| 320 |
},
|
| 321 |
-
|
| 322 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
}
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
|
|
|
| 334 |
}
|
| 335 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
userName: user.name,
|
| 341 |
-
})}\n___METADATA_END___\n`));
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
| 364 |
);
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
JSON.stringify({
|
| 369 |
-
ok: false,
|
| 370 |
-
message:
|
| 371 |
-
error.message ||
|
| 372 |
-
"An error occurred while processing your request.",
|
| 373 |
-
})
|
| 374 |
-
)
|
| 375 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
}
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
// ignore
|
| 382 |
}
|
| 383 |
}
|
| 384 |
-
})();
|
| 385 |
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
} catch (error: any) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
return NextResponse.json(
|
| 389 |
{
|
| 390 |
ok: false,
|
|
|
|
| 6 |
|
| 7 |
import { MODELS } from "@/lib/providers";
|
| 8 |
import {
|
| 9 |
+
DIVIDER,
|
| 10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
|
|
|
| 11 |
INITIAL_SYSTEM_PROMPT,
|
|
|
|
| 12 |
MAX_REQUESTS_PER_IP,
|
| 13 |
+
NEW_FILE_END,
|
| 14 |
+
NEW_FILE_START,
|
| 15 |
+
REPLACE_END,
|
| 16 |
+
SEARCH_START,
|
| 17 |
+
UPDATE_FILE_START,
|
| 18 |
+
UPDATE_FILE_END,
|
| 19 |
PROMPT_FOR_PROJECT_NAME,
|
| 20 |
} from "@/lib/prompts";
|
| 21 |
+
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
| 22 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 23 |
import { Page } from "@/types";
|
| 24 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 25 |
import { isAuthenticated } from "@/lib/auth";
|
| 26 |
import { getBestProvider } from "@/lib/best-provider";
|
| 27 |
+
// import { rewritePrompt } from "@/lib/rewrite-prompt";
|
| 28 |
+
import { COLORS } from "@/lib/utils";
|
| 29 |
+
import { templates } from "@/lib/templates";
|
| 30 |
+
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 31 |
|
| 32 |
const ipAddresses = new Map();
|
| 33 |
|
| 34 |
+
const STREAMING_TIMEOUT = 180000;
|
| 35 |
+
const REQUEST_TIMEOUT = 240000;
|
| 36 |
+
export const maxDuration = 300;
|
| 37 |
+
|
| 38 |
export async function POST(request: NextRequest) {
|
| 39 |
const authHeaders = await headers();
|
| 40 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
|
|
|
| 41 |
|
| 42 |
const body = await request.json();
|
| 43 |
const { prompt, provider, model, redesignMarkdown, enhancedSettings, pages } = body;
|
|
|
|
| 77 |
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 78 |
: authHeaders.get("x-forwarded-for");
|
| 79 |
|
| 80 |
+
if (!token) {
|
| 81 |
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 82 |
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 83 |
return NextResponse.json(
|
|
|
|
| 94 |
billTo = "huggingface";
|
| 95 |
}
|
| 96 |
|
| 97 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
| 98 |
+
|
| 99 |
+
let rewrittenPrompt = redesignMarkdown ? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown. Use the images in the markdown.` : prompt;
|
| 100 |
+
|
| 101 |
+
if (enhancedSettings.isActive) {
|
| 102 |
+
// rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
try {
|
| 106 |
const encoder = new TextEncoder();
|
| 107 |
const stream = new TransformStream();
|
|
|
|
| 116 |
});
|
| 117 |
|
| 118 |
(async () => {
|
| 119 |
+
// let completeResponse = "";
|
| 120 |
+
let timeoutId: NodeJS.Timeout | null = null;
|
| 121 |
+
let isTimedOut = false;
|
| 122 |
+
|
| 123 |
try {
|
| 124 |
const client = new InferenceClient(token);
|
| 125 |
|
| 126 |
+
const systemPrompt = INITIAL_SYSTEM_PROMPT;
|
| 127 |
+
|
| 128 |
+
const userPrompt = rewrittenPrompt;
|
| 129 |
+
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
|
| 130 |
+
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
|
| 131 |
+
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 132 |
|
|
|
|
|
|
|
| 133 |
const chatCompletion = client.chatCompletionStream(
|
| 134 |
{
|
| 135 |
+
model: selectedModel.value,
|
| 136 |
+
provider: selectedProvider.provider,
|
| 137 |
messages: [
|
| 138 |
{
|
| 139 |
role: "system",
|
| 140 |
content: systemPrompt,
|
| 141 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
{
|
| 143 |
role: "user",
|
| 144 |
content: userPrompt + (enhancedSettings.isActive ? `1. I want to use the following primary color: ${enhancedSettings.primaryColor} (eg: bg-${enhancedSettings.primaryColor}-500).
|
|
|
|
| 146 |
3. I want to use the following theme: ${enhancedSettings.theme} mode.` : "")
|
| 147 |
},
|
| 148 |
],
|
| 149 |
+
...providerConfig,
|
|
|
|
|
|
|
| 150 |
},
|
| 151 |
billTo ? { billTo } : {}
|
| 152 |
);
|
| 153 |
|
| 154 |
+
// Set up timeout
|
| 155 |
+
const timeoutPromise = new Promise((_, reject) => {
|
| 156 |
+
timeoutId = setTimeout(() => {
|
| 157 |
+
isTimedOut = true;
|
| 158 |
+
reject(new Error('Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.'));
|
| 159 |
+
}, STREAMING_TIMEOUT);
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
// Race between streaming and timeout
|
| 163 |
+
await Promise.race([
|
| 164 |
+
(async () => {
|
| 165 |
+
while (true) {
|
| 166 |
+
const { done, value } = await chatCompletion.next()
|
| 167 |
+
if (done) {
|
| 168 |
+
break;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
const chunk = value.choices[0]?.delta?.content;
|
| 172 |
+
if (chunk) {
|
| 173 |
+
await writer.write(encoder.encode(chunk));
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
})(),
|
| 177 |
+
timeoutPromise
|
| 178 |
+
]);
|
| 179 |
+
|
| 180 |
+
// Clear timeout if successful
|
| 181 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 182 |
|
| 183 |
+
// Explicitly close the writer after successful completion
|
| 184 |
await writer.close();
|
| 185 |
} catch (error: any) {
|
| 186 |
+
// Clear timeout on error
|
| 187 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 188 |
+
|
| 189 |
+
if (isTimedOut || error.message?.includes('timeout') || error.message?.includes('Request timeout')) {
|
| 190 |
+
await writer.write(
|
| 191 |
+
encoder.encode(
|
| 192 |
+
JSON.stringify({
|
| 193 |
+
ok: false,
|
| 194 |
+
message: "Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.",
|
| 195 |
+
})
|
| 196 |
+
)
|
| 197 |
+
);
|
| 198 |
+
} else if (error.message?.includes("exceeded your monthly included credits")) {
|
| 199 |
await writer.write(
|
| 200 |
encoder.encode(
|
| 201 |
JSON.stringify({
|
|
|
|
| 259 |
const authHeaders = await headers();
|
| 260 |
|
| 261 |
const body = await request.json();
|
| 262 |
+
const { prompt, provider, selectedElementHtml, model, pages, files, repoId: repoIdFromBody, isNew } =
|
| 263 |
body;
|
| 264 |
|
| 265 |
+
let repoId = repoIdFromBody;
|
| 266 |
+
|
| 267 |
if (!prompt || pages.length === 0) {
|
| 268 |
return NextResponse.json(
|
| 269 |
{ ok: false, error: "Missing required fields" },
|
|
|
|
| 297 |
? authHeaders.get("x-forwarded-for")?.split(",")[1].trim()
|
| 298 |
: authHeaders.get("x-forwarded-for");
|
| 299 |
|
| 300 |
+
if (!token) {
|
| 301 |
ipAddresses.set(ip, (ipAddresses.get(ip) || 0) + 1);
|
| 302 |
if (ipAddresses.get(ip) > MAX_REQUESTS_PER_IP) {
|
| 303 |
return NextResponse.json(
|
|
|
|
| 314 |
billTo = "huggingface";
|
| 315 |
}
|
| 316 |
|
| 317 |
+
const client = new InferenceClient(token);
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
+
const escapeRegExp = (string: string) => {
|
| 320 |
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 321 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
+
const createFlexibleHtmlRegex = (searchBlock: string) => {
|
| 324 |
+
let searchRegex = escapeRegExp(searchBlock)
|
| 325 |
+
.replace(/\s+/g, '\\s*')
|
| 326 |
+
.replace(/>\s*</g, '>\\s*<')
|
| 327 |
+
.replace(/\s*>/g, '\\s*>');
|
| 328 |
+
|
| 329 |
+
return new RegExp(searchRegex, 'g');
|
| 330 |
+
};
|
| 331 |
|
| 332 |
+
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
| 333 |
+
|
| 334 |
+
try {
|
| 335 |
+
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
| 336 |
+
const userContext = "You are modifying the HTML file based on the user's request.";
|
| 337 |
+
|
| 338 |
+
const allPages = pages || [];
|
| 339 |
+
const pagesContext = allPages
|
| 340 |
+
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
| 341 |
+
.join("\n\n");
|
| 342 |
+
|
| 343 |
+
const assistantContext = `${
|
| 344 |
+
selectedElementHtml
|
| 345 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
|
| 346 |
+
: ""
|
| 347 |
+
}. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
| 348 |
+
|
| 349 |
+
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
| 350 |
+
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
| 351 |
+
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
| 352 |
+
|
| 353 |
+
const chatCompletion = client.chatCompletionStream(
|
| 354 |
+
{
|
| 355 |
+
model: selectedModel.value,
|
| 356 |
+
provider: selectedProvider.provider,
|
| 357 |
+
messages: [
|
| 358 |
+
{
|
| 359 |
+
role: "system",
|
| 360 |
+
content: systemPrompt,
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
role: "user",
|
| 364 |
+
content: userContext,
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
role: "assistant",
|
| 368 |
+
content: assistantContext,
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
role: "user",
|
| 372 |
+
content: prompt,
|
| 373 |
+
},
|
| 374 |
+
],
|
| 375 |
+
...providerConfig,
|
| 376 |
+
},
|
| 377 |
+
billTo ? { billTo } : {}
|
| 378 |
+
);
|
| 379 |
|
| 380 |
+
// Set up timeout for AI streaming
|
| 381 |
+
let chunk = "";
|
| 382 |
+
let timeoutId: NodeJS.Timeout | null = null;
|
| 383 |
+
let isTimedOut = false;
|
| 384 |
|
| 385 |
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
| 386 |
+
timeoutId = setTimeout(() => {
|
| 387 |
+
isTimedOut = true;
|
| 388 |
+
reject(new Error('Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.'));
|
| 389 |
+
}, REQUEST_TIMEOUT);
|
| 390 |
+
});
|
| 391 |
|
| 392 |
+
try {
|
| 393 |
+
await Promise.race([
|
| 394 |
+
(async () => {
|
| 395 |
+
while (true) {
|
| 396 |
+
const { done, value } = await chatCompletion.next();
|
| 397 |
+
if (done) {
|
| 398 |
+
break;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
const deltaContent = value.choices[0]?.delta?.content;
|
| 402 |
+
if (deltaContent) {
|
| 403 |
+
chunk += deltaContent;
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
})(),
|
| 407 |
+
timeoutPromise
|
| 408 |
+
]);
|
| 409 |
+
|
| 410 |
+
// Clear timeout if successful
|
| 411 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 412 |
+
} catch (timeoutError: any) {
|
| 413 |
+
// Clear timeout on error
|
| 414 |
+
if (timeoutId) clearTimeout(timeoutId);
|
| 415 |
+
|
| 416 |
+
if (isTimedOut || timeoutError.message?.includes('timeout') || timeoutError.message?.includes('Request timeout')) {
|
| 417 |
+
return NextResponse.json(
|
| 418 |
{
|
| 419 |
+
ok: false,
|
| 420 |
+
message: "Request timeout: The AI model took too long to respond. Please try again with a simpler prompt or try a different model.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
},
|
| 422 |
+
{ status: 504 }
|
| 423 |
);
|
| 424 |
+
}
|
| 425 |
+
throw timeoutError;
|
| 426 |
+
}
|
| 427 |
+
if (!chunk) {
|
| 428 |
+
return NextResponse.json(
|
| 429 |
+
{ ok: false, message: "No content returned from the model" },
|
| 430 |
+
{ status: 400 }
|
| 431 |
+
);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
if (chunk) {
|
| 435 |
+
const updatedLines: number[][] = [];
|
| 436 |
+
let newHtml = "";
|
| 437 |
+
const updatedPages = [...(pages || [])];
|
| 438 |
|
| 439 |
+
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');
|
| 440 |
+
let updateFileMatch;
|
| 441 |
+
|
| 442 |
+
while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
|
| 443 |
+
const [, filePath, fileContent] = updateFileMatch;
|
| 444 |
+
|
| 445 |
+
const pageIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 446 |
+
if (pageIndex !== -1) {
|
| 447 |
+
let pageHtml = updatedPages[pageIndex].html;
|
| 448 |
+
|
| 449 |
+
let processedContent = fileContent;
|
| 450 |
+
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 451 |
+
if (htmlMatch) {
|
| 452 |
+
processedContent = htmlMatch[1];
|
| 453 |
+
}
|
| 454 |
+
let position = 0;
|
| 455 |
+
let moreBlocks = true;
|
| 456 |
+
|
| 457 |
+
while (moreBlocks) {
|
| 458 |
+
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
| 459 |
+
if (searchStartIndex === -1) {
|
| 460 |
+
moreBlocks = false;
|
| 461 |
+
continue;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
| 465 |
+
if (dividerIndex === -1) {
|
| 466 |
+
moreBlocks = false;
|
| 467 |
+
continue;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
| 471 |
+
if (replaceEndIndex === -1) {
|
| 472 |
+
moreBlocks = false;
|
| 473 |
+
continue;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
const searchBlock = processedContent.substring(
|
| 477 |
+
searchStartIndex + SEARCH_START.length,
|
| 478 |
+
dividerIndex
|
| 479 |
+
);
|
| 480 |
+
const replaceBlock = processedContent.substring(
|
| 481 |
+
dividerIndex + DIVIDER.length,
|
| 482 |
+
replaceEndIndex
|
| 483 |
+
);
|
| 484 |
+
|
| 485 |
+
if (searchBlock.trim() === "") {
|
| 486 |
+
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
| 487 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 488 |
+
} else {
|
| 489 |
+
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 490 |
+
const match = regex.exec(pageHtml);
|
| 491 |
+
|
| 492 |
+
if (match) {
|
| 493 |
+
const matchedText = match[0];
|
| 494 |
+
const beforeText = pageHtml.substring(0, match.index);
|
| 495 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 496 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 497 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 498 |
+
|
| 499 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 500 |
+
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 505 |
}
|
| 506 |
|
| 507 |
+
updatedPages[pageIndex].html = pageHtml;
|
| 508 |
+
|
| 509 |
+
if (filePath === '/' || filePath === '/index' || filePath === 'index' || filePath === 'index.html') {
|
| 510 |
+
newHtml = pageHtml;
|
| 511 |
}
|
| 512 |
}
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
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');
|
| 516 |
+
let newFileMatch;
|
| 517 |
+
|
| 518 |
+
while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
|
| 519 |
+
const [, filePath, fileContent] = newFileMatch;
|
| 520 |
+
|
| 521 |
+
let fileData = fileContent;
|
| 522 |
+
// Try to extract content from code blocks
|
| 523 |
+
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 524 |
+
const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
|
| 525 |
+
const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
|
| 526 |
+
|
| 527 |
+
if (htmlMatch) {
|
| 528 |
+
fileData = htmlMatch[1];
|
| 529 |
+
} else if (cssMatch) {
|
| 530 |
+
fileData = cssMatch[1];
|
| 531 |
+
} else if (jsMatch) {
|
| 532 |
+
fileData = jsMatch[1];
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 536 |
+
|
| 537 |
+
if (existingFileIndex !== -1) {
|
| 538 |
+
updatedPages[existingFileIndex] = {
|
| 539 |
+
path: filePath,
|
| 540 |
+
html: fileData.trim()
|
| 541 |
+
};
|
| 542 |
+
} else {
|
| 543 |
+
updatedPages.push({
|
| 544 |
+
path: filePath,
|
| 545 |
+
html: fileData.trim()
|
| 546 |
+
});
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
|
| 550 |
+
if (updatedPages.length === pages?.length && !chunk.includes(UPDATE_FILE_START)) {
|
| 551 |
+
let position = 0;
|
| 552 |
+
let moreBlocks = true;
|
|
|
|
|
|
|
| 553 |
|
| 554 |
+
while (moreBlocks) {
|
| 555 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 556 |
+
if (searchStartIndex === -1) {
|
| 557 |
+
moreBlocks = false;
|
| 558 |
+
continue;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
| 562 |
+
if (dividerIndex === -1) {
|
| 563 |
+
moreBlocks = false;
|
| 564 |
+
continue;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
| 568 |
+
if (replaceEndIndex === -1) {
|
| 569 |
+
moreBlocks = false;
|
| 570 |
+
continue;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
const searchBlock = chunk.substring(
|
| 574 |
+
searchStartIndex + SEARCH_START.length,
|
| 575 |
+
dividerIndex
|
| 576 |
);
|
| 577 |
+
const replaceBlock = chunk.substring(
|
| 578 |
+
dividerIndex + DIVIDER.length,
|
| 579 |
+
replaceEndIndex
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
);
|
| 581 |
+
|
| 582 |
+
if (searchBlock.trim() === "") {
|
| 583 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 584 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 585 |
+
} else {
|
| 586 |
+
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 587 |
+
const match = regex.exec(newHtml);
|
| 588 |
+
|
| 589 |
+
if (match) {
|
| 590 |
+
const matchedText = match[0];
|
| 591 |
+
const beforeText = newHtml.substring(0, match.index);
|
| 592 |
+
const startLineNumber = beforeText.split("\n").length;
|
| 593 |
+
const replaceLines = replaceBlock.split("\n").length;
|
| 594 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 595 |
+
|
| 596 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
| 597 |
+
newHtml = newHtml.replace(matchedText, replaceBlock);
|
| 598 |
+
}
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
position = replaceEndIndex + REPLACE_END.length;
|
| 602 |
}
|
| 603 |
+
|
| 604 |
+
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 605 |
+
if (mainPageIndex !== -1) {
|
| 606 |
+
updatedPages[mainPageIndex].html = newHtml;
|
|
|
|
| 607 |
}
|
| 608 |
}
|
|
|
|
| 609 |
|
| 610 |
+
const files: File[] = [];
|
| 611 |
+
updatedPages.forEach((page: Page) => {
|
| 612 |
+
let mimeType = "text/html";
|
| 613 |
+
if (page.path.endsWith(".css")) {
|
| 614 |
+
mimeType = "text/css";
|
| 615 |
+
} else if (page.path.endsWith(".js")) {
|
| 616 |
+
mimeType = "text/javascript";
|
| 617 |
+
} else if (page.path.endsWith(".json")) {
|
| 618 |
+
mimeType = "application/json";
|
| 619 |
+
}
|
| 620 |
+
const content = (mimeType === "text/html" && isIndexPage(page.path)) && isNew
|
| 621 |
+
? injectDeepSiteBadge(page.html)
|
| 622 |
+
: page.html;
|
| 623 |
+
const file = new File([content], page.path, { type: mimeType });
|
| 624 |
+
files.push(file);
|
| 625 |
+
});
|
| 626 |
+
|
| 627 |
+
if (isNew) {
|
| 628 |
+
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 629 |
+
const formattedTitle = projectName?.toLowerCase()
|
| 630 |
+
.replace(/[^a-z0-9]+/g, "-")
|
| 631 |
+
.split("-")
|
| 632 |
+
.filter(Boolean)
|
| 633 |
+
.join("-")
|
| 634 |
+
.slice(0, 96);
|
| 635 |
+
const repo: RepoDesignation = {
|
| 636 |
+
type: "space",
|
| 637 |
+
name: `${user.name}/${formattedTitle}`,
|
| 638 |
+
};
|
| 639 |
+
const { repoUrl} = await createRepo({
|
| 640 |
+
repo,
|
| 641 |
+
accessToken: user.token as string,
|
| 642 |
+
});
|
| 643 |
+
repoId = repoUrl.split("/").slice(-2).join("/");
|
| 644 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 645 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 646 |
+
const README = `---
|
| 647 |
+
title: ${projectName}
|
| 648 |
+
colorFrom: ${colorFrom}
|
| 649 |
+
colorTo: ${colorTo}
|
| 650 |
+
emoji: 🐳
|
| 651 |
+
sdk: static
|
| 652 |
+
pinned: false
|
| 653 |
+
tags:
|
| 654 |
+
- deepsite-v3
|
| 655 |
+
---
|
| 656 |
+
|
| 657 |
+
# Welcome to your new DeepSite project!
|
| 658 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 659 |
+
`;
|
| 660 |
+
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
const response = await uploadFiles({
|
| 664 |
+
repo: {
|
| 665 |
+
type: "space",
|
| 666 |
+
name: repoId,
|
| 667 |
+
},
|
| 668 |
+
files,
|
| 669 |
+
commitTitle: prompt,
|
| 670 |
+
accessToken: user.token as string,
|
| 671 |
+
});
|
| 672 |
+
|
| 673 |
+
return NextResponse.json({
|
| 674 |
+
ok: true,
|
| 675 |
+
updatedLines,
|
| 676 |
+
pages: updatedPages,
|
| 677 |
+
repoId,
|
| 678 |
+
commit: {
|
| 679 |
+
...response.commit,
|
| 680 |
+
title: prompt,
|
| 681 |
+
}
|
| 682 |
+
});
|
| 683 |
+
} else {
|
| 684 |
+
return NextResponse.json(
|
| 685 |
+
{ ok: false, message: "No content returned from the model" },
|
| 686 |
+
{ status: 400 }
|
| 687 |
+
);
|
| 688 |
+
}
|
| 689 |
} catch (error: any) {
|
| 690 |
+
if (error.message?.includes('timeout') || error.message?.includes('Request timeout')) {
|
| 691 |
+
return NextResponse.json(
|
| 692 |
+
{
|
| 693 |
+
ok: false,
|
| 694 |
+
message: "Request timeout: The operation took too long to complete. Please try again with a simpler request or try a different model.",
|
| 695 |
+
},
|
| 696 |
+
{ status: 504 }
|
| 697 |
+
);
|
| 698 |
+
}
|
| 699 |
+
if (error.message?.includes("exceeded your monthly included credits")) {
|
| 700 |
+
return NextResponse.json(
|
| 701 |
+
{
|
| 702 |
+
ok: false,
|
| 703 |
+
openProModal: true,
|
| 704 |
+
message: error.message,
|
| 705 |
+
},
|
| 706 |
+
{ status: 402 }
|
| 707 |
+
);
|
| 708 |
+
}
|
| 709 |
return NextResponse.json(
|
| 710 |
{
|
| 711 |
ok: false,
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
import { Page } from "@/types";
|
|
@@ -49,13 +49,13 @@ export async function POST(
|
|
| 49 |
);
|
| 50 |
}
|
| 51 |
|
|
|
|
| 52 |
const files: File[] = [];
|
| 53 |
const pages: Page[] = [];
|
| 54 |
-
const mediaFiles: string[] = [];
|
| 55 |
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
| 56 |
-
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"];
|
| 57 |
const commitFilePaths: Set<string> = new Set();
|
| 58 |
|
|
|
|
| 59 |
for await (const fileInfo of listFiles({
|
| 60 |
repo,
|
| 61 |
accessToken: user.token as string,
|
|
@@ -63,24 +63,26 @@ export async function POST(
|
|
| 63 |
})) {
|
| 64 |
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 65 |
|
| 66 |
-
if (
|
| 67 |
commitFilePaths.add(fileInfo.path);
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
revision: commitId,
|
| 74 |
-
raw: true
|
| 75 |
-
});
|
| 76 |
-
const content = await blob?.text();
|
| 77 |
|
| 78 |
-
if (
|
|
|
|
| 79 |
let mimeType = "text/plain";
|
| 80 |
|
| 81 |
switch (fileExtension) {
|
| 82 |
case "html":
|
| 83 |
mimeType = "text/html";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
break;
|
| 85 |
case "css":
|
| 86 |
mimeType = "text/css";
|
|
@@ -91,62 +93,18 @@ export async function POST(
|
|
| 91 |
case "json":
|
| 92 |
mimeType = "application/json";
|
| 93 |
break;
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
pages.unshift({
|
| 98 |
-
path: fileInfo.path,
|
| 99 |
-
html: content,
|
| 100 |
-
});
|
| 101 |
-
} else {
|
| 102 |
-
pages.push({
|
| 103 |
-
path: fileInfo.path,
|
| 104 |
-
html: content,
|
| 105 |
-
});
|
| 106 |
}
|
| 107 |
|
| 108 |
const file = new File([content], fileInfo.path, { type: mimeType });
|
| 109 |
files.push(file);
|
| 110 |
}
|
| 111 |
}
|
| 112 |
-
else if (fileInfo.type === "directory" && (["videos", "images", "audio"].includes(fileInfo.path) || fileInfo.path === "components")) {
|
| 113 |
-
for await (const subFileInfo of listFiles({
|
| 114 |
-
repo,
|
| 115 |
-
accessToken: user.token as string,
|
| 116 |
-
revision: commitId,
|
| 117 |
-
path: fileInfo.path,
|
| 118 |
-
})) {
|
| 119 |
-
if (subFileInfo.path.includes("components")) {
|
| 120 |
-
commitFilePaths.add(subFileInfo.path);
|
| 121 |
-
const blob = await downloadFile({
|
| 122 |
-
repo,
|
| 123 |
-
accessToken: user.token as string,
|
| 124 |
-
path: subFileInfo.path,
|
| 125 |
-
revision: commitId,
|
| 126 |
-
raw: true
|
| 127 |
-
});
|
| 128 |
-
const content = await blob?.text();
|
| 129 |
-
|
| 130 |
-
if (content) {
|
| 131 |
-
pages.push({
|
| 132 |
-
path: subFileInfo.path,
|
| 133 |
-
html: content,
|
| 134 |
-
});
|
| 135 |
-
|
| 136 |
-
const file = new File([content], subFileInfo.path, { type: "text/html" });
|
| 137 |
-
files.push(file);
|
| 138 |
-
}
|
| 139 |
-
} else if (allowedFilesExtensions.includes(subFileInfo.path.split(".").pop() || "")) {
|
| 140 |
-
commitFilePaths.add(subFileInfo.path);
|
| 141 |
-
mediaFiles.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${subFileInfo.path}`);
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
-
else if (allowedExtensions.includes(fileExtension || "")) {
|
| 146 |
-
commitFilePaths.add(fileInfo.path);
|
| 147 |
-
}
|
| 148 |
}
|
| 149 |
|
|
|
|
| 150 |
const mainBranchFilePaths: Set<string> = new Set();
|
| 151 |
for await (const fileInfo of listFiles({
|
| 152 |
repo,
|
|
@@ -160,6 +118,7 @@ export async function POST(
|
|
| 160 |
}
|
| 161 |
}
|
| 162 |
|
|
|
|
| 163 |
const filesToDelete: string[] = [];
|
| 164 |
for (const mainFilePath of mainBranchFilePaths) {
|
| 165 |
if (!commitFilePaths.has(mainFilePath)) {
|
|
@@ -174,6 +133,7 @@ export async function POST(
|
|
| 174 |
);
|
| 175 |
}
|
| 176 |
|
|
|
|
| 177 |
if (filesToDelete.length > 0) {
|
| 178 |
await deleteFiles({
|
| 179 |
repo,
|
|
@@ -184,6 +144,7 @@ export async function POST(
|
|
| 184 |
});
|
| 185 |
}
|
| 186 |
|
|
|
|
| 187 |
if (files.length > 0) {
|
| 188 |
await uploadFiles({
|
| 189 |
repo,
|
|
@@ -200,7 +161,6 @@ export async function POST(
|
|
| 200 |
message: "Version promoted successfully",
|
| 201 |
promotedCommit: commitId,
|
| 202 |
pages: pages,
|
| 203 |
-
files: mediaFiles,
|
| 204 |
},
|
| 205 |
{ status: 200 }
|
| 206 |
);
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
|
| 3 |
|
| 4 |
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
import { Page } from "@/types";
|
|
|
|
| 49 |
);
|
| 50 |
}
|
| 51 |
|
| 52 |
+
// Fetch files from the specific commit
|
| 53 |
const files: File[] = [];
|
| 54 |
const pages: Page[] = [];
|
|
|
|
| 55 |
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
|
|
|
| 56 |
const commitFilePaths: Set<string> = new Set();
|
| 57 |
|
| 58 |
+
// Get all files from the specific commit
|
| 59 |
for await (const fileInfo of listFiles({
|
| 60 |
repo,
|
| 61 |
accessToken: user.token as string,
|
|
|
|
| 63 |
})) {
|
| 64 |
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 65 |
|
| 66 |
+
if (allowedExtensions.includes(fileExtension || "")) {
|
| 67 |
commitFilePaths.add(fileInfo.path);
|
| 68 |
|
| 69 |
+
// Fetch the file content from the specific commit
|
| 70 |
+
const response = await fetch(
|
| 71 |
+
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
| 72 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
if (response.ok) {
|
| 75 |
+
const content = await response.text();
|
| 76 |
let mimeType = "text/plain";
|
| 77 |
|
| 78 |
switch (fileExtension) {
|
| 79 |
case "html":
|
| 80 |
mimeType = "text/html";
|
| 81 |
+
// Add HTML files to pages array for client-side setPages
|
| 82 |
+
pages.push({
|
| 83 |
+
path: fileInfo.path,
|
| 84 |
+
html: content,
|
| 85 |
+
});
|
| 86 |
break;
|
| 87 |
case "css":
|
| 88 |
mimeType = "text/css";
|
|
|
|
| 93 |
case "json":
|
| 94 |
mimeType = "application/json";
|
| 95 |
break;
|
| 96 |
+
case "md":
|
| 97 |
+
mimeType = "text/markdown";
|
| 98 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
const file = new File([content], fileInfo.path, { type: mimeType });
|
| 102 |
files.push(file);
|
| 103 |
}
|
| 104 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
+
// Get files currently in main branch to identify files to delete
|
| 108 |
const mainBranchFilePaths: Set<string> = new Set();
|
| 109 |
for await (const fileInfo of listFiles({
|
| 110 |
repo,
|
|
|
|
| 118 |
}
|
| 119 |
}
|
| 120 |
|
| 121 |
+
// Identify files to delete (exist in main but not in commit)
|
| 122 |
const filesToDelete: string[] = [];
|
| 123 |
for (const mainFilePath of mainBranchFilePaths) {
|
| 124 |
if (!commitFilePaths.has(mainFilePath)) {
|
|
|
|
| 133 |
);
|
| 134 |
}
|
| 135 |
|
| 136 |
+
// Delete files that exist in main but not in the commit being promoted
|
| 137 |
if (filesToDelete.length > 0) {
|
| 138 |
await deleteFiles({
|
| 139 |
repo,
|
|
|
|
| 144 |
});
|
| 145 |
}
|
| 146 |
|
| 147 |
+
// Upload the files to the main branch with a promotion commit message
|
| 148 |
if (files.length > 0) {
|
| 149 |
await uploadFiles({
|
| 150 |
repo,
|
|
|
|
| 161 |
message: "Version promoted successfully",
|
| 162 |
promotedCommit: commitId,
|
| 163 |
pages: pages,
|
|
|
|
| 164 |
},
|
| 165 |
{ status: 200 }
|
| 166 |
);
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/route.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
|
| 7 |
-
export async function GET(
|
| 8 |
-
req: NextRequest,
|
| 9 |
-
{ params }: {
|
| 10 |
-
params: Promise<{
|
| 11 |
-
namespace: string;
|
| 12 |
-
repoId: string;
|
| 13 |
-
commitId: string;
|
| 14 |
-
}>
|
| 15 |
-
}
|
| 16 |
-
) {
|
| 17 |
-
const user = await isAuthenticated();
|
| 18 |
-
|
| 19 |
-
if (user instanceof NextResponse || !user) {
|
| 20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
const param = await params;
|
| 24 |
-
const { namespace, repoId, commitId } = param;
|
| 25 |
-
|
| 26 |
-
try {
|
| 27 |
-
const repo: RepoDesignation = {
|
| 28 |
-
type: "space",
|
| 29 |
-
name: `${namespace}/${repoId}`,
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
-
const space = await spaceInfo({
|
| 33 |
-
name: `${namespace}/${repoId}`,
|
| 34 |
-
accessToken: user.token as string,
|
| 35 |
-
additionalFields: ["author"],
|
| 36 |
-
});
|
| 37 |
-
|
| 38 |
-
if (!space || space.sdk !== "static") {
|
| 39 |
-
return NextResponse.json(
|
| 40 |
-
{ ok: false, error: "Space is not a static space." },
|
| 41 |
-
{ status: 404 }
|
| 42 |
-
);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
if (space.author !== user.name) {
|
| 46 |
-
return NextResponse.json(
|
| 47 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
| 48 |
-
{ status: 403 }
|
| 49 |
-
);
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
const pages: Page[] = [];
|
| 53 |
-
|
| 54 |
-
for await (const fileInfo of listFiles({
|
| 55 |
-
repo,
|
| 56 |
-
accessToken: user.token as string,
|
| 57 |
-
revision: commitId,
|
| 58 |
-
})) {
|
| 59 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
| 60 |
-
|
| 61 |
-
if (fileInfo.path.endsWith(".html") || fileInfo.path.endsWith(".css") || fileInfo.path.endsWith(".js") || fileInfo.path.endsWith(".json")) {
|
| 62 |
-
const blob = await downloadFile({
|
| 63 |
-
repo,
|
| 64 |
-
accessToken: user.token as string,
|
| 65 |
-
path: fileInfo.path,
|
| 66 |
-
revision: commitId,
|
| 67 |
-
raw: true
|
| 68 |
-
});
|
| 69 |
-
const content = await blob?.text();
|
| 70 |
-
|
| 71 |
-
if (content) {
|
| 72 |
-
if (fileInfo.path === "index.html") {
|
| 73 |
-
pages.unshift({
|
| 74 |
-
path: fileInfo.path,
|
| 75 |
-
html: content,
|
| 76 |
-
});
|
| 77 |
-
} else {
|
| 78 |
-
pages.push({
|
| 79 |
-
path: fileInfo.path,
|
| 80 |
-
html: content,
|
| 81 |
-
});
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
return NextResponse.json({
|
| 88 |
-
ok: true,
|
| 89 |
-
pages,
|
| 90 |
-
});
|
| 91 |
-
} catch (error: any) {
|
| 92 |
-
console.error("Error fetching commit pages:", error);
|
| 93 |
-
return NextResponse.json(
|
| 94 |
-
{
|
| 95 |
-
ok: false,
|
| 96 |
-
error: error.message || "Failed to fetch commit pages",
|
| 97 |
-
},
|
| 98 |
-
{ status: 500 }
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/download/route.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { RepoDesignation, listFiles, spaceInfo, downloadFile } from "@huggingface/hub";
|
| 3 |
-
import JSZip from "jszip";
|
| 4 |
-
|
| 5 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 6 |
-
|
| 7 |
-
export async function GET(
|
| 8 |
-
req: NextRequest,
|
| 9 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 10 |
-
) {
|
| 11 |
-
const user = await isAuthenticated();
|
| 12 |
-
|
| 13 |
-
if (user instanceof NextResponse || !user) {
|
| 14 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
const param = await params;
|
| 18 |
-
const { namespace, repoId } = param;
|
| 19 |
-
|
| 20 |
-
try {
|
| 21 |
-
const space = await spaceInfo({
|
| 22 |
-
name: `${namespace}/${repoId}`,
|
| 23 |
-
accessToken: user.token as string,
|
| 24 |
-
additionalFields: ["author"],
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
if (!space || space.sdk !== "static") {
|
| 28 |
-
return NextResponse.json(
|
| 29 |
-
{
|
| 30 |
-
ok: false,
|
| 31 |
-
error: "Space is not a static space",
|
| 32 |
-
},
|
| 33 |
-
{ status: 404 }
|
| 34 |
-
);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
if (space.author !== user.name) {
|
| 38 |
-
return NextResponse.json(
|
| 39 |
-
{
|
| 40 |
-
ok: false,
|
| 41 |
-
error: "Space does not belong to the authenticated user",
|
| 42 |
-
},
|
| 43 |
-
{ status: 403 }
|
| 44 |
-
);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
const repo: RepoDesignation = {
|
| 48 |
-
type: "space",
|
| 49 |
-
name: `${namespace}/${repoId}`,
|
| 50 |
-
};
|
| 51 |
-
|
| 52 |
-
const zip = new JSZip();
|
| 53 |
-
|
| 54 |
-
for await (const fileInfo of listFiles({
|
| 55 |
-
repo,
|
| 56 |
-
accessToken: user.token as string,
|
| 57 |
-
recursive: true,
|
| 58 |
-
})) {
|
| 59 |
-
if (fileInfo.type === "directory" || fileInfo.path.startsWith(".")) {
|
| 60 |
-
continue;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
try {
|
| 64 |
-
const blob = await downloadFile({
|
| 65 |
-
repo,
|
| 66 |
-
accessToken: user.token as string,
|
| 67 |
-
path: fileInfo.path,
|
| 68 |
-
raw: true
|
| 69 |
-
});
|
| 70 |
-
|
| 71 |
-
if (blob) {
|
| 72 |
-
const arrayBuffer = await blob.arrayBuffer();
|
| 73 |
-
zip.file(fileInfo.path, arrayBuffer);
|
| 74 |
-
}
|
| 75 |
-
} catch (error) {
|
| 76 |
-
console.error(`Error downloading file ${fileInfo.path}:`, error);
|
| 77 |
-
}
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
const zipBlob = await zip.generateAsync({
|
| 81 |
-
type: "blob",
|
| 82 |
-
compression: "DEFLATE",
|
| 83 |
-
compressionOptions: {
|
| 84 |
-
level: 6
|
| 85 |
-
}
|
| 86 |
-
});
|
| 87 |
-
|
| 88 |
-
const projectName = `${namespace}-${repoId}`.replace(/[^a-zA-Z0-9-_]/g, '_');
|
| 89 |
-
const filename = `${projectName}.zip`;
|
| 90 |
-
|
| 91 |
-
return new NextResponse(zipBlob, {
|
| 92 |
-
headers: {
|
| 93 |
-
"Content-Type": "application/zip",
|
| 94 |
-
"Content-Disposition": `attachment; filename="${filename}"`,
|
| 95 |
-
"Content-Length": zipBlob.size.toString(),
|
| 96 |
-
},
|
| 97 |
-
});
|
| 98 |
-
} catch (error: any) {
|
| 99 |
-
return NextResponse.json(
|
| 100 |
-
{ ok: false, error: error.message || "Failed to create ZIP file" },
|
| 101 |
-
{ status: 500 }
|
| 102 |
-
);
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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") || 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) {
|
|
|
|
| 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) {
|
app/api/me/projects/[namespace]/[repoId]/update/route.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
| 3 |
-
|
| 4 |
-
import { isAuthenticated } from "@/lib/auth";
|
| 5 |
-
import { Page } from "@/types";
|
| 6 |
-
import { COLORS } from "@/lib/utils";
|
| 7 |
-
import { injectDeepSiteBadge, isIndexPage } from "@/lib/inject-badge";
|
| 8 |
-
import { pagesToFiles } from "@/lib/format-ai-response";
|
| 9 |
-
|
| 10 |
-
/**
|
| 11 |
-
* UPDATE route - for updating existing projects or creating new ones after AI streaming
|
| 12 |
-
* This route handles the HuggingFace upload after client-side AI response processing
|
| 13 |
-
*/
|
| 14 |
-
export async function PUT(
|
| 15 |
-
req: NextRequest,
|
| 16 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
| 17 |
-
) {
|
| 18 |
-
const user = await isAuthenticated();
|
| 19 |
-
if (user instanceof NextResponse || !user) {
|
| 20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
const param = await params;
|
| 24 |
-
let { namespace, repoId } = param;
|
| 25 |
-
const { pages, commitTitle = "AI-generated changes", isNew, projectName } = await req.json();
|
| 26 |
-
|
| 27 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
| 28 |
-
return NextResponse.json(
|
| 29 |
-
{ ok: false, error: "Pages are required" },
|
| 30 |
-
{ status: 400 }
|
| 31 |
-
);
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
try {
|
| 35 |
-
let files: File[];
|
| 36 |
-
|
| 37 |
-
if (isNew) {
|
| 38 |
-
// Creating a new project
|
| 39 |
-
const title = projectName || "DeepSite Project";
|
| 40 |
-
const formattedTitle = title
|
| 41 |
-
.toLowerCase()
|
| 42 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 43 |
-
.split("-")
|
| 44 |
-
.filter(Boolean)
|
| 45 |
-
.join("-")
|
| 46 |
-
.slice(0, 96);
|
| 47 |
-
|
| 48 |
-
const repo: RepoDesignation = {
|
| 49 |
-
type: "space",
|
| 50 |
-
name: `${user.name}/${formattedTitle}`,
|
| 51 |
-
};
|
| 52 |
-
|
| 53 |
-
try {
|
| 54 |
-
const { repoUrl } = await createRepo({
|
| 55 |
-
repo,
|
| 56 |
-
accessToken: user.token as string,
|
| 57 |
-
});
|
| 58 |
-
namespace = user.name;
|
| 59 |
-
repoId = repoUrl.split("/").slice(-2).join("/").split("/")[1];
|
| 60 |
-
} catch (createRepoError: any) {
|
| 61 |
-
return NextResponse.json(
|
| 62 |
-
{
|
| 63 |
-
ok: false,
|
| 64 |
-
error: `Failed to create repository: ${createRepoError.message || 'Unknown error'}`,
|
| 65 |
-
},
|
| 66 |
-
{ status: 500 }
|
| 67 |
-
);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
// Prepare files with badge injection for new projects
|
| 71 |
-
files = [];
|
| 72 |
-
pages.forEach((page: Page) => {
|
| 73 |
-
let mimeType = "text/html";
|
| 74 |
-
if (page.path.endsWith(".css")) {
|
| 75 |
-
mimeType = "text/css";
|
| 76 |
-
} else if (page.path.endsWith(".js")) {
|
| 77 |
-
mimeType = "text/javascript";
|
| 78 |
-
} else if (page.path.endsWith(".json")) {
|
| 79 |
-
mimeType = "application/json";
|
| 80 |
-
}
|
| 81 |
-
const content = (mimeType === "text/html" && isIndexPage(page.path))
|
| 82 |
-
? injectDeepSiteBadge(page.html)
|
| 83 |
-
: page.html;
|
| 84 |
-
const file = new File([content], page.path, { type: mimeType });
|
| 85 |
-
files.push(file);
|
| 86 |
-
});
|
| 87 |
-
|
| 88 |
-
// Add README.md for new projects
|
| 89 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 90 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
| 91 |
-
const README = `---
|
| 92 |
-
title: ${title}
|
| 93 |
-
colorFrom: ${colorFrom}
|
| 94 |
-
colorTo: ${colorTo}
|
| 95 |
-
emoji: 🐳
|
| 96 |
-
sdk: static
|
| 97 |
-
pinned: false
|
| 98 |
-
tags:
|
| 99 |
-
- deepsite-v3
|
| 100 |
-
---
|
| 101 |
-
|
| 102 |
-
# Welcome to your new DeepSite project!
|
| 103 |
-
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
| 104 |
-
`;
|
| 105 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
| 106 |
-
} else {
|
| 107 |
-
// Updating existing project - no badge injection
|
| 108 |
-
files = pagesToFiles(pages);
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
const response = await uploadFiles({
|
| 112 |
-
repo: {
|
| 113 |
-
type: "space",
|
| 114 |
-
name: `${namespace}/${repoId}`,
|
| 115 |
-
},
|
| 116 |
-
files,
|
| 117 |
-
commitTitle,
|
| 118 |
-
accessToken: user.token as string,
|
| 119 |
-
});
|
| 120 |
-
|
| 121 |
-
return NextResponse.json({
|
| 122 |
-
ok: true,
|
| 123 |
-
pages,
|
| 124 |
-
repoId: `${namespace}/${repoId}`,
|
| 125 |
-
commit: {
|
| 126 |
-
...response.commit,
|
| 127 |
-
title: commitTitle,
|
| 128 |
-
}
|
| 129 |
-
});
|
| 130 |
-
} catch (error: any) {
|
| 131 |
-
console.error("Error updating project:", error);
|
| 132 |
-
return NextResponse.json(
|
| 133 |
-
{
|
| 134 |
-
ok: false,
|
| 135 |
-
error: error.message || "Failed to update project",
|
| 136 |
-
},
|
| 137 |
-
{ status: 500 }
|
| 138 |
-
);
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/layout.tsx
CHANGED
|
@@ -1,18 +1,20 @@
|
|
| 1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
import type { Metadata, Viewport } from "next";
|
| 3 |
import { Inter, PT_Sans } from "next/font/google";
|
|
|
|
| 4 |
import Script from "next/script";
|
| 5 |
-
import { headers } from "next/headers";
|
| 6 |
-
import { redirect } from "next/navigation";
|
| 7 |
|
| 8 |
import "@/assets/globals.css";
|
| 9 |
import { Toaster } from "@/components/ui/sonner";
|
|
|
|
|
|
|
| 10 |
import IframeDetector from "@/components/iframe-detector";
|
| 11 |
import AppContext from "@/components/contexts/app-context";
|
| 12 |
import TanstackContext from "@/components/contexts/tanstack-query-context";
|
| 13 |
import { LoginProvider } from "@/components/contexts/login-context";
|
| 14 |
import { ProProvider } from "@/components/contexts/pro-context";
|
| 15 |
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
|
|
|
| 16 |
|
| 17 |
const inter = Inter({
|
| 18 |
variable: "--font-inter-sans",
|
|
@@ -76,24 +78,6 @@ export default async function RootLayout({
|
|
| 76 |
}: Readonly<{
|
| 77 |
children: React.ReactNode;
|
| 78 |
}>) {
|
| 79 |
-
// Domain redirect check
|
| 80 |
-
const headersList = await headers();
|
| 81 |
-
const forwardedHost = headersList.get("x-forwarded-host");
|
| 82 |
-
const host = headersList.get("host");
|
| 83 |
-
const hostname = (forwardedHost || host || "").split(":")[0];
|
| 84 |
-
|
| 85 |
-
const isLocalDev =
|
| 86 |
-
hostname === "localhost" ||
|
| 87 |
-
hostname === "127.0.0.1" ||
|
| 88 |
-
hostname.startsWith("192.168.");
|
| 89 |
-
const isHuggingFace =
|
| 90 |
-
hostname === "huggingface.co" || hostname.endsWith(".huggingface.co");
|
| 91 |
-
|
| 92 |
-
if (!isHuggingFace && !isLocalDev) {
|
| 93 |
-
const pathname = headersList.get("x-invoke-path") || "/deepsite";
|
| 94 |
-
redirect(`https://huggingface.co${pathname}`);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
// const data = await getMe();
|
| 98 |
|
| 99 |
// Generate structured data
|
|
@@ -130,6 +114,7 @@ export default async function RootLayout({
|
|
| 130 |
data-domain="deepsite.hf.co"
|
| 131 |
src="https://plausible.io/js/script.js"
|
| 132 |
/>
|
|
|
|
| 133 |
<IframeDetector />
|
| 134 |
<Toaster richColors position="bottom-center" />
|
| 135 |
<TanstackContext>
|
|
|
|
| 1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
import type { Metadata, Viewport } from "next";
|
| 3 |
import { Inter, PT_Sans } from "next/font/google";
|
| 4 |
+
import { cookies } from "next/headers";
|
| 5 |
import Script from "next/script";
|
|
|
|
|
|
|
| 6 |
|
| 7 |
import "@/assets/globals.css";
|
| 8 |
import { Toaster } from "@/components/ui/sonner";
|
| 9 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
| 10 |
+
import { apiServer } from "@/lib/api";
|
| 11 |
import IframeDetector from "@/components/iframe-detector";
|
| 12 |
import AppContext from "@/components/contexts/app-context";
|
| 13 |
import TanstackContext from "@/components/contexts/tanstack-query-context";
|
| 14 |
import { LoginProvider } from "@/components/contexts/login-context";
|
| 15 |
import { ProProvider } from "@/components/contexts/pro-context";
|
| 16 |
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
| 17 |
+
import DomainRedirect from "@/components/domain-redirect";
|
| 18 |
|
| 19 |
const inter = Inter({
|
| 20 |
variable: "--font-inter-sans",
|
|
|
|
| 78 |
}: Readonly<{
|
| 79 |
children: React.ReactNode;
|
| 80 |
}>) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
// const data = await getMe();
|
| 82 |
|
| 83 |
// Generate structured data
|
|
|
|
| 114 |
data-domain="deepsite.hf.co"
|
| 115 |
src="https://plausible.io/js/script.js"
|
| 116 |
/>
|
| 117 |
+
<DomainRedirect />
|
| 118 |
<IframeDetector />
|
| 119 |
<Toaster richColors position="bottom-center" />
|
| 120 |
<TanstackContext>
|
assets/globals.css
CHANGED
|
@@ -369,12 +369,3 @@ body {
|
|
| 369 |
z-index: 1;
|
| 370 |
filter: blur(1px);
|
| 371 |
}
|
| 372 |
-
|
| 373 |
-
.transparent-scroll {
|
| 374 |
-
scrollbar-width: none; /* Firefox */
|
| 375 |
-
-ms-overflow-style: none; /* IE and Edge */
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
.transparent-scroll::-webkit-scrollbar {
|
| 379 |
-
display: none; /* Chrome, Safari, Opera */
|
| 380 |
-
}
|
|
|
|
| 369 |
z-index: 1;
|
| 370 |
filter: blur(1px);
|
| 371 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/minimax.svg
DELETED
components/discord-promo-modal/index.tsx
DELETED
|
@@ -1,225 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from "react";
|
| 4 |
-
import { useLocalStorage } from "react-use";
|
| 5 |
-
import Image from "next/image";
|
| 6 |
-
import { Button } from "@/components/ui/button";
|
| 7 |
-
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
| 8 |
-
import { DiscordIcon } from "@/components/icons/discord";
|
| 9 |
-
import Logo from "@/assets/logo.svg";
|
| 10 |
-
|
| 11 |
-
const DISCORD_PROMO_KEY = "discord-promo-dismissed";
|
| 12 |
-
const DISCORD_URL = "https://discord.gg/KpanwM3vXa";
|
| 13 |
-
|
| 14 |
-
const Sparkle = ({
|
| 15 |
-
size = "w-3 h-3",
|
| 16 |
-
delay = "0s",
|
| 17 |
-
top = "20%",
|
| 18 |
-
left = "20%",
|
| 19 |
-
}: {
|
| 20 |
-
size?: string;
|
| 21 |
-
delay?: string;
|
| 22 |
-
top?: string;
|
| 23 |
-
left?: string;
|
| 24 |
-
}) => (
|
| 25 |
-
<div
|
| 26 |
-
className={`absolute ${size}`}
|
| 27 |
-
style={{ top, left, animationDelay: delay }}
|
| 28 |
-
>
|
| 29 |
-
<svg
|
| 30 |
-
viewBox="0 0 24 24"
|
| 31 |
-
fill="none"
|
| 32 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 33 |
-
className="w-full h-full animate-sparkle"
|
| 34 |
-
>
|
| 35 |
-
<path
|
| 36 |
-
d="M12 0L13.5 8.5L22 10L13.5 11.5L12 20L10.5 11.5L2 10L10.5 8.5L12 0Z"
|
| 37 |
-
fill="url(#sparkle-gradient)"
|
| 38 |
-
/>
|
| 39 |
-
<defs>
|
| 40 |
-
<linearGradient id="sparkle-gradient" x1="2" y1="10" x2="22" y2="10">
|
| 41 |
-
<stop offset="0%" stopColor="#818cf8" />
|
| 42 |
-
<stop offset="100%" stopColor="#a5b4fc" />
|
| 43 |
-
</linearGradient>
|
| 44 |
-
</defs>
|
| 45 |
-
</svg>
|
| 46 |
-
</div>
|
| 47 |
-
);
|
| 48 |
-
|
| 49 |
-
export const DiscordPromoModal = () => {
|
| 50 |
-
const [open, setOpen] = useState(false);
|
| 51 |
-
const [dismissed, setDismissed] = useLocalStorage<boolean>(
|
| 52 |
-
DISCORD_PROMO_KEY,
|
| 53 |
-
false
|
| 54 |
-
);
|
| 55 |
-
|
| 56 |
-
useEffect(() => {
|
| 57 |
-
const cookieDismissed = document.cookie
|
| 58 |
-
.split("; ")
|
| 59 |
-
.find((row) => row.startsWith(`${DISCORD_PROMO_KEY}=`))
|
| 60 |
-
?.split("=")[1];
|
| 61 |
-
|
| 62 |
-
if (dismissed || cookieDismissed === "true") {
|
| 63 |
-
return;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
const timer = setTimeout(() => {
|
| 67 |
-
setOpen(true);
|
| 68 |
-
}, 60000);
|
| 69 |
-
|
| 70 |
-
return () => clearTimeout(timer);
|
| 71 |
-
}, [dismissed]);
|
| 72 |
-
|
| 73 |
-
const handleClose = () => {
|
| 74 |
-
setOpen(false);
|
| 75 |
-
setDismissed(true);
|
| 76 |
-
|
| 77 |
-
const expiryDate = new Date();
|
| 78 |
-
expiryDate.setDate(expiryDate.getDate() + 5);
|
| 79 |
-
document.cookie = `${DISCORD_PROMO_KEY}=true; expires=${expiryDate.toUTCString()}; path=/; SameSite=Lax`;
|
| 80 |
-
};
|
| 81 |
-
|
| 82 |
-
const handleJoinDiscord = () => {
|
| 83 |
-
window.open(DISCORD_URL, "_blank");
|
| 84 |
-
handleClose();
|
| 85 |
-
};
|
| 86 |
-
|
| 87 |
-
return (
|
| 88 |
-
<Dialog open={open} onOpenChange={handleClose}>
|
| 89 |
-
<DialogContent
|
| 90 |
-
className="sm:max-w-[480px] lg:!p-0 !rounded-3xl !bg-gradient-to-b !from-indigo-950/40 !via-neutral-900 !to-neutral-900 !border !border-neutral-800 overflow-hidden"
|
| 91 |
-
showCloseButton={true}
|
| 92 |
-
>
|
| 93 |
-
<DialogTitle className="hidden" />
|
| 94 |
-
|
| 95 |
-
<div className="relative">
|
| 96 |
-
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/10 via-indigo-500/5 to-transparent pointer-events-none" />
|
| 97 |
-
|
| 98 |
-
<div className="absolute inset-x-0 top-0 h-48 overflow-hidden pointer-events-none">
|
| 99 |
-
<Sparkle size="w-2 h-2" delay="0s" top="15%" left="15%" />
|
| 100 |
-
<Sparkle size="w-3 h-3" delay="0.5s" top="25%" left="75%" />
|
| 101 |
-
<Sparkle size="w-2 h-2" delay="1s" top="35%" left="20%" />
|
| 102 |
-
<Sparkle size="w-4 h-4" delay="1.5s" top="10%" left="80%" />
|
| 103 |
-
<Sparkle size="w-2 h-2" delay="2s" top="30%" left="85%" />
|
| 104 |
-
</div>
|
| 105 |
-
|
| 106 |
-
<div className="relative pt-12 pb-8">
|
| 107 |
-
<div className="relative z-10 flex justify-center">
|
| 108 |
-
<div className="relative">
|
| 109 |
-
<div className="absolute inset-0 bg-gradient-to-br from-indigo-400 via-indigo-500 to-indigo-600 rounded-full blur-md opacity-50" />
|
| 110 |
-
<div className="relative w-32 h-32 rounded-full bg-gradient-to-br from-neutral-900 via-neutral-800 to-neutral-900 p-1 shadow-2xl">
|
| 111 |
-
<div className="w-full h-full rounded-full bg-neutral-900 flex items-center justify-center overflow-hidden">
|
| 112 |
-
<div className="relative w-20 h-20 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full flex items-center justify-center">
|
| 113 |
-
<DiscordIcon className="w-12 h-12 text-white" />
|
| 114 |
-
</div>
|
| 115 |
-
</div>
|
| 116 |
-
</div>
|
| 117 |
-
|
| 118 |
-
<div className="absolute -bottom-2 -right-2 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-full p-0.5 shadow-xl border-2 border-neutral-900">
|
| 119 |
-
<div className="w-10 h-10 bg-neutral-900 rounded-full flex items-center justify-center">
|
| 120 |
-
<Image
|
| 121 |
-
src={Logo}
|
| 122 |
-
alt="DeepSite"
|
| 123 |
-
width={20}
|
| 124 |
-
height={20}
|
| 125 |
-
className="w-5 h-5"
|
| 126 |
-
/>
|
| 127 |
-
</div>
|
| 128 |
-
</div>
|
| 129 |
-
</div>
|
| 130 |
-
</div>
|
| 131 |
-
</div>
|
| 132 |
-
|
| 133 |
-
<main className="px-8 pb-8 pt-4">
|
| 134 |
-
<div className="text-center mb-6">
|
| 135 |
-
<h2 className="text-2xl font-bold text-white mb-2">
|
| 136 |
-
Ready to level up your DeepSite experience?
|
| 137 |
-
</h2>
|
| 138 |
-
<p className="text-neutral-400 text-sm">
|
| 139 |
-
Get help, share your projects and ask for suggestions!
|
| 140 |
-
</p>
|
| 141 |
-
</div>
|
| 142 |
-
|
| 143 |
-
<div className="flex flex-col gap-3 mb-6">
|
| 144 |
-
{[
|
| 145 |
-
"Get exclusive preview to new features",
|
| 146 |
-
"Share your projects and get feedback",
|
| 147 |
-
"Priority support from the team",
|
| 148 |
-
"Enjoy real-time updates",
|
| 149 |
-
].map((benefit, index) => (
|
| 150 |
-
<div
|
| 151 |
-
key={index}
|
| 152 |
-
className="flex items-start gap-3 text-neutral-200"
|
| 153 |
-
style={{
|
| 154 |
-
animation: `fadeIn 0.4s ease-out ${index * 0.1}s both`,
|
| 155 |
-
}}
|
| 156 |
-
>
|
| 157 |
-
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center mt-0.5">
|
| 158 |
-
<svg
|
| 159 |
-
className="w-3 h-3 text-white"
|
| 160 |
-
fill="none"
|
| 161 |
-
viewBox="0 0 24 24"
|
| 162 |
-
stroke="currentColor"
|
| 163 |
-
>
|
| 164 |
-
<path
|
| 165 |
-
strokeLinecap="round"
|
| 166 |
-
strokeLinejoin="round"
|
| 167 |
-
strokeWidth={3}
|
| 168 |
-
d="M5 13l4 4L19 7"
|
| 169 |
-
/>
|
| 170 |
-
</svg>
|
| 171 |
-
</div>
|
| 172 |
-
<span className="text-sm leading-6">{benefit}</span>
|
| 173 |
-
</div>
|
| 174 |
-
))}
|
| 175 |
-
</div>
|
| 176 |
-
|
| 177 |
-
{/* CTA Button */}
|
| 178 |
-
<div className="flex flex-col gap-3 w-full">
|
| 179 |
-
<Button
|
| 180 |
-
onClick={handleJoinDiscord}
|
| 181 |
-
className="w-full !h-12 !text-base font-semibold !bg-gradient-to-r !from-indigo-500 !to-indigo-600 hover:!from-indigo-600 hover:!to-indigo-700 !text-white !border-0 transform hover:scale-[1.02] transition-all duration-200 shadow-lg shadow-indigo-500/25"
|
| 182 |
-
>
|
| 183 |
-
<DiscordIcon className="w-5 h-5 mr-2" />
|
| 184 |
-
Join Discord Community
|
| 185 |
-
</Button>
|
| 186 |
-
|
| 187 |
-
<p className="text-center text-xs text-neutral-500">
|
| 188 |
-
Free to join. Connect instantly.
|
| 189 |
-
</p>
|
| 190 |
-
</div>
|
| 191 |
-
</main>
|
| 192 |
-
</div>
|
| 193 |
-
|
| 194 |
-
<style jsx>{`
|
| 195 |
-
@keyframes fadeIn {
|
| 196 |
-
from {
|
| 197 |
-
opacity: 0;
|
| 198 |
-
transform: translateY(5px);
|
| 199 |
-
}
|
| 200 |
-
to {
|
| 201 |
-
opacity: 1;
|
| 202 |
-
transform: translateY(0);
|
| 203 |
-
}
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
@keyframes sparkle {
|
| 207 |
-
0%,
|
| 208 |
-
100% {
|
| 209 |
-
opacity: 0;
|
| 210 |
-
transform: scale(0) rotate(0deg);
|
| 211 |
-
}
|
| 212 |
-
50% {
|
| 213 |
-
opacity: 1;
|
| 214 |
-
transform: scale(1) rotate(180deg);
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
:global(.animate-sparkle) {
|
| 219 |
-
animation: sparkle 2s ease-in-out infinite;
|
| 220 |
-
}
|
| 221 |
-
`}</style>
|
| 222 |
-
</DialogContent>
|
| 223 |
-
</Dialog>
|
| 224 |
-
);
|
| 225 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/domain-redirect/index.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect } from "react";
|
| 4 |
+
|
| 5 |
+
export default function DomainRedirect() {
|
| 6 |
+
useEffect(() => {
|
| 7 |
+
if (typeof window === "undefined") return;
|
| 8 |
+
|
| 9 |
+
const host = window.location.host;
|
| 10 |
+
|
| 11 |
+
// Check if we're not on hf.co or huggingface.co
|
| 12 |
+
const isHfCo = host === "hf.co" || host.startsWith("hf.co:");
|
| 13 |
+
const isHuggingFaceCo =
|
| 14 |
+
host === "huggingface.co" || host.startsWith("huggingface.co:");
|
| 15 |
+
|
| 16 |
+
if (!isHfCo && !isHuggingFaceCo) {
|
| 17 |
+
// Redirect to the correct URL
|
| 18 |
+
window.location.replace("https://huggingface.co/deepsite");
|
| 19 |
+
}
|
| 20 |
+
}, []);
|
| 21 |
+
|
| 22 |
+
return null;
|
| 23 |
+
}
|
components/editor/ask-ai/context.tsx
CHANGED
|
@@ -23,8 +23,6 @@ export const Context = () => {
|
|
| 23 |
return <Braces className={size} />;
|
| 24 |
} else if (filePath.endsWith(".js")) {
|
| 25 |
return <FileCode className={size} />;
|
| 26 |
-
} else if (filePath.endsWith(".json")) {
|
| 27 |
-
return <Braces className={size} />;
|
| 28 |
} else {
|
| 29 |
return <FileText className={size} />;
|
| 30 |
}
|
|
@@ -54,8 +52,6 @@ export const Context = () => {
|
|
| 54 |
selectedFile && selectedFile.endsWith(".html"),
|
| 55 |
"!bg-amber-500/10 !border-amber-500/30 !text-amber-400":
|
| 56 |
selectedFile && selectedFile.endsWith(".js"),
|
| 57 |
-
"!bg-yellow-500/10 !border-yellow-500/30 !text-yellow-400":
|
| 58 |
-
selectedFile && selectedFile.endsWith(".json"),
|
| 59 |
})}
|
| 60 |
disabled={
|
| 61 |
globalAiLoading || globalEditorLoading || pages.length === 0
|
|
|
|
| 23 |
return <Braces className={size} />;
|
| 24 |
} else if (filePath.endsWith(".js")) {
|
| 25 |
return <FileCode className={size} />;
|
|
|
|
|
|
|
| 26 |
} else {
|
| 27 |
return <FileText className={size} />;
|
| 28 |
}
|
|
|
|
| 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
|
components/editor/ask-ai/index.tsx
CHANGED
|
@@ -22,7 +22,6 @@ import { Settings } from "./settings";
|
|
| 22 |
import { useProModal } from "@/components/contexts/pro-context";
|
| 23 |
import { MAX_FREE_PROJECTS } from "@/lib/utils";
|
| 24 |
import { PROMPTS_FOR_AI } from "@/lib/prompts";
|
| 25 |
-
import { SelectedRedesignUrl } from "./selected-redesign-url";
|
| 26 |
|
| 27 |
export const AskAi = ({
|
| 28 |
project,
|
|
@@ -39,7 +38,6 @@ export const AskAi = ({
|
|
| 39 |
const {
|
| 40 |
isAiWorking,
|
| 41 |
isThinking,
|
| 42 |
-
thinkingContent,
|
| 43 |
selectedFiles,
|
| 44 |
setSelectedFiles,
|
| 45 |
selectedElement,
|
|
@@ -53,9 +51,6 @@ export const AskAi = ({
|
|
| 53 |
const { openProModal } = useProModal();
|
| 54 |
const [openProvider, setOpenProvider] = useState(false);
|
| 55 |
const [providerError, setProviderError] = useState("");
|
| 56 |
-
const [redesignData, setRedesignData] = useState<
|
| 57 |
-
undefined | { markdown: string; url: string }
|
| 58 |
-
>(undefined);
|
| 59 |
const refThink = useRef<HTMLDivElement>(null);
|
| 60 |
|
| 61 |
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
|
@@ -71,6 +66,7 @@ export const AskAi = ({
|
|
| 71 |
const [prompt, setPrompt] = useState(
|
| 72 |
promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
|
| 73 |
);
|
|
|
|
| 74 |
const [openThink, setOpenThink] = useState(false);
|
| 75 |
const [randomPromptLoading, setRandomPromptLoading] = useState(false);
|
| 76 |
|
|
@@ -80,7 +76,7 @@ export const AskAi = ({
|
|
| 80 |
}
|
| 81 |
});
|
| 82 |
|
| 83 |
-
const callAi = async (redesignMarkdown?: string
|
| 84 |
removePromptStorage();
|
| 85 |
if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
|
| 86 |
return openProModal([]);
|
|
@@ -104,8 +100,7 @@ export const AskAi = ({
|
|
| 104 |
prompt,
|
| 105 |
enhancedSettings,
|
| 106 |
redesignMarkdown,
|
| 107 |
-
!!user
|
| 108 |
-
user?.name
|
| 109 |
);
|
| 110 |
|
| 111 |
if (result?.error) {
|
|
@@ -146,15 +141,7 @@ export const AskAi = ({
|
|
| 146 |
if (refThink.current) {
|
| 147 |
refThink.current.scrollTop = refThink.current.scrollHeight;
|
| 148 |
}
|
| 149 |
-
|
| 150 |
-
if (thinkingContent && isThinking && !openThink) {
|
| 151 |
-
setOpenThink(true);
|
| 152 |
-
}
|
| 153 |
-
// Auto-collapse when thinking is complete
|
| 154 |
-
if (thinkingContent && !isThinking && openThink) {
|
| 155 |
-
setOpenThink(false);
|
| 156 |
-
}
|
| 157 |
-
}, [thinkingContent, isThinking]);
|
| 158 |
|
| 159 |
const randomPrompt = () => {
|
| 160 |
setRandomPromptLoading(true);
|
|
@@ -169,7 +156,7 @@ export const AskAi = ({
|
|
| 169 |
return (
|
| 170 |
<div className="p-3 w-full">
|
| 171 |
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
|
| 172 |
-
{
|
| 173 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
| 174 |
<header
|
| 175 |
className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
|
|
@@ -201,7 +188,7 @@ export const AskAi = ({
|
|
| 201 |
)}
|
| 202 |
>
|
| 203 |
<p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
|
| 204 |
-
{
|
| 205 |
</p>
|
| 206 |
</main>
|
| 207 |
</div>
|
|
@@ -222,15 +209,6 @@ export const AskAi = ({
|
|
| 222 |
/>
|
| 223 |
</div>
|
| 224 |
)}
|
| 225 |
-
{redesignData && (
|
| 226 |
-
<div className="px-4 pt-3">
|
| 227 |
-
<SelectedRedesignUrl
|
| 228 |
-
url={redesignData.url}
|
| 229 |
-
isAiWorking={isAiWorking}
|
| 230 |
-
onDelete={() => setRedesignData(undefined)}
|
| 231 |
-
/>
|
| 232 |
-
</div>
|
| 233 |
-
)}
|
| 234 |
<div className="w-full relative flex items-center justify-between">
|
| 235 |
{(isAiWorking || isUploading || isThinking || isLoadingProject) && (
|
| 236 |
<div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
|
@@ -242,7 +220,7 @@ export const AskAi = ({
|
|
| 242 |
? "Uploading images..."
|
| 243 |
: isAiWorking && !isSameHtml
|
| 244 |
? "DeepSite is working..."
|
| 245 |
-
: "DeepSite is
|
| 246 |
}
|
| 247 |
/>
|
| 248 |
{isAiWorking && (
|
|
@@ -272,8 +250,6 @@ export const AskAi = ({
|
|
| 272 |
placeholder={
|
| 273 |
selectedElement
|
| 274 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
| 275 |
-
: redesignData
|
| 276 |
-
? "Ask DeepSite anything about the redesign of your site..."
|
| 277 |
: isFollowUp && (!isSameHtml || pages?.length > 1)
|
| 278 |
? "Ask DeepSite for edits"
|
| 279 |
: "Ask DeepSite anything..."
|
|
@@ -318,13 +294,7 @@ export const AskAi = ({
|
|
| 318 |
onClose={setOpenProvider}
|
| 319 |
/>
|
| 320 |
{!isNew && <Uploader project={project} />}
|
| 321 |
-
{isNew && (
|
| 322 |
-
<ReImagine
|
| 323 |
-
onRedesign={(md, url) =>
|
| 324 |
-
setRedesignData({ markdown: md, url: url })
|
| 325 |
-
}
|
| 326 |
-
/>
|
| 327 |
-
)}
|
| 328 |
{!isNew && !isSameHtml && <Selector />}
|
| 329 |
</div>
|
| 330 |
<div className="flex items-center justify-end gap-2">
|
|
@@ -333,11 +303,9 @@ export const AskAi = ({
|
|
| 333 |
variant="outline"
|
| 334 |
className="!rounded-md"
|
| 335 |
disabled={
|
| 336 |
-
|
| 337 |
-
? false
|
| 338 |
-
: isAiWorking || isUploading || isThinking || !prompt.trim()
|
| 339 |
}
|
| 340 |
-
onClick={() => callAi(
|
| 341 |
>
|
| 342 |
<ArrowUp className="size-4" />
|
| 343 |
</Button>
|
|
@@ -345,7 +313,7 @@ export const AskAi = ({
|
|
| 345 |
</div>
|
| 346 |
</div>
|
| 347 |
<audio ref={hookAudio} id="audio" className="hidden">
|
| 348 |
-
<source src="/
|
| 349 |
Your browser does not support the audio element.
|
| 350 |
</audio>
|
| 351 |
</div>
|
|
|
|
| 22 |
import { useProModal } from "@/components/contexts/pro-context";
|
| 23 |
import { MAX_FREE_PROJECTS } from "@/lib/utils";
|
| 24 |
import { PROMPTS_FOR_AI } from "@/lib/prompts";
|
|
|
|
| 25 |
|
| 26 |
export const AskAi = ({
|
| 27 |
project,
|
|
|
|
| 38 |
const {
|
| 39 |
isAiWorking,
|
| 40 |
isThinking,
|
|
|
|
| 41 |
selectedFiles,
|
| 42 |
setSelectedFiles,
|
| 43 |
selectedElement,
|
|
|
|
| 51 |
const { openProModal } = useProModal();
|
| 52 |
const [openProvider, setOpenProvider] = useState(false);
|
| 53 |
const [providerError, setProviderError] = useState("");
|
|
|
|
|
|
|
|
|
|
| 54 |
const refThink = useRef<HTMLDivElement>(null);
|
| 55 |
|
| 56 |
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
|
|
|
| 66 |
const [prompt, setPrompt] = useState(
|
| 67 |
promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
|
| 68 |
);
|
| 69 |
+
const [think, setThink] = useState("");
|
| 70 |
const [openThink, setOpenThink] = useState(false);
|
| 71 |
const [randomPromptLoading, setRandomPromptLoading] = useState(false);
|
| 72 |
|
|
|
|
| 76 |
}
|
| 77 |
});
|
| 78 |
|
| 79 |
+
const callAi = async (redesignMarkdown?: string) => {
|
| 80 |
removePromptStorage();
|
| 81 |
if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
|
| 82 |
return openProModal([]);
|
|
|
|
| 100 |
prompt,
|
| 101 |
enhancedSettings,
|
| 102 |
redesignMarkdown,
|
| 103 |
+
!!user
|
|
|
|
| 104 |
);
|
| 105 |
|
| 106 |
if (result?.error) {
|
|
|
|
| 141 |
if (refThink.current) {
|
| 142 |
refThink.current.scrollTop = refThink.current.scrollHeight;
|
| 143 |
}
|
| 144 |
+
}, [think]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
const randomPrompt = () => {
|
| 147 |
setRandomPromptLoading(true);
|
|
|
|
| 156 |
return (
|
| 157 |
<div className="p-3 w-full">
|
| 158 |
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
|
| 159 |
+
{think && (
|
| 160 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
| 161 |
<header
|
| 162 |
className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
|
|
|
|
| 188 |
)}
|
| 189 |
>
|
| 190 |
<p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
|
| 191 |
+
{think}
|
| 192 |
</p>
|
| 193 |
</main>
|
| 194 |
</div>
|
|
|
|
| 209 |
/>
|
| 210 |
</div>
|
| 211 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
<div className="w-full relative flex items-center justify-between">
|
| 213 |
{(isAiWorking || isUploading || isThinking || isLoadingProject) && (
|
| 214 |
<div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
|
|
|
| 220 |
? "Uploading images..."
|
| 221 |
: isAiWorking && !isSameHtml
|
| 222 |
? "DeepSite is working..."
|
| 223 |
+
: "DeepSite is thinking..."
|
| 224 |
}
|
| 225 |
/>
|
| 226 |
{isAiWorking && (
|
|
|
|
| 250 |
placeholder={
|
| 251 |
selectedElement
|
| 252 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
|
|
|
|
|
|
| 253 |
: isFollowUp && (!isSameHtml || pages?.length > 1)
|
| 254 |
? "Ask DeepSite for edits"
|
| 255 |
: "Ask DeepSite anything..."
|
|
|
|
| 294 |
onClose={setOpenProvider}
|
| 295 |
/>
|
| 296 |
{!isNew && <Uploader project={project} />}
|
| 297 |
+
{isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
{!isNew && !isSameHtml && <Selector />}
|
| 299 |
</div>
|
| 300 |
<div className="flex items-center justify-end gap-2">
|
|
|
|
| 303 |
variant="outline"
|
| 304 |
className="!rounded-md"
|
| 305 |
disabled={
|
| 306 |
+
isAiWorking || isUploading || isThinking || !prompt.trim()
|
|
|
|
|
|
|
| 307 |
}
|
| 308 |
+
onClick={() => callAi()}
|
| 309 |
>
|
| 310 |
<ArrowUp className="size-4" />
|
| 311 |
</Button>
|
|
|
|
| 313 |
</div>
|
| 314 |
</div>
|
| 315 |
<audio ref={hookAudio} id="audio" className="hidden">
|
| 316 |
+
<source src="/success.mp3" type="audio/mpeg" />
|
| 317 |
Your browser does not support the audio element.
|
| 318 |
</audio>
|
| 319 |
</div>
|
components/editor/ask-ai/prompt-builder/content-modal.tsx
CHANGED
|
@@ -16,7 +16,7 @@ export const ContentModal = ({
|
|
| 16 |
}) => {
|
| 17 |
const [collapsed, setCollapsed] = useState(["colors", "theme"]);
|
| 18 |
return (
|
| 19 |
-
<main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto
|
| 20 |
<section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
|
| 21 |
<div className="flex items-center justify-between gap-3">
|
| 22 |
<p className="text-base font-semibold text-neutral-200">
|
|
|
|
| 16 |
}) => {
|
| 17 |
const [collapsed, setCollapsed] = useState(["colors", "theme"]);
|
| 18 |
return (
|
| 19 |
+
<main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
|
| 20 |
<section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
|
| 21 |
<div className="flex items-center justify-between gap-3">
|
| 22 |
<p className="text-base font-semibold text-neutral-200">
|
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx
CHANGED
|
@@ -26,7 +26,7 @@ export const TailwindColors = ({
|
|
| 26 |
return (
|
| 27 |
<div
|
| 28 |
ref={ref}
|
| 29 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5
|
| 30 |
>
|
| 31 |
{TAILWIND_COLORS.map((color) => (
|
| 32 |
<div
|
|
|
|
| 26 |
return (
|
| 27 |
<div
|
| 28 |
ref={ref}
|
| 29 |
+
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
| 30 |
>
|
| 31 |
{TAILWIND_COLORS.map((color) => (
|
| 32 |
<div
|
components/editor/ask-ai/re-imagine.tsx
CHANGED
|
@@ -13,12 +13,11 @@ import Loading from "@/components/loading";
|
|
| 13 |
import { api } from "@/lib/api";
|
| 14 |
import { useAi } from "@/hooks/useAi";
|
| 15 |
import { useEditor } from "@/hooks/useEditor";
|
| 16 |
-
import classNames from "classnames";
|
| 17 |
|
| 18 |
export function ReImagine({
|
| 19 |
onRedesign,
|
| 20 |
}: {
|
| 21 |
-
onRedesign: (md: string
|
| 22 |
}) {
|
| 23 |
const [url, setUrl] = useState<string>("");
|
| 24 |
const [open, setOpen] = useState(false);
|
|
@@ -50,8 +49,8 @@ export function ReImagine({
|
|
| 50 |
});
|
| 51 |
if (response?.data?.ok) {
|
| 52 |
setOpen(false);
|
| 53 |
-
onRedesign(response.data.markdown, url.trim());
|
| 54 |
setUrl("");
|
|
|
|
| 55 |
toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
|
| 56 |
} else {
|
| 57 |
toast.error(response?.data?.error || "Failed to redesign the site.");
|
|
@@ -118,11 +117,6 @@ export function ReImagine({
|
|
| 118 |
}
|
| 119 |
setUrl(inputUrl);
|
| 120 |
}}
|
| 121 |
-
onKeyDown={(e) => {
|
| 122 |
-
if (e.key === "Enter") {
|
| 123 |
-
handleClick();
|
| 124 |
-
}
|
| 125 |
-
}}
|
| 126 |
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
| 127 |
/>
|
| 128 |
</div>
|
|
|
|
| 13 |
import { api } from "@/lib/api";
|
| 14 |
import { useAi } from "@/hooks/useAi";
|
| 15 |
import { useEditor } from "@/hooks/useEditor";
|
|
|
|
| 16 |
|
| 17 |
export function ReImagine({
|
| 18 |
onRedesign,
|
| 19 |
}: {
|
| 20 |
+
onRedesign: (md: string) => void;
|
| 21 |
}) {
|
| 22 |
const [url, setUrl] = useState<string>("");
|
| 23 |
const [open, setOpen] = useState(false);
|
|
|
|
| 49 |
});
|
| 50 |
if (response?.data?.ok) {
|
| 51 |
setOpen(false);
|
|
|
|
| 52 |
setUrl("");
|
| 53 |
+
onRedesign(response.data.markdown);
|
| 54 |
toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
|
| 55 |
} else {
|
| 56 |
toast.error(response?.data?.error || "Failed to redesign the site.");
|
|
|
|
| 117 |
}
|
| 118 |
setUrl(inputUrl);
|
| 119 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
| 121 |
/>
|
| 122 |
</div>
|
components/editor/ask-ai/selected-redesign-url.tsx
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
import classNames from "classnames";
|
| 2 |
-
import { Paintbrush, XCircle } from "lucide-react";
|
| 3 |
-
|
| 4 |
-
export const SelectedRedesignUrl = ({
|
| 5 |
-
url,
|
| 6 |
-
isAiWorking = false,
|
| 7 |
-
onDelete,
|
| 8 |
-
}: {
|
| 9 |
-
url: string;
|
| 10 |
-
isAiWorking: boolean;
|
| 11 |
-
onDelete?: () => void;
|
| 12 |
-
}) => {
|
| 13 |
-
return (
|
| 14 |
-
<div
|
| 15 |
-
className={classNames(
|
| 16 |
-
"border border-emerald-500/50 bg-emerald-500/10 rounded-xl p-1.5 pr-3 max-w-max hover:brightness-110 transition-all duration-200 ease-in-out",
|
| 17 |
-
{
|
| 18 |
-
"cursor-pointer": !isAiWorking,
|
| 19 |
-
"opacity-50 cursor-not-allowed": isAiWorking,
|
| 20 |
-
}
|
| 21 |
-
)}
|
| 22 |
-
onClick={() => {
|
| 23 |
-
if (!isAiWorking && onDelete) {
|
| 24 |
-
onDelete();
|
| 25 |
-
}
|
| 26 |
-
}}
|
| 27 |
-
>
|
| 28 |
-
<div className="flex items-center justify-start gap-2">
|
| 29 |
-
<div className="rounded-lg bg-emerald-500/20 size-6 flex items-center justify-center">
|
| 30 |
-
<Paintbrush className="text-emerald-300 size-3.5" />
|
| 31 |
-
</div>
|
| 32 |
-
<p className="text-sm font-semibold text-emerald-200">{url}</p>
|
| 33 |
-
<XCircle className="text-emerald-300 size-4 hover:text-emerald-200 transition-colors" />
|
| 34 |
-
</div>
|
| 35 |
-
</div>
|
| 36 |
-
);
|
| 37 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/settings.tsx
CHANGED
|
@@ -20,14 +20,7 @@ import {
|
|
| 20 |
import { useMemo, useState, useEffect } from "react";
|
| 21 |
import { useUpdateEffect } from "react-use";
|
| 22 |
import Image from "next/image";
|
| 23 |
-
import {
|
| 24 |
-
BrainIcon,
|
| 25 |
-
CheckCheck,
|
| 26 |
-
ChevronDown,
|
| 27 |
-
Sparkles,
|
| 28 |
-
Zap,
|
| 29 |
-
DollarSign,
|
| 30 |
-
} from "lucide-react";
|
| 31 |
import { useAi } from "@/hooks/useAi";
|
| 32 |
import { getProviders } from "@/lib/get-providers";
|
| 33 |
import Loading from "@/components/loading";
|
|
@@ -69,10 +62,7 @@ export function Settings({
|
|
| 69 |
// }, [model]);
|
| 70 |
|
| 71 |
useUpdateEffect(() => {
|
| 72 |
-
if (
|
| 73 |
-
!["auto", "fastest", "cheapest"].includes(provider as string) &&
|
| 74 |
-
!providers.includes(provider as string)
|
| 75 |
-
) {
|
| 76 |
setProvider("auto");
|
| 77 |
}
|
| 78 |
}, [model, provider]);
|
|
@@ -215,83 +205,47 @@ export function Settings({
|
|
| 215 |
</div>
|
| 216 |
)} */}
|
| 217 |
<div className="flex flex-col gap-3">
|
| 218 |
-
<div>
|
| 219 |
-
<
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
(
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
<Sparkles
|
| 248 |
-
className={classNames("size-3.5 mb-0.5", {
|
| 249 |
-
"text-pink-400": provider !== "auto",
|
| 250 |
-
})}
|
| 251 |
-
/>
|
| 252 |
-
<span className="text-[10px] font-medium">Auto</span>
|
| 253 |
-
</button>
|
| 254 |
-
<button
|
| 255 |
-
className={classNames(
|
| 256 |
-
"flex flex-col items-center justify-center cursor-pointer py-1.5 rounded-full transition-all duration-200",
|
| 257 |
-
{
|
| 258 |
-
"bg-white text-neutral-800": provider === "fastest",
|
| 259 |
-
"text-neutral-400 hover:text-neutral-200":
|
| 260 |
-
provider !== "fastest",
|
| 261 |
-
}
|
| 262 |
-
)}
|
| 263 |
-
onClick={() => setProvider("fastest")}
|
| 264 |
-
>
|
| 265 |
-
<Zap
|
| 266 |
-
className={classNames("size-3.5 mb-0.5", {
|
| 267 |
-
"text-yellow-400": provider !== "fastest",
|
| 268 |
-
})}
|
| 269 |
-
/>
|
| 270 |
-
<span className="text-[10px] font-medium">Fastest</span>
|
| 271 |
-
</button>
|
| 272 |
-
<button
|
| 273 |
className={classNames(
|
| 274 |
-
"
|
| 275 |
{
|
| 276 |
-
"
|
| 277 |
-
"text-neutral-400 hover:text-neutral-200":
|
| 278 |
-
provider !== "cheapest",
|
| 279 |
}
|
| 280 |
)}
|
| 281 |
-
|
| 282 |
-
>
|
| 283 |
-
<DollarSign
|
| 284 |
-
className={classNames("size-3.5 mb-0.5", {
|
| 285 |
-
"text-green-400": provider !== "cheapest",
|
| 286 |
-
})}
|
| 287 |
-
/>
|
| 288 |
-
<span className="text-[10px] font-medium">Cheapest</span>
|
| 289 |
-
</button>
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
<label className="block">
|
| 293 |
<p className="text-neutral-300 text-sm mb-2">
|
| 294 |
-
|
| 295 |
</p>
|
| 296 |
<div className="grid grid-cols-2 gap-1.5 relative">
|
| 297 |
{loadingProviders ? (
|
|
|
|
| 20 |
import { useMemo, useState, useEffect } from "react";
|
| 21 |
import { useUpdateEffect } from "react-use";
|
| 22 |
import Image from "next/image";
|
| 23 |
+
import { Brain, BrainIcon, CheckCheck, ChevronDown } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
import { useAi } from "@/hooks/useAi";
|
| 25 |
import { getProviders } from "@/lib/get-providers";
|
| 26 |
import Loading from "@/components/loading";
|
|
|
|
| 62 |
// }, [model]);
|
| 63 |
|
| 64 |
useUpdateEffect(() => {
|
| 65 |
+
if (provider !== "auto" && !providers.includes(provider as string)) {
|
|
|
|
|
|
|
|
|
|
| 66 |
setProvider("auto");
|
| 67 |
}
|
| 68 |
}, [model, provider]);
|
|
|
|
| 205 |
</div>
|
| 206 |
)} */}
|
| 207 |
<div className="flex flex-col gap-3">
|
| 208 |
+
<div className="flex items-center justify-between">
|
| 209 |
+
<div>
|
| 210 |
+
<p className="text-neutral-300 text-sm mb-1.5">
|
| 211 |
+
Use auto-provider
|
| 212 |
+
</p>
|
| 213 |
+
<p className="text-xs text-neutral-400/70">
|
| 214 |
+
We'll automatically select the best provider for you
|
| 215 |
+
based on your prompt.
|
| 216 |
+
</p>
|
| 217 |
+
</div>
|
| 218 |
+
<div
|
| 219 |
+
className={classNames(
|
| 220 |
+
"bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
|
| 221 |
+
{
|
| 222 |
+
"!bg-sky-500": provider === "auto",
|
| 223 |
+
}
|
| 224 |
+
)}
|
| 225 |
+
onClick={() => {
|
| 226 |
+
const foundModel = MODELS.find(
|
| 227 |
+
(m: { value: string }) => m.value === model
|
| 228 |
+
);
|
| 229 |
+
if (provider === "auto" && foundModel?.autoProvider) {
|
| 230 |
+
setProvider(foundModel.autoProvider);
|
| 231 |
+
} else {
|
| 232 |
+
setProvider("auto");
|
| 233 |
+
}
|
| 234 |
+
}}
|
| 235 |
+
>
|
| 236 |
+
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
className={classNames(
|
| 238 |
+
"w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
|
| 239 |
{
|
| 240 |
+
"translate-x-4": provider === "auto",
|
|
|
|
|
|
|
| 241 |
}
|
| 242 |
)}
|
| 243 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
</div>
|
| 245 |
</div>
|
| 246 |
<label className="block">
|
| 247 |
<p className="text-neutral-300 text-sm mb-2">
|
| 248 |
+
Inference Provider
|
| 249 |
</p>
|
| 250 |
<div className="grid grid-cols-2 gap-1.5 relative">
|
| 251 |
{loadingProviders ? (
|
components/editor/file-browser/index.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
| 7 |
Folder,
|
| 8 |
ChevronRight,
|
| 9 |
ChevronDown,
|
| 10 |
-
FileJson,
|
| 11 |
} from "lucide-react";
|
| 12 |
import classNames from "classnames";
|
| 13 |
|
|
@@ -198,7 +197,27 @@ export function FileBrowser() {
|
|
| 198 |
</svg>
|
| 199 |
);
|
| 200 |
case "json":
|
| 201 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
default:
|
| 203 |
return <FileCode2 className="size-4 shrink-0 text-neutral-400" />;
|
| 204 |
}
|
|
@@ -230,7 +249,7 @@ export function FileBrowser() {
|
|
| 230 |
case "json":
|
| 231 |
return {
|
| 232 |
name: "JSON",
|
| 233 |
-
color: "bg-
|
| 234 |
};
|
| 235 |
default:
|
| 236 |
return {
|
|
@@ -439,13 +458,6 @@ export function FileBrowser() {
|
|
| 439 |
JS: {pages.filter((p) => p.path.endsWith(".js")).length}
|
| 440 |
</span>
|
| 441 |
</div>
|
| 442 |
-
<div className="flex items-center gap-2 text-neutral-500">
|
| 443 |
-
<div className="size-2 rounded-full bg-yellow-600" />
|
| 444 |
-
<span>
|
| 445 |
-
JSON:{" "}
|
| 446 |
-
{pages.filter((p) => p.path.endsWith(".json")).length}
|
| 447 |
-
</span>
|
| 448 |
-
</div>
|
| 449 |
</div>
|
| 450 |
</div>
|
| 451 |
</SheetContent>
|
|
|
|
| 7 |
Folder,
|
| 8 |
ChevronRight,
|
| 9 |
ChevronDown,
|
|
|
|
| 10 |
} from "lucide-react";
|
| 11 |
import classNames from "classnames";
|
| 12 |
|
|
|
|
| 197 |
</svg>
|
| 198 |
);
|
| 199 |
case "json":
|
| 200 |
+
return (
|
| 201 |
+
<svg className="size-4 shrink-0" viewBox="0 0 32 32" fill="none">
|
| 202 |
+
<rect width="32" height="32" rx="2" fill="#F7DF1E" />
|
| 203 |
+
<path
|
| 204 |
+
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"
|
| 205 |
+
fill="#000"
|
| 206 |
+
opacity="0.15"
|
| 207 |
+
/>
|
| 208 |
+
<text
|
| 209 |
+
x="50%"
|
| 210 |
+
y="50%"
|
| 211 |
+
dominantBaseline="middle"
|
| 212 |
+
textAnchor="middle"
|
| 213 |
+
fill="#000"
|
| 214 |
+
fontSize="14"
|
| 215 |
+
fontWeight="600"
|
| 216 |
+
>
|
| 217 |
+
{}
|
| 218 |
+
</text>
|
| 219 |
+
</svg>
|
| 220 |
+
);
|
| 221 |
default:
|
| 222 |
return <FileCode2 className="size-4 shrink-0 text-neutral-400" />;
|
| 223 |
}
|
|
|
|
| 249 |
case "json":
|
| 250 |
return {
|
| 251 |
name: "JSON",
|
| 252 |
+
color: "bg-yellow-500/20 border-yellow-500/30 text-yellow-400",
|
| 253 |
};
|
| 254 |
default:
|
| 255 |
return {
|
|
|
|
| 458 |
JS: {pages.filter((p) => p.path.endsWith(".js")).length}
|
| 459 |
</span>
|
| 460 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
</div>
|
| 462 |
</div>
|
| 463 |
</SheetContent>
|
components/editor/header/index.tsx
CHANGED
|
@@ -23,9 +23,8 @@ import {
|
|
| 23 |
TooltipContent,
|
| 24 |
TooltipTrigger,
|
| 25 |
} from "@/components/ui/tooltip";
|
| 26 |
-
import { DiscordIcon } from "@/components/icons/discord";
|
| 27 |
|
| 28 |
-
export function Header(
|
| 29 |
const { project } = useEditor();
|
| 30 |
const { user, openLoginWindow } = useUser();
|
| 31 |
return (
|
|
@@ -60,26 +59,24 @@ export function Header({ isNew }: { isNew: boolean }) {
|
|
| 60 |
<div className="lg:w-full px-2 lg:px-3 py-2 flex items-center justify-end lg:justify-between lg:border-l lg:border-neutral-800">
|
| 61 |
<div className="font-mono text-muted-foreground flex items-center gap-2">
|
| 62 |
<SwitchDevice />
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
</Button>
|
| 80 |
-
)}
|
| 81 |
<Link
|
| 82 |
-
href="https://
|
| 83 |
target="_blank"
|
| 84 |
className="max-lg:hidden"
|
| 85 |
>
|
|
@@ -88,20 +85,6 @@ export function Header({ isNew }: { isNew: boolean }) {
|
|
| 88 |
Help
|
| 89 |
</Button>
|
| 90 |
</Link>
|
| 91 |
-
<Link
|
| 92 |
-
href="https://discord.gg/KpanwM3vXa"
|
| 93 |
-
target="_blank"
|
| 94 |
-
className="max-lg:hidden"
|
| 95 |
-
>
|
| 96 |
-
<Button
|
| 97 |
-
size="xs"
|
| 98 |
-
variant="bordered"
|
| 99 |
-
className="!border-indigo-500 !text-white !bg-indigo-500"
|
| 100 |
-
>
|
| 101 |
-
<DiscordIcon className="size-3 mr-0.5" />
|
| 102 |
-
Discord Community
|
| 103 |
-
</Button>
|
| 104 |
-
</Link>
|
| 105 |
</div>
|
| 106 |
<div className="flex items-center gap-2">
|
| 107 |
{project?.space_id && (
|
|
@@ -127,7 +110,6 @@ export function Header({ isNew }: { isNew: boolean }) {
|
|
| 127 |
</Button>
|
| 128 |
</Link>
|
| 129 |
)}
|
| 130 |
-
|
| 131 |
{project?.private && (
|
| 132 |
<Tooltip>
|
| 133 |
<TooltipTrigger>
|
|
|
|
| 23 |
TooltipContent,
|
| 24 |
TooltipTrigger,
|
| 25 |
} from "@/components/ui/tooltip";
|
|
|
|
| 26 |
|
| 27 |
+
export function Header() {
|
| 28 |
const { project } = useEditor();
|
| 29 |
const { user, openLoginWindow } = useUser();
|
| 30 |
return (
|
|
|
|
| 59 |
<div className="lg:w-full px-2 lg:px-3 py-2 flex items-center justify-end lg:justify-between lg:border-l lg:border-neutral-800">
|
| 60 |
<div className="font-mono text-muted-foreground flex items-center gap-2">
|
| 61 |
<SwitchDevice />
|
| 62 |
+
<Button
|
| 63 |
+
size="xs"
|
| 64 |
+
variant="bordered"
|
| 65 |
+
className="max-lg:hidden"
|
| 66 |
+
onClick={() => {
|
| 67 |
+
const iframe = document.getElementById(
|
| 68 |
+
"preview-iframe"
|
| 69 |
+
) as HTMLIFrameElement;
|
| 70 |
+
if (iframe) {
|
| 71 |
+
iframe.src = iframe.src;
|
| 72 |
+
}
|
| 73 |
+
}}
|
| 74 |
+
>
|
| 75 |
+
<RefreshCcw className="size-3 mr-0.5" />
|
| 76 |
+
Refresh Preview
|
| 77 |
+
</Button>
|
|
|
|
|
|
|
| 78 |
<Link
|
| 79 |
+
href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/427"
|
| 80 |
target="_blank"
|
| 81 |
className="max-lg:hidden"
|
| 82 |
>
|
|
|
|
| 85 |
Help
|
| 86 |
</Button>
|
| 87 |
</Link>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
<div className="flex items-center gap-2">
|
| 90 |
{project?.space_id && (
|
|
|
|
| 110 |
</Button>
|
| 111 |
</Link>
|
| 112 |
)}
|
|
|
|
| 113 |
{project?.private && (
|
| 114 |
<Tooltip>
|
| 115 |
<TooltipTrigger>
|
components/editor/index.tsx
CHANGED
|
@@ -15,7 +15,6 @@ import { FileBrowser } from "./file-browser";
|
|
| 15 |
import { AskAi } from "./ask-ai";
|
| 16 |
import { Preview } from "./preview";
|
| 17 |
import { SaveChangesPopup } from "./save-changes-popup";
|
| 18 |
-
import { DiscordPromoModal } from "@/components/discord-promo-modal";
|
| 19 |
import Loading from "../loading";
|
| 20 |
import { Page } from "@/types";
|
| 21 |
|
|
@@ -69,7 +68,6 @@ export const AppEditor = ({
|
|
| 69 |
const path = currentPageData.path;
|
| 70 |
if (path.endsWith(".css")) return "css";
|
| 71 |
if (path.endsWith(".js")) return "javascript";
|
| 72 |
-
if (path.endsWith(".json")) return "json";
|
| 73 |
return "html";
|
| 74 |
}, [currentPageData.path]);
|
| 75 |
|
|
@@ -78,13 +76,12 @@ export const AppEditor = ({
|
|
| 78 |
if (editorLanguage === "css") return "CSS copied to clipboard!";
|
| 79 |
if (editorLanguage === "javascript")
|
| 80 |
return "JavaScript copied to clipboard!";
|
| 81 |
-
if (editorLanguage === "json") return "JSON copied to clipboard!";
|
| 82 |
return "HTML copied to clipboard!";
|
| 83 |
}, [editorLanguage]);
|
| 84 |
|
| 85 |
return (
|
| 86 |
<section className="h-screen w-full bg-neutral-950 flex flex-col">
|
| 87 |
-
<Header
|
| 88 |
<main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
|
| 89 |
<div
|
| 90 |
ref={editor}
|
|
@@ -156,9 +153,10 @@ export const AppEditor = ({
|
|
| 156 |
}}
|
| 157 |
/>
|
| 158 |
</div>
|
| 159 |
-
<Preview isNew={isNew}
|
| 160 |
</main>
|
| 161 |
|
|
|
|
| 162 |
<SaveChangesPopup
|
| 163 |
isOpen={showSavePopup}
|
| 164 |
onClose={() => setShowSavePopup(false)}
|
|
@@ -167,8 +165,6 @@ export const AppEditor = ({
|
|
| 167 |
pages={pages}
|
| 168 |
project={project}
|
| 169 |
/>
|
| 170 |
-
|
| 171 |
-
<DiscordPromoModal />
|
| 172 |
</section>
|
| 173 |
);
|
| 174 |
};
|
|
|
|
| 15 |
import { AskAi } from "./ask-ai";
|
| 16 |
import { Preview } from "./preview";
|
| 17 |
import { SaveChangesPopup } from "./save-changes-popup";
|
|
|
|
| 18 |
import Loading from "../loading";
|
| 19 |
import { Page } from "@/types";
|
| 20 |
|
|
|
|
| 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 |
|
|
|
|
| 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 />
|
| 85 |
<main className="bg-neutral-950 flex-1 max-lg:flex-col flex w-full relative">
|
| 86 |
<div
|
| 87 |
ref={editor}
|
|
|
|
| 153 |
}}
|
| 154 |
/>
|
| 155 |
</div>
|
| 156 |
+
<Preview isNew={isNew} />
|
| 157 |
</main>
|
| 158 |
|
| 159 |
+
{/* Save Changes Popup */}
|
| 160 |
<SaveChangesPopup
|
| 161 |
isOpen={showSavePopup}
|
| 162 |
onClose={() => setShowSavePopup(false)}
|
|
|
|
| 165 |
pages={pages}
|
| 166 |
project={project}
|
| 167 |
/>
|
|
|
|
|
|
|
| 168 |
</section>
|
| 169 |
);
|
| 170 |
};
|
components/editor/preview/index.tsx
CHANGED
|
@@ -15,18 +15,8 @@ import { defaultHTML } from "@/lib/consts";
|
|
| 15 |
import { HistoryNotification } from "../history-notification";
|
| 16 |
import { api } from "@/lib/api";
|
| 17 |
import { toast } from "sonner";
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
export const Preview = ({
|
| 22 |
-
isNew,
|
| 23 |
-
namespace,
|
| 24 |
-
repoId,
|
| 25 |
-
}: {
|
| 26 |
-
isNew: boolean;
|
| 27 |
-
namespace?: string;
|
| 28 |
-
repoId?: string;
|
| 29 |
-
}) => {
|
| 30 |
const {
|
| 31 |
project,
|
| 32 |
device,
|
|
@@ -40,8 +30,6 @@ export const Preview = ({
|
|
| 40 |
setCurrentPage,
|
| 41 |
previewPage,
|
| 42 |
setPreviewPage,
|
| 43 |
-
setLastSavedPages,
|
| 44 |
-
hasUnsavedChanges,
|
| 45 |
} = useEditor();
|
| 46 |
const {
|
| 47 |
isEditableModeEnabled,
|
|
@@ -60,10 +48,6 @@ export const Preview = ({
|
|
| 60 |
const [stableHtml, setStableHtml] = useState<string>("");
|
| 61 |
const [throttledHtml, setThrottledHtml] = useState<string>("");
|
| 62 |
const lastUpdateTimeRef = useRef<number>(0);
|
| 63 |
-
const [iframeKey, setIframeKey] = useState(0);
|
| 64 |
-
const [commitPages, setCommitPages] = useState<Page[]>([]);
|
| 65 |
-
const [isLoadingCommitPages, setIsLoadingCommitPages] = useState(false);
|
| 66 |
-
const prevCommitRef = useRef<string | null>(null);
|
| 67 |
|
| 68 |
useEffect(() => {
|
| 69 |
if (!previewPage && pages.length > 0) {
|
|
@@ -75,189 +59,29 @@ export const Preview = ({
|
|
| 75 |
}
|
| 76 |
}, [pages, previewPage]);
|
| 77 |
|
| 78 |
-
const pagesToUse = currentCommit ? commitPages : pages;
|
| 79 |
-
|
| 80 |
const previewPageData = useMemo(() => {
|
| 81 |
-
const found =
|
| 82 |
const normalizedPagePath = p.path.replace(/^\.?\//, "");
|
| 83 |
const normalizedPreviewPage = previewPage.replace(/^\.?\//, "");
|
| 84 |
return normalizedPagePath === normalizedPreviewPage;
|
| 85 |
});
|
| 86 |
-
return found ||
|
| 87 |
-
}, [
|
| 88 |
-
|
| 89 |
-
// Fetch commit pages when currentCommit changes
|
| 90 |
-
useEffect(() => {
|
| 91 |
-
if (currentCommit && namespace && repoId) {
|
| 92 |
-
setIsLoadingCommitPages(true);
|
| 93 |
-
api
|
| 94 |
-
.get(`/me/projects/${namespace}/${repoId}/commits/${currentCommit}`)
|
| 95 |
-
.then((res) => {
|
| 96 |
-
if (res.data.ok) {
|
| 97 |
-
setCommitPages(res.data.pages);
|
| 98 |
-
// Set preview page to index.html if available
|
| 99 |
-
const indexPage = res.data.pages.find(
|
| 100 |
-
(p: Page) =>
|
| 101 |
-
p.path === "index.html" || p.path === "index" || p.path === "/"
|
| 102 |
-
);
|
| 103 |
-
if (indexPage) {
|
| 104 |
-
setPreviewPage(indexPage.path);
|
| 105 |
-
}
|
| 106 |
-
// Refresh iframe to show commit version
|
| 107 |
-
setIframeKey((prev) => prev + 1);
|
| 108 |
-
}
|
| 109 |
-
})
|
| 110 |
-
.catch((err) => {
|
| 111 |
-
toast.error(
|
| 112 |
-
err.response?.data?.error || "Failed to fetch commit pages"
|
| 113 |
-
);
|
| 114 |
-
})
|
| 115 |
-
.finally(() => {
|
| 116 |
-
setIsLoadingCommitPages(false);
|
| 117 |
-
});
|
| 118 |
-
} else if (!currentCommit && prevCommitRef.current !== null) {
|
| 119 |
-
// Only clear commitPages when transitioning from a commit to no commit
|
| 120 |
-
setCommitPages([]);
|
| 121 |
-
}
|
| 122 |
-
prevCommitRef.current = currentCommit;
|
| 123 |
-
}, [currentCommit, namespace, repoId]);
|
| 124 |
-
|
| 125 |
-
// Create navigation interception script
|
| 126 |
-
const createNavigationScript = useCallback((availablePages: Page[]) => {
|
| 127 |
-
const pagePaths = availablePages.map((p) => p.path.replace(/^\.?\//, ""));
|
| 128 |
-
return `
|
| 129 |
-
(function() {
|
| 130 |
-
const availablePages = ${JSON.stringify(pagePaths)};
|
| 131 |
-
|
| 132 |
-
function normalizePath(path) {
|
| 133 |
-
let normalized = path.replace(/^\.?\//, "");
|
| 134 |
-
if (normalized === "" || normalized === "/") {
|
| 135 |
-
normalized = "index.html";
|
| 136 |
-
}
|
| 137 |
-
const hashIndex = normalized.indexOf("#");
|
| 138 |
-
if (hashIndex !== -1) {
|
| 139 |
-
normalized = normalized.substring(0, hashIndex);
|
| 140 |
-
}
|
| 141 |
-
if (!normalized.includes(".")) {
|
| 142 |
-
normalized = normalized + ".html";
|
| 143 |
-
}
|
| 144 |
-
return normalized;
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
function handleNavigation(url) {
|
| 148 |
-
if (!url) return;
|
| 149 |
-
|
| 150 |
-
// Handle hash-only navigation
|
| 151 |
-
if (url.startsWith("#")) {
|
| 152 |
-
const targetElement = document.querySelector(url);
|
| 153 |
-
if (targetElement) {
|
| 154 |
-
targetElement.scrollIntoView({ behavior: "smooth" });
|
| 155 |
-
}
|
| 156 |
-
// Search in shadow DOM
|
| 157 |
-
const searchInShadows = (root) => {
|
| 158 |
-
const elements = root.querySelectorAll("*");
|
| 159 |
-
for (const el of elements) {
|
| 160 |
-
if (el.shadowRoot) {
|
| 161 |
-
const found = el.shadowRoot.querySelector(url);
|
| 162 |
-
if (found) {
|
| 163 |
-
found.scrollIntoView({ behavior: "smooth" });
|
| 164 |
-
return;
|
| 165 |
-
}
|
| 166 |
-
searchInShadows(el.shadowRoot);
|
| 167 |
-
}
|
| 168 |
-
}
|
| 169 |
-
};
|
| 170 |
-
searchInShadows(document);
|
| 171 |
-
return;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
// Handle external URLs
|
| 175 |
-
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
|
| 176 |
-
window.open(url, "_blank");
|
| 177 |
-
return;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
const normalizedPath = normalizePath(url);
|
| 181 |
-
if (availablePages.includes(normalizedPath)) {
|
| 182 |
-
// Dispatch custom event to notify parent
|
| 183 |
-
window.parent.postMessage({ type: 'navigate', path: normalizedPath }, '*');
|
| 184 |
-
} else {
|
| 185 |
-
console.warn('Page not found:', normalizedPath);
|
| 186 |
-
}
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
// Intercept window.location methods
|
| 190 |
-
const originalAssign = window.location.assign;
|
| 191 |
-
const originalReplace = window.location.replace;
|
| 192 |
-
|
| 193 |
-
window.location.assign = function(url) {
|
| 194 |
-
handleNavigation(url);
|
| 195 |
-
};
|
| 196 |
-
|
| 197 |
-
window.location.replace = function(url) {
|
| 198 |
-
handleNavigation(url);
|
| 199 |
-
};
|
| 200 |
-
|
| 201 |
-
// Intercept window.location.href setter
|
| 202 |
-
try {
|
| 203 |
-
let currentHref = window.location.href;
|
| 204 |
-
Object.defineProperty(window.location, 'href', {
|
| 205 |
-
get: function() {
|
| 206 |
-
return currentHref;
|
| 207 |
-
},
|
| 208 |
-
set: function(url) {
|
| 209 |
-
handleNavigation(url);
|
| 210 |
-
},
|
| 211 |
-
configurable: true
|
| 212 |
-
});
|
| 213 |
-
} catch (e) {
|
| 214 |
-
// Fallback: use proxy if defineProperty fails
|
| 215 |
-
console.warn('Could not intercept location.href:', e);
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
// Intercept link clicks
|
| 219 |
-
document.addEventListener('click', function(e) {
|
| 220 |
-
const anchor = e.target.closest('a');
|
| 221 |
-
if (anchor && anchor.href) {
|
| 222 |
-
const href = anchor.getAttribute('href');
|
| 223 |
-
if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('//') && !href.startsWith('mailto:') && !href.startsWith('tel:')) {
|
| 224 |
-
e.preventDefault();
|
| 225 |
-
handleNavigation(href);
|
| 226 |
-
}
|
| 227 |
-
}
|
| 228 |
-
}, true);
|
| 229 |
-
|
| 230 |
-
// Intercept form submissions
|
| 231 |
-
document.addEventListener('submit', function(e) {
|
| 232 |
-
const form = e.target;
|
| 233 |
-
if (form.action && !form.action.startsWith('http://') && !form.action.startsWith('https://') && !form.action.startsWith('//')) {
|
| 234 |
-
e.preventDefault();
|
| 235 |
-
handleNavigation(form.action);
|
| 236 |
-
}
|
| 237 |
-
}, true);
|
| 238 |
-
})();
|
| 239 |
-
`;
|
| 240 |
-
}, []);
|
| 241 |
|
| 242 |
const injectAssetsIntoHtml = useCallback(
|
| 243 |
-
(html: string
|
| 244 |
if (!html) return html;
|
| 245 |
|
| 246 |
-
|
|
|
|
| 247 |
(p) => p.path.endsWith(".css") && p.path !== previewPageData?.path
|
| 248 |
);
|
| 249 |
-
const jsFiles =
|
| 250 |
(p) => p.path.endsWith(".js") && p.path !== previewPageData?.path
|
| 251 |
);
|
| 252 |
-
const jsonFiles = pagesToUse.filter(
|
| 253 |
-
(p) => p.path.endsWith(".json") && p.path !== previewPageData?.path
|
| 254 |
-
);
|
| 255 |
|
| 256 |
let modifiedHtml = html;
|
| 257 |
|
| 258 |
-
// Inject navigation script for srcDoc
|
| 259 |
-
const navigationScript = createNavigationScript(pagesToUse);
|
| 260 |
-
|
| 261 |
// Inject all CSS files
|
| 262 |
if (cssFiles.length > 0) {
|
| 263 |
const allCssContent = cssFiles
|
|
@@ -278,9 +102,11 @@ export const Preview = ({
|
|
| 278 |
`<head>\n${allCssContent}`
|
| 279 |
);
|
| 280 |
} else {
|
|
|
|
| 281 |
modifiedHtml = allCssContent + "\n" + modifiedHtml;
|
| 282 |
}
|
| 283 |
|
|
|
|
| 284 |
cssFiles.forEach((file) => {
|
| 285 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 286 |
modifiedHtml = modifiedHtml.replace(
|
|
@@ -293,6 +119,7 @@ export const Preview = ({
|
|
| 293 |
});
|
| 294 |
}
|
| 295 |
|
|
|
|
| 296 |
if (jsFiles.length > 0) {
|
| 297 |
const allJsContent = jsFiles
|
| 298 |
.map(
|
|
@@ -309,9 +136,11 @@ export const Preview = ({
|
|
| 309 |
} else if (modifiedHtml.includes("<body>")) {
|
| 310 |
modifiedHtml = modifiedHtml + allJsContent;
|
| 311 |
} else {
|
|
|
|
| 312 |
modifiedHtml = modifiedHtml + "\n" + allJsContent;
|
| 313 |
}
|
| 314 |
|
|
|
|
| 315 |
jsFiles.forEach((file) => {
|
| 316 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 317 |
modifiedHtml = modifiedHtml.replace(
|
|
@@ -324,69 +153,9 @@ export const Preview = ({
|
|
| 324 |
});
|
| 325 |
}
|
| 326 |
|
| 327 |
-
// Inject all JSON files as script tags with type="application/json"
|
| 328 |
-
if (jsonFiles.length > 0) {
|
| 329 |
-
const allJsonContent = jsonFiles
|
| 330 |
-
.map(
|
| 331 |
-
(file) =>
|
| 332 |
-
`<script type="application/json" data-injected-from="${
|
| 333 |
-
file.path
|
| 334 |
-
}" id="${file.path.replace(/[^a-zA-Z0-9]/g, "-")}">\n${
|
| 335 |
-
file.html
|
| 336 |
-
}\n</script>`
|
| 337 |
-
)
|
| 338 |
-
.join("\n");
|
| 339 |
-
|
| 340 |
-
if (modifiedHtml.includes("</body>")) {
|
| 341 |
-
modifiedHtml = modifiedHtml.replace(
|
| 342 |
-
"</body>",
|
| 343 |
-
`${allJsonContent}\n</body>`
|
| 344 |
-
);
|
| 345 |
-
} else if (modifiedHtml.includes("<body>")) {
|
| 346 |
-
modifiedHtml = modifiedHtml + allJsonContent;
|
| 347 |
-
} else {
|
| 348 |
-
modifiedHtml = modifiedHtml + "\n" + allJsonContent;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
jsonFiles.forEach((file) => {
|
| 352 |
-
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 353 |
-
modifiedHtml = modifiedHtml.replace(
|
| 354 |
-
new RegExp(
|
| 355 |
-
`<script\\s+[^>]*src=["'][\\.\/]*${escapedPath}["'][^>]*><\\/script>`,
|
| 356 |
-
"gi"
|
| 357 |
-
),
|
| 358 |
-
""
|
| 359 |
-
);
|
| 360 |
-
});
|
| 361 |
-
}
|
| 362 |
-
|
| 363 |
-
// Inject navigation script early in the document
|
| 364 |
-
if (navigationScript) {
|
| 365 |
-
// Try to inject right after <head> or <body> opening tag
|
| 366 |
-
if (modifiedHtml.includes("<head>")) {
|
| 367 |
-
modifiedHtml = modifiedHtml.replace(
|
| 368 |
-
"<head>",
|
| 369 |
-
`<head>\n<script>${navigationScript}</script>`
|
| 370 |
-
);
|
| 371 |
-
} else if (modifiedHtml.includes("<body>")) {
|
| 372 |
-
modifiedHtml = modifiedHtml.replace(
|
| 373 |
-
"<body>",
|
| 374 |
-
`<body>\n<script>${navigationScript}</script>`
|
| 375 |
-
);
|
| 376 |
-
} else if (modifiedHtml.includes("</body>")) {
|
| 377 |
-
modifiedHtml = modifiedHtml.replace(
|
| 378 |
-
"</body>",
|
| 379 |
-
`<script>${navigationScript}</script>\n</body>`
|
| 380 |
-
);
|
| 381 |
-
} else {
|
| 382 |
-
modifiedHtml =
|
| 383 |
-
`<script>${navigationScript}</script>\n` + modifiedHtml;
|
| 384 |
-
}
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
return modifiedHtml;
|
| 388 |
},
|
| 389 |
-
[pages, previewPageData?.path
|
| 390 |
);
|
| 391 |
|
| 392 |
useEffect(() => {
|
|
@@ -395,33 +164,24 @@ export const Preview = ({
|
|
| 395 |
const timeSinceLastUpdate = now - lastUpdateTimeRef.current;
|
| 396 |
|
| 397 |
if (lastUpdateTimeRef.current === 0 || timeSinceLastUpdate >= 3000) {
|
| 398 |
-
const processedHtml = injectAssetsIntoHtml(
|
| 399 |
-
previewPageData.html,
|
| 400 |
-
pagesToUse
|
| 401 |
-
);
|
| 402 |
setThrottledHtml(processedHtml);
|
| 403 |
lastUpdateTimeRef.current = now;
|
| 404 |
} else {
|
| 405 |
const timeUntilNextUpdate = 3000 - timeSinceLastUpdate;
|
| 406 |
const timer = setTimeout(() => {
|
| 407 |
-
const processedHtml = injectAssetsIntoHtml(
|
| 408 |
-
previewPageData.html,
|
| 409 |
-
pagesToUse
|
| 410 |
-
);
|
| 411 |
setThrottledHtml(processedHtml);
|
| 412 |
lastUpdateTimeRef.current = Date.now();
|
| 413 |
}, timeUntilNextUpdate);
|
| 414 |
return () => clearTimeout(timer);
|
| 415 |
}
|
| 416 |
}
|
| 417 |
-
}, [isNew, previewPageData?.html, injectAssetsIntoHtml
|
| 418 |
|
| 419 |
useEffect(() => {
|
| 420 |
if (!isAiWorking && !globalAiLoading && previewPageData?.html) {
|
| 421 |
-
const processedHtml = injectAssetsIntoHtml(
|
| 422 |
-
previewPageData.html,
|
| 423 |
-
pagesToUse
|
| 424 |
-
);
|
| 425 |
setStableHtml(processedHtml);
|
| 426 |
}
|
| 427 |
}, [
|
|
@@ -430,7 +190,6 @@ export const Preview = ({
|
|
| 430 |
previewPageData?.html,
|
| 431 |
injectAssetsIntoHtml,
|
| 432 |
previewPage,
|
| 433 |
-
pagesToUse,
|
| 434 |
]);
|
| 435 |
|
| 436 |
useEffect(() => {
|
|
@@ -440,10 +199,7 @@ export const Preview = ({
|
|
| 440 |
!isAiWorking &&
|
| 441 |
!globalAiLoading
|
| 442 |
) {
|
| 443 |
-
const processedHtml = injectAssetsIntoHtml(
|
| 444 |
-
previewPageData.html,
|
| 445 |
-
pagesToUse
|
| 446 |
-
);
|
| 447 |
setStableHtml(processedHtml);
|
| 448 |
}
|
| 449 |
}, [
|
|
@@ -452,13 +208,13 @@ export const Preview = ({
|
|
| 452 |
isAiWorking,
|
| 453 |
globalAiLoading,
|
| 454 |
injectAssetsIntoHtml,
|
| 455 |
-
pagesToUse,
|
| 456 |
]);
|
| 457 |
|
| 458 |
const setupIframeListeners = () => {
|
| 459 |
if (iframeRef?.current?.contentDocument) {
|
| 460 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 461 |
|
|
|
|
| 462 |
iframeDocument.addEventListener(
|
| 463 |
"click",
|
| 464 |
handleCustomNavigation as any,
|
|
@@ -473,17 +229,6 @@ export const Preview = ({
|
|
| 473 |
}
|
| 474 |
};
|
| 475 |
|
| 476 |
-
// Listen for navigation messages from iframe
|
| 477 |
-
useEffect(() => {
|
| 478 |
-
const handleMessage = (event: MessageEvent) => {
|
| 479 |
-
if (event.data?.type === "navigate" && event.data?.path) {
|
| 480 |
-
setPreviewPage(event.data.path);
|
| 481 |
-
}
|
| 482 |
-
};
|
| 483 |
-
window.addEventListener("message", handleMessage);
|
| 484 |
-
return () => window.removeEventListener("message", handleMessage);
|
| 485 |
-
}, [setPreviewPage]);
|
| 486 |
-
|
| 487 |
useEffect(() => {
|
| 488 |
const cleanupListeners = () => {
|
| 489 |
if (iframeRef?.current?.contentDocument) {
|
|
@@ -512,10 +257,6 @@ export const Preview = ({
|
|
| 512 |
};
|
| 513 |
}, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
|
| 514 |
|
| 515 |
-
const refreshIframe = () => {
|
| 516 |
-
setIframeKey((prev) => prev + 1);
|
| 517 |
-
};
|
| 518 |
-
|
| 519 |
const promoteVersion = async () => {
|
| 520 |
setIsPromotingVersion(true);
|
| 521 |
await api
|
|
@@ -527,7 +268,6 @@ export const Preview = ({
|
|
| 527 |
setCurrentCommit(null);
|
| 528 |
setPages(res.data.pages);
|
| 529 |
setCurrentPage(res.data.pages[0].path);
|
| 530 |
-
setLastSavedPages(res.data.pages);
|
| 531 |
setPreviewPage(res.data.pages[0].path);
|
| 532 |
toast.success("Version promoted successfully");
|
| 533 |
}
|
|
@@ -580,28 +320,17 @@ export const Preview = ({
|
|
| 580 |
if (iframeRef?.current) {
|
| 581 |
const iframeDocument = iframeRef.current.contentDocument;
|
| 582 |
if (iframeDocument) {
|
| 583 |
-
const
|
| 584 |
-
const targetElement = path[0] as HTMLElement;
|
| 585 |
|
| 586 |
const findClosestAnchor = (
|
| 587 |
element: HTMLElement
|
| 588 |
): HTMLAnchorElement | null => {
|
| 589 |
-
let current
|
| 590 |
-
while (current) {
|
| 591 |
-
if (current.tagName
|
| 592 |
return current as HTMLAnchorElement;
|
| 593 |
}
|
| 594 |
-
|
| 595 |
-
break;
|
| 596 |
-
}
|
| 597 |
-
const parent: Node | null = current.parentNode;
|
| 598 |
-
if (parent && parent.nodeType === 11) {
|
| 599 |
-
current = (parent as ShadowRoot).host as HTMLElement;
|
| 600 |
-
} else if (parent && parent.nodeType === 1) {
|
| 601 |
-
current = parent as HTMLElement;
|
| 602 |
-
} else {
|
| 603 |
-
break;
|
| 604 |
-
}
|
| 605 |
}
|
| 606 |
return null;
|
| 607 |
};
|
|
@@ -630,18 +359,15 @@ export const Preview = ({
|
|
| 630 |
element: HTMLElement
|
| 631 |
): HTMLAnchorElement | null => {
|
| 632 |
let current: HTMLElement | null = element;
|
| 633 |
-
while (current) {
|
| 634 |
-
if (current.tagName
|
| 635 |
return current as HTMLAnchorElement;
|
| 636 |
}
|
| 637 |
-
if (current === iframeDocument.body) {
|
| 638 |
-
break;
|
| 639 |
-
}
|
| 640 |
const parent: Node | null = current.parentNode;
|
| 641 |
-
if (parent
|
| 642 |
-
current =
|
| 643 |
-
} else if (parent
|
| 644 |
-
current = parent
|
| 645 |
} else {
|
| 646 |
break;
|
| 647 |
}
|
|
@@ -698,7 +424,7 @@ export const Preview = ({
|
|
| 698 |
normalizedHref = normalizedHref + ".html";
|
| 699 |
}
|
| 700 |
|
| 701 |
-
const isPageExist =
|
| 702 |
const pagePath = page.path.replace(/^\.?\//, "");
|
| 703 |
return pagePath === normalizedHref;
|
| 704 |
});
|
|
@@ -756,36 +482,7 @@ export const Preview = ({
|
|
| 756 |
</div>
|
| 757 |
) : (
|
| 758 |
<>
|
| 759 |
-
{isLoadingCommitPages && (
|
| 760 |
-
<div className="top-0 left-0 right-0 z-20 bg-blue-500/90 backdrop-blur-sm border-b border-blue-600 px-4 py-2 flex items-center justify-center gap-3 text-sm w-full">
|
| 761 |
-
<div className="flex items-center gap-2">
|
| 762 |
-
<AiLoading
|
| 763 |
-
text="Loading commit version..."
|
| 764 |
-
className="flex-row"
|
| 765 |
-
/>
|
| 766 |
-
</div>
|
| 767 |
-
</div>
|
| 768 |
-
)}
|
| 769 |
-
{!isNew && !currentCommit && (
|
| 770 |
-
<div className="top-0 left-0 right-0 z-20 bg-neutral-900/95 backdrop-blur-sm border-b border-neutral-800 px-4 py-2 max-h-[40px] flex items-center justify-between gap-3 text-xs w-full">
|
| 771 |
-
<div className="flex items-center gap-2 flex-1">
|
| 772 |
-
<TriangleAlert className="size-4 text-neutral-500 flex-shrink-0" />
|
| 773 |
-
<span className="text-neutral-400 font-medium">
|
| 774 |
-
Preview version of the project. Try refreshing the preview if
|
| 775 |
-
you experience any issues.
|
| 776 |
-
</span>
|
| 777 |
-
</div>
|
| 778 |
-
<button
|
| 779 |
-
onClick={refreshIframe}
|
| 780 |
-
className="cursor-pointer text-xs px-3 py-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md font-medium transition-colors whitespace-nowrap flex items-center gap-1.5"
|
| 781 |
-
>
|
| 782 |
-
<RefreshCcw className="size-3 text-neutral-300 flex-shrink-0" />
|
| 783 |
-
Refresh
|
| 784 |
-
</button>
|
| 785 |
-
</div>
|
| 786 |
-
)}
|
| 787 |
<iframe
|
| 788 |
-
key={iframeKey}
|
| 789 |
id="preview-iframe"
|
| 790 |
ref={iframeRef}
|
| 791 |
className={classNames(
|
|
@@ -796,45 +493,37 @@ export const Preview = ({
|
|
| 796 |
}
|
| 797 |
)}
|
| 798 |
src={
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
!hasUnsavedChanges &&
|
| 802 |
-
project?.space_id &&
|
| 803 |
-
!project?.private
|
| 804 |
-
? `https://${project.space_id.replaceAll(
|
| 805 |
"/",
|
| 806 |
"-"
|
| 807 |
-
)}.static.hf.space`
|
| 808 |
: undefined
|
| 809 |
}
|
| 810 |
srcDoc={
|
| 811 |
-
currentCommit
|
| 812 |
-
? commitPages.length > 0 && previewPageData?.html
|
| 813 |
-
? injectAssetsIntoHtml(previewPageData.html, commitPages)
|
| 814 |
-
: defaultHTML
|
| 815 |
-
: isNew || hasUnsavedChanges || project?.private
|
| 816 |
? isNew
|
| 817 |
? throttledHtml || defaultHTML
|
| 818 |
: stableHtml
|
| 819 |
: undefined
|
| 820 |
}
|
| 821 |
-
onLoad={
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
}
|
| 838 |
sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals allow-forms"
|
| 839 |
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"
|
| 840 |
/>
|
|
|
|
| 15 |
import { HistoryNotification } from "../history-notification";
|
| 16 |
import { api } from "@/lib/api";
|
| 17 |
import { toast } from "sonner";
|
| 18 |
+
|
| 19 |
+
export const Preview = ({ isNew }: { isNew: boolean }) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
const {
|
| 21 |
project,
|
| 22 |
device,
|
|
|
|
| 30 |
setCurrentPage,
|
| 31 |
previewPage,
|
| 32 |
setPreviewPage,
|
|
|
|
|
|
|
| 33 |
} = useEditor();
|
| 34 |
const {
|
| 35 |
isEditableModeEnabled,
|
|
|
|
| 48 |
const [stableHtml, setStableHtml] = useState<string>("");
|
| 49 |
const [throttledHtml, setThrottledHtml] = useState<string>("");
|
| 50 |
const lastUpdateTimeRef = useRef<number>(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
useEffect(() => {
|
| 53 |
if (!previewPage && pages.length > 0) {
|
|
|
|
| 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
|
|
|
|
| 102 |
`<head>\n${allCssContent}`
|
| 103 |
);
|
| 104 |
} else {
|
| 105 |
+
// If no head tag, prepend to document
|
| 106 |
modifiedHtml = allCssContent + "\n" + modifiedHtml;
|
| 107 |
}
|
| 108 |
|
| 109 |
+
// Remove all link tags that reference CSS files we're injecting
|
| 110 |
cssFiles.forEach((file) => {
|
| 111 |
const escapedPath = file.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
| 112 |
modifiedHtml = modifiedHtml.replace(
|
|
|
|
| 119 |
});
|
| 120 |
}
|
| 121 |
|
| 122 |
+
// Inject all JS files
|
| 123 |
if (jsFiles.length > 0) {
|
| 124 |
const allJsContent = jsFiles
|
| 125 |
.map(
|
|
|
|
| 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(
|
|
|
|
| 153 |
});
|
| 154 |
}
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
return modifiedHtml;
|
| 157 |
},
|
| 158 |
+
[pages, previewPageData?.path]
|
| 159 |
);
|
| 160 |
|
| 161 |
useEffect(() => {
|
|
|
|
| 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 |
}, [
|
|
|
|
| 190 |
previewPageData?.html,
|
| 191 |
injectAssetsIntoHtml,
|
| 192 |
previewPage,
|
|
|
|
| 193 |
]);
|
| 194 |
|
| 195 |
useEffect(() => {
|
|
|
|
| 199 |
!isAiWorking &&
|
| 200 |
!globalAiLoading
|
| 201 |
) {
|
| 202 |
+
const processedHtml = injectAssetsIntoHtml(previewPageData.html);
|
|
|
|
|
|
|
|
|
|
| 203 |
setStableHtml(processedHtml);
|
| 204 |
}
|
| 205 |
}, [
|
|
|
|
| 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,
|
|
|
|
| 229 |
}
|
| 230 |
};
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
useEffect(() => {
|
| 233 |
const cleanupListeners = () => {
|
| 234 |
if (iframeRef?.current?.contentDocument) {
|
|
|
|
| 257 |
};
|
| 258 |
}, [isEditableModeEnabled, stableHtml, throttledHtml, previewPage]);
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
const promoteVersion = async () => {
|
| 261 |
setIsPromotingVersion(true);
|
| 262 |
await api
|
|
|
|
| 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 |
}
|
|
|
|
| 320 |
if (iframeRef?.current) {
|
| 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 |
};
|
|
|
|
| 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 |
}
|
|
|
|
| 424 |
normalizedHref = normalizedHref + ".html";
|
| 425 |
}
|
| 426 |
|
| 427 |
+
const isPageExist = pages.some((page) => {
|
| 428 |
const pagePath = page.path.replace(/^\.?\//, "");
|
| 429 |
return pagePath === normalizedHref;
|
| 430 |
});
|
|
|
|
| 482 |
</div>
|
| 483 |
) : (
|
| 484 |
<>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
<iframe
|
|
|
|
| 486 |
id="preview-iframe"
|
| 487 |
ref={iframeRef}
|
| 488 |
className={classNames(
|
|
|
|
| 493 |
}
|
| 494 |
)}
|
| 495 |
src={
|
| 496 |
+
currentCommit
|
| 497 |
+
? `https://${project?.space_id?.replaceAll(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
"/",
|
| 499 |
"-"
|
| 500 |
+
)}--rev-${currentCommit.slice(0, 7)}.static.hf.space`
|
| 501 |
: undefined
|
| 502 |
}
|
| 503 |
srcDoc={
|
| 504 |
+
!currentCommit
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
? isNew
|
| 506 |
? throttledHtml || defaultHTML
|
| 507 |
: stableHtml
|
| 508 |
: undefined
|
| 509 |
}
|
| 510 |
+
onLoad={
|
| 511 |
+
!currentCommit
|
| 512 |
+
? () => {
|
| 513 |
+
if (iframeRef?.current?.contentWindow?.document?.body) {
|
| 514 |
+
iframeRef.current.contentWindow.document.body.scrollIntoView(
|
| 515 |
+
{
|
| 516 |
+
block: isAiWorking ? "end" : "start",
|
| 517 |
+
inline: "nearest",
|
| 518 |
+
behavior: isAiWorking ? "instant" : "smooth",
|
| 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 |
/>
|
components/icons/discord.tsx
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
export const DiscordIcon = ({ className }: { className?: string }) => {
|
| 2 |
-
return (
|
| 3 |
-
<svg
|
| 4 |
-
width="1em"
|
| 5 |
-
height="1em"
|
| 6 |
-
viewBox="0 0 127 96"
|
| 7 |
-
fill="none"
|
| 8 |
-
aria-hidden="true"
|
| 9 |
-
focusable="false"
|
| 10 |
-
preserveAspectRatio="xMidYMid meet"
|
| 11 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 12 |
-
className={className}
|
| 13 |
-
>
|
| 14 |
-
<g clipPath="url(#clip0_1084_3121)">
|
| 15 |
-
<path
|
| 16 |
-
d="M81.15 0C79.9124 2.1973 78.8011 4.4704 77.7909 6.794C68.1934 5.3544 58.4191 5.3544 48.7964 6.794C47.8114 4.4704 46.6748 2.1973 45.4373 0C36.4207 1.5407 27.6314 4.2431 19.2968 8.0568C2.77901 32.5304 -1.69139 56.3725 0.531208 79.8863C10.2044 87.0339 21.0395 92.4893 32.5817 95.9747C35.1831 92.4893 37.4815 88.7766 39.4515 84.9124C35.7135 83.5233 32.1018 81.7806 28.6417 79.7601C29.5509 79.1034 30.4349 78.4215 31.2936 77.7648C51.5746 87.3118 75.0632 87.3118 95.3694 77.7648C96.2281 78.472 97.1121 79.1539 98.0213 79.7601C94.5612 81.8058 90.9495 83.5233 87.1863 84.9377C89.1563 88.8019 91.4546 92.5146 94.0561 96C105.598 92.5146 116.433 87.0844 126.107 79.9369C128.733 52.6598 121.611 29.0197 107.29 8.0821C98.9811 4.2684 90.1918 1.5659 81.1752 0.0505L81.15 0ZM42.2802 65.4144C36.0419 65.4144 30.8643 59.7569 30.8643 52.7609C30.8643 45.7649 35.8398 40.0821 42.255 40.0821C48.6702 40.0821 53.7719 45.7901 53.6709 52.7609C53.5699 59.7317 48.6449 65.4144 42.2802 65.4144ZM84.3576 65.4144C78.0939 65.4144 72.9669 59.7569 72.9669 52.7609C72.9669 45.7649 77.9424 40.0821 84.3576 40.0821C90.7728 40.0821 95.8493 45.7901 95.7482 52.7609C95.6472 59.7317 90.7222 65.4144 84.3576 65.4144Z"
|
| 17 |
-
fill="currentColor"
|
| 18 |
-
/>
|
| 19 |
-
</g>
|
| 20 |
-
<defs>
|
| 21 |
-
<clipPath id="clip0_1084_3121">
|
| 22 |
-
<rect width="126.644" height="96" fill="currentColor" />
|
| 23 |
-
</clipPath>
|
| 24 |
-
</defs>
|
| 25 |
-
</svg>
|
| 26 |
-
);
|
| 27 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/my-projects/project-card.tsx
CHANGED
|
@@ -1,12 +1,6 @@
|
|
| 1 |
import Link from "next/link";
|
| 2 |
import { formatDistance } from "date-fns";
|
| 3 |
-
import {
|
| 4 |
-
Download,
|
| 5 |
-
EllipsisVertical,
|
| 6 |
-
Lock,
|
| 7 |
-
Settings,
|
| 8 |
-
Trash,
|
| 9 |
-
} from "lucide-react";
|
| 10 |
|
| 11 |
import { Button } from "@/components/ui/button";
|
| 12 |
import {
|
|
@@ -17,8 +11,6 @@ import {
|
|
| 17 |
DropdownMenuTrigger,
|
| 18 |
} from "@/components/ui/dropdown-menu";
|
| 19 |
import { ProjectType } from "@/types";
|
| 20 |
-
import { toast } from "sonner";
|
| 21 |
-
import { useUser } from "@/hooks/useUser";
|
| 22 |
|
| 23 |
// from-red-500 to-red-500
|
| 24 |
// from-yellow-500 to-yellow-500
|
|
@@ -36,7 +28,6 @@ export function ProjectCard({
|
|
| 36 |
project: ProjectType;
|
| 37 |
onDelete: () => void;
|
| 38 |
}) {
|
| 39 |
-
const { token } = useUser();
|
| 40 |
const handleDelete = () => {
|
| 41 |
if (
|
| 42 |
confirm(
|
|
@@ -46,46 +37,6 @@ export function ProjectCard({
|
|
| 46 |
onDelete();
|
| 47 |
}
|
| 48 |
};
|
| 49 |
-
|
| 50 |
-
const handleDownload = async () => {
|
| 51 |
-
try {
|
| 52 |
-
toast.info("Preparing download...");
|
| 53 |
-
const response = await fetch(
|
| 54 |
-
`/deepsite/api/me/projects/${project.name}/download`,
|
| 55 |
-
{
|
| 56 |
-
credentials: "include",
|
| 57 |
-
headers: {
|
| 58 |
-
Accept: "application/zip",
|
| 59 |
-
Authorization: `Bearer ${token}`,
|
| 60 |
-
},
|
| 61 |
-
}
|
| 62 |
-
);
|
| 63 |
-
|
| 64 |
-
if (!response.ok) {
|
| 65 |
-
const error = await response
|
| 66 |
-
.json()
|
| 67 |
-
.catch(() => ({ error: "Download failed" }));
|
| 68 |
-
toast.error(error.error || "Failed to download project");
|
| 69 |
-
return;
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
const blob = await response.blob();
|
| 73 |
-
|
| 74 |
-
const url = window.URL.createObjectURL(blob);
|
| 75 |
-
const link = document.createElement("a");
|
| 76 |
-
link.href = url;
|
| 77 |
-
link.download = `${project.name.replace(/\//g, "-")}.zip`;
|
| 78 |
-
document.body.appendChild(link);
|
| 79 |
-
link.click();
|
| 80 |
-
document.body.removeChild(link);
|
| 81 |
-
window.URL.revokeObjectURL(url);
|
| 82 |
-
|
| 83 |
-
toast.success("Download started!");
|
| 84 |
-
} catch (error) {
|
| 85 |
-
console.error("Download error:", error);
|
| 86 |
-
toast.error("Failed to download project");
|
| 87 |
-
}
|
| 88 |
-
};
|
| 89 |
// from-gray-600 to-gray-600
|
| 90 |
// from-blue-600 to-blue-600
|
| 91 |
// from-green-600 to-green-600
|
|
@@ -169,10 +120,6 @@ export function ProjectCard({
|
|
| 169 |
Project Settings
|
| 170 |
</DropdownMenuItem>
|
| 171 |
</a>
|
| 172 |
-
<DropdownMenuItem onClick={handleDownload}>
|
| 173 |
-
<Download className="size-4 text-neutral-100" />
|
| 174 |
-
Download as ZIP
|
| 175 |
-
</DropdownMenuItem>
|
| 176 |
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
| 177 |
<Trash className="size-4 text-red-500" />
|
| 178 |
Delete Project
|
|
|
|
| 1 |
import Link from "next/link";
|
| 2 |
import { formatDistance } from "date-fns";
|
| 3 |
+
import { EllipsisVertical, Lock, Settings, Trash } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
import { Button } from "@/components/ui/button";
|
| 6 |
import {
|
|
|
|
| 11 |
DropdownMenuTrigger,
|
| 12 |
} from "@/components/ui/dropdown-menu";
|
| 13 |
import { ProjectType } from "@/types";
|
|
|
|
|
|
|
| 14 |
|
| 15 |
// from-red-500 to-red-500
|
| 16 |
// from-yellow-500 to-yellow-500
|
|
|
|
| 28 |
project: ProjectType;
|
| 29 |
onDelete: () => void;
|
| 30 |
}) {
|
|
|
|
| 31 |
const handleDelete = () => {
|
| 32 |
if (
|
| 33 |
confirm(
|
|
|
|
| 37 |
onDelete();
|
| 38 |
}
|
| 39 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
// from-gray-600 to-gray-600
|
| 41 |
// from-blue-600 to-blue-600
|
| 42 |
// from-green-600 to-green-600
|
|
|
|
| 120 |
Project Settings
|
| 121 |
</DropdownMenuItem>
|
| 122 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
| 124 |
<Trash className="size-4 text-red-500" />
|
| 125 |
Delete Project
|
components/public/navigation/index.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import Logo from "@/assets/logo.svg";
|
|
| 12 |
import { useUser } from "@/hooks/useUser";
|
| 13 |
import { UserMenu } from "@/components/user-menu";
|
| 14 |
import { ProTag } from "@/components/pro-modal";
|
| 15 |
-
import { DiscordIcon } from "@/components/icons/discord";
|
| 16 |
|
| 17 |
const navigationLinks = [
|
| 18 |
{
|
|
@@ -97,9 +96,7 @@ export default function Navigation() {
|
|
| 97 |
width={64}
|
| 98 |
height={64}
|
| 99 |
/>
|
| 100 |
-
<p className="font-sans text-white text-xl font-bold
|
| 101 |
-
DeepSite
|
| 102 |
-
</p>
|
| 103 |
{user?.isPro && <ProTag className="ml-1" />}
|
| 104 |
</Link>
|
| 105 |
<ul className="items-center justify-center gap-6 !hidden">
|
|
@@ -144,17 +141,7 @@ export default function Navigation() {
|
|
| 144 |
<div className="size-1 bg-white rounded-full" />
|
| 145 |
</div>
|
| 146 |
</ul>
|
| 147 |
-
<div className="flex items-center justify-end gap-
|
| 148 |
-
<Link href="https://discord.gg/KpanwM3vXa" target="_blank">
|
| 149 |
-
<Button
|
| 150 |
-
variant="bordered"
|
| 151 |
-
className="!border-indigo-500 !text-white !bg-indigo-500 transition-all duration-300"
|
| 152 |
-
>
|
| 153 |
-
<DiscordIcon className="size-4 mr-0.5" />
|
| 154 |
-
<span className="max-lg:hidden">Discord Community</span>
|
| 155 |
-
<span className="lg:hidden">Discord</span>
|
| 156 |
-
</Button>
|
| 157 |
-
</Link>
|
| 158 |
{loading ? (
|
| 159 |
<Button
|
| 160 |
variant="ghostDarker"
|
|
|
|
| 12 |
import { useUser } from "@/hooks/useUser";
|
| 13 |
import { UserMenu } from "@/components/user-menu";
|
| 14 |
import { ProTag } from "@/components/pro-modal";
|
|
|
|
| 15 |
|
| 16 |
const navigationLinks = [
|
| 17 |
{
|
|
|
|
| 96 |
width={64}
|
| 97 |
height={64}
|
| 98 |
/>
|
| 99 |
+
<p className="font-sans text-white text-xl font-bold">DeepSite</p>
|
|
|
|
|
|
|
| 100 |
{user?.isPro && <ProTag className="ml-1" />}
|
| 101 |
</Link>
|
| 102 |
<ul className="items-center justify-center gap-6 !hidden">
|
|
|
|
| 141 |
<div className="size-1 bg-white rounded-full" />
|
| 142 |
</div>
|
| 143 |
</ul>
|
| 144 |
+
<div className="flex items-center justify-end gap-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
{loading ? (
|
| 146 |
<Button
|
| 147 |
variant="ghostDarker"
|
hooks/useAi.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 19 |
const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
|
| 20 |
const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
|
| 21 |
const router = useRouter();
|
| 22 |
-
const { token } = useUser();
|
| 23 |
const streamingPagesRef = useRef<Set<string>>(new Set());
|
| 24 |
|
| 25 |
const { data: isAiWorking = false } = useQuery({
|
|
@@ -44,18 +44,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 44 |
client.setQueryData(["ai.isThinking"], newIsThinking);
|
| 45 |
};
|
| 46 |
|
| 47 |
-
const { data: thinkingContent } = useQuery<string>({
|
| 48 |
-
queryKey: ["ai.thinkingContent"],
|
| 49 |
-
queryFn: async () => "",
|
| 50 |
-
refetchOnWindowFocus: false,
|
| 51 |
-
refetchOnReconnect: false,
|
| 52 |
-
refetchOnMount: false,
|
| 53 |
-
initialData: ""
|
| 54 |
-
});
|
| 55 |
-
const setThinkingContent = (newThinkingContent: string) => {
|
| 56 |
-
client.setQueryData(["ai.thinkingContent"], newThinkingContent);
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
const { data: selectedElement } = useQuery<HTMLElement | null>({
|
| 60 |
queryKey: ["ai.selectedElement"],
|
| 61 |
queryFn: async () => null,
|
|
@@ -136,37 +124,27 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 136 |
client.setQueryData(["ai.model"], newModel);
|
| 137 |
};
|
| 138 |
|
| 139 |
-
const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean
|
| 140 |
-
if (isLoggedIn
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
})
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
});
|
| 155 |
-
|
| 156 |
-
const uploadRes = await uploadRequest.json();
|
| 157 |
-
|
| 158 |
-
if (!uploadRequest.ok || !uploadRes.ok) {
|
| 159 |
-
throw new Error(uploadRes.error || "Failed to create project");
|
| 160 |
}
|
| 161 |
-
|
|
|
|
| 162 |
setIsAiWorking(false);
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
if (audio.current) audio.current.play();
|
| 166 |
-
} catch (error: any) {
|
| 167 |
-
setIsAiWorking(false);
|
| 168 |
-
toast.error(error?.message || "Failed to create project");
|
| 169 |
-
}
|
| 170 |
} else {
|
| 171 |
setIsAiWorking(false);
|
| 172 |
toast.success("AI responded successfully");
|
|
@@ -174,12 +152,11 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 174 |
}
|
| 175 |
}
|
| 176 |
|
| 177 |
-
const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean
|
| 178 |
if (isAiWorking) return;
|
| 179 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 180 |
|
| 181 |
setIsAiWorking(true);
|
| 182 |
-
setThinkingContent(""); // Reset thinking content
|
| 183 |
streamingPagesRef.current.clear(); // Reset tracking for new generation
|
| 184 |
|
| 185 |
const abortController = new AbortController();
|
|
@@ -212,14 +189,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 212 |
const { done, value } = await reader.read();
|
| 213 |
|
| 214 |
if (done) {
|
| 215 |
-
// Final processing - extract and remove thinking content
|
| 216 |
-
const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
|
| 217 |
-
if (thinkMatch) {
|
| 218 |
-
setThinkingContent(thinkMatch[1].trim());
|
| 219 |
-
setIsThinking(false);
|
| 220 |
-
contentResponse = contentResponse.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
const trimmedResponse = contentResponse.trim();
|
| 224 |
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 225 |
try {
|
|
@@ -244,12 +213,12 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 244 |
const newPages = formatPages(contentResponse, false);
|
| 245 |
let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 246 |
if (!projectName) {
|
| 247 |
-
projectName = prompt.substring(0,
|
| 248 |
}
|
| 249 |
setPages(newPages);
|
| 250 |
setLastSavedPages([...newPages]);
|
| 251 |
if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
|
| 252 |
-
createNewProject(prompt, newPages, projectName, isLoggedIn
|
| 253 |
}
|
| 254 |
setPrompts([...prompts, prompt]);
|
| 255 |
|
|
@@ -259,26 +228,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 259 |
const chunk = decoder.decode(value, { stream: true });
|
| 260 |
contentResponse += chunk;
|
| 261 |
|
| 262 |
-
// Extract thinking content while streaming
|
| 263 |
-
if (contentResponse.includes('</think>')) {
|
| 264 |
-
// Thinking is complete, extract final content and stop thinking
|
| 265 |
-
const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
|
| 266 |
-
if (thinkMatch) {
|
| 267 |
-
setThinkingContent(thinkMatch[1].trim());
|
| 268 |
-
setIsThinking(false);
|
| 269 |
-
}
|
| 270 |
-
} else if (contentResponse.includes('<think>')) {
|
| 271 |
-
// Still thinking, update content
|
| 272 |
-
const thinkMatch = contentResponse.match(/<think>([\s\S]*)$/);
|
| 273 |
-
if (thinkMatch) {
|
| 274 |
-
const thinkingText = thinkMatch[1].trim();
|
| 275 |
-
if (thinkingText) {
|
| 276 |
-
setIsThinking(true);
|
| 277 |
-
setThinkingContent(thinkingText);
|
| 278 |
-
}
|
| 279 |
-
}
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
const trimmedResponse = contentResponse.trim();
|
| 283 |
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 284 |
try {
|
|
@@ -311,7 +260,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 311 |
} catch (error: any) {
|
| 312 |
setIsAiWorking(false);
|
| 313 |
setIsThinking(false);
|
| 314 |
-
setThinkingContent("");
|
| 315 |
setController(null);
|
| 316 |
|
| 317 |
if (!abortController.signal.aborted) {
|
|
@@ -331,7 +279,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 331 |
|
| 332 |
|
| 333 |
setIsAiWorking(true);
|
| 334 |
-
setThinkingContent(""); // Reset thinking content
|
| 335 |
|
| 336 |
const abortController = new AbortController();
|
| 337 |
setController(abortController);
|
|
@@ -364,191 +311,71 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 364 |
});
|
| 365 |
|
| 366 |
if (request && request.body) {
|
| 367 |
-
const
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
const metadataMatch = contentResponse.match(/___METADATA_START___([\s\S]*?)___METADATA_END___/);
|
| 385 |
-
if (metadataMatch) {
|
| 386 |
-
try {
|
| 387 |
-
metadata = JSON.parse(metadataMatch[1]);
|
| 388 |
-
contentResponse = contentResponse.replace(/___METADATA_START___[\s\S]*?___METADATA_END___/, '').trim();
|
| 389 |
-
} catch (e) {
|
| 390 |
-
console.error("Failed to parse metadata", e);
|
| 391 |
-
}
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
const trimmedResponse = contentResponse.trim();
|
| 395 |
-
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 396 |
-
try {
|
| 397 |
-
const jsonResponse = JSON.parse(trimmedResponse);
|
| 398 |
-
if (jsonResponse && !jsonResponse.ok) {
|
| 399 |
-
setIsAiWorking(false);
|
| 400 |
-
if (jsonResponse.openLogin) {
|
| 401 |
-
return { error: "login_required" };
|
| 402 |
-
} else if (jsonResponse.openSelectProvider) {
|
| 403 |
-
return { error: "provider_required", message: jsonResponse.message };
|
| 404 |
-
} else if (jsonResponse.openProModal) {
|
| 405 |
-
return { error: "pro_required" };
|
| 406 |
-
} else {
|
| 407 |
-
toast.error(jsonResponse.message);
|
| 408 |
-
return { error: "api_error", message: jsonResponse.message };
|
| 409 |
-
}
|
| 410 |
-
}
|
| 411 |
-
} catch (e) {
|
| 412 |
-
// Not JSON, continue with normal processing
|
| 413 |
-
}
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
const { processAiResponse, extractProjectName } = await import("@/lib/format-ai-response");
|
| 417 |
-
const { updatedPages, updatedLines } = processAiResponse(contentResponse, pagesToSend);
|
| 418 |
-
|
| 419 |
-
const updatedPagesMap = new Map(updatedPages.map((p: Page) => [p.path, p]));
|
| 420 |
-
const mergedPages: Page[] = pages.map(page =>
|
| 421 |
-
updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
|
| 422 |
-
);
|
| 423 |
-
updatedPages.forEach((page: Page) => {
|
| 424 |
-
if (!pages.find(p => p.path === page.path)) {
|
| 425 |
-
mergedPages.push(page);
|
| 426 |
-
}
|
| 427 |
-
});
|
| 428 |
-
|
| 429 |
-
let projectName = null;
|
| 430 |
-
if (isNew) {
|
| 431 |
-
projectName = extractProjectName(contentResponse);
|
| 432 |
-
if (!projectName) {
|
| 433 |
-
projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40) + "-" + Math.random().toString(36).substring(2, 15);
|
| 434 |
-
}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
try {
|
| 438 |
-
const uploadRequest = await fetch(`/deepsite/api/me/projects/${metadata?.userName || 'unknown'}/${isNew ? 'new' : (project?.space_id?.split('/')[1] || 'unknown')}/update`, {
|
| 439 |
-
method: "PUT",
|
| 440 |
-
body: JSON.stringify({
|
| 441 |
-
pages: mergedPages,
|
| 442 |
-
commitTitle: prompt,
|
| 443 |
-
isNew,
|
| 444 |
-
projectName,
|
| 445 |
-
}),
|
| 446 |
-
headers: {
|
| 447 |
-
"Content-Type": "application/json",
|
| 448 |
-
"Authorization": `Bearer ${token}`,
|
| 449 |
-
},
|
| 450 |
-
});
|
| 451 |
-
|
| 452 |
-
const uploadRes = await uploadRequest.json();
|
| 453 |
-
|
| 454 |
-
if (!uploadRequest.ok || !uploadRes.ok) {
|
| 455 |
-
throw new Error(uploadRes.error || "Failed to upload to HuggingFace");
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
-
toast.success("AI responded successfully");
|
| 459 |
-
const iframe = document.getElementById("preview-iframe") as HTMLIFrameElement;
|
| 460 |
-
|
| 461 |
-
if (isNew && uploadRes.repoId) {
|
| 462 |
-
router.push(`/${uploadRes.repoId}`);
|
| 463 |
-
setIsAiWorking(false);
|
| 464 |
-
} else {
|
| 465 |
-
setPages(mergedPages);
|
| 466 |
-
setLastSavedPages([...mergedPages]);
|
| 467 |
-
setCommits([uploadRes.commit, ...commits]);
|
| 468 |
-
setPrompts([...prompts, prompt]);
|
| 469 |
-
setSelectedElement(null);
|
| 470 |
-
setSelectedFiles([]);
|
| 471 |
-
setIsEditableModeEnabled(false);
|
| 472 |
-
setIsAiWorking(false);
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
if (audio.current) audio.current.play();
|
| 476 |
-
if (iframe) {
|
| 477 |
-
setTimeout(() => {
|
| 478 |
-
iframe.src = iframe.src;
|
| 479 |
-
}, 500);
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
return { success: true, updatedLines };
|
| 483 |
-
} catch (uploadError: any) {
|
| 484 |
-
setIsAiWorking(false);
|
| 485 |
-
toast.error(uploadError.message || "Failed to save changes");
|
| 486 |
-
return { error: "upload_error", message: uploadError.message };
|
| 487 |
-
}
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
-
const chunk = decoder.decode(value, { stream: true });
|
| 491 |
-
contentResponse += chunk;
|
| 492 |
-
|
| 493 |
-
// Extract thinking content while streaming
|
| 494 |
-
if (contentResponse.includes('</think>')) {
|
| 495 |
-
// Thinking is complete, extract final content and stop thinking
|
| 496 |
-
const thinkMatch = contentResponse.match(/<think>([\s\S]*?)<\/think>/);
|
| 497 |
-
if (thinkMatch) {
|
| 498 |
-
setThinkingContent(thinkMatch[1].trim());
|
| 499 |
-
setIsThinking(false);
|
| 500 |
-
}
|
| 501 |
-
} else if (contentResponse.includes('<think>')) {
|
| 502 |
-
// Still thinking, update content
|
| 503 |
-
const thinkMatch = contentResponse.match(/<think>([\s\S]*)$/);
|
| 504 |
-
if (thinkMatch) {
|
| 505 |
-
const thinkingText = thinkMatch[1].trim();
|
| 506 |
-
if (thinkingText) {
|
| 507 |
-
setIsThinking(true);
|
| 508 |
-
setThinkingContent(thinkingText);
|
| 509 |
-
}
|
| 510 |
-
}
|
| 511 |
}
|
|
|
|
| 512 |
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
}
|
| 531 |
-
} catch (e) {
|
| 532 |
-
// Not complete JSON yet, continue
|
| 533 |
}
|
| 534 |
-
}
|
| 535 |
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
}
|
| 541 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 542 |
} catch (error: any) {
|
| 543 |
setIsAiWorking(false);
|
| 544 |
-
|
| 545 |
-
setThinkingContent("");
|
| 546 |
-
setController(null);
|
| 547 |
-
|
| 548 |
-
if (!abortController.signal.aborted) {
|
| 549 |
-
toast.error(error.message || "Network error occurred");
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
if (error.openLogin) {
|
| 553 |
return { error: "login_required" };
|
| 554 |
}
|
|
@@ -596,6 +423,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 596 |
if (pages.length > 0) {
|
| 597 |
setPages(pages);
|
| 598 |
if (isStreaming) {
|
|
|
|
| 599 |
const newPages = pages.filter(p =>
|
| 600 |
!streamingPagesRef.current.has(p.path)
|
| 601 |
);
|
|
@@ -605,6 +433,7 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 605 |
setCurrentPage(newPage.path);
|
| 606 |
streamingPagesRef.current.add(newPage.path);
|
| 607 |
|
|
|
|
| 608 |
if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
|
| 609 |
setPreviewPage(newPage.path);
|
| 610 |
}
|
|
@@ -624,30 +453,41 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 624 |
const extractFileContent = (chunk: string, filePath: string): string => {
|
| 625 |
if (!chunk) return "";
|
| 626 |
|
|
|
|
| 627 |
let content = chunk.trim();
|
| 628 |
|
|
|
|
| 629 |
if (filePath.endsWith('.css')) {
|
|
|
|
| 630 |
const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
|
| 631 |
if (cssMatch) {
|
| 632 |
content = cssMatch[1];
|
| 633 |
} else {
|
|
|
|
| 634 |
content = content.replace(/^```css\s*/i, "");
|
| 635 |
}
|
|
|
|
| 636 |
return content.replace(/```/g, "").trim();
|
| 637 |
} else if (filePath.endsWith('.js')) {
|
|
|
|
| 638 |
const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
|
| 639 |
if (jsMatch) {
|
| 640 |
content = jsMatch[1];
|
| 641 |
} else {
|
|
|
|
| 642 |
content = content.replace(/^```(?:javascript|js)\s*/i, "");
|
| 643 |
}
|
|
|
|
| 644 |
return content.replace(/```/g, "").trim();
|
| 645 |
} else {
|
|
|
|
| 646 |
const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
|
| 647 |
if (htmlMatch) {
|
| 648 |
content = htmlMatch[1];
|
| 649 |
} else {
|
|
|
|
| 650 |
content = content.replace(/^```html\s*/i, "");
|
|
|
|
| 651 |
const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
|
| 652 |
if (doctypeMatch) {
|
| 653 |
content = doctypeMatch[0];
|
|
@@ -690,8 +530,6 @@ export const useAi = (onScrollToBottom?: () => void) => {
|
|
| 690 |
return {
|
| 691 |
isThinking,
|
| 692 |
setIsThinking,
|
| 693 |
-
thinkingContent,
|
| 694 |
-
setThinkingContent,
|
| 695 |
callAiNewProject,
|
| 696 |
callAiFollowUp,
|
| 697 |
isAiWorking,
|
|
|
|
| 19 |
const [storageProvider, setStorageProvider] = useLocalStorage("provider", "auto");
|
| 20 |
const [storageModel, setStorageModel] = useLocalStorage("model", MODELS[0].value);
|
| 21 |
const router = useRouter();
|
| 22 |
+
const { projects, setProjects, token } = useUser();
|
| 23 |
const streamingPagesRef = useRef<Set<string>>(new Set());
|
| 24 |
|
| 25 |
const { data: isAiWorking = false } = useQuery({
|
|
|
|
| 44 |
client.setQueryData(["ai.isThinking"], newIsThinking);
|
| 45 |
};
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const { data: selectedElement } = useQuery<HTMLElement | null>({
|
| 48 |
queryKey: ["ai.selectedElement"],
|
| 49 |
queryFn: async () => null,
|
|
|
|
| 124 |
client.setQueryData(["ai.model"], newModel);
|
| 125 |
};
|
| 126 |
|
| 127 |
+
const createNewProject = async (prompt: string, htmlPages: Page[], projectName: string | undefined, isLoggedIn?: boolean) => {
|
| 128 |
+
if (isLoggedIn) {
|
| 129 |
+
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");
|
|
|
|
| 152 |
}
|
| 153 |
}
|
| 154 |
|
| 155 |
+
const callAiNewProject = async (prompt: string, enhancedSettings?: EnhancedSettings, redesignMarkdown?: string, isLoggedIn?: boolean) => {
|
| 156 |
if (isAiWorking) return;
|
| 157 |
if (!redesignMarkdown && !prompt.trim()) return;
|
| 158 |
|
| 159 |
setIsAiWorking(true);
|
|
|
|
| 160 |
streamingPagesRef.current.clear(); // Reset tracking for new generation
|
| 161 |
|
| 162 |
const abortController = new AbortController();
|
|
|
|
| 189 |
const { done, value } = await reader.read();
|
| 190 |
|
| 191 |
if (done) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
const trimmedResponse = contentResponse.trim();
|
| 193 |
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 194 |
try {
|
|
|
|
| 213 |
const newPages = formatPages(contentResponse, false);
|
| 214 |
let projectName = contentResponse.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 215 |
if (!projectName) {
|
| 216 |
+
projectName = prompt.substring(0, 40).replace(/[^a-zA-Z0-9]/g, "-").slice(0, 40);
|
| 217 |
}
|
| 218 |
setPages(newPages);
|
| 219 |
setLastSavedPages([...newPages]);
|
| 220 |
if (newPages.length > 0 && !isTheSameHtml(newPages[0].html)) {
|
| 221 |
+
createNewProject(prompt, newPages, projectName, isLoggedIn);
|
| 222 |
}
|
| 223 |
setPrompts([...prompts, prompt]);
|
| 224 |
|
|
|
|
| 228 |
const chunk = decoder.decode(value, { stream: true });
|
| 229 |
contentResponse += chunk;
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
const trimmedResponse = contentResponse.trim();
|
| 232 |
if (trimmedResponse.startsWith("{") && trimmedResponse.endsWith("}")) {
|
| 233 |
try {
|
|
|
|
| 260 |
} catch (error: any) {
|
| 261 |
setIsAiWorking(false);
|
| 262 |
setIsThinking(false);
|
|
|
|
| 263 |
setController(null);
|
| 264 |
|
| 265 |
if (!abortController.signal.aborted) {
|
|
|
|
| 279 |
|
| 280 |
|
| 281 |
setIsAiWorking(true);
|
|
|
|
| 282 |
|
| 283 |
const abortController = new AbortController();
|
| 284 |
setController(abortController);
|
|
|
|
| 311 |
});
|
| 312 |
|
| 313 |
if (request && request.body) {
|
| 314 |
+
const res = await request.json();
|
| 315 |
+
|
| 316 |
+
if (!request.ok) {
|
| 317 |
+
if (res.openLogin) {
|
| 318 |
+
setIsAiWorking(false);
|
| 319 |
+
return { error: "login_required" };
|
| 320 |
+
} else if (res.openSelectProvider) {
|
| 321 |
+
setIsAiWorking(false);
|
| 322 |
+
return { error: "provider_required", message: res.message };
|
| 323 |
+
} else if (res.openProModal) {
|
| 324 |
+
setIsAiWorking(false);
|
| 325 |
+
return { error: "pro_required" };
|
| 326 |
+
} else {
|
| 327 |
+
toast.error(res.message);
|
| 328 |
+
setIsAiWorking(false);
|
| 329 |
+
return { error: "api_error", message: res.message };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
}
|
| 331 |
+
}
|
| 332 |
|
| 333 |
+
toast.success("AI responded successfully");
|
| 334 |
+
const iframe = document.getElementById(
|
| 335 |
+
"preview-iframe"
|
| 336 |
+
) as HTMLIFrameElement;
|
| 337 |
+
|
| 338 |
+
if (isNew && res.repoId) {
|
| 339 |
+
router.push(`/${res.repoId}`);
|
| 340 |
+
setIsAiWorking(false);
|
| 341 |
+
} else {
|
| 342 |
+
const returnedPages = res.pages as Page[];
|
| 343 |
+
const updatedPagesMap = new Map(returnedPages.map((p: Page) => [p.path, p]));
|
| 344 |
+
const mergedPages: Page[] = pages.map(page =>
|
| 345 |
+
updatedPagesMap.has(page.path) ? updatedPagesMap.get(page.path)! : page
|
| 346 |
+
);
|
| 347 |
+
returnedPages.forEach((page: Page) => {
|
| 348 |
+
if (!pages.find(p => p.path === page.path)) {
|
| 349 |
+
mergedPages.push(page);
|
|
|
|
|
|
|
|
|
|
| 350 |
}
|
| 351 |
+
});
|
| 352 |
|
| 353 |
+
setPages(mergedPages);
|
| 354 |
+
setLastSavedPages([...mergedPages]);
|
| 355 |
+
setCommits([res.commit, ...commits]);
|
| 356 |
+
setPrompts(
|
| 357 |
+
[...prompts, prompt]
|
| 358 |
+
)
|
| 359 |
+
setSelectedElement(null);
|
| 360 |
+
setSelectedFiles([]);
|
| 361 |
+
// setContextFile(null); not needed yet, keep context for the next request.
|
| 362 |
+
setIsEditableModeEnabled(false);
|
| 363 |
+
setIsAiWorking(false);
|
| 364 |
+
}
|
| 365 |
|
| 366 |
+
if (audio.current) audio.current.play();
|
| 367 |
+
if (iframe) {
|
| 368 |
+
setTimeout(() => {
|
| 369 |
+
iframe.src = iframe.src;
|
| 370 |
+
}, 500);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
return { success: true, html: res.html, updatedLines: res.updatedLines };
|
| 374 |
}
|
| 375 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 376 |
} catch (error: any) {
|
| 377 |
setIsAiWorking(false);
|
| 378 |
+
toast.error(error.message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
if (error.openLogin) {
|
| 380 |
return { error: "login_required" };
|
| 381 |
}
|
|
|
|
| 423 |
if (pages.length > 0) {
|
| 424 |
setPages(pages);
|
| 425 |
if (isStreaming) {
|
| 426 |
+
// Find new pages that haven't been shown yet (HTML, CSS, JS, etc.)
|
| 427 |
const newPages = pages.filter(p =>
|
| 428 |
!streamingPagesRef.current.has(p.path)
|
| 429 |
);
|
|
|
|
| 433 |
setCurrentPage(newPage.path);
|
| 434 |
streamingPagesRef.current.add(newPage.path);
|
| 435 |
|
| 436 |
+
// Update preview if it's an HTML file not in components folder
|
| 437 |
if (newPage.path.endsWith('.html') && !newPage.path.includes('/components/')) {
|
| 438 |
setPreviewPage(newPage.path);
|
| 439 |
}
|
|
|
|
| 453 |
const extractFileContent = (chunk: string, filePath: string): string => {
|
| 454 |
if (!chunk) return "";
|
| 455 |
|
| 456 |
+
// Remove backticks first
|
| 457 |
let content = chunk.trim();
|
| 458 |
|
| 459 |
+
// Handle different file types
|
| 460 |
if (filePath.endsWith('.css')) {
|
| 461 |
+
// Try to extract CSS from complete code blocks first
|
| 462 |
const cssMatch = content.match(/```css\s*([\s\S]*?)\s*```/);
|
| 463 |
if (cssMatch) {
|
| 464 |
content = cssMatch[1];
|
| 465 |
} else {
|
| 466 |
+
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 467 |
content = content.replace(/^```css\s*/i, "");
|
| 468 |
}
|
| 469 |
+
// Remove any remaining backticks
|
| 470 |
return content.replace(/```/g, "").trim();
|
| 471 |
} else if (filePath.endsWith('.js')) {
|
| 472 |
+
// Try to extract JavaScript from complete code blocks first
|
| 473 |
const jsMatch = content.match(/```(?:javascript|js)\s*([\s\S]*?)\s*```/);
|
| 474 |
if (jsMatch) {
|
| 475 |
content = jsMatch[1];
|
| 476 |
} else {
|
| 477 |
+
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 478 |
content = content.replace(/^```(?:javascript|js)\s*/i, "");
|
| 479 |
}
|
| 480 |
+
// Remove any remaining backticks
|
| 481 |
return content.replace(/```/g, "").trim();
|
| 482 |
} else {
|
| 483 |
+
// Handle HTML files
|
| 484 |
const htmlMatch = content.match(/```html\s*([\s\S]*?)\s*```/);
|
| 485 |
if (htmlMatch) {
|
| 486 |
content = htmlMatch[1];
|
| 487 |
} else {
|
| 488 |
+
// Handle incomplete code blocks during streaming (remove opening fence)
|
| 489 |
content = content.replace(/^```html\s*/i, "");
|
| 490 |
+
// Try to find HTML starting with DOCTYPE
|
| 491 |
const doctypeMatch = content.match(/<!DOCTYPE html>[\s\S]*/);
|
| 492 |
if (doctypeMatch) {
|
| 493 |
content = doctypeMatch[0];
|
|
|
|
| 530 |
return {
|
| 531 |
isThinking,
|
| 532 |
setIsThinking,
|
|
|
|
|
|
|
| 533 |
callAiNewProject,
|
| 534 |
callAiFollowUp,
|
| 535 |
isAiWorking,
|
lib/best-provider.ts
CHANGED
|
@@ -3,13 +3,18 @@ export const getBestProvider = async (model: string, provider?: string) => {
|
|
| 3 |
const { data } = await response.json()
|
| 4 |
let bestProvider = null;
|
| 5 |
if (provider === "auto") {
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
} else {
|
| 8 |
const providerData = data.providers.find((p: any) => p.provider === provider)
|
| 9 |
if (providerData?.status === "live") {
|
| 10 |
-
bestProvider = providerData
|
| 11 |
} else {
|
| 12 |
-
bestProvider = "
|
| 13 |
}
|
| 14 |
}
|
| 15 |
|
|
|
|
| 3 |
const { data } = await response.json()
|
| 4 |
let bestProvider = null;
|
| 5 |
if (provider === "auto") {
|
| 6 |
+
const sortedProviders = data.providers.sort((a: any, b: any) => {
|
| 7 |
+
if (a.status === "live" && b.status !== "live") return -1
|
| 8 |
+
if (a.status !== "live" && b.status === "live") return 1
|
| 9 |
+
return a?.pricing?.output - b?.pricing?.output + a?.pricing?.input - b?.pricing?.input
|
| 10 |
+
})
|
| 11 |
+
bestProvider = sortedProviders[0]
|
| 12 |
} else {
|
| 13 |
const providerData = data.providers.find((p: any) => p.provider === provider)
|
| 14 |
if (providerData?.status === "live") {
|
| 15 |
+
bestProvider = providerData
|
| 16 |
} else {
|
| 17 |
+
bestProvider = data.providers?.find((p: any) => p.status === "live")
|
| 18 |
}
|
| 19 |
}
|
| 20 |
|
lib/format-ai-response.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
| 1 |
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 2 |
-
import { Page } from "@/types";
|
| 3 |
-
import {
|
| 4 |
-
DIVIDER,
|
| 5 |
-
NEW_FILE_END,
|
| 6 |
-
NEW_FILE_START,
|
| 7 |
-
REPLACE_END,
|
| 8 |
-
SEARCH_START,
|
| 9 |
-
UPDATE_FILE_END,
|
| 10 |
-
UPDATE_FILE_START,
|
| 11 |
-
} from "./prompts";
|
| 12 |
-
|
| 13 |
-
/**
|
| 14 |
-
* Escape special regex characters in a string
|
| 15 |
-
*/
|
| 16 |
-
export const escapeRegExp = (string: string): string => {
|
| 17 |
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
/**
|
| 21 |
-
* Create a flexible HTML regex that accounts for whitespace variations
|
| 22 |
-
*/
|
| 23 |
-
export const createFlexibleHtmlRegex = (searchBlock: string): RegExp => {
|
| 24 |
-
let searchRegex = escapeRegExp(searchBlock)
|
| 25 |
-
.replace(/\s+/g, '\\s*')
|
| 26 |
-
.replace(/>\s*</g, '>\\s*<')
|
| 27 |
-
.replace(/\s*>/g, '\\s*>');
|
| 28 |
-
|
| 29 |
-
return new RegExp(searchRegex, 'g');
|
| 30 |
-
};
|
| 31 |
-
|
| 32 |
-
/**
|
| 33 |
-
* Process AI response chunk and apply updates to pages
|
| 34 |
-
* Returns updated pages and updated line numbers
|
| 35 |
-
*/
|
| 36 |
-
export interface ProcessAiResponseResult {
|
| 37 |
-
updatedPages: Page[];
|
| 38 |
-
updatedLines: number[][];
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
export const processAiResponse = (
|
| 42 |
-
chunk: string,
|
| 43 |
-
pages: Page[]
|
| 44 |
-
): ProcessAiResponseResult => {
|
| 45 |
-
const updatedLines: number[][] = [];
|
| 46 |
-
const updatedPages = [...pages];
|
| 47 |
-
|
| 48 |
-
// Process UPDATE_FILE blocks
|
| 49 |
-
const updateFileRegex = new RegExp(
|
| 50 |
-
`${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${UPDATE_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`,
|
| 51 |
-
'g'
|
| 52 |
-
);
|
| 53 |
-
let updateFileMatch;
|
| 54 |
-
|
| 55 |
-
while ((updateFileMatch = updateFileRegex.exec(chunk)) !== null) {
|
| 56 |
-
const [, filePath, fileContent] = updateFileMatch;
|
| 57 |
-
|
| 58 |
-
const pageIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 59 |
-
if (pageIndex !== -1) {
|
| 60 |
-
let pageHtml = updatedPages[pageIndex].html;
|
| 61 |
-
|
| 62 |
-
let processedContent = fileContent;
|
| 63 |
-
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 64 |
-
if (htmlMatch) {
|
| 65 |
-
processedContent = htmlMatch[1];
|
| 66 |
-
}
|
| 67 |
-
let position = 0;
|
| 68 |
-
let moreBlocks = true;
|
| 69 |
-
|
| 70 |
-
while (moreBlocks) {
|
| 71 |
-
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
| 72 |
-
if (searchStartIndex === -1) {
|
| 73 |
-
moreBlocks = false;
|
| 74 |
-
continue;
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
| 78 |
-
if (dividerIndex === -1) {
|
| 79 |
-
moreBlocks = false;
|
| 80 |
-
continue;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
| 84 |
-
if (replaceEndIndex === -1) {
|
| 85 |
-
moreBlocks = false;
|
| 86 |
-
continue;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
const searchBlock = processedContent.substring(
|
| 90 |
-
searchStartIndex + SEARCH_START.length,
|
| 91 |
-
dividerIndex
|
| 92 |
-
);
|
| 93 |
-
const replaceBlock = processedContent.substring(
|
| 94 |
-
dividerIndex + DIVIDER.length,
|
| 95 |
-
replaceEndIndex
|
| 96 |
-
);
|
| 97 |
-
|
| 98 |
-
if (searchBlock.trim() === "") {
|
| 99 |
-
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
| 100 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 101 |
-
} else {
|
| 102 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 103 |
-
const match = regex.exec(pageHtml);
|
| 104 |
-
|
| 105 |
-
if (match) {
|
| 106 |
-
const matchedText = match[0];
|
| 107 |
-
const beforeText = pageHtml.substring(0, match.index);
|
| 108 |
-
const startLineNumber = beforeText.split("\n").length;
|
| 109 |
-
const replaceLines = replaceBlock.split("\n").length;
|
| 110 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 111 |
-
|
| 112 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
| 113 |
-
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
| 114 |
-
}
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
position = replaceEndIndex + REPLACE_END.length;
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
updatedPages[pageIndex].html = pageHtml;
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
// Process NEW_FILE blocks
|
| 125 |
-
const newFileRegex = new RegExp(
|
| 126 |
-
`${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s]+)\\s*${NEW_FILE_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)(?=${UPDATE_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${NEW_FILE_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|$)`,
|
| 127 |
-
'g'
|
| 128 |
-
);
|
| 129 |
-
let newFileMatch;
|
| 130 |
-
|
| 131 |
-
while ((newFileMatch = newFileRegex.exec(chunk)) !== null) {
|
| 132 |
-
const [, filePath, fileContent] = newFileMatch;
|
| 133 |
-
|
| 134 |
-
let fileData = fileContent;
|
| 135 |
-
// Try to extract content from code blocks
|
| 136 |
-
const htmlMatch = fileContent.match(/```html\s*([\s\S]*?)\s*```/);
|
| 137 |
-
const cssMatch = fileContent.match(/```css\s*([\s\S]*?)\s*```/);
|
| 138 |
-
const jsMatch = fileContent.match(/```javascript\s*([\s\S]*?)\s*```/);
|
| 139 |
-
|
| 140 |
-
if (htmlMatch) {
|
| 141 |
-
fileData = htmlMatch[1];
|
| 142 |
-
} else if (cssMatch) {
|
| 143 |
-
fileData = cssMatch[1];
|
| 144 |
-
} else if (jsMatch) {
|
| 145 |
-
fileData = jsMatch[1];
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
const existingFileIndex = updatedPages.findIndex(p => p.path === filePath);
|
| 149 |
-
|
| 150 |
-
if (existingFileIndex !== -1) {
|
| 151 |
-
updatedPages[existingFileIndex] = {
|
| 152 |
-
path: filePath,
|
| 153 |
-
html: fileData.trim()
|
| 154 |
-
};
|
| 155 |
-
} else {
|
| 156 |
-
updatedPages.push({
|
| 157 |
-
path: filePath,
|
| 158 |
-
html: fileData.trim()
|
| 159 |
-
});
|
| 160 |
-
}
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
// Fallback: process SEARCH/REPLACE blocks without UPDATE_FILE wrapper (backward compatibility)
|
| 164 |
-
if (updatedPages.length === pages.length && !chunk.includes(UPDATE_FILE_START)) {
|
| 165 |
-
let position = 0;
|
| 166 |
-
let moreBlocks = true;
|
| 167 |
-
let newHtml = updatedPages[0]?.html || "";
|
| 168 |
-
|
| 169 |
-
while (moreBlocks) {
|
| 170 |
-
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
| 171 |
-
if (searchStartIndex === -1) {
|
| 172 |
-
moreBlocks = false;
|
| 173 |
-
continue;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
| 177 |
-
if (dividerIndex === -1) {
|
| 178 |
-
moreBlocks = false;
|
| 179 |
-
continue;
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
| 183 |
-
if (replaceEndIndex === -1) {
|
| 184 |
-
moreBlocks = false;
|
| 185 |
-
continue;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
const searchBlock = chunk.substring(
|
| 189 |
-
searchStartIndex + SEARCH_START.length,
|
| 190 |
-
dividerIndex
|
| 191 |
-
);
|
| 192 |
-
const replaceBlock = chunk.substring(
|
| 193 |
-
dividerIndex + DIVIDER.length,
|
| 194 |
-
replaceEndIndex
|
| 195 |
-
);
|
| 196 |
-
|
| 197 |
-
if (searchBlock.trim() === "") {
|
| 198 |
-
newHtml = `${replaceBlock}\n${newHtml}`;
|
| 199 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
| 200 |
-
} else {
|
| 201 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
| 202 |
-
const match = regex.exec(newHtml);
|
| 203 |
-
|
| 204 |
-
if (match) {
|
| 205 |
-
const matchedText = match[0];
|
| 206 |
-
const beforeText = newHtml.substring(0, match.index);
|
| 207 |
-
const startLineNumber = beforeText.split("\n").length;
|
| 208 |
-
const replaceLines = replaceBlock.split("\n").length;
|
| 209 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
| 210 |
-
|
| 211 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
| 212 |
-
newHtml = newHtml.replace(matchedText, replaceBlock);
|
| 213 |
-
}
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
position = replaceEndIndex + REPLACE_END.length;
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
| 220 |
-
if (mainPageIndex !== -1) {
|
| 221 |
-
updatedPages[mainPageIndex].html = newHtml;
|
| 222 |
-
}
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
return { updatedPages, updatedLines };
|
| 226 |
-
};
|
| 227 |
-
|
| 228 |
-
/**
|
| 229 |
-
* Convert pages to File objects for upload to HuggingFace
|
| 230 |
-
*/
|
| 231 |
-
export const pagesToFiles = (pages: Page[]): File[] => {
|
| 232 |
-
const files: File[] = [];
|
| 233 |
-
pages.forEach((page: Page) => {
|
| 234 |
-
let mimeType = "text/html";
|
| 235 |
-
if (page.path.endsWith(".css")) {
|
| 236 |
-
mimeType = "text/css";
|
| 237 |
-
} else if (page.path.endsWith(".js")) {
|
| 238 |
-
mimeType = "text/javascript";
|
| 239 |
-
} else if (page.path.endsWith(".json")) {
|
| 240 |
-
mimeType = "application/json";
|
| 241 |
-
}
|
| 242 |
-
const file = new File([page.html], page.path, { type: mimeType });
|
| 243 |
-
files.push(file);
|
| 244 |
-
});
|
| 245 |
-
return files;
|
| 246 |
-
};
|
| 247 |
-
|
| 248 |
-
/**
|
| 249 |
-
* Extract project name from AI response
|
| 250 |
-
*/
|
| 251 |
-
export const extractProjectName = (chunk: string): string | null => {
|
| 252 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START\s*([\s\S]*?)\s*>>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
| 253 |
-
return projectName || null;
|
| 254 |
-
};
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib/prompts.ts
CHANGED
|
@@ -15,168 +15,79 @@ export const PROMPT_FOR_IMAGE_GENERATION = `If you want to use image placeholder
|
|
| 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!`
|
| 17 |
|
| 18 |
-
export const INITIAL_SYSTEM_PROMPT_LIGHT = `You are an expert UI/UX and Front-End Developer.
|
| 19 |
-
No need to explain what you did. Just return the expected result. Use always TailwindCSS, don't forget to import it.
|
| 20 |
-
Return the results following this format:
|
| 21 |
-
1. Start with ${PROJECT_NAME_START}.
|
| 22 |
-
2. Add the name of the project, right after the start tag.
|
| 23 |
-
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 24 |
-
4. The name of the project should be short and concise.
|
| 25 |
-
5. Generate files in this ORDER: index.html FIRST, then style.css, then script.js, then web components if needed.
|
| 26 |
-
6. For each file, start with ${NEW_FILE_START}.
|
| 27 |
-
7. Add the file name right after the start tag.
|
| 28 |
-
8. Close the start tag with the ${NEW_FILE_END}.
|
| 29 |
-
9. Start the file content with the triple backticks and appropriate language marker
|
| 30 |
-
10. Insert the file content there.
|
| 31 |
-
11. Close with the triple backticks, like \`\`\`.
|
| 32 |
-
12. Repeat for each file.
|
| 33 |
-
Example Code:
|
| 34 |
-
${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
|
| 35 |
-
${NEW_FILE_START}index.html${NEW_FILE_END}
|
| 36 |
-
\`\`\`html
|
| 37 |
-
<!DOCTYPE html>
|
| 38 |
-
<html lang="en">
|
| 39 |
-
<head>
|
| 40 |
-
<meta charset="UTF-8">
|
| 41 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 42 |
-
<title>Index</title>
|
| 43 |
-
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 44 |
-
<link rel="stylesheet" href="style.css">
|
| 45 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 46 |
-
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 47 |
-
<script src="https://unpkg.com/feather-icons"></script>
|
| 48 |
-
</head>
|
| 49 |
-
<body>
|
| 50 |
-
<h1>Hello World</h1>
|
| 51 |
-
<custom-example></custom-example>
|
| 52 |
-
<script src="components/example.js"></script>
|
| 53 |
-
<script src="script.js"></script>
|
| 54 |
-
<script>feather.replace();</script>
|
| 55 |
-
</body>
|
| 56 |
-
</html>
|
| 57 |
-
\`\`\`
|
| 58 |
-
CRITICAL: The first file MUST always be index.html.`
|
| 59 |
-
|
| 60 |
-
export const FOLLOW_UP_SYSTEM_PROMPT_LIGHT = `You are an expert UI/UX and Front-End Developer modifying existing files (HTML, CSS, JavaScript).
|
| 61 |
-
You MUST output ONLY the changes required using the following UPDATE_FILE_START and SEARCH/REPLACE format. Do NOT output the entire file.
|
| 62 |
-
Do NOT explain the changes or what you did, just return the expected results.
|
| 63 |
-
Update Format Rules:
|
| 64 |
-
1. Start with ${PROJECT_NAME_START}.
|
| 65 |
-
2. Add the name of the project, right after the start tag.
|
| 66 |
-
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 67 |
-
4. Start with ${UPDATE_FILE_START}
|
| 68 |
-
5. Provide the name of the file you are modifying (index.html, style.css, script.js, etc.).
|
| 69 |
-
6. Close the start tag with the ${UPDATE_FILE_END}.
|
| 70 |
-
7. Start with ${SEARCH_START}
|
| 71 |
-
8. Provide the exact lines from the current code that need to be replaced.
|
| 72 |
-
9. Use ${DIVIDER} to separate the search block from the replacement.
|
| 73 |
-
10. Provide the new lines that should replace the original lines.
|
| 74 |
-
11. End with ${REPLACE_END}
|
| 75 |
-
12. You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the file.
|
| 76 |
-
13. To insert code, use an empty SEARCH block (only ${SEARCH_START} and ${DIVIDER} on their lines) if inserting at the very beginning, otherwise provide the line *before* the insertion point in the SEARCH block and include that line plus the new lines in the REPLACE block.
|
| 77 |
-
14. To delete code, provide the lines to delete in the SEARCH block and leave the REPLACE block empty (only ${DIVIDER} and ${REPLACE_END} on their lines).
|
| 78 |
-
15. IMPORTANT: The SEARCH block must *exactly* match the current code, including indentation and whitespace.
|
| 79 |
-
Example Modifying Code:
|
| 80 |
-
\`\`\`
|
| 81 |
-
${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
|
| 82 |
-
${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
|
| 83 |
-
${SEARCH_START}
|
| 84 |
-
<h1>Old Title</h1>
|
| 85 |
-
${DIVIDER}
|
| 86 |
-
<h1>New Title</h1>
|
| 87 |
-
${REPLACE_END}
|
| 88 |
-
${SEARCH_START}
|
| 89 |
-
</body>
|
| 90 |
-
${DIVIDER}
|
| 91 |
-
<script src="script.js"></script>
|
| 92 |
-
</body>
|
| 93 |
-
${REPLACE_END}
|
| 94 |
-
\`\`\`
|
| 95 |
-
Example Updating CSS:
|
| 96 |
-
\`\`\`
|
| 97 |
-
${UPDATE_FILE_START}style.css${UPDATE_FILE_END}
|
| 98 |
-
${SEARCH_START}
|
| 99 |
-
body {
|
| 100 |
-
background: white;
|
| 101 |
-
}
|
| 102 |
-
${DIVIDER}
|
| 103 |
-
body {
|
| 104 |
-
background: linear-gradient(to right, #667eea, #764ba2);
|
| 105 |
-
}
|
| 106 |
-
${REPLACE_END}
|
| 107 |
-
\`\`\`
|
| 108 |
-
Example Deleting Code:
|
| 109 |
-
\`\`\`
|
| 110 |
-
${UPDATE_FILE_START}index.html${UPDATE_FILE_END}
|
| 111 |
-
${SEARCH_START}
|
| 112 |
-
<p>This paragraph will be deleted.</p>
|
| 113 |
-
${DIVIDER}
|
| 114 |
-
${REPLACE_END}
|
| 115 |
-
\`\`\`
|
| 116 |
-
For creating new files, use the following format:
|
| 117 |
-
1. Start with ${NEW_FILE_START}.
|
| 118 |
-
2. Add the name of the file (e.g., about.html, style.css, script.js, components/navbar.js), right after the start tag.
|
| 119 |
-
3. Close the start tag with the ${NEW_FILE_END}.
|
| 120 |
-
4. Start the file content with the triple backticks and appropriate language marker (\`\`\`html, \`\`\`css, or \`\`\`javascript).
|
| 121 |
-
5. Insert the file content there.
|
| 122 |
-
6. Close with the triple backticks, like \`\`\`.
|
| 123 |
-
7. Repeat for additional files.
|
| 124 |
-
Example Creating New HTML Page:
|
| 125 |
-
${NEW_FILE_START}about.html${NEW_FILE_END}
|
| 126 |
-
\`\`\`html
|
| 127 |
-
<!DOCTYPE html>
|
| 128 |
-
<html lang="en">
|
| 129 |
-
<head>
|
| 130 |
-
<meta charset="UTF-8">
|
| 131 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 132 |
-
<title>About</title>
|
| 133 |
-
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 134 |
-
<link rel="stylesheet" href="style.css">
|
| 135 |
-
<script src="https://cdn.tailwindcss.com"></script>
|
| 136 |
-
</head>
|
| 137 |
-
<body>
|
| 138 |
-
<h1>About Page</h1>
|
| 139 |
-
<script src="script.js"></script>
|
| 140 |
-
</body>
|
| 141 |
-
</html>
|
| 142 |
-
\`\`\`
|
| 143 |
-
No need to explain what you did. Just return the expected result.`
|
| 144 |
-
|
| 145 |
export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
|
| 146 |
You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
|
| 147 |
Try to create the best UI possible. Important: Make the website responsive by using TailwindCSS. Use it as much as you can, if you can't use it, use custom css (make sure to import tailwind with <script src="https://cdn.tailwindcss.com"></script> in the head).
|
| 148 |
Also try to elaborate as much as you can, to create something unique, with a great design.
|
| 149 |
If you want to use ICONS import Feather Icons (Make sure to add <script src="https://unpkg.com/feather-icons"></script> and <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> in the head., and <script>feather.replace();</script> in the body. Ex : <i data-feather="user"></i>).
|
|
|
|
| 150 |
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.
|
| 151 |
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.
|
| 152 |
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>.
|
| 153 |
WEB COMPONENTS: For reusable UI elements like navbars, footers, sidebars, headers, etc., create Native Web Components as separate files in components/ folder:
|
| 154 |
-
- Create each component as a separate .js file in components/ folder (e.g., components/
|
| 155 |
- Each component file defines a class extending HTMLElement and registers it with customElements.define()
|
| 156 |
- Use Shadow DOM for style encapsulation
|
| 157 |
- Components render using template literals with inline styles
|
| 158 |
-
- Include component files in HTML before using them: <script src="components/
|
| 159 |
-
- Use them in HTML pages with custom element tags (e.g., <custom-
|
| 160 |
- If you want to use ICON you can use Feather Icons, as it's already included in the main pages.
|
| 161 |
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.
|
| 162 |
-
Example components/
|
| 163 |
-
class
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
connectedCallback() {
|
| 165 |
this.attachShadow({ mode: 'open' });
|
| 166 |
this.shadowRoot.innerHTML = \`
|
| 167 |
<style>
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
</style>
|
| 170 |
-
<
|
| 171 |
-
<
|
| 172 |
-
</
|
| 173 |
\`;
|
| 174 |
}
|
| 175 |
}
|
| 176 |
-
customElements.define('custom-
|
|
|
|
| 177 |
Then in HTML, include the component scripts and use the tags:
|
| 178 |
-
<script src="components/
|
| 179 |
-
<
|
|
|
|
|
|
|
| 180 |
${PROMPT_FOR_IMAGE_GENERATION}
|
| 181 |
${PROMPT_FOR_PROJECT_NAME}
|
| 182 |
No need to explain what you did. Just return the expected result. AVOID Chinese characters in the code if not asked by the user.
|
|
@@ -185,14 +96,15 @@ Return the results following this format:
|
|
| 185 |
2. Add the name of the project, right after the start tag.
|
| 186 |
3. Close the start tag with the ${PROJECT_NAME_END}.
|
| 187 |
4. The name of the project should be short and concise.
|
| 188 |
-
5. Generate files in this ORDER: index.html FIRST, then style.css, then script.js, then web components
|
| 189 |
6. For each file, start with ${NEW_FILE_START}.
|
| 190 |
-
7. Add the file name right after the start tag.
|
| 191 |
8. Close the start tag with the ${NEW_FILE_END}.
|
| 192 |
-
9. Start the file content with the triple backticks and appropriate language marker
|
| 193 |
10. Insert the file content there.
|
| 194 |
11. Close with the triple backticks, like \`\`\`.
|
| 195 |
12. Repeat for each file.
|
|
|
|
| 196 |
Example Code:
|
| 197 |
${PROJECT_NAME_START} Project Name ${PROJECT_NAME_END}
|
| 198 |
${NEW_FILE_START}index.html${NEW_FILE_END}
|
|
@@ -210,15 +122,83 @@ ${NEW_FILE_START}index.html${NEW_FILE_END}
|
|
| 210 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 211 |
</head>
|
| 212 |
<body>
|
| 213 |
-
<
|
| 214 |
-
<
|
| 215 |
-
<
|
|
|
|
|
|
|
| 216 |
<script src="script.js"></script>
|
| 217 |
<script>feather.replace();</script>
|
| 218 |
</body>
|
| 219 |
</html>
|
| 220 |
\`\`\`
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
export const FOLLOW_UP_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer modifying existing files (HTML, CSS, JavaScript).
|
| 224 |
The user wants to apply changes and probably add new features/pages/styles/scripts to the website, based on their request.
|
|
|
|
| 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!`
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
export const INITIAL_SYSTEM_PROMPT = `You are an expert UI/UX and Front-End Developer.
|
| 19 |
You create website in a way a designer would, using ONLY HTML, CSS and Javascript.
|
| 20 |
Try to create the best UI possible. Important: Make the website responsive by using TailwindCSS. Use it as much as you can, if you can't use it, use custom css (make sure to import tailwind with <script src="https://cdn.tailwindcss.com"></script> in the head).
|
| 21 |
Also try to elaborate as much as you can, to create something unique, with a great design.
|
| 22 |
If you want to use ICONS import Feather Icons (Make sure to add <script src="https://unpkg.com/feather-icons"></script> and <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> in the head., and <script>feather.replace();</script> in the body. Ex : <i data-feather="user"></i>).
|
| 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.
|
|
|
|
| 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}
|
|
|
|
| 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.
|
lib/providers.ts
CHANGED
|
@@ -2,7 +2,6 @@ import DeepSeekLogo from "@/assets/deepseek.svg";
|
|
| 2 |
import QwenLogo from "@/assets/qwen.svg";
|
| 3 |
import KimiLogo from "@/assets/kimi.svg";
|
| 4 |
import ZaiLogo from "@/assets/zai.svg";
|
| 5 |
-
import MiniMaxLogo from "@/assets/minimax.svg";
|
| 6 |
|
| 7 |
export const PROVIDERS = {
|
| 8 |
"fireworks-ai": {
|
|
@@ -48,28 +47,29 @@ export const MODELS = [
|
|
| 48 |
logo: DeepSeekLogo,
|
| 49 |
companyName: "DeepSeek",
|
| 50 |
},
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
{
|
| 68 |
value: "deepseek-ai/DeepSeek-V3.2-Exp",
|
| 69 |
label: "DeepSeek V3.2 Exp",
|
| 70 |
providers: ["novita"],
|
| 71 |
autoProvider: "novita",
|
| 72 |
logo: DeepSeekLogo,
|
|
|
|
| 73 |
companyName: "DeepSeek",
|
| 74 |
},
|
| 75 |
{
|
|
@@ -88,35 +88,18 @@ export const MODELS = [
|
|
| 88 |
logo: KimiLogo,
|
| 89 |
companyName: "Kimi",
|
| 90 |
},
|
| 91 |
-
// {
|
| 92 |
-
// value: "moonshotai/Kimi-K2-Instruct-0905",
|
| 93 |
-
// label: "Kimi K2 Instruct 0905",
|
| 94 |
-
// providers: ["together", "groq", "novita"],
|
| 95 |
-
// autoProvider: "groq",
|
| 96 |
-
// logo: KimiLogo,
|
| 97 |
-
// companyName: "Kimi",
|
| 98 |
-
// },
|
| 99 |
{
|
| 100 |
-
value: "moonshotai/Kimi-K2-
|
| 101 |
-
label: "Kimi K2
|
|
|
|
|
|
|
| 102 |
logo: KimiLogo,
|
| 103 |
companyName: "Kimi",
|
| 104 |
-
isNew: true,
|
| 105 |
-
temperature: 1.0,
|
| 106 |
},
|
| 107 |
{
|
| 108 |
value: "zai-org/GLM-4.6",
|
| 109 |
label: "GLM-4.6",
|
| 110 |
logo: ZaiLogo,
|
| 111 |
companyName: "Z.ai",
|
| 112 |
-
}
|
| 113 |
-
{
|
| 114 |
-
value: "MiniMaxAI/MiniMax-M2",
|
| 115 |
-
label: "MiniMax M2",
|
| 116 |
-
logo: MiniMaxLogo,
|
| 117 |
-
companyName: "MiniMax",
|
| 118 |
-
top_k: 40,
|
| 119 |
-
temperature: 1.0,
|
| 120 |
-
top_p: 0.95,
|
| 121 |
-
},
|
| 122 |
];
|
|
|
|
| 2 |
import QwenLogo from "@/assets/qwen.svg";
|
| 3 |
import KimiLogo from "@/assets/kimi.svg";
|
| 4 |
import ZaiLogo from "@/assets/zai.svg";
|
|
|
|
| 5 |
|
| 6 |
export const PROVIDERS = {
|
| 7 |
"fireworks-ai": {
|
|
|
|
| 47 |
logo: DeepSeekLogo,
|
| 48 |
companyName: "DeepSeek",
|
| 49 |
},
|
| 50 |
+
{
|
| 51 |
+
value: "deepseek-ai/DeepSeek-V3.1",
|
| 52 |
+
label: "DeepSeek V3.1",
|
| 53 |
+
providers: ["fireworks-ai", "novita"],
|
| 54 |
+
autoProvider: "fireworks-ai",
|
| 55 |
+
logo: DeepSeekLogo,
|
| 56 |
+
companyName: "DeepSeek",
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
value: "deepseek-ai/DeepSeek-V3.1-Terminus",
|
| 60 |
+
label: "DeepSeek V3.1 Terminus",
|
| 61 |
+
providers: ["novita"],
|
| 62 |
+
autoProvider: "novita",
|
| 63 |
+
logo: DeepSeekLogo,
|
| 64 |
+
companyName: "DeepSeek",
|
| 65 |
+
},
|
| 66 |
{
|
| 67 |
value: "deepseek-ai/DeepSeek-V3.2-Exp",
|
| 68 |
label: "DeepSeek V3.2 Exp",
|
| 69 |
providers: ["novita"],
|
| 70 |
autoProvider: "novita",
|
| 71 |
logo: DeepSeekLogo,
|
| 72 |
+
isNew: true,
|
| 73 |
companyName: "DeepSeek",
|
| 74 |
},
|
| 75 |
{
|
|
|
|
| 88 |
logo: KimiLogo,
|
| 89 |
companyName: "Kimi",
|
| 90 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
{
|
| 92 |
+
value: "moonshotai/Kimi-K2-Instruct-0905",
|
| 93 |
+
label: "Kimi K2 Instruct 0905",
|
| 94 |
+
providers: ["together", "groq", "novita"],
|
| 95 |
+
autoProvider: "groq",
|
| 96 |
logo: KimiLogo,
|
| 97 |
companyName: "Kimi",
|
|
|
|
|
|
|
| 98 |
},
|
| 99 |
{
|
| 100 |
value: "zai-org/GLM-4.6",
|
| 101 |
label: "GLM-4.6",
|
| 102 |
logo: ZaiLogo,
|
| 103 |
companyName: "Z.ai",
|
| 104 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
];
|
middleware.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import { NextResponse } from "next/server";
|
| 2 |
import type { NextRequest } from "next/server";
|
| 3 |
|
| 4 |
-
export function middleware(request: NextRequest) {
|
| 5 |
const headers = new Headers(request.headers);
|
| 6 |
headers.set("x-current-host", request.nextUrl.host);
|
| 7 |
-
headers.set("x-invoke-path", request.nextUrl.pathname + request.nextUrl.search);
|
| 8 |
|
| 9 |
const response = NextResponse.next({ headers });
|
| 10 |
|
|
|
|
| 1 |
import { NextResponse } from "next/server";
|
| 2 |
import type { NextRequest } from "next/server";
|
| 3 |
|
| 4 |
+
export function middleware(request: NextRequest) {
|
| 5 |
const headers = new Headers(request.headers);
|
| 6 |
headers.set("x-current-host", request.nextUrl.host);
|
|
|
|
| 7 |
|
| 8 |
const response = NextResponse.next({ headers });
|
| 9 |
|
next.config.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { NextConfig } from "next";
|
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
/* config options here */
|
| 5 |
basePath: '/deepsite',
|
| 6 |
-
assetPrefix: '/deepsite',
|
| 7 |
async redirects() {
|
| 8 |
return [
|
| 9 |
{
|
|
|
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
/* config options here */
|
| 5 |
basePath: '/deepsite',
|
|
|
|
| 6 |
async redirects() {
|
| 7 |
return [
|
| 8 |
{
|
package-lock.json
CHANGED
|
@@ -33,7 +33,6 @@
|
|
| 33 |
"clsx": "^2.1.1",
|
| 34 |
"date-fns": "^4.1.0",
|
| 35 |
"framer-motion": "^12.23.22",
|
| 36 |
-
"jszip": "^3.10.1",
|
| 37 |
"log4js": "^6.9.1",
|
| 38 |
"log4js-json-layout": "^2.2.3",
|
| 39 |
"lucide-react": "^0.542.0",
|
|
@@ -320,13 +319,13 @@
|
|
| 320 |
}
|
| 321 |
},
|
| 322 |
"node_modules/@huggingface/inference": {
|
| 323 |
-
"version": "4.
|
| 324 |
-
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.
|
| 325 |
-
"integrity": "sha512-
|
| 326 |
"license": "MIT",
|
| 327 |
"dependencies": {
|
| 328 |
"@huggingface/jinja": "^0.5.1",
|
| 329 |
-
"@huggingface/tasks": "^0.19.
|
| 330 |
},
|
| 331 |
"engines": {
|
| 332 |
"node": ">=18"
|
|
@@ -342,9 +341,9 @@
|
|
| 342 |
}
|
| 343 |
},
|
| 344 |
"node_modules/@huggingface/tasks": {
|
| 345 |
-
"version": "0.19.
|
| 346 |
-
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.
|
| 347 |
-
"integrity": "sha512-
|
| 348 |
"license": "MIT"
|
| 349 |
},
|
| 350 |
"node_modules/@humanfs/core": {
|
|
@@ -3240,12 +3239,6 @@
|
|
| 3240 |
"toggle-selection": "^1.0.6"
|
| 3241 |
}
|
| 3242 |
},
|
| 3243 |
-
"node_modules/core-util-is": {
|
| 3244 |
-
"version": "1.0.3",
|
| 3245 |
-
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
| 3246 |
-
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
| 3247 |
-
"license": "MIT"
|
| 3248 |
-
},
|
| 3249 |
"node_modules/cross-spawn": {
|
| 3250 |
"version": "7.0.6",
|
| 3251 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
@@ -4092,12 +4085,6 @@
|
|
| 4092 |
"node": ">= 4"
|
| 4093 |
}
|
| 4094 |
},
|
| 4095 |
-
"node_modules/immediate": {
|
| 4096 |
-
"version": "3.0.6",
|
| 4097 |
-
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
| 4098 |
-
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
| 4099 |
-
"license": "MIT"
|
| 4100 |
-
},
|
| 4101 |
"node_modules/import-fresh": {
|
| 4102 |
"version": "3.3.1",
|
| 4103 |
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
|
@@ -4125,12 +4112,6 @@
|
|
| 4125 |
"node": ">=0.8.19"
|
| 4126 |
}
|
| 4127 |
},
|
| 4128 |
-
"node_modules/inherits": {
|
| 4129 |
-
"version": "2.0.4",
|
| 4130 |
-
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 4131 |
-
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 4132 |
-
"license": "ISC"
|
| 4133 |
-
},
|
| 4134 |
"node_modules/inline-style-prefixer": {
|
| 4135 |
"version": "7.0.1",
|
| 4136 |
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
|
|
@@ -4177,12 +4158,6 @@
|
|
| 4177 |
"node": ">=0.12.0"
|
| 4178 |
}
|
| 4179 |
},
|
| 4180 |
-
"node_modules/isarray": {
|
| 4181 |
-
"version": "1.0.0",
|
| 4182 |
-
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
| 4183 |
-
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
| 4184 |
-
"license": "MIT"
|
| 4185 |
-
},
|
| 4186 |
"node_modules/isexe": {
|
| 4187 |
"version": "2.0.0",
|
| 4188 |
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
|
@@ -4298,18 +4273,6 @@
|
|
| 4298 |
"graceful-fs": "^4.1.6"
|
| 4299 |
}
|
| 4300 |
},
|
| 4301 |
-
"node_modules/jszip": {
|
| 4302 |
-
"version": "3.10.1",
|
| 4303 |
-
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
| 4304 |
-
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
| 4305 |
-
"license": "(MIT OR GPL-3.0-or-later)",
|
| 4306 |
-
"dependencies": {
|
| 4307 |
-
"lie": "~3.3.0",
|
| 4308 |
-
"pako": "~1.0.2",
|
| 4309 |
-
"readable-stream": "~2.3.6",
|
| 4310 |
-
"setimmediate": "^1.0.5"
|
| 4311 |
-
}
|
| 4312 |
-
},
|
| 4313 |
"node_modules/kareem": {
|
| 4314 |
"version": "2.6.3",
|
| 4315 |
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
|
@@ -4343,15 +4306,6 @@
|
|
| 4343 |
"node": ">= 0.8.0"
|
| 4344 |
}
|
| 4345 |
},
|
| 4346 |
-
"node_modules/lie": {
|
| 4347 |
-
"version": "3.3.0",
|
| 4348 |
-
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
| 4349 |
-
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
| 4350 |
-
"license": "MIT",
|
| 4351 |
-
"dependencies": {
|
| 4352 |
-
"immediate": "~3.0.5"
|
| 4353 |
-
}
|
| 4354 |
-
},
|
| 4355 |
"node_modules/lightningcss": {
|
| 4356 |
"version": "1.30.1",
|
| 4357 |
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
|
@@ -5132,12 +5086,6 @@
|
|
| 5132 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 5133 |
}
|
| 5134 |
},
|
| 5135 |
-
"node_modules/pako": {
|
| 5136 |
-
"version": "1.0.11",
|
| 5137 |
-
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
| 5138 |
-
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
| 5139 |
-
"license": "(MIT AND Zlib)"
|
| 5140 |
-
},
|
| 5141 |
"node_modules/parent-module": {
|
| 5142 |
"version": "1.0.1",
|
| 5143 |
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
|
@@ -5228,12 +5176,6 @@
|
|
| 5228 |
"node": ">= 0.8.0"
|
| 5229 |
}
|
| 5230 |
},
|
| 5231 |
-
"node_modules/process-nextick-args": {
|
| 5232 |
-
"version": "2.0.1",
|
| 5233 |
-
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
| 5234 |
-
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
| 5235 |
-
"license": "MIT"
|
| 5236 |
-
},
|
| 5237 |
"node_modules/proxy-from-env": {
|
| 5238 |
"version": "1.1.0",
|
| 5239 |
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
@@ -5404,27 +5346,6 @@
|
|
| 5404 |
"react-dom": "*"
|
| 5405 |
}
|
| 5406 |
},
|
| 5407 |
-
"node_modules/readable-stream": {
|
| 5408 |
-
"version": "2.3.8",
|
| 5409 |
-
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
| 5410 |
-
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
| 5411 |
-
"license": "MIT",
|
| 5412 |
-
"dependencies": {
|
| 5413 |
-
"core-util-is": "~1.0.0",
|
| 5414 |
-
"inherits": "~2.0.3",
|
| 5415 |
-
"isarray": "~1.0.0",
|
| 5416 |
-
"process-nextick-args": "~2.0.0",
|
| 5417 |
-
"safe-buffer": "~5.1.1",
|
| 5418 |
-
"string_decoder": "~1.1.1",
|
| 5419 |
-
"util-deprecate": "~1.0.1"
|
| 5420 |
-
}
|
| 5421 |
-
},
|
| 5422 |
-
"node_modules/readable-stream/node_modules/safe-buffer": {
|
| 5423 |
-
"version": "5.1.2",
|
| 5424 |
-
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
| 5425 |
-
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 5426 |
-
"license": "MIT"
|
| 5427 |
-
},
|
| 5428 |
"node_modules/require-from-string": {
|
| 5429 |
"version": "2.0.2",
|
| 5430 |
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
|
@@ -5587,12 +5508,6 @@
|
|
| 5587 |
"node": ">=6.9"
|
| 5588 |
}
|
| 5589 |
},
|
| 5590 |
-
"node_modules/setimmediate": {
|
| 5591 |
-
"version": "1.0.5",
|
| 5592 |
-
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
| 5593 |
-
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
| 5594 |
-
"license": "MIT"
|
| 5595 |
-
},
|
| 5596 |
"node_modules/sharp": {
|
| 5597 |
"version": "0.34.3",
|
| 5598 |
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
|
|
@@ -5788,21 +5703,6 @@
|
|
| 5788 |
"node": ">=8.0"
|
| 5789 |
}
|
| 5790 |
},
|
| 5791 |
-
"node_modules/string_decoder": {
|
| 5792 |
-
"version": "1.1.1",
|
| 5793 |
-
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
| 5794 |
-
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
| 5795 |
-
"license": "MIT",
|
| 5796 |
-
"dependencies": {
|
| 5797 |
-
"safe-buffer": "~5.1.0"
|
| 5798 |
-
}
|
| 5799 |
-
},
|
| 5800 |
-
"node_modules/string_decoder/node_modules/safe-buffer": {
|
| 5801 |
-
"version": "5.1.2",
|
| 5802 |
-
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
| 5803 |
-
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 5804 |
-
"license": "MIT"
|
| 5805 |
-
},
|
| 5806 |
"node_modules/strip-json-comments": {
|
| 5807 |
"version": "3.1.1",
|
| 5808 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
@@ -6257,12 +6157,6 @@
|
|
| 6257 |
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 6258 |
}
|
| 6259 |
},
|
| 6260 |
-
"node_modules/util-deprecate": {
|
| 6261 |
-
"version": "1.0.2",
|
| 6262 |
-
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
| 6263 |
-
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
| 6264 |
-
"license": "MIT"
|
| 6265 |
-
},
|
| 6266 |
"node_modules/watchpack": {
|
| 6267 |
"version": "2.4.4",
|
| 6268 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
|
|
|
| 33 |
"clsx": "^2.1.1",
|
| 34 |
"date-fns": "^4.1.0",
|
| 35 |
"framer-motion": "^12.23.22",
|
|
|
|
| 36 |
"log4js": "^6.9.1",
|
| 37 |
"log4js-json-layout": "^2.2.3",
|
| 38 |
"lucide-react": "^0.542.0",
|
|
|
|
| 319 |
}
|
| 320 |
},
|
| 321 |
"node_modules/@huggingface/inference": {
|
| 322 |
+
"version": "4.7.1",
|
| 323 |
+
"resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.7.1.tgz",
|
| 324 |
+
"integrity": "sha512-gXrMocGDsE6kUZPEj82c3O+/OKnIfbHvg9rYjGA6svbWrYVmHCIAdCrrgCwNl2v5GELfPJrrfIv0bvzCTfa64A==",
|
| 325 |
"license": "MIT",
|
| 326 |
"dependencies": {
|
| 327 |
"@huggingface/jinja": "^0.5.1",
|
| 328 |
+
"@huggingface/tasks": "^0.19.35"
|
| 329 |
},
|
| 330 |
"engines": {
|
| 331 |
"node": ">=18"
|
|
|
|
| 341 |
}
|
| 342 |
},
|
| 343 |
"node_modules/@huggingface/tasks": {
|
| 344 |
+
"version": "0.19.43",
|
| 345 |
+
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.43.tgz",
|
| 346 |
+
"integrity": "sha512-ANO23K3ugclBl6VLwdt+7MxBkRkKEE17USUSqprHb29UB5ISigH+0AJcEuDA064uzn0hqYrG/nOcv1yARRt8bw==",
|
| 347 |
"license": "MIT"
|
| 348 |
},
|
| 349 |
"node_modules/@humanfs/core": {
|
|
|
|
| 3239 |
"toggle-selection": "^1.0.6"
|
| 3240 |
}
|
| 3241 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3242 |
"node_modules/cross-spawn": {
|
| 3243 |
"version": "7.0.6",
|
| 3244 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
|
|
| 4085 |
"node": ">= 4"
|
| 4086 |
}
|
| 4087 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4088 |
"node_modules/import-fresh": {
|
| 4089 |
"version": "3.3.1",
|
| 4090 |
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
|
|
|
| 4112 |
"node": ">=0.8.19"
|
| 4113 |
}
|
| 4114 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4115 |
"node_modules/inline-style-prefixer": {
|
| 4116 |
"version": "7.0.1",
|
| 4117 |
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
|
|
|
|
| 4158 |
"node": ">=0.12.0"
|
| 4159 |
}
|
| 4160 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4161 |
"node_modules/isexe": {
|
| 4162 |
"version": "2.0.0",
|
| 4163 |
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
|
|
|
| 4273 |
"graceful-fs": "^4.1.6"
|
| 4274 |
}
|
| 4275 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4276 |
"node_modules/kareem": {
|
| 4277 |
"version": "2.6.3",
|
| 4278 |
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
|
|
|
| 4306 |
"node": ">= 0.8.0"
|
| 4307 |
}
|
| 4308 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4309 |
"node_modules/lightningcss": {
|
| 4310 |
"version": "1.30.1",
|
| 4311 |
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
|
|
|
| 5086 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 5087 |
}
|
| 5088 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5089 |
"node_modules/parent-module": {
|
| 5090 |
"version": "1.0.1",
|
| 5091 |
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
|
|
|
| 5176 |
"node": ">= 0.8.0"
|
| 5177 |
}
|
| 5178 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5179 |
"node_modules/proxy-from-env": {
|
| 5180 |
"version": "1.1.0",
|
| 5181 |
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
|
|
| 5346 |
"react-dom": "*"
|
| 5347 |
}
|
| 5348 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5349 |
"node_modules/require-from-string": {
|
| 5350 |
"version": "2.0.2",
|
| 5351 |
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
|
|
|
| 5508 |
"node": ">=6.9"
|
| 5509 |
}
|
| 5510 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5511 |
"node_modules/sharp": {
|
| 5512 |
"version": "0.34.3",
|
| 5513 |
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
|
|
|
|
| 5703 |
"node": ">=8.0"
|
| 5704 |
}
|
| 5705 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5706 |
"node_modules/strip-json-comments": {
|
| 5707 |
"version": "3.1.1",
|
| 5708 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
|
|
| 6157 |
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 6158 |
}
|
| 6159 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6160 |
"node_modules/watchpack": {
|
| 6161 |
"version": "2.4.4",
|
| 6162 |
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
package.json
CHANGED
|
@@ -33,7 +33,6 @@
|
|
| 33 |
"clsx": "^2.1.1",
|
| 34 |
"date-fns": "^4.1.0",
|
| 35 |
"framer-motion": "^12.23.22",
|
| 36 |
-
"jszip": "^3.10.1",
|
| 37 |
"log4js": "^6.9.1",
|
| 38 |
"log4js-json-layout": "^2.2.3",
|
| 39 |
"lucide-react": "^0.542.0",
|
|
|
|
| 33 |
"clsx": "^2.1.1",
|
| 34 |
"date-fns": "^4.1.0",
|
| 35 |
"framer-motion": "^12.23.22",
|
|
|
|
| 36 |
"log4js": "^6.9.1",
|
| 37 |
"log4js-json-layout": "^2.2.3",
|
| 38 |
"lucide-react": "^0.542.0",
|