diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..d7419a983b3e8d6606db990addea8d78dd660692
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+AUTH_HUGGINGFACE_ID=
+AUTH_HUGGINGFACE_SECRET=
+NEXTAUTH_URL=http://localhost:3001
+AUTH_SECRET=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e72b4d6a488ccacb6296f0db7c609e15ff3bac14
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..a2b0759c0a612c3be02d8c1eb3021252cdd4a7f8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,22 @@
+FROM node:20-alpine
+USER root
+
+# Install pnpm
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+USER 1000
+WORKDIR /usr/src/app
+# Copy package.json and pnpm-lock.yaml to the container
+COPY --chown=1000 package.json pnpm-lock.yaml ./
+
+# Copy the rest of the application files to the container
+COPY --chown=1000 . .
+
+RUN pnpm install
+RUN pnpm run build
+
+# Expose the application port (assuming your app runs on port 3000)
+EXPOSE 3001
+
+# Start the application
+CMD ["pnpm", "start"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c55ddca7d407fc08447fbfb7c2c98d3b3a9e9af3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+---
+title: DeepSite v4
+emoji: 🐳
+colorFrom: blue
+colorTo: blue
+sdk: docker
+pinned: false
+app_port: 3001
+license: mit
+failure_strategy: rollback
+short_description: Generate any application by Vibe Coding it
+models:
+ - deepseek-ai/DeepSeek-V3-0324
+ - deepseek-ai/DeepSeek-V3.2
+ - Qwen/Qwen3-Coder-30B-A3B-Instruct
+ - moonshotai/Kimi-K2-Instruct-0905
+ - zai-org/GLM-4.7
+ - MiniMaxAI/MiniMax-M2.1
+---
+
+# DeepSite 🏗️
+
+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.
diff --git a/actions/mentions.ts b/actions/mentions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c6b7dabfbaa6f0efe0d492dfe5470353b0139bae
--- /dev/null
+++ b/actions/mentions.ts
@@ -0,0 +1,31 @@
+"use client";
+
+import { File } from "@/lib/type";
+
+export const searchMentions = async (query: string) => {
+ const promises = [searchModels(query), searchDatasets(query)];
+ const results = await Promise.all(promises);
+ return { models: results[0], datasets: results[1] };
+};
+
+const searchModels = async (query: string) => {
+ const response = await fetch(
+ `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3`
+ );
+ const data = await response.json();
+ return data?.models ?? [];
+};
+
+const searchDatasets = async (query: string) => {
+ const response = await fetch(
+ `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3`
+ );
+ const data = await response.json();
+ return data?.datasets ?? [];
+};
+
+export const searchFilesMentions = async (query: string, files: File[]) => {
+ if (!query) return files;
+ const lowerQuery = query.toLowerCase();
+ return files.filter((file) => file.path.toLowerCase().includes(lowerQuery));
+};
diff --git a/actions/projects.ts b/actions/projects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dfc399a2e448dbac7261d332189239ba418fc279
--- /dev/null
+++ b/actions/projects.ts
@@ -0,0 +1,156 @@
+"use server";
+import {
+ downloadFile,
+ listCommits,
+ listFiles,
+ listSpaces,
+ RepoDesignation,
+ SpaceEntry,
+ spaceInfo,
+} from "@huggingface/hub";
+
+import { auth } from "@/lib/auth";
+import { Commit, File } from "@/lib/type";
+
+export interface ProjectWithCommits extends SpaceEntry {
+ commits?: Commit[];
+}
+
+const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
+
+export const getProjects = async () => {
+ const projects: SpaceEntry[] = [];
+ const session = await auth();
+ if (!session?.user) {
+ return projects;
+ }
+ const token = session.accessToken;
+ for await (const space of listSpaces({
+ accessToken: token,
+ additionalFields: ["author", "cardData"],
+ search: {
+ owner: "enzostvs",
+ },
+ })) {
+ if (
+ space.sdk === "static" &&
+ Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
+ (space.cardData as { tags?: string[] })?.tags?.some((tag) =>
+ tag.includes("deepsite")
+ )
+ ) {
+ projects.push(space);
+ }
+ }
+ return projects;
+};
+export const getProject = async (id: string, commitId?: string) => {
+ const session = await auth();
+ if (!session?.user) {
+ return null;
+ }
+ const token = session.accessToken;
+ try {
+ const project: ProjectWithCommits | null = await spaceInfo({
+ name: id,
+ accessToken: token,
+ additionalFields: ["author", "cardData"],
+ });
+ const repo: RepoDesignation = {
+ type: "space",
+ name: id,
+ };
+ const files: File[] = [];
+ const params = { repo, accessToken: token };
+ if (commitId) {
+ Object.assign(params, { revision: commitId });
+ }
+ for await (const fileInfo of listFiles(params)) {
+ if (IGNORED_PATHS.includes(fileInfo.path)) continue;
+ if (
+ fileInfo.path.endsWith(".html") ||
+ fileInfo.path.endsWith(".css") ||
+ fileInfo.path.endsWith(".js") ||
+ fileInfo.path.endsWith(".json")
+ ) {
+ const blob = await downloadFile({
+ repo,
+ accessToken: token,
+ path: fileInfo.path,
+ raw: true,
+ ...(commitId ? { revision: commitId } : {}),
+ }).catch((_) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const html = await blob?.text();
+ if (!html) {
+ continue;
+ }
+ files[fileInfo.path === "index.html" ? "unshift" : "push"]({
+ path: fileInfo.path,
+ content: html,
+ });
+ }
+ if (fileInfo.type === "directory") {
+ for await (const subFile of listFiles({
+ repo,
+ accessToken: token,
+ path: fileInfo.path,
+ })) {
+ if (
+ subFile.path.endsWith(".html") ||
+ subFile.path.endsWith(".css") ||
+ subFile.path.endsWith(".js") ||
+ subFile.path.endsWith(".json")
+ ) {
+ const blob = await downloadFile({
+ repo,
+ accessToken: token,
+ path: subFile.path,
+ raw: true,
+ ...(commitId ? { revision: commitId } : {}),
+ }).catch((_) => {
+ return null;
+ });
+ if (!blob) {
+ continue;
+ }
+ const html = await blob?.text();
+ if (!html) {
+ continue;
+ }
+ files[subFile.path === "index.html" ? "unshift" : "push"]({
+ path: subFile.path,
+ content: html,
+ });
+ }
+ }
+ }
+ }
+ const commits: Commit[] = [];
+ const commitIterator = listCommits({ repo, accessToken: token });
+ for await (const commit of commitIterator) {
+ if (commit.title?.toLowerCase() === "initial commit") continue;
+ commits.push({
+ title: commit.title,
+ oid: commit.oid,
+ date: commit.date,
+ });
+ if (commits.length >= 20) {
+ break;
+ }
+ }
+
+ project.commits = commits;
+
+ return { project, files };
+ } catch (error) {
+ return {
+ project: null,
+ files: [],
+ };
+ }
+};
diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0eb2c8fbb85b745d5be01c9e79fa0ebc93a71d00
--- /dev/null
+++ b/app/(public)/layout.tsx
@@ -0,0 +1,14 @@
+import { Navigation } from "@/components/public/navigation";
+
+export default function PublicLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/app/(public)/page.tsx b/app/(public)/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cba36b46276880eb96f5bb70a85ffe3a46518f8d
--- /dev/null
+++ b/app/(public)/page.tsx
@@ -0,0 +1,21 @@
+import { AnimatedDotsBackground } from "@/components/public/animated-dots-background";
+import { HeroHeader } from "@/components/public/hero-header";
+import { UserProjects } from "@/components/projects/user-projects";
+import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing";
+
+export default async function Homepage() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/[owner]/[repoId]/page.tsx b/app/[owner]/[repoId]/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4bb68e064370e3afc1b3720cc1100077c2790a0c
--- /dev/null
+++ b/app/[owner]/[repoId]/page.tsx
@@ -0,0 +1,25 @@
+import { getProject } from "@/actions/projects";
+import { AppEditor } from "@/components/editor";
+import { notFound } from "next/navigation";
+
+export default async function ProjectPage({
+ params,
+ searchParams,
+}: {
+ params: Promise<{ owner: string; repoId: string }>;
+ searchParams: Promise<{ commit?: string }>;
+}) {
+ const { owner, repoId } = await params;
+ const { commit } = await searchParams;
+ const datas = await getProject(`${owner}/${repoId}`, commit);
+ if (!datas?.project) {
+ return notFound();
+ }
+ return (
+
+ );
+}
diff --git a/app/api/ask/route.ts b/app/api/ask/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5e458964d50270da7d6a61f3149181bcb581a7fd
--- /dev/null
+++ b/app/api/ask/route.ts
@@ -0,0 +1,144 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { NextResponse } from "next/server";
+import { InferenceClient } from "@huggingface/inference";
+
+import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts";
+import { auth } from "@/lib/auth";
+import { File, Message } from "@/lib/type";
+import { MODELS } from "@/lib/providers";
+
+export async function POST(request: Request) {
+ const session = await auth();
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const token = session.accessToken;
+
+ const body = await request.json();
+ const {
+ prompt,
+ previousMessages = [],
+ files = [],
+ provider: initialProvider,
+ mentions = [],
+ model,
+ redesignMd,
+ } = body;
+ const provider = initialProvider ?? "auto";
+
+ if (!prompt) {
+ return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
+ }
+ if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) {
+ return NextResponse.json({ error: "Model is required" }, { status: 400 });
+ }
+
+ const client = new InferenceClient(token);
+
+ try {
+ const encoder = new TextEncoder();
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+
+ const response = new NextResponse(stream.readable, {
+ headers: {
+ "Content-Type": "text/plain; charset=utf-8",
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+ },
+ });
+ (async () => {
+ try {
+ const chatCompletion = client.chatCompletionStream({
+ model: model + (provider !== "auto" ? `:${provider}` : ""),
+ messages: [
+ {
+ role: "system",
+ content:
+ files.length > 0
+ ? FOLLOW_UP_SYSTEM_PROMPT
+ : INITIAL_SYSTEM_PROMPT,
+ },
+ ...previousMessages.map((message: Message) => ({
+ role: message.role,
+ content: message.content,
+ })),
+ ...(files?.length > 0
+ ? [
+ {
+ role: "user",
+ content: `Here are the files that the user has provider:${files
+ .map(
+ (file: File) =>
+ `File: ${file.path}\nContent: ${file.content}`
+ )
+ .join("\n")}\n\n${prompt}`,
+ },
+ ]
+ : []),
+ {
+ role: "user",
+ content: `
+${prompt}
+${
+ mentions?.length > 0
+ ? `\n\nHere are the informations about the model or dataset that the user has mentioned to use: ${mentions
+ .map(
+ (mention: any) =>
+ `Library: ${mention.library_name}\nPipeline: ${mention.pipeline_tag}\nModel: ${mention.model_id}\nReadme for more information: \n${mention.readme}`
+ )
+ .join("\n")}`
+ : ""
+}
+ `,
+ },
+ ],
+ stream: true,
+ max_tokens: 16_000,
+ });
+ while (true) {
+ const { done, value } = await chatCompletion.next();
+ if (done) {
+ break;
+ }
+
+ const chunk = value.choices[0]?.delta?.content;
+ if (chunk) {
+ await writer.write(encoder.encode(chunk));
+ }
+ }
+
+ await writer.close();
+ } catch (error) {
+ console.error(error);
+ try {
+ const errorMessage =
+ error instanceof Error
+ ? error.message
+ : "An error occurred while processing your request";
+ const errorPayload = JSON.stringify({
+ messageError: errorMessage,
+ isError: true,
+ });
+ await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`));
+ await writer.close();
+ } catch (closeError) {
+ console.error("Failed to send error message:", closeError);
+ try {
+ await writer.abort(error);
+ } catch (abortError) {
+ console.error("Failed to abort writer:", abortError);
+ }
+ }
+ }
+ })();
+
+ return response;
+ } catch (error) {
+ console.error(error);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ce43504c71cdf14baa13c95ca81c99dbda682fe
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,4 @@
+import { handlers } from "@/lib/auth";
+
+export const { GET, POST } = handlers;
+
diff --git a/app/api/projects/[repoId]/[commitId]/route.ts b/app/api/projects/[repoId]/[commitId]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0362d6c43ec226bf97837f2f88196ad43eb056f2
--- /dev/null
+++ b/app/api/projects/[repoId]/[commitId]/route.ts
@@ -0,0 +1,49 @@
+import { auth } from "@/lib/auth";
+import { createBranch, RepoDesignation } from "@huggingface/hub";
+import { format } from "date-fns";
+import { NextResponse } from "next/server";
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ repoId: string; commitId: string }> }
+) {
+ const { repoId, commitId }: { repoId: string; commitId: string } =
+ await params;
+ const session = await auth();
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const token = session.accessToken;
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: session.user?.username + "/" + repoId,
+ };
+
+ const commitTitle = `🔖 ${format(new Date(), "dd/MM")} - ${format(
+ new Date(),
+ "HH:mm"
+ )} - Set commit ${commitId} as default.`;
+
+ await fetch(
+ `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ startingPoint: commitId,
+ overwrite: true,
+ }),
+ }
+ ).catch((error) => {
+ return NextResponse.json(
+ { error: error ?? "Failed to create branch" },
+ { status: 500 }
+ );
+ });
+
+ return NextResponse.json({ success: true }, { status: 200 });
+}
diff --git a/app/api/projects/[repoId]/route.ts b/app/api/projects/[repoId]/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3e600936254e01c2f858082c191e827610656a17
--- /dev/null
+++ b/app/api/projects/[repoId]/route.ts
@@ -0,0 +1,97 @@
+import { auth } from "@/lib/auth";
+import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub";
+import { format } from "date-fns";
+import { NextResponse } from "next/server";
+
+export async function PUT(
+ request: Request,
+ { params }: { params: Promise<{ repoId: string }> }
+) {
+ const { repoId }: { repoId: string } = await params;
+ const session = await auth();
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const token = session.accessToken;
+
+ const body = await request.json();
+ const { files, prompt, isManualChanges } = body;
+
+ if (!files) {
+ return NextResponse.json({ error: "Files are required" }, { status: 400 });
+ }
+
+ if (!prompt) {
+ return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
+ }
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: session.user?.username + "/" + repoId,
+ };
+
+ const filesToUpload: File[] = [];
+ for (const file of files) {
+ let mimeType = "text/x-python";
+ if (file.path.endsWith(".txt")) {
+ mimeType = "text/plain";
+ } else if (file.path.endsWith(".md")) {
+ mimeType = "text/markdown";
+ } else if (file.path.endsWith(".json")) {
+ mimeType = "application/json";
+ }
+ filesToUpload.push(new File([file.content], file.path, { type: mimeType }));
+ }
+ const baseTitle = isManualChanges
+ ? ""
+ : `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
+ const commitTitle = baseTitle + (prompt ?? "Follow-up DeepSite commit");
+ const response = await uploadFiles({
+ repo,
+ files: filesToUpload,
+ accessToken: token,
+ commitTitle,
+ });
+
+ return NextResponse.json(
+ {
+ success: true,
+ commit: {
+ oid: response.commit,
+ title: commitTitle,
+ date: new Date(),
+ },
+ },
+ { status: 200 }
+ );
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ repoId: string }> }
+) {
+ const { repoId }: { repoId: string } = await params;
+ const session = await auth();
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const token = session.accessToken;
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: session.user?.username + "/" + repoId,
+ };
+
+ try {
+ await deleteRepo({
+ repo,
+ accessToken: token as string,
+ });
+
+ return NextResponse.json({ success: true }, { status: 200 });
+ } catch (error) {
+ const errMsg =
+ error instanceof Error ? error.message : "Failed to delete project";
+ return NextResponse.json({ error: errMsg }, { status: 500 });
+ }
+}
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..501b27349617a9faf507bbd37925621278770d04
--- /dev/null
+++ b/app/api/projects/route.ts
@@ -0,0 +1,97 @@
+import { NextResponse } from "next/server";
+import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub";
+
+import { auth } from "@/lib/auth";
+import { COLORS, injectDeepSiteBadge, isIndexPage } from "@/lib/utils";
+
+export async function POST(request: Request) {
+ const session = await auth();
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+ const token = session.accessToken;
+
+ const body = await request.json();
+ const { projectTitle, files, prompt } = body;
+
+ if (!files) {
+ return NextResponse.json(
+ { error: "Project title and files are required" },
+ { status: 400 }
+ );
+ }
+
+ const title =
+ projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
+
+ const formattedTitle = title
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .split("-")
+ .filter(Boolean)
+ .join("-")
+ .slice(0, 96);
+
+ const repo: RepoDesignation = {
+ type: "space",
+ name: session.user?.username + "/" + formattedTitle,
+ };
+
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
+ const README = `---
+title: ${projectTitle}
+colorFrom: ${colorFrom}
+colorTo: ${colorTo}
+sdk: static
+tags:
+ - deepsite-v4
+---
+
+# ${title}
+
+This project has been created with [DeepSite](https://huggingface.co/deepsite) AI Vibe Coding.
+`;
+
+ const filesToUpload: File[] = [
+ new File([README], "README.md", { type: "text/markdown" }),
+ ];
+ for (const file of files) {
+ let mimeType = "text/html";
+ if (file.path.endsWith(".css")) {
+ mimeType = "text/css";
+ } else if (file.path.endsWith(".js")) {
+ mimeType = "text/javascript";
+ }
+ const content =
+ mimeType === "text/html" && isIndexPage(file.path)
+ ? injectDeepSiteBadge(file.content)
+ : file.content;
+
+ filesToUpload.push(new File([content], file.path, { type: mimeType }));
+ }
+
+ try {
+ const { repoUrl } = await createRepo({
+ accessToken: token as string,
+ repo: repo,
+ sdk: "static",
+ });
+
+ const commitTitle = prompt ?? "Initial DeepSite commit";
+ await uploadFiles({
+ repo,
+ files: filesToUpload,
+ accessToken: token as string,
+ commitTitle,
+ });
+
+ const path = repoUrl.split("/").slice(-2).join("/");
+
+ return NextResponse.json({ repoUrl: path }, { status: 200 });
+ } catch (error) {
+ const errMsg =
+ error instanceof Error ? error.message : "Failed to upload files";
+ return NextResponse.json({ error: errMsg }, { status: 500 });
+ }
+}
diff --git a/app/api/redesign/route.ts b/app/api/redesign/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6b898d6fd364c5f3ef267706b62c37ee559e19cd
--- /dev/null
+++ b/app/api/redesign/route.ts
@@ -0,0 +1,73 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { NextRequest, NextResponse } from "next/server";
+
+const FETCH_TIMEOUT = 30_000;
+export const maxDuration = 60;
+
+export async function PUT(request: NextRequest) {
+ const body = await request.json();
+ const { url } = body;
+
+ if (!url) {
+ return NextResponse.json({ error: "URL is required" }, { status: 400 });
+ }
+
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
+
+ try {
+ const response = await fetch(
+ `https://r.jina.ai/${encodeURIComponent(url)}`,
+ {
+ method: "POST",
+ signal: controller.signal,
+ }
+ );
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ return NextResponse.json(
+ { error: "Failed to fetch redesign" },
+ { status: 500 }
+ );
+ }
+ const markdown = await response.text();
+ return NextResponse.json(
+ {
+ ok: true,
+ markdown,
+ },
+ { status: 200 }
+ );
+ } catch (fetchError: any) {
+ clearTimeout(timeoutId);
+
+ if (fetchError.name === "AbortError") {
+ return NextResponse.json(
+ {
+ error:
+ "Request timeout: The external service took too long to respond. Please try again.",
+ },
+ { status: 504 }
+ );
+ }
+ throw fetchError;
+ }
+ } catch (error: any) {
+ if (error.name === "AbortError" || error.message?.includes("timeout")) {
+ return NextResponse.json(
+ {
+ error:
+ "Request timeout: The external service took too long to respond. Please try again.",
+ },
+ { status: 504 }
+ );
+ }
+ return NextResponse.json(
+ { error: error.message || "An error occurred" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000000000000000000000000000000000000..a64b15d9b0025a6d85a71dd47c9dda2cf4337b6d
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,161 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.65rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+.monaco-editor .margin {
+ @apply bg-background!;
+}
+.monaco-editor .monaco-editor-background {
+ @apply bg-background!;
+}
+.monaco-editor .decorationsOverviewRuler {
+ @apply opacity-0!;
+}
+.monaco-editor .view-line {
+ /* @apply bg-primary/50!; */
+}
+.monaco-editor .scroll-decoration {
+ @apply opacity-0!;
+}
+.monaco-editor .cursors-layer .cursor {
+ @apply bg-primary!;
+}
+
+.content-placeholder::before {
+ content: attr(data-placeholder);
+ position: absolute;
+ pointer-events: none;
+ opacity: 0.5;
+ @apply top-5 left-6;
+}
+
+.sp-layout .sp-file-explorer .sp-file-explorer-list .sp-explorer[data-active="true"] {
+ @apply text-indigo-500!;
+}
+
+.sp-layout .sp-stack .sp-tabs .sp-tab-container[aria-selected="true"] .sp-tab-button {
+ @apply text-indigo-500!;
+}
+.sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) {
+ @apply outline-none! border-none!;
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..47b5b6ffe71b10da4c9b98058b80e4c28eebb463
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,60 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+import { ThemeProvider } from "@/components/providers/theme";
+import { AuthProvider } from "@/components/providers/session";
+import { Toaster } from "@/components/ui/sonner";
+import { ReactQueryProvider } from "@/components/providers/react-query";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Build your next application | DeepSite",
+ description:
+ "Build your next application with ease and speed by using AI Vibe Coding.",
+ icons: {
+ icon: "/logo.svg",
+ shortcut: "/logo.svg",
+ apple: "/logo.svg",
+ other: {
+ rel: "icon",
+ url: "/logo.svg",
+ },
+ },
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/app/new/page.tsx b/app/new/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b6c05f51448fcfce40547d63e04040d6c87198d3
--- /dev/null
+++ b/app/new/page.tsx
@@ -0,0 +1,10 @@
+import { AppEditor } from "@/components/editor";
+
+export default async function NewProjectPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ prompt: string }>;
+}) {
+ const { prompt } = await searchParams;
+ return ;
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ae102854e6245bcc2f0d55780d9419b2679d6f38
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,17 @@
+import { NotFoundButtons } from "@/components/not-found/buttons";
+import { Navigation } from "@/components/public/navigation";
+
+export default function NotFound() {
+ return (
+
+
+
+
Oh no! Page not found.
+
+ The page you are looking for does not exist.
+
+
+
+
+ );
+}
diff --git a/assets/deepseek.svg b/assets/deepseek.svg
new file mode 100644
index 0000000000000000000000000000000000000000..dc224e43a4d68070ca6eed494476c8ddd900bf80
--- /dev/null
+++ b/assets/deepseek.svg
@@ -0,0 +1 @@
+DeepSeek
\ No newline at end of file
diff --git a/assets/kimi.svg b/assets/kimi.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4355c522a2dece99e187d9e5c898a66313f4a374
--- /dev/null
+++ b/assets/kimi.svg
@@ -0,0 +1 @@
+Kimi
\ No newline at end of file
diff --git a/assets/minimax.svg b/assets/minimax.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1d32449ab8fb0fe9a6c50006a41e67ef49c8dd1c
--- /dev/null
+++ b/assets/minimax.svg
@@ -0,0 +1 @@
+Minimax
\ No newline at end of file
diff --git a/assets/qwen.svg b/assets/qwen.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a4bb382a6359b82c581fd3e7fb7169fe8fba1657
--- /dev/null
+++ b/assets/qwen.svg
@@ -0,0 +1 @@
+Qwen
\ No newline at end of file
diff --git a/assets/space.svg b/assets/space.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f133cf120bb1f4fe43c949d099965ae9a84db240
--- /dev/null
+++ b/assets/space.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/assets/zai.svg b/assets/zai.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2adcac387aaca06eb362a177e45c28ae3429b0d0
--- /dev/null
+++ b/assets/zai.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components.json b/components.json
new file mode 100644
index 0000000000000000000000000000000000000000..d5005f0974a11b0ea57843b9f52f82f995743963
--- /dev/null
+++ b/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/components/ask-ai/ask-ai-landing.tsx b/components/ask-ai/ask-ai-landing.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f945c84726d2680ab9b2f3129be9472af4234144
--- /dev/null
+++ b/components/ask-ai/ask-ai-landing.tsx
@@ -0,0 +1,66 @@
+"use client";
+import { ArrowUp } from "lucide-react";
+import { useState } from "react";
+import { useInterval, useLocalStorage } from "react-use";
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/components/ui/button";
+import { ProviderType } from "@/lib/type";
+import { Models } from "./models";
+import { MODELS } from "@/lib/providers";
+import { cn } from "@/lib/utils";
+import { AiLoading } from "./loading";
+import { EXAMPLES_OF_PROJECT_SUGGESTIONS } from "@/lib/prompts";
+
+export function AskAiLanding({ className }: { className?: string }) {
+ const [model, setModel] = useState(MODELS[0].value);
+ const [provider, setProvider] = useLocalStorage(
+ "provider",
+ "auto" as ProviderType
+ );
+ const router = useRouter();
+ const [prompt, setPrompt] = useState("");
+ return (
+
+ );
+}
diff --git a/components/ask-ai/ask-ai.tsx b/components/ask-ai/ask-ai.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a976b360fa99da870b9c9dc36fa1571b6c0f5428
--- /dev/null
+++ b/components/ask-ai/ask-ai.tsx
@@ -0,0 +1,162 @@
+"use client";
+import { ArrowUp, Paintbrush, X } from "lucide-react";
+import { useState } from "react";
+import { HiStop } from "react-icons/hi2";
+import { useLocalStorage, useMount } from "react-use";
+import { useRouter } from "next/navigation";
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { useGeneration } from "./useGeneration";
+import { File, MobileTabType, ProviderType } from "@/lib/type";
+import { Models } from "./models";
+import { MODELS } from "@/lib/providers";
+import { Redesign } from "./redesign";
+import { Uploader } from "./uploader";
+import { InputMentions } from "./input-mentions";
+
+// todo: send redesignMd to callAi + add it to the prompt
+
+export function AskAI({
+ initialPrompt,
+ className,
+ onToggleMobileTab,
+ files,
+ isNew = false,
+ isHistoryView,
+ projectName = "new",
+}: {
+ initialPrompt?: string;
+ className?: string;
+ files?: File[] | null;
+ onToggleMobileTab?: (tab: MobileTabType) => void;
+ isNew?: boolean;
+ isHistoryView?: boolean;
+ projectName?: string;
+}) {
+ const [prompt, setPrompt] = useState(initialPrompt ?? "");
+ const [model = MODELS[0].value, setModel] = useLocalStorage(
+ "model",
+ MODELS[0].value
+ );
+ const [provider, setProvider] = useLocalStorage(
+ "provider",
+ "auto" as ProviderType
+ );
+ const [redesignMd, setRedesignMd] = useState<{
+ md: string;
+ url: string;
+ } | null>(null);
+
+ const router = useRouter();
+ const { callAi, isLoading, stopGeneration, audio } =
+ useGeneration(projectName);
+
+ // const messages =
+ // queryClient.getQueryData(MESSAGES_QUERY_KEY(projectName)) ?? [];
+
+ // const clearMessages = () => {
+ // queryClient.setQueryData(MESSAGES_QUERY_KEY(projectName), []);
+ // localStorage.removeItem(`messages-${projectName}`);
+ // };
+
+ const onComplete = () => {
+ onToggleMobileTab?.("right-sidebar");
+ };
+
+ useMount(() => {
+ if (initialPrompt && initialPrompt.trim() !== "" && isNew) {
+ setTimeout(() => {
+ if (isHistoryView) return;
+ callAi({
+ prompt: initialPrompt,
+ model,
+ onComplete,
+ provider,
+ });
+ router.replace("/new");
+ }, 200);
+ }
+ });
+
+ const onSubmit = () => {
+ if (isHistoryView) return;
+ callAi({ prompt, model, onComplete, provider, redesignMd });
+ };
+
+ return (
+
+
+
+
+
+ Your browser does not support the audio element.
+
+
+ );
+}
diff --git a/components/ask-ai/context.tsx b/components/ask-ai/context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c22f17fc81f95788b6d3e690cc70ac5d3b6f817a
--- /dev/null
+++ b/components/ask-ai/context.tsx
@@ -0,0 +1,123 @@
+import { AtSign, Braces, FileCode, FileText, X } from "lucide-react";
+import { useMemo, useState } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+import { File } from "@/lib/type";
+import { cn } from "@/lib/utils";
+
+export const Context = ({
+ files,
+ setFiles,
+}: {
+ files: File[];
+ setFiles: (files: File[]) => void;
+}) => {
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+
+ const getFileIcon = (filePath: string, size = "size-3.5") => {
+ if (filePath.endsWith(".css")) {
+ return ;
+ } else if (filePath.endsWith(".js")) {
+ return ;
+ } else if (filePath.endsWith(".json")) {
+ return ;
+ } else {
+ return ;
+ }
+ };
+
+ const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
+
+ return (
+
+
+
+
+
+ Add Context...
+
+
+
+
+
+ Select a file to send as context
+
+
+ {getFiles().length === 0 ? (
+
+ No files available
+
+ ) : (
+ <>
+
{
+ setFiles([]);
+ setOpen(false);
+ }}
+ className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-accent hover:text-accent-foreground transition-colors ${
+ files.length === 0
+ ? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
+ : "text-muted-foreground"
+ }`}
+ >
+ All files (default)
+
+ {getFiles()?.map((page) => (
+
{
+ if (files.some((f) => f.path === page.path))
+ setFiles(files.filter((f) => f.path !== page.path));
+ else setFiles(files ? [...files, page] : [page]);
+ setOpen(false);
+ }}
+ className={`cursor-pointer w-full px-2 py-1.5 text-xs text-left rounded-md hover:bg-accent hover:text-accent-foreground transition-colors flex items-center gap-1.5 ${
+ files?.some((f) => f.path === page.path)
+ ? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
+ : "text-muted-foreground"
+ }`}
+ >
+
+ {getFileIcon(page.path, "size-3")}
+
+ {page.path}
+
+ ))}
+ >
+ )}
+
+
+
+
+ {files?.map((file) => (
+
+ {getFileIcon(file.path, "size-3")}
+ {file.path}
+ {
+ setFiles(files.filter((f) => f.path !== file.path));
+ }}
+ >
+
+
+
+ ))}
+
+ );
+};
diff --git a/components/ask-ai/input-mentions.tsx b/components/ask-ai/input-mentions.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..22522fac1add9d157d67ba2223c7915b7c3dbea2
--- /dev/null
+++ b/components/ask-ai/input-mentions.tsx
@@ -0,0 +1,317 @@
+import { useRef, useState, useEffect } from "react";
+import { useClickAway } from "react-use";
+import { useQueryClient } from "@tanstack/react-query";
+
+import { searchFilesMentions } from "@/actions/mentions";
+import { File } from "@/lib/type";
+import { Braces, FileCode, FileText } from "lucide-react";
+
+export function InputMentions({
+ prompt,
+ files,
+ setPrompt,
+ redesignMdUrl,
+ onSubmit,
+}: {
+ prompt: string;
+ files?: File[] | null;
+ redesignMdUrl?: string;
+ setPrompt: (prompt: string) => void;
+ onSubmit: () => void;
+}) {
+ const queryClient = useQueryClient();
+ const [showMentionDropdown, setShowMentionDropdown] = useState(false);
+ const [, setMentionSearch] = useState("");
+ const contentEditableRef = useRef(null);
+ const dropdownRef = useRef(null);
+ const [results, setResults] = useState([]);
+
+ useClickAway(dropdownRef, () => {
+ setShowMentionDropdown(false);
+ });
+
+ const getTextContent = (element: HTMLElement): string => {
+ let text = "";
+ const childNodes = element.childNodes;
+
+ for (let i = 0; i < childNodes.length; i++) {
+ const node = childNodes[i];
+ if (node.nodeType === Node.TEXT_NODE) {
+ text += node.textContent || "";
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ const el = node as HTMLElement;
+ if (el.classList.contains("mention-chip")) {
+ text += el.getAttribute("data-mention-id") || "";
+ } else {
+ text += el.textContent || "";
+ }
+ }
+ }
+ return text + "\u0020";
+ };
+
+ const extractPromptWithIds = (): string => {
+ if (!contentEditableRef.current) return "";
+
+ let text = "";
+ const childNodes = contentEditableRef.current.childNodes;
+
+ for (let i = 0; i < childNodes.length; i++) {
+ const node = childNodes[i];
+ if (node.nodeType === Node.TEXT_NODE) {
+ text += node.textContent || "";
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ const el = node as HTMLElement;
+ if (el.classList.contains("mention-chip")) {
+ text += el.getAttribute("data-mention-id") || "";
+ } else {
+ text += el.textContent || "";
+ }
+ }
+ }
+ return text;
+ };
+
+ const shouldDetectMention = (): {
+ detect: boolean;
+ textBeforeCursor: string;
+ } => {
+ const selection = window.getSelection();
+ if (!selection || !contentEditableRef.current) {
+ return { detect: false, textBeforeCursor: "" };
+ }
+
+ const range = selection.getRangeAt(0);
+ const node = range.startContainer;
+
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as HTMLElement;
+ if (element.classList?.contains("mention-chip")) {
+ return { detect: false, textBeforeCursor: "" };
+ }
+ }
+
+ if (node.parentElement?.classList?.contains("mention-chip")) {
+ return { detect: false, textBeforeCursor: "" };
+ }
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ const textContent = node.textContent || "";
+ const cursorOffset = range.startOffset;
+ const textBeforeCursor = textContent.substring(0, cursorOffset);
+
+ const lastAtIndex = textBeforeCursor.lastIndexOf("@");
+ if (lastAtIndex !== -1) {
+ const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
+ if (!textAfterAt.includes(" ")) {
+ return { detect: true, textBeforeCursor: textAfterAt };
+ }
+ }
+ }
+
+ return { detect: false, textBeforeCursor: "" };
+ };
+
+ const handleInput = async () => {
+ if (!contentEditableRef.current) return;
+ const text = getTextContent(contentEditableRef.current);
+ if (text.trim() === "") {
+ contentEditableRef.current.innerHTML = "";
+ }
+ setPrompt(text);
+
+ const { detect, textBeforeCursor } = shouldDetectMention();
+
+ if (detect && files && files?.length > 0) {
+ setMentionSearch(textBeforeCursor);
+ setShowMentionDropdown(true);
+ const files = queryClient.getQueryData(["files"]) ?? [];
+ const results = await searchFilesMentions(textBeforeCursor, files);
+ setResults(results);
+ } else {
+ setShowMentionDropdown(false);
+ }
+ };
+
+ const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
+ const mentionChip = document.createElement("span");
+
+ const baseClasses =
+ "mention-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium";
+ const typeClasses =
+ "bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
+
+ mentionChip.className = `${baseClasses} ${typeClasses}`;
+ mentionChip.contentEditable = "false";
+ mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
+ mentionChip.textContent = `@${mentionId}`;
+
+ return mentionChip;
+ };
+
+ const insertMention = (mentionId: string) => {
+ if (!contentEditableRef.current) return;
+
+ const selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0) return;
+
+ const range = selection.getRangeAt(0);
+ const textNode = range.startContainer;
+
+ if (textNode.nodeType !== Node.TEXT_NODE) return;
+
+ const textContent = textNode.textContent || "";
+ const cursorOffset = range.startOffset;
+
+ const textBeforeCursor = textContent.substring(0, cursorOffset);
+ const lastAtIndex = textBeforeCursor.lastIndexOf("@");
+
+ if (lastAtIndex !== -1) {
+ const mentionChip = createMentionChipElement(mentionId);
+
+ const beforeText = textContent.substring(0, lastAtIndex);
+ const afterText = textContent.substring(cursorOffset);
+ const parent = textNode.parentNode;
+ if (!parent) return;
+
+ const beforeNode = beforeText
+ ? document.createTextNode(beforeText)
+ : null;
+ const spaceNode = document.createTextNode("\u0020");
+ const afterNode = afterText ? document.createTextNode(afterText) : null;
+
+ if (beforeNode) {
+ parent.insertBefore(beforeNode, textNode);
+ }
+ parent.insertBefore(mentionChip, textNode);
+ parent.insertBefore(spaceNode, textNode);
+ if (afterNode) {
+ parent.insertBefore(afterNode, textNode);
+ }
+
+ parent.removeChild(textNode);
+
+ const newRange = document.createRange();
+ if (afterNode) {
+ newRange.setStart(afterNode, 0);
+ } else {
+ newRange.setStartAfter(spaceNode);
+ }
+ newRange.collapse(true);
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ const newText = getTextContent(contentEditableRef.current);
+ setPrompt(newText);
+ setShowMentionDropdown(false);
+ setMentionSearch("");
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!prompt || prompt.trim() === "") return;
+
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ const promptWithIds = extractPromptWithIds();
+ setPrompt(promptWithIds);
+ onSubmit();
+
+ if (contentEditableRef.current) {
+ contentEditableRef.current.innerHTML = "";
+ }
+ setPrompt("");
+ setShowMentionDropdown(false);
+ } else if (e.key === "Escape") {
+ setShowMentionDropdown(false);
+ }
+ };
+
+ useEffect(() => {
+ if (
+ contentEditableRef.current &&
+ prompt === "" &&
+ contentEditableRef.current.innerHTML !== ""
+ ) {
+ contentEditableRef.current.innerHTML = "";
+ }
+ }, [prompt]);
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ e.preventDefault();
+ const text = e.clipboardData.getData("text/plain");
+ document.execCommand("insertText", false, text);
+ };
+
+ return (
+
+
0
+ ? "Ask me anything. Type @ to mention a file..."
+ : "Ask me anything..."
+ }
+ onInput={handleInput}
+ onKeyDown={handleKeyDown}
+ onPaste={handlePaste}
+ suppressContentEditableWarning
+ >
+ {showMentionDropdown && (
+
+
+ {results?.length > 0 && (
+
+ {results.map((file) => (
+ insertMention(file.path)}
+ />
+ ))}
+
+ )}
+
+
+ )}
+
+ );
+}
+
+export const getFileIcon = (filePath: string, size = "size-3.5") => {
+ if (filePath.endsWith(".css")) {
+ return ;
+ } else if (filePath.endsWith(".js")) {
+ return ;
+ } else if (filePath.endsWith(".json")) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+function MentionResult({
+ file,
+ onSelect,
+}: {
+ file: File;
+ onSelect: () => void;
+}) {
+ return (
+
+ {getFileIcon(file.path, "size-3")}
+ {file.path}
+
+ );
+}
diff --git a/components/ask-ai/loading.tsx b/components/ask-ai/loading.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..060be494fb586788eab5c2139bb080fa51e209a2
--- /dev/null
+++ b/components/ask-ai/loading.tsx
@@ -0,0 +1,35 @@
+"use client";
+import Loading from "@/components/loading";
+
+export const AiLoading = ({
+ text = "AI Assistant is thinking...",
+ showCircle = true,
+ className,
+}: {
+ text?: string;
+ showCircle?: boolean;
+ className?: string;
+}) => {
+ return (
+
+ {showCircle &&
}
+
+
+ {text.split("").map((char, index) => (
+
+ {char === " " ? "\u00A0" : char}
+
+ ))}
+
+
+
+ );
+};
diff --git a/components/ask-ai/models.tsx b/components/ask-ai/models.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed21fd218320e4886ecee4e40ee3fc64c9bd3a44
--- /dev/null
+++ b/components/ask-ai/models.tsx
@@ -0,0 +1,210 @@
+import {
+ BrainIcon,
+ ChevronDown,
+ DollarSign,
+ StarsIcon,
+ Zap,
+} from "lucide-react";
+import { useMemo, useState } from "react";
+
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { ProviderType } from "@/lib/type";
+import { MODELS } from "@/lib/providers";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+export function Models({
+ model,
+ setModel,
+ provider,
+ setProvider,
+}: {
+ model: string;
+ setModel: (model: string) => void;
+ provider: ProviderType;
+ setProvider: (provider: ProviderType) => void;
+}) {
+ const [open, setOpen] = useState(false);
+
+ const formattedModels = useMemo(() => {
+ const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
+ [];
+ const keys = new Set();
+ MODELS.forEach((model) => {
+ if (!keys.has(model.companyName)) {
+ lists.push({
+ isCategory: true,
+ name: model.companyName,
+ logo: model.logo,
+ });
+ keys.add(model.companyName);
+ }
+ lists.push(model);
+ });
+ return lists;
+ }, []);
+
+ return (
+
+
+
+
+ {model.split("/").pop()?.toLowerCase()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formattedModels.map(
+ (
+ item:
+ | (typeof MODELS)[0]
+ | { isCategory: true; name: string }
+ ) => {
+ if ("isCategory" in item) {
+ return (
+
+ {item.name}
+
+ );
+ }
+ const {
+ value,
+ label,
+ isNew = false,
+ isBestSeller = false,
+ } = item;
+ return (
+
+ {value.split("/").pop() || label}
+ {isNew && (
+
+ New
+
+ )}
+ {isBestSeller && (
+
+ )}
+
+ );
+ }
+ )}
+
+
+
+
+
+
Provider mode:
+
+ {(
+ [
+ { value: "cheapest", icon: DollarSign, color: "emerald" },
+ {
+ value: "auto",
+ icon: BrainIcon,
+ color: "indigo",
+ name: "Smartest",
+ },
+ { value: "fastest", icon: Zap, color: "amber" },
+ ] as const
+ ).map(
+ ({
+ value,
+ icon: Icon,
+ color,
+ name,
+ }: {
+ value: string;
+ icon: React.ElementType;
+ color: string;
+ name?: string;
+ }) => (
+
setProvider(value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ setProvider(value);
+ }
+ }}
+ className={cn(
+ "inline-flex items-center gap-1.5 h-7 px-2.5 text-xs font-medium border border-border rounded-md cursor-pointer transition-colors select-none",
+ "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
+ provider === value && [
+ "bg-transparent",
+ color === "emerald" &&
+ "[&_svg]:fill-emerald-500 [&_svg]:stroke-emerald-500 bg-emerald-500/10! border-emerald-500/10! text-emerald-500!",
+ color === "indigo" &&
+ "[&_svg]:fill-indigo-500 [&_svg]:stroke-indigo-500 bg-indigo-500/10! border-indigo-500/10! text-indigo-500!",
+ color === "amber" &&
+ "[&_svg]:fill-amber-500 [&_svg]:stroke-amber-500 bg-amber-500/10! border-amber-500/10! text-amber-500!",
+ ]
+ )}
+ >
+
+ {name ?? value.charAt(0).toUpperCase() + value.slice(1)}
+
+ )
+ )}
+
+
+
+
+
+ );
+}
diff --git a/components/ask-ai/redesign.tsx b/components/ask-ai/redesign.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..71497fedbf417c2d0f3eeaf3020391f7a4b738a1
--- /dev/null
+++ b/components/ask-ai/redesign.tsx
@@ -0,0 +1,157 @@
+import { useState } from "react";
+import { Paintbrush } from "lucide-react";
+import { toast } from "sonner";
+
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Input } from "@/components/ui/input";
+import Loading from "@/components/loading";
+
+export function Redesign({
+ onRedesign,
+}: {
+ onRedesign: (md: string, url: string) => void;
+}) {
+ const [url, setUrl] = useState("");
+ const [open, setOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const checkIfUrlIsValid = (url: string) => {
+ const urlPattern = new RegExp(
+ /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
+ "i"
+ );
+ return urlPattern.test(url);
+ };
+
+ const handleClick = async () => {
+ setError(null);
+ if (isLoading) return;
+ if (!url) {
+ toast.error("Please enter a URL.");
+ return;
+ }
+ if (!checkIfUrlIsValid(url)) {
+ toast.error("Please enter a valid URL.");
+ return;
+ }
+ setIsLoading(true);
+ const response = await fetch("/api/redesign", {
+ method: "PUT",
+ body: JSON.stringify({ url: url.trim() }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then(async (response) => {
+ if (response?.ok) {
+ const data = await response.json();
+ return data;
+ }
+ })
+ .catch((error) => {
+ setError(error.message);
+ });
+ if (response.ok) {
+ onRedesign(response.markdown, url);
+ }
+ setIsLoading(false);
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/components/ask-ai/uploader.tsx b/components/ask-ai/uploader.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f48cac3e1373790eecc2416896e081cf20ab64d2
--- /dev/null
+++ b/components/ask-ai/uploader.tsx
@@ -0,0 +1,39 @@
+import { Paperclip } from "lucide-react";
+import { useState } from "react";
+
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+// todo: implement the uploader component
+
+export const Uploader = () => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
+
+
+
+
+
+
+
+
+ Upload a file
+
+
+
+
+
+ );
+};
diff --git a/components/ask-ai/useGeneration.ts b/components/ask-ai/useGeneration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..36801f892633d246dec4dc23ed0fe17cd9f18f35
--- /dev/null
+++ b/components/ask-ai/useGeneration.ts
@@ -0,0 +1,394 @@
+"use client";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useRef } from "react";
+import { toast } from "sonner";
+import { useRouter } from "next/navigation";
+import { v4 as uuidv4 } from "uuid";
+
+import { formatResponse } from "@/lib/format";
+import { File, Message, MessageActionType, ProviderType } from "@/lib/type";
+import { getContextFilesFromPrompt } from "@/lib/utils";
+import { useLocalStorage } from "react-use";
+
+const MESSAGES_QUERY_KEY = (projectName: string) =>
+ ["messages", projectName] as const;
+
+export const useGeneration = (projectName: string) => {
+ const router = useRouter();
+ const audio = useRef(null);
+ const queryClient = useQueryClient();
+ const abortController = useRef(null);
+ const [, setStoredMessages, clearStoredMessages] = useLocalStorage(
+ `messages-${projectName}`,
+ []
+ );
+
+ const { data: isLoading } = useQuery({
+ queryKey: ["ai.generation.isLoading"],
+ queryFn: () => false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchOnMount: false,
+ staleTime: Infinity,
+ });
+
+ const setIsLoading = (isLoading: boolean) => {
+ queryClient.setQueryData(["ai.generation.isLoading"], isLoading);
+ };
+
+ const getFiles = () => queryClient.getQueryData(["files"]) ?? [];
+ const setFiles = (newFiles: File[]) => {
+ queryClient.setQueryData(["files"], (oldFiles: File[] = []) => {
+ const currentFiles = oldFiles.filter(
+ (file) => !newFiles.some((f) => f.path === file.path)
+ );
+ return [...currentFiles, ...newFiles];
+ });
+ };
+
+ const getMessages = () =>
+ queryClient.getQueryData(
+ MESSAGES_QUERY_KEY(projectName ?? "new")
+ ) ?? [];
+ const addMessage = (message: Omit) => {
+ const id = uuidv4();
+ const key = MESSAGES_QUERY_KEY(projectName ?? "new");
+ queryClient.setQueryData(key, (oldMessages = []) => {
+ const newMessages = [...oldMessages, { ...message, id }];
+ if (projectName !== "new") {
+ localStorage.setItem(
+ `messages-${projectName}`,
+ JSON.stringify(newMessages)
+ );
+ }
+ return newMessages;
+ });
+ return id;
+ };
+
+ const updateLastMessage = (content: string, files?: File[]) => {
+ queryClient.setQueryData(
+ MESSAGES_QUERY_KEY(projectName),
+ (oldMessages = []) => {
+ const newMessages = [
+ ...oldMessages.slice(0, -1),
+ {
+ ...oldMessages[oldMessages.length - 1],
+ content,
+ isThinking: false,
+ files: files?.map((file) => file.path),
+ },
+ ];
+ if (projectName !== "new") {
+ setStoredMessages(newMessages);
+ }
+ return newMessages;
+ }
+ );
+ };
+
+ const updateMessage = (messageId: string, message: Partial) => {
+ const key = MESSAGES_QUERY_KEY(projectName ?? "new");
+ const currentMessages = queryClient.getQueryData(key);
+ if (!currentMessages) return;
+ const index = currentMessages.findIndex((m) => m.id === messageId);
+ if (index === -1) return;
+ const newMessages = [
+ ...currentMessages.slice(0, index),
+ { ...currentMessages[index], ...message },
+ ...currentMessages.slice(index + 1),
+ ];
+ if (projectName !== "new") {
+ setStoredMessages(newMessages);
+ }
+ queryClient.setQueryData(key, newMessages);
+ };
+
+ const storeMessages = async (newProjectName: string) => {
+ return new Promise((resolve) => {
+ const currentMessages = queryClient.getQueryData(
+ MESSAGES_QUERY_KEY("new")
+ );
+ localStorage.setItem(
+ `messages-${newProjectName}`,
+ JSON.stringify(currentMessages)
+ );
+ queryClient.setQueryData(
+ MESSAGES_QUERY_KEY(newProjectName),
+ currentMessages
+ );
+ setTimeout(() => resolve(true), 100);
+ });
+ };
+
+ const createProject = async (
+ files: File[],
+ projectTitle: string,
+ indexMessage: string,
+ prompt: string
+ ) => {
+ updateMessage(indexMessage, {
+ actions: [
+ {
+ label: "Publishing on Hugging Face...",
+ variant: "default",
+ loading: true,
+ type: MessageActionType.PUBLISH_PROJECT,
+ },
+ ],
+ });
+ try {
+ const response = await fetch("/api/projects", {
+ method: "POST",
+ body: JSON.stringify({
+ projectTitle,
+ files,
+ prompt,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }).then(async (response) => {
+ if (response.ok) {
+ const data = await response.json();
+ return data;
+ }
+ throw new Error("Failed to publish project");
+ });
+
+ if (response.repoUrl) {
+ toast.success("Project has been published, build in progress...");
+ updateMessage(indexMessage, {
+ actions: [
+ {
+ label: "See Live preview",
+ variant: "default",
+ type: MessageActionType.SEE_LIVE_PREVIEW,
+ },
+ ],
+ });
+ storeMessages(response.repoUrl).then(() => {
+ router.push(`/${response.repoUrl}`);
+ });
+ }
+ } catch (error) {
+ toast.error("Failed to publish project");
+ updateMessage(indexMessage, {
+ actions: [
+ {
+ label: "Publish on Hugging Face",
+ variant: "default",
+ type: MessageActionType.PUBLISH_PROJECT,
+ projectTitle,
+ prompt,
+ },
+ ],
+ });
+ }
+ };
+
+ const callAi = async ({
+ prompt,
+ model,
+ onComplete,
+ provider = "auto",
+ redesignMd,
+ }: {
+ prompt: string;
+ model: string;
+ redesignMd?: {
+ url: string;
+ md: string;
+ } | null;
+ onComplete: () => void;
+ provider?: ProviderType;
+ }) => {
+ setIsLoading(true);
+ const messages = getMessages();
+ const files = getFiles();
+ const filesToUse = await getContextFilesFromPrompt(prompt, files);
+ const previousMessages = [...messages]?.filter(
+ (message) => !message.isAutomated || !message.isAborted
+ );
+ addMessage({
+ role: "user",
+ content: prompt,
+ createdAt: new Date(),
+ });
+ addMessage({
+ role: "assistant",
+ isThinking: true,
+ createdAt: new Date(),
+ model,
+ });
+
+ const isFollowUp = files?.length > 0;
+ abortController.current = new AbortController();
+
+ const request = await fetch("/api/ask", {
+ method: "POST",
+ body: JSON.stringify({
+ prompt,
+ model,
+ files: filesToUse,
+ previousMessages,
+ provider,
+ redesignMd,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ ...(abortController.current
+ ? { signal: abortController.current.signal }
+ : {}),
+ });
+
+ const currentMessages = getMessages();
+
+ if (!request.ok) {
+ const lastMessageId = currentMessages[currentMessages.length - 1].id;
+ updateMessage(lastMessageId, {
+ isThinking: false,
+ isAborted: true,
+ content: `Error: Failed to connect to AI service`,
+ });
+ setIsLoading(false);
+ return;
+ }
+
+ if (request && request.body) {
+ const reader = request.body.getReader();
+ const decoder = new TextDecoder();
+ let completeResponse = "";
+ const read = async () => {
+ const { done, value } = await reader.read();
+ if (done) {
+ audio.current?.play();
+ const files = getFiles();
+ const {
+ messageContent,
+ files: newFiles,
+ projectTitle,
+ } = formatResponse(completeResponse, files ?? []);
+ updateLastMessage(messageContent, newFiles);
+ if (newFiles && newFiles.length > 0) {
+ setFiles(newFiles);
+ onComplete();
+ if (projectName === "new") {
+ addMessage({
+ role: "assistant",
+ content:
+ "I've finished the generation. Now you can decide to publish the project on Hugging Face to share it!",
+ createdAt: new Date(),
+ isAutomated: true,
+ actions: [
+ {
+ label: "Publish on Hugging Face",
+ variant: "default",
+ type: MessageActionType.PUBLISH_PROJECT,
+ prompt,
+ projectTitle,
+ },
+ ],
+ });
+ } else {
+ const response = await fetch(
+ `/api/projects/${projectName.split("/")[1]}`,
+ {
+ method: "PUT",
+ body: JSON.stringify({
+ files: newFiles,
+ prompt,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ ).then(async (response) => {
+ if (response.ok) {
+ const data = await response.json();
+ return data;
+ }
+ });
+ if (response.success) {
+ toast.success("Project has been updated");
+ } else {
+ toast.error("Failed to update project");
+ }
+ }
+ }
+
+ setIsLoading(false);
+ return;
+ }
+ // if (abortController.current?.signal.aborted) {
+ // toast.error("Generation aborted");
+ // setIsLoading(false);
+ // return;
+ // }
+ const chunk = decoder.decode(value, { stream: true });
+ completeResponse += chunk;
+
+ if (completeResponse.includes("__ERROR__:")) {
+ const errorMatch = completeResponse.match(/__ERROR__:(.+)/);
+ if (errorMatch) {
+ try {
+ const errorData = JSON.parse(errorMatch[1]);
+ if (errorData.isError) {
+ const lastMessageId =
+ currentMessages[currentMessages.length - 1].id;
+ updateMessage(lastMessageId, {
+ isThinking: false,
+ isAborted: true,
+ content: `Error: ${errorData.messageError}`,
+ });
+ setIsLoading(false);
+ return;
+ }
+ } catch (e) {
+ console.error("Failed to parse error message:", e);
+ }
+ }
+ }
+
+ const files = getFiles();
+ const { messageContent, files: newFiles } = formatResponse(
+ completeResponse,
+ files ?? []
+ );
+ if (messageContent) updateLastMessage(messageContent);
+ if (newFiles && newFiles.length > 0) {
+ if (!isFollowUp) {
+ setFiles(newFiles);
+ }
+ updateLastMessage(messageContent, newFiles);
+ }
+ read();
+ };
+ return await read();
+ }
+ };
+
+ const stopGeneration = () => {
+ if (abortController.current) {
+ abortController.current.abort();
+ abortController.current = null;
+ setIsLoading(false);
+ const currentMessages = getMessages();
+ const lastMessageId = currentMessages[currentMessages.length - 1].id;
+ updateMessage(lastMessageId, {
+ isAborted: true,
+ isThinking: false,
+ });
+ }
+ };
+
+ return {
+ callAi,
+ isLoading,
+ stopGeneration,
+ files: getFiles(),
+ createProject,
+ audio,
+ };
+};
diff --git a/components/chat/index.tsx b/components/chat/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..09b610f3ee6927fd2b0b64bb149c5be2c04fd56e
--- /dev/null
+++ b/components/chat/index.tsx
@@ -0,0 +1,315 @@
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { useSession } from "next-auth/react";
+import { cn } from "@/lib/utils";
+import { ChevronRight, ExternalLink } from "lucide-react";
+import { useEffect, useRef } from "react";
+import Markdown from "react-markdown";
+import { useQueryClient } from "@tanstack/react-query";
+import { formatDistanceToNow } from "date-fns";
+import { SpaceEntry } from "@huggingface/hub";
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
+
+import { useChat } from "./useChat";
+import { AiLoading } from "../ask-ai/loading";
+import Loading from "../loading";
+import { useGeneration } from "../ask-ai/useGeneration";
+import { MessageAction, MessageActionType, File } from "@/lib/type";
+import { Button } from "../ui/button";
+import { getFileIcon } from "../ask-ai/input-mentions";
+
+export function AppEditorChat({
+ isNew,
+ projectName,
+ onSelectFile,
+}: {
+ isNew?: boolean;
+ projectName?: string;
+ onSelectFile: (file: string) => void;
+}) {
+ const chatContainerRef = useRef(null);
+ const { data: session } = useSession();
+ const queryClient = useQueryClient();
+ const chatProjectName = isNew ? "new" : projectName ?? "new";
+ const { messages } = useChat(chatProjectName);
+ const { isLoading, createProject } = useGeneration(chatProjectName);
+
+ const project = queryClient.getQueryData(["project"]);
+ const files = queryClient.getQueryData(["files"]) ?? [];
+
+ const handleShowLastFile = (files?: string[]) => {
+ if (files && files.length > 0) {
+ const lastGeneratedFile = files[files.length - 1];
+ if (lastGeneratedFile) {
+ onSelectFile?.(lastGeneratedFile);
+ }
+ }
+ };
+ const handleActions = (action: MessageAction, messageId: string) => {
+ if (!action) return;
+ switch (action.type) {
+ case MessageActionType.PUBLISH_PROJECT:
+ return createProject(
+ files ?? [],
+ action.projectTitle ?? "",
+ messageId,
+ action.prompt ?? ""
+ );
+ case MessageActionType.SEE_LIVE_PREVIEW:
+ return window.open(
+ `https://huggingface.co/spaces/${project?.name}`,
+ "_blank"
+ );
+ }
+ };
+
+ useEffect(() => {
+ if (chatContainerRef.current) {
+ chatContainerRef.current.scrollTop =
+ chatContainerRef.current.scrollHeight;
+ }
+ }, [messages]);
+
+ return (
+
+ {isNew ? (
+
+ Start a new conversation with the AI
+
+ ) : (
+
+ Your last conversation has not been restored.
+
+ )}
+
+ {messages.map((message, id) => (
+
+
+
+
+ {message.role === "user"
+ ? session?.user?.name?.charAt(0) ?? ""
+ : ""}
+
+
+
+
+
(
+ {children}
+ ),
+ code: ({ className, children }) => {
+ const isCodeBlock = className?.startsWith("language-");
+ const language = className?.replace("language-", "");
+ if (isCodeBlock) {
+ return (
+
+ {String(children).trim()}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+ },
+ ul: ({ children }) => (
+
+ ),
+ ol: ({ children }) => (
+
+ {children}
+
+ ),
+ li: ({ children }) => (
+ {children}
+ ),
+ a: ({ children, href }) => (
+
+
+ {children}
+
+ ),
+ pre: ({ children }) => <>{children}>,
+ p: ({ children }) => {
+ if (
+ typeof children === "string" &&
+ String(children).includes("file:/")
+ ) {
+ const parts = children.split(/(file:\/\S+)/g);
+ return (
+
+ {parts.map((part, index) =>
+ part.startsWith("file:/") ? (
+
+ {getFileIcon(part, "size-2.5")}
+ {part.replace("file:/", "")}
+
+ ) : (
+ part
+ )
+ )}
+
+ );
+ }
+ return {children}
;
+ },
+ }}
+ >
+ {message.content}
+
+ {message.isThinking && (
+
+ )}
+ {message.isAborted && (
+
+ The request has been aborted due to an error OR the user has
+ stopped the generation.
+
+ )}
+
+ {message.model && message.createdAt && (
+
+
+ via{" "}
+
+ {message.model}
+
+
+ {!message.isThinking && (
+
+ {formatDistanceToNow(message.createdAt, {
+ addSuffix: true,
+ })}
+
+ )}
+
+ )}
+ {message.files && message.files.length > 0 && (
+
+
+ {isLoading &&
+ messages[messages.length - 1].id === message.id ? (
+ <>
+
+
+ {isNew ? "Creating" : "Editing"}{" "}
+
+
+ >
+ ) : (
+ <>
+
+ {isNew ? "Created" : "Edited"}{" "}
+
+ {message.files.length > 1 && (
+
+ (+ {message.files.length - 1} files)
+
+ )}
+
+
handleShowLastFile(message.files)}
+ >
+
+
+ >
+ )}
+
+
+ )}
+ {message.actions &&
+ message.actions.length > 0 &&
+ messages.length - 1 === id && (
+
+ {message.actions.map((action, id) => (
+ handleActions(action, message.id)}
+ >
+ {action.loading && (
+
+ )}
+ {action.label}
+
+ ))}
+
+ )}
+
+
+ ))}
+
+
+ );
+}
+
+const FileCode = ({ file }: { file: string }) => (
+
+ {file}
+
+);
diff --git a/components/chat/useChat.ts b/components/chat/useChat.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77d6eaa19e3b96c796fd1663d40a8a4f828468eb
--- /dev/null
+++ b/components/chat/useChat.ts
@@ -0,0 +1,121 @@
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useLocalStorage } from "react-use";
+import { useEffect } from "react";
+import { v4 as uuidv4 } from "uuid";
+
+import { File, Message } from "@/lib/type";
+
+const MESSAGES_QUERY_KEY = (projectName: string) =>
+ ["messages", projectName] as const;
+const DEFAULT_FIRST_MESSAGE = (projectName: string): Message[] => {
+ if (projectName === "new") {
+ return [
+ {
+ id: uuidv4(),
+ role: "assistant",
+ content: `Hello! Welcome to DeepSite!
+Ask me anything about DeepSite and AI, and I'll help you build it.`,
+ createdAt: new Date(),
+ },
+ ];
+ }
+ return [
+ {
+ id: uuidv4(),
+ role: "assistant",
+ content: "Welcome back to your project! 🤩",
+ createdAt: new Date(),
+ },
+ ];
+};
+
+export function useChat(projectName: string) {
+ const queryClient = useQueryClient();
+ const [, setStoredMessages, clearStoredMessages] = useLocalStorage(
+ `messages-${projectName}`,
+ []
+ );
+
+ useEffect(() => {
+ if (projectName !== "new") {
+ queryClient.invalidateQueries({
+ queryKey: MESSAGES_QUERY_KEY(projectName),
+ });
+ }
+ }, [projectName, queryClient]);
+
+ const { data: messages = [] } = useQuery({
+ queryKey: MESSAGES_QUERY_KEY(projectName),
+ queryFn: () => {
+ if (projectName === "new") {
+ return DEFAULT_FIRST_MESSAGE(projectName);
+ }
+ const storedData = localStorage.getItem(`messages-${projectName}`);
+ if (storedData) {
+ try {
+ const parsedMessages = JSON.parse(storedData);
+ if (parsedMessages && parsedMessages.length > 0) {
+ return parsedMessages;
+ }
+ } catch (error) {
+ console.error("Failed to parse stored messages:", error);
+ }
+ }
+ return DEFAULT_FIRST_MESSAGE(projectName);
+ },
+ refetchOnMount: false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ refetchInterval: false,
+ refetchIntervalInBackground: false,
+ });
+
+ const addMessage = (message: Omit) => {
+ const id = uuidv4();
+ queryClient.setQueryData(
+ MESSAGES_QUERY_KEY(projectName),
+ (oldMessages = []) => {
+ const newMessages = [
+ ...oldMessages,
+ {
+ ...message,
+ id,
+ },
+ ];
+ if (projectName !== "new") {
+ setStoredMessages(newMessages);
+ }
+ return newMessages;
+ }
+ );
+ return id;
+ };
+ const clearMessages = () => {
+ queryClient.setQueryData(MESSAGES_QUERY_KEY(projectName), []);
+ clearStoredMessages();
+ };
+
+ const storeMessages = async (newProjectName: string) => {
+ return new Promise((resolve) => {
+ const currentMessages = queryClient.getQueryData(
+ MESSAGES_QUERY_KEY("new")
+ );
+ localStorage.setItem(
+ `messages-${newProjectName}`,
+ JSON.stringify(currentMessages)
+ );
+ queryClient.setQueryData(
+ MESSAGES_QUERY_KEY(newProjectName),
+ currentMessages
+ );
+ setTimeout(() => resolve(true), 100);
+ });
+ };
+
+ return {
+ messages,
+ addMessage,
+ clearMessages,
+ storeMessages,
+ };
+}
diff --git a/components/code/index.tsx b/components/code/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6bb6fb494d884472a9e5b2c5bd20a830c3241f9a
--- /dev/null
+++ b/components/code/index.tsx
@@ -0,0 +1,215 @@
+import { useState } from "react";
+import {
+ SandpackLayout,
+ SandpackFileExplorer,
+} from "@codesandbox/sandpack-react";
+import { ChevronRight, ChevronLeft } from "lucide-react";
+import { useParams } from "next/navigation";
+import { useQueryClient } from "@tanstack/react-query";
+import { format } from "date-fns";
+import { useUpdateEffect } from "react-use";
+
+import { AppEditorMonacoEditor } from "./monaco-editor";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import Loading from "../loading";
+import { useManualUpdates } from "../projects/useManualUpdates";
+import { ProjectWithCommits } from "@/actions/projects";
+
+export function AppEditorCode() {
+ const queryClient = useQueryClient();
+ const { isManuallyUpdatedSameAsFiles } = useManualUpdates();
+ const { repoId } = useParams<{ repoId: string }>();
+
+ const [isFileExplorerCollapsed, setIsFileExplorerCollapsed] = useState(true);
+ const [isSavingChanges, setIsSavingChanges] = useState(false);
+ const [isSavingChangesSuccess, setIsSavingChangesSuccess] = useState(false);
+ const [isSavingChangesError, setIsSavingChangesError] = useState(false);
+ const [isClosing, setIsClosing] = useState(false);
+ const [showSaveChanges, setShowSaveChanges] = useState(false);
+
+ const handleSaveChanges = async () => {
+ if (repoId === "new") return;
+ setIsSavingChanges(true);
+ const manuallyUpdatedFiles =
+ queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
+ const response = await fetch(`/api/projects/${repoId}`, {
+ method: "PUT",
+ body: JSON.stringify({
+ files: manuallyUpdatedFiles,
+ prompt: `✍️ ${format(new Date(), "dd/MM")} - ${format(
+ new Date(),
+ "HH:mm"
+ )} - Manual changes.`,
+ isManualChanges: true,
+ }),
+ }).then(async (response) => {
+ if (response.ok) {
+ const data = await response.json();
+ return data;
+ }
+ throw new Error("Failed to save changes");
+ });
+ if (response.success) {
+ setIsSavingChangesSuccess(true);
+ queryClient.invalidateQueries({ queryKey: ["manuallyUpdatedFiles"] });
+ queryClient.setQueryData(
+ ["project"],
+ (oldProject: ProjectWithCommits) => ({
+ ...oldProject,
+ commits: [response.commit, ...(oldProject?.commits ?? [])],
+ })
+ );
+ } else {
+ setIsSavingChangesError(true);
+ }
+ setIsSavingChanges(false);
+ setShowSaveChanges(false);
+ };
+
+ const undoChanges = () => {
+ queryClient.setQueryData(["manuallyUpdatedFiles"], []);
+ setShowSaveChanges(false);
+ };
+
+ useUpdateEffect(() => {
+ if (isSavingChangesSuccess) {
+ setTimeout(() => setIsSavingChangesSuccess(false), 3000);
+ }
+ }, [isSavingChangesSuccess]);
+
+ useUpdateEffect(() => {
+ if (isClosing) {
+ setTimeout(() => {
+ setIsSavingChangesSuccess(false);
+ setIsSavingChangesError(false);
+ setIsClosing(false);
+ }, 300);
+ }
+ }, [isClosing]);
+
+ return (
+
+
+ setIsFileExplorerCollapsed(!isFileExplorerCollapsed)}
+ title={
+ isFileExplorerCollapsed
+ ? "Expand file explorer"
+ : "Collapse file explorer"
+ }
+ >
+ {isFileExplorerCollapsed ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {repoId && (
+
+
+
+
+ {isSavingChangesSuccess
+ ? "Changes saved"
+ : isSavingChangesError
+ ? "Failed to save changes"
+ : "Save Changes"}
+
+
+ {isSavingChangesSuccess
+ ? "Changes saved successfully"
+ : isSavingChangesError
+ ? "Something went wrong, please try again."
+ : "You have unsaved manual changes. Click the button to save your changes."}
+
+
+
+ {!isSavingChangesSuccess ||
+ (!isSavingChanges && (
+
+ Undo
+
+ ))}
+
+ {isSavingChangesSuccess || isSavingChangesError ? (
+ setIsClosing(true)}
+ >
+ Close
+
+ ) : (
+
+ Save Changes
+ {isSavingChanges && (
+
+ )}
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/components/code/monaco-editor.tsx b/components/code/monaco-editor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b8839f74b26d722b26d4d8d682b7efa737eaaa2f
--- /dev/null
+++ b/components/code/monaco-editor.tsx
@@ -0,0 +1,149 @@
+import { useMemo, useRef, useCallback, useEffect } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { Monaco } from "@monaco-editor/react";
+import {
+ useActiveCode,
+ SandpackStack,
+ FileTabs,
+ useSandpack,
+} from "@codesandbox/sandpack-react";
+import { useTheme } from "next-themes";
+
+import NightLight from "@/components/editor/night-light.json";
+import Night from "@/components/editor/night.json";
+import { File } from "@/lib/type";
+import dynamic from "next/dynamic";
+
+const Editor = dynamic(
+ () => import("@monaco-editor/react").then((mod) => mod.Editor),
+ { ssr: false }
+);
+
+const LANGUAGE_MAP = {
+ js: "javascript",
+ ts: "typescript",
+ html: "html",
+ css: "css",
+ json: "json",
+ txt: "text",
+};
+
+export function AppEditorMonacoEditor({
+ setShowSaveChanges,
+}: {
+ setShowSaveChanges: (show: boolean) => void;
+}) {
+ const { theme } = useTheme();
+ const { code, updateCode } = useActiveCode();
+ const { sandpack } = useSandpack();
+ const queryClient = useQueryClient();
+ const updateTimeoutRef = useRef(null);
+
+ const handleEditorDidMount = (monaco: Monaco) => {
+ monaco.editor.defineTheme("NightLight", {
+ base: "vs",
+ inherit: true,
+ ...NightLight,
+ rules: [],
+ });
+ monaco.editor.defineTheme("Night", {
+ base: "vs-dark",
+ ...Night,
+ rules: [],
+ });
+ };
+
+ const language = useMemo(() => {
+ const extension = sandpack.activeFile
+ .split(".")
+ .pop()
+ ?.toLowerCase() as string;
+ return LANGUAGE_MAP[extension as keyof typeof LANGUAGE_MAP] ?? "text";
+ }, [sandpack.activeFile]);
+
+ const updateFile = useCallback(
+ (newValue: string, activeFile: string) => {
+ if (updateTimeoutRef.current) {
+ clearTimeout(updateTimeoutRef.current);
+ }
+
+ updateTimeoutRef.current = setTimeout(() => {
+ const manuallyUpdatedFiles =
+ queryClient.getQueryData(["manuallyUpdatedFiles"]) ?? [];
+ const fileIndex = manuallyUpdatedFiles.findIndex(
+ (file) => file.path === activeFile
+ );
+ if (fileIndex !== -1) {
+ manuallyUpdatedFiles[fileIndex].content = newValue;
+ } else {
+ manuallyUpdatedFiles.push({
+ path: activeFile,
+ content: newValue,
+ });
+ }
+ queryClient.setQueryData(
+ ["manuallyUpdatedFiles"],
+ manuallyUpdatedFiles
+ );
+ }, 100);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [queryClient]
+ );
+
+ const handleEditorChange = (value: string | undefined) => {
+ setShowSaveChanges(true);
+ const newValue = value || "";
+ updateCode(newValue);
+ const activeFile = sandpack.activeFile?.replace(/^\//, "");
+ updateFile(newValue, activeFile);
+ };
+
+ useEffect(() => {
+ return () => {
+ if (updateTimeoutRef.current) {
+ clearTimeout(updateTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/components/code/useEditor.ts b/components/code/useEditor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..06ed500729a17413283a42c8f9bf395f255b1518
--- /dev/null
+++ b/components/code/useEditor.ts
@@ -0,0 +1,71 @@
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useProject } from "@/components/projects/useProject";
+import { useMemo } from "react";
+
+const LANGUAGE_MAP = {
+ "py": "python",
+ "js": "javascript",
+ "ts": "typescript",
+ "html": "html",
+ "css": "css",
+ "json": "json",
+ "txt": "text",
+}
+
+export const useEditor = () => {
+ const queryClient = useQueryClient();
+ const { files } = useProject();
+
+ const { data: isFileListOpen } = useQuery({
+ queryKey: ["isFileListOpen"],
+ queryFn: () => {
+ return false;
+ },
+ initialData: false,
+ refetchInterval: false,
+ refetchOnWindowFocus: false,
+ });
+
+ const setIsFileListOpen = (isOpen: boolean) => {
+ queryClient.setQueryData(["isFileListOpen"], () => isOpen);
+ }
+
+ const { data: editorFilePath } = useQuery({
+ queryKey: ["editorFile"],
+ queryFn: () => {
+ return "app.py";
+ },
+ initialData: "app.py",
+ refetchInterval: false,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchIntervalInBackground: false,
+ });
+
+ const setEditorFilePath = (path: string) => {
+ queryClient.setQueryData(["editorFile"], () => path);
+ }
+
+ const editorFileData = useMemo(() => {
+ const finalFile = { path: "", content: "", language: "" };
+ if (editorFilePath) {
+ const file = files?.find((file) => file.path === editorFilePath);
+ if (file) {
+ finalFile.path = file.path;
+ finalFile.content = file.content ?? "";
+ finalFile.language = LANGUAGE_MAP[file.path.split(".").pop()?.toLowerCase() as keyof typeof LANGUAGE_MAP] ?? "text";
+ }
+ }
+
+ return finalFile
+ }, [editorFilePath, files])
+
+ return {
+ editorFilePath,
+ editorFileData,
+ setEditorFilePath,
+ isFileListOpen,
+ setIsFileListOpen,
+ }
+}
\ No newline at end of file
diff --git a/components/editor/commits.tsx b/components/editor/commits.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5e9d4ad4eed0e93b06ed81e51a03a45a408bdabb
--- /dev/null
+++ b/components/editor/commits.tsx
@@ -0,0 +1,115 @@
+import { useRef, useState } from "react";
+import { HistoryIcon } from "lucide-react";
+import { useSearchParams } from "next/navigation";
+import { useUpdateEffect } from "react-use";
+
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Commit } from "@/lib/type";
+import { cn, humanizeNumber } from "@/lib/utils";
+
+export const Commits = function ({ commits }: { commits?: Commit[] }) {
+ const searchParams = useSearchParams();
+ const commitParam = searchParams.get("commit");
+
+ const commitsContainer = useRef(null);
+ const commitSelected = useRef(null);
+ const [open, setOpen] = useState(false);
+ const [selectedCommit, setSelectedCommit] = useState(
+ commitParam
+ ? commitParam
+ : commits && commits.length > 0
+ ? commits[0]?.oid
+ : null
+ );
+
+ const handleViewCommit = (commitId: string) => {
+ let url = window.location.pathname;
+ if (commitId !== commits?.[0].oid) {
+ url += `?commit=${commitId}`;
+ }
+ window.location.replace(url);
+ };
+
+ useUpdateEffect(() => {
+ if (open) {
+ setTimeout(() => {
+ if (commitSelected.current && commitsContainer.current) {
+ const container = commitsContainer.current;
+ const selected = commitSelected.current;
+ if (!container || !selected) return;
+ const offsetTop = selected.offsetTop;
+ const offsetHeight = selected.offsetHeight;
+ const containerHeight = container.offsetHeight;
+ const scrollTop = offsetTop - containerHeight / 2 + offsetHeight / 2;
+ container.scrollTo({
+ top: scrollTop,
+ behavior: "smooth",
+ });
+ }
+ }, 200);
+ }
+ }, [open]);
+
+ useUpdateEffect(() => {
+ if (selectedCommit !== commits?.[0].oid) {
+ setSelectedCommit(commits ? commits[0]?.oid : null);
+ }
+ }, [commits]);
+
+ return (
+
+
+
+ );
+};
diff --git a/components/editor/header.tsx b/components/editor/header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a783e7440141dfd7c10d475d77d81dbb4aaa4ca7
--- /dev/null
+++ b/components/editor/header.tsx
@@ -0,0 +1,208 @@
+"use client";
+
+import { useState } from "react";
+import {
+ Earth,
+ RefreshCw,
+ Monitor,
+ Smartphone,
+ Code,
+ ChevronDown,
+ MessageCircle,
+ ExternalLink,
+} from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import { useSandpackNavigation } from "@codesandbox/sandpack-react";
+
+import { Button } from "@/components/ui/button";
+import { ActivityType, DeviceType, MobileTabType } from "@/lib/type";
+import { UserMenu } from "@/components/user-menu";
+import { useProject } from "@/components/projects/useProject";
+import { cn } from "@/lib/utils";
+import { Commits } from "./commits";
+
+export function AppEditorHeader({
+ currentActivity,
+ onToggleActivity,
+ onToggleDevice,
+ device,
+ mobileTab,
+ isHistoryView,
+}: {
+ isNew?: boolean;
+ isHistoryView?: boolean;
+ currentActivity: ActivityType;
+ onToggleActivity: (activity: ActivityType) => void;
+ onToggleDevice: () => void;
+ device: DeviceType;
+ mobileTab: MobileTabType;
+ onToggleMobileTab: (tab: MobileTabType) => void;
+}) {
+ const { project, files } = useProject();
+ const { refresh } = useSandpackNavigation();
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [isHovered, setIsHovered] = useState(false);
+
+ const handleRefresh = () => {
+ setIsRefreshing(true);
+ setTimeout(() => {
+ refresh();
+ setIsRefreshing(false);
+ }, 1000);
+ };
+
+ const handleToggleActivity = () => {
+ onToggleActivity(currentActivity === "chat" ? "code" : "chat");
+ };
+
+ return (
+
+ );
+}
diff --git a/components/editor/history-view.tsx b/components/editor/history-view.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..99bb8805aef5da7fe8b0cd702edd00b90d9e7fc6
--- /dev/null
+++ b/components/editor/history-view.tsx
@@ -0,0 +1,84 @@
+import { Button } from "@/components/ui/button";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+import { useParams, useSearchParams } from "next/navigation";
+import { toast } from "sonner";
+
+export const HistoryView = function () {
+ const { repoId } = useParams<{ repoId: string }>();
+ const searchParams = useSearchParams();
+ const commitId = searchParams.get("commit");
+
+ const handleSetAsDefault = async () => {
+ const confirmation = confirm(
+ "This action will set this historical version as the default version of the project. Are you sure you want to proceed?"
+ );
+ if (!confirmation) return;
+ const response = await fetch(`/api/projects/${repoId}/${commitId}`, {
+ method: "POST",
+ }).then(async (response) => {
+ if (response.ok) {
+ const data = await response.json();
+ return data;
+ }
+ throw new Error("Failed to save changes");
+ });
+ if (response.success) {
+ toast.success("Set as default version successfully!");
+ setTimeout(() => {
+ close();
+ }, 500);
+ } else {
+ alert("Failed to set as default version, try again later.");
+ }
+ };
+
+ const close = () => {
+ window.location.replace(window.location.pathname);
+ };
+
+ return (
+
+
+
+
+
close()}
+ >
+
+ Go Back
+
+
+ Set as Default Version
+
+
+
+
+ );
+};
diff --git a/components/editor/index.tsx b/components/editor/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..639c02bea263e336732a40a12a701ded623d2703
--- /dev/null
+++ b/components/editor/index.tsx
@@ -0,0 +1,148 @@
+"use client";
+import { useState, useMemo } from "react";
+import dynamic from "next/dynamic";
+import { useBeforeUnload } from "react-use";
+import { ArrowRight } from "lucide-react";
+import { useTheme } from "next-themes";
+import { amethyst } from "@codesandbox/sandpack-themes";
+
+import { AppEditorHeader } from "./header";
+import { AppViewer } from "@/components/viewer";
+import { ActivityType, DeviceType, File, MobileTabType } from "@/lib/type";
+import { useProject } from "@/components/projects/useProject";
+import { AskAI } from "@/components/ask-ai/ask-ai";
+import { AppEditorChat } from "@/components/chat";
+import { cn, defaultHTML } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { AppEditorCode } from "@/components/code";
+import { ProjectWithCommits } from "@/actions/projects";
+import { HistoryView } from "./history-view";
+
+const SandpackProvider = dynamic(
+ () =>
+ import("@codesandbox/sandpack-react").then((mod) => mod.SandpackProvider),
+ { ssr: false }
+);
+
+export function AppEditor({
+ project: initialProject,
+ files: initialFiles,
+ initialPrompt,
+ isNew = false,
+ isHistoryView = false,
+}: {
+ project?: ProjectWithCommits | null;
+ files?: File[];
+ initialPrompt?: string;
+ isNew?: boolean;
+ isHistoryView?: boolean;
+}) {
+ const [currentActivity, setCurrentActivity] = useState("chat");
+ const [device, setDevice] = useState("desktop");
+ const [mobileTab, setMobileTab] = useState("left-sidebar");
+ const { project, files } = useProject(initialProject, initialFiles, isNew);
+ const { resolvedTheme } = useTheme();
+ const projectName = isNew ? "new" : project?.name ?? "new";
+
+ const toggleDevice = () => {
+ setDevice((prev) => (prev === "desktop" ? "mobile" : "desktop"));
+ };
+
+ useBeforeUnload(
+ !project && (files?.length ?? 0) > 0,
+ "You have unsaved changes, are you sure?"
+ );
+
+ const sandpackFiles = useMemo(() => {
+ if (files && files.length > 0) {
+ return files.reduce(
+ (acc, file) => ({
+ ...acc,
+ [file.path]: { code: file.content ?? "" },
+ }),
+ {}
+ );
+ } else {
+ return {
+ "/index.html": {
+ code: defaultHTML,
+ },
+ };
+ }
+ }, [files]);
+
+ return (
+
+
+
+
+ {currentActivity === "chat" ? (
+
+
{
+ setCurrentActivity("code");
+ }}
+ />
+
+
+
setMobileTab("right-sidebar")}
+ >
+ Go to Preview
+
+
+
+
+
+ ) : (
+
+ )}
+ {isHistoryView &&
}
+
+
+
+
+ );
+}
diff --git a/components/editor/night-light.json b/components/editor/night-light.json
new file mode 100644
index 0000000000000000000000000000000000000000..77ed5495abb7d663d2d2002a5a750080ba6ed679
--- /dev/null
+++ b/components/editor/night-light.json
@@ -0,0 +1,1728 @@
+{
+ "name": "Night Owl Light",
+ "type": "light",
+ "semanticHighlighting": true,
+ "colors": {
+ "foreground": "#403f53",
+ "focusBorder": "#93A1A1",
+ "errorForeground": "#403f53",
+ "selection.background": "#7a8181ad",
+ "descriptionForeground": "#403f53",
+ "widget.shadow": "#d9d9d9",
+ "titleBar.activeBackground": "#F0F0F0",
+ "notifications.background": "#F0F0F0",
+ "notifications.foreground": "#403f53",
+ "notificationLink.foreground": "#994cc3",
+ "notifications.border": "#CCCCCC",
+ "notificationCenter.border": "#CCCCCC",
+ "notificationToast.border": "#CCCCCC",
+ "notificationCenterHeader.foreground": "#403f53",
+ "notificationCenterHeader.background": "#F0F0F0",
+ "button.background": "#2AA298",
+ "button.foreground": "#F0F0F0",
+ "dropdown.background": "#F0F0F0",
+ "dropdown.foreground": "#403f53",
+ "dropdown.border": "#d9d9d9",
+ "input.background": "#F0F0F0",
+ "input.foreground": "#403f53",
+ "input.border": "#d9d9d9",
+ "input.placeholderForeground": "#93A1A1",
+ "inputOption.activeBorder": "#2AA298",
+ "inputValidation.infoBorder": "#D0D0D0",
+ "inputValidation.infoBackground": "#F0F0F0",
+ "inputValidation.warningBackground": "#daaa01",
+ "inputValidation.warningBorder": "#E0AF02",
+ "inputValidation.errorBackground": "#f76e6e",
+ "inputValidation.errorBorder": "#de3d3b",
+ "badge.background": "#2AA298",
+ "badge.foreground": "#F0F0F0",
+ "progressBar.background": "#2AA298",
+ "list.activeSelectionBackground": "#d3e8f8",
+ "list.activeSelectionForeground": "#403f53",
+ "list.inactiveSelectionBackground": "#E0E7EA",
+ "list.inactiveSelectionForeground": "#403f53",
+ "list.focusBackground": "#d3e8f8",
+ "list.hoverBackground": "#d3e8f8",
+ "list.focusForeground": "#403f53",
+ "list.hoverForeground": "#403f53",
+ "list.highlightForeground": "#403f53",
+ "list.errorForeground": "#E64D49",
+ "list.warningForeground": "#daaa01",
+ "activityBar.background": "#F0F0F0",
+ "activityBar.foreground": "#403f53",
+ "activityBar.dropBackground": "#D0D0D0",
+ "activityBarBadge.background": "#403f53",
+ "activityBarBadge.foreground": "#F0F0F0",
+ "activityBar.border": "#F0F0F0",
+ "sideBar.background": "#F0F0F0",
+ "sideBar.foreground": "#403f53",
+ "sideBarTitle.foreground": "#403f53",
+ "sideBar.border": "#F0F0F0",
+ "scrollbar.shadow": "#CCCCCC",
+ "tab.border": "#F0F0F0",
+ "tab.activeBackground": "#F6F6F6",
+ "tab.activeForeground": "#403f53",
+ "tab.inactiveForeground": "#403f53",
+ "tab.inactiveBackground": "#F0F0F0",
+ "editorGroup.border": "#F0F0F0",
+ "editorGroup.background": "#F6F6F6",
+ "editorGroupHeader.tabsBackground": "#F0F0F0",
+ "editorGroupHeader.tabsBorder": "#F0F0F0",
+ "editorGroupHeader.noTabsBackground": "#F0F0F0",
+ "tab.activeModifiedBorder": "#2AA298",
+ "tab.inactiveModifiedBorder": "#93A1A1",
+ "tab.unfocusedActiveModifiedBorder": "#93A1A1",
+ "tab.unfocusedInactiveModifiedBorder": "#93A1A1",
+ "editor.background": "#FBFBFB",
+ "editor.foreground": "#403f53",
+ "editorCursor.foreground": "#90A7B2",
+ "editorLineNumber.foreground": "#90A7B2",
+ "editorLineNumber.activeForeground": "#403f53",
+ "editor.selectionBackground": "#E0E0E0",
+ "editor.selectionHighlightBackground": "#339cec33",
+ "editor.wordHighlightBackground": "#339cec33",
+ "editor.wordHighlightStrongBackground": "#007dd659",
+ "editor.findMatchBackground": "#93A1A16c",
+ "editor.findMatchHighlightBackground": "#93a1a16c",
+ "editor.findRangeHighlightBackground": "#7497a633",
+ "editor.hoverHighlightBackground": "#339cec33",
+ "editor.lineHighlightBackground": "#F0F0F0",
+ "editor.rangeHighlightBackground": "#000000",
+ "editorWhitespace.foreground": "#d9d9d9",
+ "editorIndentGuide.background": "#d9d9d9",
+ "editorCodeLens.foreground": "#403f53",
+ "editorError.foreground": "#E64D49",
+ "editorError.border": "#FBFBFB",
+ "editorWarning.foreground": "#daaa01",
+ "editorWarning.border": "#daaa01",
+ "editorGutter.addedBackground": "#49d0c5",
+ "editorGutter.modifiedBackground": "#6fbef6",
+ "editorGutter.deletedBackground": "#f76e6e",
+ "editorRuler.foreground": "#d9d9d9",
+ "editorOverviewRuler.errorForeground": "#E64D49",
+ "editorOverviewRuler.warningForeground": "#daaa01",
+ "editorInlayHint.background": "#F0F0F0",
+ "editorInlayHint.foreground": "#403f53",
+ "editorWidget.background": "#F0F0F0",
+ "editorWidget.border": "#d9d9d9",
+ "editorSuggestWidget.background": "#F0F0F0",
+ "editorSuggestWidget.foreground": "#403f53",
+ "editorSuggestWidget.highlightForeground": "#403f53",
+ "editorSuggestWidget.selectedBackground": "#d3e8f8",
+ "editorSuggestWidget.border": "#d9d9d9",
+ "editorHoverWidget.background": "#F0F0F0",
+ "editorHoverWidget.border": "#d9d9d9",
+ "debugExceptionWidget.background": "#F0F0F0",
+ "debugExceptionWidget.border": "#d9d9d9",
+ "editorMarkerNavigation.background": "#D0D0D0",
+ "editorMarkerNavigationError.background": "#f76e6e",
+ "editorMarkerNavigationWarning.background": "#daaa01",
+ "debugToolBar.background": "#F0F0F0",
+ "pickerGroup.border": "#d9d9d9",
+ "pickerGroup.foreground": "#403f53",
+ "extensionButton.prominentBackground": "#2AA298",
+ "extensionButton.prominentForeground": "#F0F0F0",
+ "statusBar.background": "#F0F0F0",
+ "statusBar.border": "#F0F0F0",
+ "statusBar.debuggingBackground": "#F0F0F0",
+ "statusBar.debuggingForeground": "#403f53",
+ "statusBar.foreground": "#403f53",
+ "statusBar.noFolderBackground": "#F0F0F0",
+ "statusBar.noFolderForeground": "#403f53",
+ "panel.background": "#F0F0F0",
+ "panel.border": "#d9d9d9",
+ "peekView.border": "#d9d9d9",
+ "peekViewEditor.background": "#F6F6F6",
+ "peekViewEditorGutter.background": "#F6F6F6",
+ "peekViewEditor.matchHighlightBackground": "#49d0c5",
+ "peekViewResult.background": "#F0F0F0",
+ "peekViewResult.fileForeground": "#403f53",
+ "peekViewResult.lineForeground": "#403f53",
+ "peekViewResult.matchHighlightBackground": "#49d0c5",
+ "peekViewResult.selectionBackground": "#E0E7EA",
+ "peekViewResult.selectionForeground": "#403f53",
+ "peekViewTitle.background": "#F0F0F0",
+ "peekViewTitleLabel.foreground": "#403f53",
+ "peekViewTitleDescription.foreground": "#403f53",
+ "terminal.ansiBrightBlack": "#403f53",
+ "terminal.ansiBlack": "#403f53",
+ "terminal.ansiBrightBlue": "#288ed7",
+ "terminal.ansiBlue": "#288ed7",
+ "terminal.ansiBrightCyan": "#2AA298",
+ "terminal.ansiCyan": "#2AA298",
+ "terminal.ansiBrightGreen": "#08916a",
+ "terminal.ansiGreen": "#08916a",
+ "terminal.ansiBrightMagenta": "#d6438a",
+ "terminal.ansiMagenta": "#d6438a",
+ "terminal.ansiBrightRed": "#de3d3b",
+ "terminal.ansiRed": "#de3d3b",
+ "terminal.ansiBrightWhite": "#93A1A1",
+ "terminal.ansiWhite": "#93A1A1",
+ "terminal.ansiBrightYellow": "#daaa01",
+ "terminal.ansiYellow": "#E0AF02",
+ "terminal.background": "#F6F6F6",
+ "terminal.foreground": "#403f53"
+ },
+ "tokenColors": [
+ {
+ "name": "Changed",
+ "scope": [
+ "markup.changed",
+ "meta.diff.header.git",
+ "meta.diff.header.from-file",
+ "meta.diff.header.to-file"
+ ],
+ "settings": {
+ "foreground": "#a2bffc",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Deleted",
+ "scope": "markup.deleted.diff",
+ "settings": {
+ "foreground": "#EF535090",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Inserted",
+ "scope": "markup.inserted.diff",
+ "settings": {
+ "foreground": "#4876d6ff",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Global settings",
+ "settings": {
+ "background": "#011627",
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Comment",
+ "scope": ["comment", "punctuation.definition.comment"],
+ "settings": {
+ "foreground": "#989fb1",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "String",
+ "scope": "string",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "String Quoted",
+ "scope": ["string.quoted", "variable.other.readwrite.js"],
+ "settings": {
+ "foreground": "#c96765"
+ }
+ },
+ {
+ "name": "Support Constant Math",
+ "scope": "support.constant.math",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Number",
+ "scope": ["constant.numeric", "constant.character.numeric"],
+ "settings": {
+ "foreground": "#aa0982",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Built-in constant",
+ "scope": [
+ "constant.language",
+ "punctuation.definition.constant",
+ "variable.other.constant"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "User-defined constant",
+ "scope": ["constant.character", "constant.other"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Constant Character Escape",
+ "scope": "constant.character.escape",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "RegExp String",
+ "scope": ["string.regexp", "string.regexp keyword.other"],
+ "settings": {
+ "foreground": "#5ca7e4"
+ }
+ },
+ {
+ "name": "Comma in functions",
+ "scope": "meta.function punctuation.separator.comma",
+ "settings": {
+ "foreground": "#5f7e97"
+ }
+ },
+ {
+ "name": "Variable",
+ "scope": "variable",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Keyword",
+ "scope": ["punctuation.accessor", "keyword"],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Storage",
+ "scope": [
+ "storage",
+ "meta.var.expr",
+ "meta.class meta.method.declaration meta.var.expr storage.type.js",
+ "storage.type.property.js",
+ "storage.type.property.ts",
+ "storage.type.property.tsx"
+ ],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Storage type",
+ "scope": "storage.type",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "Storage type",
+ "scope": "storage.type.function.arrow.js",
+ "settings": {
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Class name",
+ "scope": ["entity.name.class", "meta.class entity.name.type.class"],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Inherited class",
+ "scope": "entity.other.inherited-class",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Function name",
+ "scope": "entity.name.function",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Meta Tag",
+ "scope": ["punctuation.definition.tag", "meta.tag"],
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "HTML Tag names",
+ "scope": [
+ "entity.name.tag",
+ "meta.tag.other.html",
+ "meta.tag.other.js",
+ "meta.tag.other.tsx",
+ "entity.name.tag.tsx",
+ "entity.name.tag.js",
+ "entity.name.tag",
+ "meta.tag.js",
+ "meta.tag.tsx",
+ "meta.tag.html"
+ ],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Tag attribute",
+ "scope": "entity.other.attribute-name",
+ "settings": {
+ "fontStyle": "italic",
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Entity Name Tag Custom",
+ "scope": "entity.name.tag.custom",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Library (function & constant)",
+ "scope": ["support.function", "support.constant"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Support Constant Property Value meta",
+ "scope": "support.constant.meta.property-value",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Library class/type",
+ "scope": ["support.type", "support.class"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Support Variable DOM",
+ "scope": "support.variable.dom",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Invalid",
+ "scope": "invalid",
+ "settings": {
+ "foreground": "#ff2c83"
+ }
+ },
+ {
+ "name": "Invalid deprecated",
+ "scope": "invalid.deprecated",
+ "settings": {
+ "foreground": "#d3423e"
+ }
+ },
+ {
+ "name": "Keyword Operator",
+ "scope": "keyword.operator",
+ "settings": {
+ "foreground": "#0c969b",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Keyword Operator Relational",
+ "scope": "keyword.operator.relational",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Keyword Operator Assignment",
+ "scope": "keyword.operator.assignment",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "Keyword Operator Arithmetic",
+ "scope": "keyword.operator.arithmetic",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "Keyword Operator Bitwise",
+ "scope": "keyword.operator.bitwise",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "Keyword Operator Increment",
+ "scope": "keyword.operator.increment",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "Keyword Operator Ternary",
+ "scope": "keyword.operator.ternary",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "Double-Slashed Comment",
+ "scope": "comment.line.double-slash",
+ "settings": {
+ "foreground": "#939dbb"
+ }
+ },
+ {
+ "name": "Object",
+ "scope": "object",
+ "settings": {
+ "foreground": "#cdebf7"
+ }
+ },
+ {
+ "name": "Null",
+ "scope": "constant.language.null",
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "Meta Brace",
+ "scope": "meta.brace",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Meta Delimiter Period",
+ "scope": "meta.delimiter.period",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Punctuation Definition String",
+ "scope": "punctuation.definition.string",
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Punctuation Definition String Markdown",
+ "scope": "punctuation.definition.string.begin.markdown",
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "Boolean",
+ "scope": "constant.language.boolean",
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "Object Comma",
+ "scope": "object.comma",
+ "settings": {
+ "foreground": "#ffffff"
+ }
+ },
+ {
+ "name": "Variable Parameter Function",
+ "scope": "variable.parameter.function",
+ "settings": {
+ "foreground": "#0c969b",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Support Type Property Name & entity name tags",
+ "scope": [
+ "support.type.vendor.property-name",
+ "support.constant.vendor.property-value",
+ "support.type.property-name",
+ "meta.property-list entity.name.tag"
+ ],
+ "settings": {
+ "foreground": "#0c969b",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Entity Name tag reference in stylesheets",
+ "scope": "meta.property-list entity.name.tag.reference",
+ "settings": {
+ "foreground": "#57eaf1"
+ }
+ },
+ {
+ "name": "Constant Other Color RGB Value Punctuation Definition Constant",
+ "scope": "constant.other.color.rgb-value punctuation.definition.constant",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Constant Other Color",
+ "scope": "constant.other.color",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Keyword Other Unit",
+ "scope": "keyword.other.unit",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Meta Selector",
+ "scope": "meta.selector",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Entity Other Attribute Name Id",
+ "scope": "entity.other.attribute-name.id",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Meta Property Name",
+ "scope": "meta.property-name",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Doctypes",
+ "scope": ["entity.name.tag.doctype", "meta.tag.sgml.doctype"],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Punctuation Definition Parameters",
+ "scope": "punctuation.definition.parameters",
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Keyword Control Operator",
+ "scope": "keyword.control.operator",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Keyword Operator Logical",
+ "scope": "keyword.operator.logical",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Variable Instances",
+ "scope": [
+ "variable.instance",
+ "variable.other.instance",
+ "variable.readwrite.instance",
+ "variable.other.readwrite.instance",
+ "variable.other.property"
+ ],
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Variable Property Other object property",
+ "scope": ["variable.other.object.property"],
+ "settings": {
+ "foreground": "#111111",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Variable Property Other object",
+ "scope": ["variable.other.object.js"],
+ "settings": {
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Entity Name Function",
+ "scope": ["entity.name.function"],
+ "settings": {
+ "foreground": "#4876d6",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Keyword Operator Comparison, imports, returns and Keyword Operator Ruby",
+ "scope": [
+ "keyword.operator.comparison",
+ "keyword.control.flow.js",
+ "keyword.control.flow.ts",
+ "keyword.control.flow.tsx",
+ "keyword.control.ruby",
+ "keyword.control.module.ruby",
+ "keyword.control.class.ruby",
+ "keyword.control.def.ruby",
+ "keyword.control.loop.js",
+ "keyword.control.loop.ts",
+ "keyword.control.import.js",
+ "keyword.control.import.ts",
+ "keyword.control.import.tsx",
+ "keyword.control.from.js",
+ "keyword.control.from.ts",
+ "keyword.control.from.tsx",
+ "keyword.operator.instanceof.js",
+ "keyword.operator.expression.instanceof.ts",
+ "keyword.operator.expression.instanceof.tsx"
+ ],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Keyword Control Conditional",
+ "scope": [
+ "keyword.control.conditional.js",
+ "keyword.control.conditional.ts",
+ "keyword.control.switch.js",
+ "keyword.control.switch.ts"
+ ],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Support Constant, `new` keyword, Special Method Keyword, `debugger`, other keywords",
+ "scope": [
+ "support.constant",
+ "keyword.other.special-method",
+ "keyword.other.new",
+ "keyword.other.debugger",
+ "keyword.control"
+ ],
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Support Function",
+ "scope": "support.function",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Invalid Broken",
+ "scope": "invalid.broken",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Invalid Unimplemented",
+ "scope": "invalid.unimplemented",
+ "settings": {
+ "foreground": "#8BD649"
+ }
+ },
+ {
+ "name": "Invalid Illegal",
+ "scope": "invalid.illegal",
+ "settings": {
+ "foreground": "#c96765"
+ }
+ },
+ {
+ "name": "Language Variable",
+ "scope": "variable.language",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Support Variable Property",
+ "scope": "support.variable.property",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Variable Function",
+ "scope": "variable.function",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Variable Interpolation",
+ "scope": "variable.interpolation",
+ "settings": {
+ "foreground": "#ec5f67"
+ }
+ },
+ {
+ "name": "Meta Function Call",
+ "scope": "meta.function-call",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Punctuation Section Embedded",
+ "scope": "punctuation.section.embedded",
+ "settings": {
+ "foreground": "#d3423e"
+ }
+ },
+ {
+ "name": "Punctuation Tweaks",
+ "scope": [
+ "punctuation.terminator.expression",
+ "punctuation.definition.arguments",
+ "punctuation.definition.array",
+ "punctuation.section.array",
+ "meta.array"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "More Punctuation Tweaks",
+ "scope": [
+ "punctuation.definition.list.begin",
+ "punctuation.definition.list.end",
+ "punctuation.separator.arguments",
+ "punctuation.definition.list"
+ ],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Template Strings",
+ "scope": "string.template meta.template.expression",
+ "settings": {
+ "foreground": "#d3423e"
+ }
+ },
+ {
+ "name": "Backtics(``) in Template Strings",
+ "scope": "string.template punctuation.definition.string",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Italics",
+ "scope": "italic",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Bold",
+ "scope": "bold",
+ "settings": {
+ "foreground": "#4876d6",
+ "fontStyle": "bold"
+ }
+ },
+ {
+ "name": "Quote",
+ "scope": "quote",
+ "settings": {
+ "foreground": "#697098",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Raw Code",
+ "scope": "raw",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "CoffeScript Variable Assignment",
+ "scope": "variable.assignment.coffee",
+ "settings": {
+ "foreground": "#31e1eb"
+ }
+ },
+ {
+ "name": "CoffeScript Parameter Function",
+ "scope": "variable.parameter.function.coffee",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "CoffeeScript Assignments",
+ "scope": "variable.assignment.coffee",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "C# Readwrite Variables",
+ "scope": "variable.other.readwrite.cs",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "C# Classes & Storage types",
+ "scope": ["entity.name.type.class.cs", "storage.type.cs"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "C# Namespaces",
+ "scope": "entity.name.type.namespace.cs",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Tag names in Stylesheets",
+ "scope": [
+ "entity.name.tag.css",
+ "entity.name.tag.less",
+ "entity.name.tag.custom.css",
+ "support.constant.property-value.css"
+ ],
+ "settings": {
+ "foreground": "#c96765",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Wildcard(*) selector in Stylesheets",
+ "scope": [
+ "entity.name.tag.wildcard.css",
+ "entity.name.tag.wildcard.less",
+ "entity.name.tag.wildcard.scss",
+ "entity.name.tag.wildcard.sass"
+ ],
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "CSS Keyword Other Unit",
+ "scope": "keyword.other.unit.css",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Attribute Name for CSS",
+ "scope": [
+ "meta.attribute-selector.css entity.other.attribute-name.attribute",
+ "variable.other.readwrite.js"
+ ],
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Elixir Classes",
+ "scope": [
+ "source.elixir support.type.elixir",
+ "source.elixir meta.module.elixir entity.name.class.elixir"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Elixir Functions",
+ "scope": "source.elixir entity.name.function",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Elixir Constants",
+ "scope": [
+ "source.elixir constant.other.symbol.elixir",
+ "source.elixir constant.other.keywords.elixir"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Elixir String Punctuations",
+ "scope": "source.elixir punctuation.definition.string",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Elixir",
+ "scope": [
+ "source.elixir variable.other.readwrite.module.elixir",
+ "source.elixir variable.other.readwrite.module.elixir punctuation.definition.variable.elixir"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Elixir Binary Punctuations",
+ "scope": "source.elixir .punctuation.binary.elixir",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Closure Constant Keyword",
+ "scope": "constant.keyword.clojure",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Go Function Calls",
+ "scope": "source.go meta.function-call.go",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Go Keywords",
+ "scope": [
+ "source.go keyword.package.go",
+ "source.go keyword.import.go",
+ "source.go keyword.function.go",
+ "source.go keyword.type.go",
+ "source.go keyword.struct.go",
+ "source.go keyword.interface.go",
+ "source.go keyword.const.go",
+ "source.go keyword.var.go",
+ "source.go keyword.map.go",
+ "source.go keyword.channel.go",
+ "source.go keyword.control.go"
+ ],
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Go Constants e.g. nil, string format (%s, %d, etc.)",
+ "scope": [
+ "source.go constant.language.go",
+ "source.go constant.other.placeholder.go"
+ ],
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "C++ Functions",
+ "scope": [
+ "entity.name.function.preprocessor.cpp",
+ "entity.scope.name.cpp"
+ ],
+ "settings": {
+ "foreground": "#0c969bff"
+ }
+ },
+ {
+ "name": "C++ Meta Namespace",
+ "scope": ["meta.namespace-block.cpp"],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "C++ Language Primitive Storage",
+ "scope": ["storage.type.language.primitive.cpp"],
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "C++ Preprocessor Macro",
+ "scope": ["meta.preprocessor.macro.cpp"],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "C++ Variable Parameter",
+ "scope": ["variable.parameter"],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Powershell Variables",
+ "scope": ["variable.other.readwrite.powershell"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Powershell Function",
+ "scope": ["support.function.powershell"],
+ "settings": {
+ "foreground": "#0c969bff"
+ }
+ },
+ {
+ "name": "ID Attribute Name in HTML",
+ "scope": "entity.other.attribute-name.id.html",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "HTML Punctuation Definition Tag",
+ "scope": "punctuation.definition.tag.html",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "HTML Doctype",
+ "scope": "meta.tag.sgml.doctype.html",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "JavaScript Classes",
+ "scope": "meta.class entity.name.type.class.js",
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "JavaScript Method Declaration e.g. `constructor`",
+ "scope": "meta.method.declaration storage.type.js",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "JavaScript Terminator",
+ "scope": "terminator.js",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "JavaScript Meta Punctuation Definition",
+ "scope": "meta.js punctuation.definition.js",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Entity Names in Code Documentations",
+ "scope": [
+ "entity.name.type.instance.jsdoc",
+ "entity.name.type.instance.phpdoc"
+ ],
+ "settings": {
+ "foreground": "#5f7e97"
+ }
+ },
+ {
+ "name": "Other Variables in Code Documentations",
+ "scope": ["variable.other.jsdoc", "variable.other.phpdoc"],
+ "settings": {
+ "foreground": "#78ccf0"
+ }
+ },
+ {
+ "name": "JavaScript module imports and exports",
+ "scope": [
+ "variable.other.meta.import.js",
+ "meta.import.js variable.other",
+ "variable.other.meta.export.js",
+ "meta.export.js variable.other"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "JavaScript Variable Parameter Function",
+ "scope": "variable.parameter.function.js",
+ "settings": {
+ "foreground": "#7986E7"
+ }
+ },
+ {
+ "name": "JavaScript[React] Variable Other Object",
+ "scope": [
+ "variable.other.object.js",
+ "variable.other.object.jsx",
+ "variable.object.property.js",
+ "variable.object.property.jsx"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "JavaScript Variables",
+ "scope": ["variable.js", "variable.other.js"],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "JavaScript Entity Name Type",
+ "scope": ["entity.name.type.js", "entity.name.type.module.js"],
+ "settings": {
+ "foreground": "#111111",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "JavaScript Support Classes",
+ "scope": "support.class.js",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "JSON Property Names",
+ "scope": "support.type.property-name.json",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "JSON Support Constants",
+ "scope": "support.constant.json",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "JSON Property values (string)",
+ "scope": "meta.structure.dictionary.value.json string.quoted.double",
+ "settings": {
+ "foreground": "#c789d6"
+ }
+ },
+ {
+ "name": "Strings in JSON values",
+ "scope": "string.quoted.double.json punctuation.definition.string.json",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Specific JSON Property values like null",
+ "scope": "meta.structure.dictionary.json meta.structure.dictionary.value constant.language",
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "JavaScript Other Variable",
+ "scope": "variable.other.object.js",
+ "settings": {
+ "foreground": "#0c969b",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Ruby Variables",
+ "scope": ["variable.other.ruby"],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Ruby Class",
+ "scope": ["entity.name.type.class.ruby"],
+ "settings": {
+ "foreground": "#c96765"
+ }
+ },
+ {
+ "name": "Ruby Hashkeys",
+ "scope": "constant.language.symbol.hashkey.ruby",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Ruby Symbols",
+ "scope": "constant.language.symbol.ruby",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "LESS Tag names",
+ "scope": "entity.name.tag.less",
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "LESS Keyword Other Unit",
+ "scope": "keyword.other.unit.css",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Attribute Name for LESS",
+ "scope": "meta.attribute-selector.less entity.other.attribute-name.attribute",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Markdown Headings",
+ "scope": [
+ "markup.heading",
+ "markup.heading.setext.1",
+ "markup.heading.setext.2"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Markdown Italics",
+ "scope": "markup.italic",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Markdown Bold",
+ "scope": "markup.bold",
+ "settings": {
+ "foreground": "#4876d6",
+ "fontStyle": "bold"
+ }
+ },
+ {
+ "name": "Markdown Quote + others",
+ "scope": "markup.quote",
+ "settings": {
+ "foreground": "#697098",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "Markdown Raw Code + others",
+ "scope": "markup.inline.raw",
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Markdown Links",
+ "scope": [
+ "markup.underline.link",
+ "markup.underline.link.image"
+ ],
+ "settings": {
+ "foreground": "#ff869a"
+ }
+ },
+ {
+ "name": "Markdown Link Title and Description",
+ "scope": [
+ "string.other.link.title.markdown",
+ "string.other.link.description.markdown"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Markdown Punctuation",
+ "scope": [
+ "punctuation.definition.string.markdown",
+ "punctuation.definition.string.begin.markdown",
+ "punctuation.definition.string.end.markdown",
+ "meta.link.inline.markdown punctuation.definition.string"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Markdown MetaData Punctuation",
+ "scope": ["punctuation.definition.metadata.markdown"],
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Markdown List Punctuation",
+ "scope": ["beginning.punctuation.definition.list.markdown"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Markdown Inline Raw String",
+ "scope": "markup.inline.raw.string.markdown",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "PHP Variables",
+ "scope": ["variable.other.php", "variable.other.property.php"],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Support Classes in PHP",
+ "scope": "support.class.php",
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Punctuations in PHP function calls",
+ "scope": "meta.function-call.php punctuation",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "PHP Global Variables",
+ "scope": "variable.other.global.php",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Declaration Punctuation in PHP Global Variables",
+ "scope": "variable.other.global.php punctuation.definition.variable",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Language Constants in Python",
+ "scope": "constant.language.python",
+ "settings": {
+ "foreground": "#bc5454"
+ }
+ },
+ {
+ "name": "Python Function Parameter and Arguments",
+ "scope": [
+ "variable.parameter.function.python",
+ "meta.function-call.arguments.python"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Python Function Call",
+ "scope": [
+ "meta.function-call.python",
+ "meta.function-call.generic.python"
+ ],
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "Punctuations in Python",
+ "scope": "punctuation.python",
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Decorator Functions in Python",
+ "scope": "entity.name.function.decorator.python",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Python Language Variable",
+ "scope": "source.python variable.language.special",
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Python import control keyword",
+ "scope": "keyword.control",
+ "settings": {
+ "foreground": "#994cc3",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "name": "SCSS Variable",
+ "scope": [
+ "variable.scss",
+ "variable.sass",
+ "variable.parameter.url.scss",
+ "variable.parameter.url.sass"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Variables in SASS At-Rules",
+ "scope": [
+ "source.css.scss meta.at-rule variable",
+ "source.css.sass meta.at-rule variable"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "Variables in SASS At-Rules",
+ "scope": [
+ "source.css.scss meta.at-rule variable",
+ "source.css.sass meta.at-rule variable"
+ ],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "Attribute Name for SASS",
+ "scope": [
+ "meta.attribute-selector.scss entity.other.attribute-name.attribute",
+ "meta.attribute-selector.sass entity.other.attribute-name.attribute"
+ ],
+ "settings": {
+ "foreground": "#aa0982"
+ }
+ },
+ {
+ "name": "Tag names in SASS",
+ "scope": ["entity.name.tag.scss", "entity.name.tag.sass"],
+ "settings": {
+ "foreground": "#0c969b"
+ }
+ },
+ {
+ "name": "SASS Keyword Other Unit",
+ "scope": ["keyword.other.unit.scss", "keyword.other.unit.sass"],
+ "settings": {
+ "foreground": "#994cc3"
+ }
+ },
+ {
+ "name": "TypeScript[React] Variables and Object Properties",
+ "scope": [
+ "variable.other.readwrite.alias.ts",
+ "variable.other.readwrite.alias.tsx",
+ "variable.other.readwrite.ts",
+ "variable.other.readwrite.tsx",
+ "variable.other.object.ts",
+ "variable.other.object.tsx",
+ "variable.object.property.ts",
+ "variable.object.property.tsx",
+ "variable.other.ts",
+ "variable.other.tsx",
+ "variable.tsx",
+ "variable.ts"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "TypeScript[React] Entity Name Types",
+ "scope": ["entity.name.type.ts", "entity.name.type.tsx"],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "TypeScript[React] Node Classes",
+ "scope": ["support.class.node.ts", "support.class.node.tsx"],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "TypeScript[React] Entity Name Types as Parameters",
+ "scope": [
+ "meta.type.parameters.ts entity.name.type",
+ "meta.type.parameters.tsx entity.name.type"
+ ],
+ "settings": {
+ "foreground": "#5f7e97"
+ }
+ },
+ {
+ "name": "TypeScript[React] Import/Export Punctuations",
+ "scope": [
+ "meta.import.ts punctuation.definition.block",
+ "meta.import.tsx punctuation.definition.block",
+ "meta.export.ts punctuation.definition.block",
+ "meta.export.tsx punctuation.definition.block"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "TypeScript[React] Punctuation Decorators",
+ "scope": [
+ "meta.decorator punctuation.decorator.ts",
+ "meta.decorator punctuation.decorator.tsx"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "TypeScript[React] Punctuation Decorators",
+ "scope": "meta.tag.js meta.jsx.children.tsx",
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "YAML Entity Name Tags",
+ "scope": "entity.name.tag.yaml",
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "JavaScript Variable Other ReadWrite",
+ "scope": ["variable.other.readwrite.js", "variable.parameter"],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "Support Class Component",
+ "scope": ["support.class.component.js", "support.class.component.tsx"],
+ "settings": {
+ "foreground": "#aa0982",
+ "fontStyle": ""
+ }
+ },
+ {
+ "name": "Text nested in React tags",
+ "scope": [
+ "meta.jsx.children",
+ "meta.jsx.children.js",
+ "meta.jsx.children.tsx"
+ ],
+ "settings": {
+ "foreground": "#403f53"
+ }
+ },
+ {
+ "name": "TypeScript Classes",
+ "scope": "meta.class entity.name.type.class.tsx",
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "TypeScript Entity Name Type",
+ "scope": ["entity.name.type.tsx", "entity.name.type.module.tsx"],
+ "settings": {
+ "foreground": "#111111"
+ }
+ },
+ {
+ "name": "TypeScript Class Variable Keyword",
+ "scope": [
+ "meta.class.ts meta.var.expr.ts storage.type.ts",
+ "meta.class.tsx meta.var.expr.tsx storage.type.tsx"
+ ],
+ "settings": {
+ "foreground": "#994CC3"
+ }
+ },
+ {
+ "name": "TypeScript Method Declaration e.g. `constructor`",
+ "scope": [
+ "meta.method.declaration storage.type.ts",
+ "meta.method.declaration storage.type.tsx"
+ ],
+ "settings": {
+ "foreground": "#4876d6"
+ }
+ },
+ {
+ "name": "normalize font style of certain components",
+ "scope": [
+ "meta.property-list.css meta.property-value.css variable.other.less",
+ "meta.property-list.scss variable.scss",
+ "meta.property-list.sass variable.sass",
+ "meta.brace",
+ "keyword.operator.operator",
+ "keyword.operator.or.regexp",
+ "keyword.operator.expression.in",
+ "keyword.operator.relational",
+ "keyword.operator.assignment",
+ "keyword.operator.comparison",
+ "keyword.operator.type",
+ "keyword.operator",
+ "keyword",
+ "punctuation.definintion.string",
+ "punctuation",
+ "variable.other.readwrite.js",
+ "storage.type",
+ "source.css",
+ "string.quoted"
+ ],
+ "settings": {
+ "fontStyle": ""
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/components/editor/night.json b/components/editor/night.json
new file mode 100644
index 0000000000000000000000000000000000000000..993c6496f2d1a9f783e395ed572fa4148796533f
--- /dev/null
+++ b/components/editor/night.json
@@ -0,0 +1,347 @@
+{
+ "inherit": true,
+ "rules": [
+ {
+ "background": "24292e",
+ "token": ""
+ },
+ {
+ "foreground": "959da5",
+ "token": "comment"
+ },
+ {
+ "foreground": "959da5",
+ "token": "punctuation.definition.comment"
+ },
+ {
+ "foreground": "959da5",
+ "token": "string.comment"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "constant"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "entity.name.constant"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "variable.other.constant"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "variable.language"
+ },
+ {
+ "foreground": "b392f0",
+ "token": "entity"
+ },
+ {
+ "foreground": "b392f0",
+ "token": "entity.name"
+ },
+ {
+ "foreground": "f6f8fa",
+ "token": "variable.parameter.function"
+ },
+ {
+ "foreground": "7bcc72",
+ "token": "entity.name.tag"
+ },
+ {
+ "foreground": "ea4a5a",
+ "token": "keyword"
+ },
+ {
+ "foreground": "ea4a5a",
+ "token": "storage"
+ },
+ {
+ "foreground": "ea4a5a",
+ "token": "storage.type"
+ },
+ {
+ "foreground": "f6f8fa",
+ "token": "storage.modifier.package"
+ },
+ {
+ "foreground": "f6f8fa",
+ "token": "storage.modifier.import"
+ },
+ {
+ "foreground": "f6f8fa",
+ "token": "storage.type.java"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "punctuation.definition.string"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string punctuation.section.embedded source"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "support"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "meta.property-name"
+ },
+ {
+ "foreground": "fb8532",
+ "token": "variable"
+ },
+ {
+ "foreground": "f6f8fa",
+ "token": "variable.other"
+ },
+ {
+ "foreground": "d73a49",
+ "fontStyle": "bold italic underline",
+ "token": "invalid.broken"
+ },
+ {
+ "foreground": "d73a49",
+ "fontStyle": "bold italic underline",
+ "token": "invalid.deprecated"
+ },
+ {
+ "foreground": "fafbfc",
+ "background": "d73a49",
+ "fontStyle": "italic underline",
+ "token": "invalid.illegal"
+ },
+ {
+ "foreground": "fafbfc",
+ "background": "d73a49",
+ "fontStyle": "italic underline",
+ "token": "carriage-return"
+ },
+ {
+ "foreground": "d73a49",
+ "fontStyle": "bold italic underline",
+ "token": "invalid.unimplemented"
+ },
+ {
+ "foreground": "d73a49",
+ "token": "message.error"
+ },
+ {
+ "foreground": "f6f8fa",
+ "token": "string source"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "string variable"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "source.regexp"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string.regexp"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string.regexp.character-class"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string.regexp constant.character.escape"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string.regexp source.ruby.embedded"
+ },
+ {
+ "foreground": "79b8ff",
+ "token": "string.regexp string.regexp.arbitrary-repitition"
+ },
+ {
+ "foreground": "7bcc72",
+ "fontStyle": "bold",
+ "token": "string.regexp constant.character.escape"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "support.constant"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "support.variable"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "meta.module-reference"
+ },
+ {
+ "foreground": "fb8532",
+ "token": "markup.list"
+ },
+ {
+ "foreground": "0366d6",
+ "fontStyle": "bold",
+ "token": "markup.heading"
+ },
+ {
+ "foreground": "0366d6",
+ "fontStyle": "bold",
+ "token": "markup.heading entity.name"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "markup.quote"
+ },
+ {
+ "foreground": "f6f8fa",
+ "fontStyle": "italic",
+ "token": "markup.italic"
+ },
+ {
+ "foreground": "f6f8fa",
+ "fontStyle": "bold",
+ "token": "markup.bold"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "markup.raw"
+ },
+ {
+ "foreground": "b31d28",
+ "background": "ffeef0",
+ "token": "markup.deleted"
+ },
+ {
+ "foreground": "b31d28",
+ "background": "ffeef0",
+ "token": "meta.diff.header.from-file"
+ },
+ {
+ "foreground": "b31d28",
+ "background": "ffeef0",
+ "token": "punctuation.definition.deleted"
+ },
+ {
+ "foreground": "176f2c",
+ "background": "f0fff4",
+ "token": "markup.inserted"
+ },
+ {
+ "foreground": "176f2c",
+ "background": "f0fff4",
+ "token": "meta.diff.header.to-file"
+ },
+ {
+ "foreground": "176f2c",
+ "background": "f0fff4",
+ "token": "punctuation.definition.inserted"
+ },
+ {
+ "foreground": "b08800",
+ "background": "fffdef",
+ "token": "markup.changed"
+ },
+ {
+ "foreground": "b08800",
+ "background": "fffdef",
+ "token": "punctuation.definition.changed"
+ },
+ {
+ "foreground": "2f363d",
+ "background": "959da5",
+ "token": "markup.ignored"
+ },
+ {
+ "foreground": "2f363d",
+ "background": "959da5",
+ "token": "markup.untracked"
+ },
+ {
+ "foreground": "b392f0",
+ "fontStyle": "bold",
+ "token": "meta.diff.range"
+ },
+ {
+ "foreground": "c8e1ff",
+ "token": "meta.diff.header"
+ },
+ {
+ "foreground": "0366d6",
+ "fontStyle": "bold",
+ "token": "meta.separator"
+ },
+ {
+ "foreground": "0366d6",
+ "token": "meta.output"
+ },
+ {
+ "foreground": "ffeef0",
+ "token": "brackethighlighter.tag"
+ },
+ {
+ "foreground": "ffeef0",
+ "token": "brackethighlighter.curly"
+ },
+ {
+ "foreground": "ffeef0",
+ "token": "brackethighlighter.round"
+ },
+ {
+ "foreground": "ffeef0",
+ "token": "brackethighlighter.square"
+ },
+ {
+ "foreground": "ffeef0",
+ "token": "brackethighlighter.angle"
+ },
+ {
+ "foreground": "ffeef0",
+ "token": "brackethighlighter.quote"
+ },
+ {
+ "foreground": "d73a49",
+ "token": "brackethighlighter.unmatched"
+ },
+ {
+ "foreground": "d73a49",
+ "token": "sublimelinter.mark.error"
+ },
+ {
+ "foreground": "fb8532",
+ "token": "sublimelinter.mark.warning"
+ },
+ {
+ "foreground": "6a737d",
+ "token": "sublimelinter.gutter-mark"
+ },
+ {
+ "foreground": "79b8ff",
+ "fontStyle": "underline",
+ "token": "constant.other.reference.link"
+ },
+ {
+ "foreground": "79b8ff",
+ "fontStyle": "underline",
+ "token": "string.other.link"
+ }
+ ],
+ "colors": {
+ "editor.foreground": "#f6f8fa",
+ "editor.background": "#24292e",
+ "editor.selectionBackground": "#4c2889",
+ "editor.inactiveSelectionBackground": "#444d56",
+ "editor.lineHighlightBackground": "#444d56",
+ "editorCursor.foreground": "#ffffff",
+ "editorWhitespace.foreground": "#6a737d",
+ "editorIndentGuide.background": "#6a737d",
+ "editorIndentGuide.activeBackground": "#f6f8fa",
+ "editor.selectionHighlightBorder": "#444d56"
+ }
+}
\ No newline at end of file
diff --git a/components/grid-pattern/index.tsx b/components/grid-pattern/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1006f5253f4f5d749d15c07eb75c81b924c54aae
--- /dev/null
+++ b/components/grid-pattern/index.tsx
@@ -0,0 +1,70 @@
+"use client";
+import { useId } from "react";
+import { cn } from "@/lib/utils";
+
+interface GridPatternProps extends React.SVGProps {
+ width?: number;
+ height?: number;
+ x?: number;
+ y?: number;
+ squares?: Array<[x: number, y: number]>;
+ strokeDasharray?: string;
+ className?: string;
+ [key: string]: unknown;
+}
+
+export function GridPattern({
+ width = 40,
+ height = 40,
+ x = -1,
+ y = -1,
+ strokeDasharray = "0",
+ squares,
+ className,
+ ...props
+}: GridPatternProps) {
+ const id = useId();
+
+ return (
+
+
+
+
+
+
+
+ {squares && (
+
+ {squares.map(([x, y]) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/loading/index.tsx b/components/loading/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f812ff62fae2b935ae8016152bda3b7d9403dca9
--- /dev/null
+++ b/components/loading/index.tsx
@@ -0,0 +1,41 @@
+import { cn } from "@/lib/utils";
+
+function Loading({
+ overlay = true,
+ className,
+}: {
+ overlay?: boolean;
+ className?: string;
+}) {
+ return (
+
+ );
+}
+
+export default Loading;
diff --git a/components/not-found/buttons.tsx b/components/not-found/buttons.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a30f436d48121858411d727b457abff9c3bb56e2
--- /dev/null
+++ b/components/not-found/buttons.tsx
@@ -0,0 +1,16 @@
+"use client";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+
+export function NotFoundButtons() {
+ return (
+
+
+ Return Home
+
+ window.history.back()}>
+ Back to previous page
+
+
+ );
+}
diff --git a/components/projects/big-project-card.tsx b/components/projects/big-project-card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..28a47800b800d49b2e5a7c561958c00c8b409e76
--- /dev/null
+++ b/components/projects/big-project-card.tsx
@@ -0,0 +1,110 @@
+import Link from "next/link";
+import { SpaceEntry } from "@huggingface/hub";
+import { format } from "date-fns";
+import {
+ CogIcon,
+ EllipsisVertical,
+ ExternalLink,
+ TrashIcon,
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+
+// from-red-500 to-red-500
+// from-yellow-500 to-yellow-500
+// from-green-500 to-green-500
+// from-purple-500 to-purple-500
+// from-blue-500 to-blue-500
+// from-pink-500 to-pink-500
+// from-gray-500 to-gray-500
+// from-indigo-500 to-indigo-500
+
+export function BigProjectCard({
+ project,
+ onOpenDeleteDialog,
+}: {
+ project: SpaceEntry;
+ onOpenDeleteDialog: (id: string) => void;
+}) {
+ return (
+
+
+ {project.private ? (
+
+
{project.cardData?.emoji}
+
+ {project.cardData?.title}
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ Open project
+
+
+
+ {project.cardData?.title}
+
+
+
+
+ Last update: {format(project.updatedAt, "MMM d, yyyy")}
+
+
+
+
+
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ window.open(
+ `https://huggingface.co/spaces/${project.name}/settings`,
+ "_blank"
+ );
+ }}
+ >
+
+ Settings
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ onOpenDeleteDialog(project.name);
+ }}
+ >
+
+ Delete this project
+
+
+
+
+
+ );
+}
diff --git a/components/projects/project-card.tsx b/components/projects/project-card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4d4eaff23df839694ca65771fbd5e28a4adb33a2
--- /dev/null
+++ b/components/projects/project-card.tsx
@@ -0,0 +1,22 @@
+import Link from "next/link";
+import { SpaceEntry } from "@huggingface/hub";
+
+export function ProjectCard({ project }: { project: SpaceEntry }) {
+ return (
+
+
+
+ {project?.cardData?.emoji || "🚀"}
+
+
+
+ {project.cardData?.title}
+
+
+ {project.name}
+
+
+
+
+ );
+}
diff --git a/components/projects/useManualUpdates.ts b/components/projects/useManualUpdates.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6d3cdeee36528c027635a85d1183f470c887aa7f
--- /dev/null
+++ b/components/projects/useManualUpdates.ts
@@ -0,0 +1,30 @@
+import { File } from "@/lib/type";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useMemo } from "react";
+
+export const useManualUpdates = () => {
+ const queryClient = useQueryClient();
+
+ const { data: manuallyUpdatedFiles } = useQuery({
+ queryKey: ["manuallyUpdatedFiles"],
+ queryFn: () => {
+ return [];
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchInterval: false,
+ refetchIntervalInBackground: false,
+ });
+ const files = useMemo(() => {
+ return queryClient.getQueryData(["files"]) ?? [];
+ }, [queryClient]);
+
+ const isManuallyUpdatedSameAsFiles = useMemo(() => {
+ return manuallyUpdatedFiles?.every((file) =>
+ files?.some((f) => f.path === file.path && f.content === file.content)
+ );
+ }, [manuallyUpdatedFiles, files]);
+
+ return { manuallyUpdatedFiles, isManuallyUpdatedSameAsFiles };
+};
diff --git a/components/projects/useProject.ts b/components/projects/useProject.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5a8b4f4bcd21d45bfc1eb1bc8ccf184404bbb066
--- /dev/null
+++ b/components/projects/useProject.ts
@@ -0,0 +1,76 @@
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useEffect } from "react";
+
+import { getProject, ProjectWithCommits } from "@/actions/projects";
+import { File } from "@/lib/type";
+
+export const useProject = (
+ initialProject?: ProjectWithCommits | null,
+ initialFiles?: File[] | null,
+ isNew?: boolean
+) => {
+ const queryClient = useQueryClient();
+
+ useEffect(() => {
+ if (isNew) {
+ queryClient.setQueryData(["project"], null);
+ queryClient.setQueryData(["files"], []);
+ } else if (initialProject) {
+ queryClient.setQueryData(["project"], initialProject);
+ if (initialFiles) {
+ queryClient.setQueryData(["files"], initialFiles);
+ }
+ }
+ }, [initialProject, initialFiles, isNew, queryClient]);
+
+ const {
+ data: project,
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: ["project"],
+ initialData: initialProject,
+ queryFn: async () => {
+ if (isNew) return null;
+ const datas = await getProject(initialProject?.name as string);
+ if (datas?.files) {
+ setFiles(datas.files);
+ }
+ return datas?.project ?? null;
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchInterval: false,
+ refetchIntervalInBackground: false,
+ });
+
+ const { data: files } = useQuery({
+ queryKey: ["files"],
+ initialData: initialFiles,
+ queryFn: () => {
+ return [];
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchInterval: false,
+ refetchIntervalInBackground: false,
+ });
+
+ const setFiles = (newFiles: File[]) => {
+ queryClient.setQueryData(["files"], (oldFiles: File[] = []) => {
+ const currentFiles = oldFiles.filter(
+ (file) => !newFiles.some((f) => f.path === file.path)
+ );
+ return [...currentFiles, ...newFiles];
+ });
+ };
+
+ return {
+ project,
+ isLoading,
+ error,
+ files,
+ };
+};
diff --git a/components/projects/useProjects.ts b/components/projects/useProjects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4b67775c489e76b3b8ffef0fa5929f06f42df896
--- /dev/null
+++ b/components/projects/useProjects.ts
@@ -0,0 +1,10 @@
+import { getProjects } from "@/actions/projects";
+import { useQuery } from "@tanstack/react-query";
+
+export const useProjects = () => {
+ const { data: projects, isLoading, error, refetch } = useQuery({
+ queryKey: ["projects"],
+ queryFn: () => getProjects(),
+ });
+ return { projects, isLoading, error, refetch };
+}
\ No newline at end of file
diff --git a/components/projects/user-projects.tsx b/components/projects/user-projects.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c9307ecf6af2563a8e670f6327943e6c2ab636fd
--- /dev/null
+++ b/components/projects/user-projects.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import { Plus, TrashIcon } from "lucide-react";
+import { useSession } from "next-auth/react";
+
+import { useProjects } from "./useProjects";
+import { Button } from "@/components/ui/button";
+import { BigProjectCard } from "./big-project-card";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { toast } from "sonner";
+
+export function UserProjects() {
+ const { data: session } = useSession();
+ const { projects, isLoading, error, refetch } = useProjects();
+ const [open, setOpen] = useState(false);
+ const [deleteProjectId, setDeleteProjectId] = useState(null);
+ const [input, setInput] = useState("");
+ if (isLoading || error || !session?.user?.name) return null;
+
+ const handleDeleteProject = async () => {
+ const response = await fetch(
+ `/api/projects/${deleteProjectId?.split("/")[1]}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ ).then(async (response) => {
+ if (response.ok) {
+ return response.json();
+ }
+ });
+ if (response?.success) {
+ toast.success("Project has been deleted");
+ refetch();
+ setOpen(false);
+ setDeleteProjectId(null);
+ setInput("");
+ } else {
+ toast.error("Failed to delete project");
+ }
+ };
+
+ return (
+
+
+
+
+ {projects?.map((project) => (
+
{
+ setOpen(true);
+ setDeleteProjectId(id);
+ }}
+ />
+ ))}
+ {projects?.length === 0 && (
+
+
+
+ )}
+
+
+
+
+
+ Delete project
+
+ This action is irreversible. Are you sure you want to delete this
+ project?
+
+
+
+
+
+ Type{" "}
+
+ {deleteProjectId}
+ {" "}
+ to confirm deletion
+
+
setInput(e.target.value)}
+ />
+
+
+
+
+ Cancel
+
+ handleDeleteProject()}
+ >
+
+ Delete project
+
+
+
+
+
+ );
+}
diff --git a/components/providers/react-query.tsx b/components/providers/react-query.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..70f1710fbfb56a37bfe8f6fd888a8df1690acefc
--- /dev/null
+++ b/components/providers/react-query.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { useState } from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+
+export function ReactQueryProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [queryClient] = useState(() => new QueryClient());
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/components/providers/session.tsx b/components/providers/session.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bb80a62580f2062dc86fa317eceda61904af48e2
--- /dev/null
+++ b/components/providers/session.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import { SessionProvider } from "next-auth/react";
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}
diff --git a/components/providers/theme.tsx b/components/providers/theme.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..189a2b1a12231255b53038166eb92d2d03bd0e9d
--- /dev/null
+++ b/components/providers/theme.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+
+export function ThemeProvider({
+ children,
+ ...props
+}: React.ComponentProps) {
+ return {children} ;
+}
diff --git a/components/public/animated-dots-background.tsx b/components/public/animated-dots-background.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..941c0d049500e14a31a87c2f1d7db92c1cd4c8c0
--- /dev/null
+++ b/components/public/animated-dots-background.tsx
@@ -0,0 +1,181 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+export function AnimatedDotsBackground() {
+ const canvasRef = useRef(null);
+ const dotsRef = useRef<
+ Array<{ x: number; y: number; targetColor: number; currentColor: number }>
+ >([]);
+ const mouseRef = useRef({ x: -1000, y: -1000 });
+ const animationFrameRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return undefined;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ // Helper function to convert OKLCH to RGB
+ const oklchToRgb = (
+ oklchString: string
+ ): { r: number; g: number; b: number } => {
+ // Create a temporary element to compute the color
+ const temp = document.createElement("div");
+ temp.style.color = oklchString;
+ document.body.appendChild(temp);
+ const computed = window.getComputedStyle(temp).color;
+ document.body.removeChild(temp);
+
+ // Parse RGB values from the computed color string
+ const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
+ if (match) {
+ return {
+ r: parseInt(match[1]),
+ g: parseInt(match[2]),
+ b: parseInt(match[3]),
+ };
+ }
+ return { r: 200, g: 200, b: 200 }; // Fallback
+ };
+
+ // Get theme colors from CSS variables
+ const getThemeColors = () => {
+ const style = getComputedStyle(document.documentElement);
+ const mutedFg = style.getPropertyValue("--muted-foreground").trim();
+ const primary = style.getPropertyValue("--primary").trim();
+
+ return {
+ base: oklchToRgb(`oklch(${mutedFg})`),
+ accent: oklchToRgb(`oklch(${primary})`),
+ };
+ };
+
+ const colors = getThemeColors();
+
+ // Set canvas size
+ const updateCanvasSize = () => {
+ const rect = canvas.getBoundingClientRect();
+ canvas.width = rect.width * window.devicePixelRatio;
+ canvas.height = rect.height * window.devicePixelRatio;
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+
+ // Reinitialize dots when canvas size changes
+ initializeDots();
+ };
+
+ const initializeDots = () => {
+ const rect = canvas.getBoundingClientRect();
+ const spacing = 30; // Space between dots
+ const cols = Math.ceil(rect.width / spacing);
+ const rows = Math.ceil(rect.height / spacing);
+
+ dotsRef.current = [];
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < rows; j++) {
+ dotsRef.current.push({
+ x: i * spacing + spacing / 2,
+ y: j * spacing + spacing / 2,
+ targetColor: 0, // 0 = gray, 1 = orange
+ currentColor: 0,
+ });
+ }
+ }
+ };
+
+ updateCanvasSize();
+
+ // Handle mouse move
+ const handleMouseMove = (e: MouseEvent) => {
+ const rect = canvas.getBoundingClientRect();
+ mouseRef.current = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ };
+ };
+
+ // Handle mouse leave
+ const handleMouseLeave = () => {
+ mouseRef.current = { x: -1000, y: -1000 };
+ };
+
+ canvas.addEventListener("mousemove", handleMouseMove);
+ canvas.addEventListener("mouseleave", handleMouseLeave);
+ window.addEventListener("resize", updateCanvasSize);
+
+ // Animation loop
+ const animate = () => {
+ const rect = canvas.getBoundingClientRect();
+ ctx.clearRect(0, 0, rect.width, rect.height);
+
+ const hoverRadius = 80; // Distance for hover effect
+ const { x: mouseX, y: mouseY } = mouseRef.current;
+
+ dotsRef.current.forEach((dot) => {
+ // Calculate distance to mouse
+ const dx = dot.x - mouseX;
+ const dy = dot.y - mouseY;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ // Update target color based on proximity to mouse
+ if (distance < hoverRadius) {
+ const intensity = 1 - distance / hoverRadius;
+ dot.targetColor = intensity;
+ } else {
+ dot.targetColor = 0;
+ }
+
+ // Smooth transition with ease (lerp with easing factor)
+ const easeSpeed = 0.1;
+ dot.currentColor += (dot.targetColor - dot.currentColor) * easeSpeed;
+
+ // Interpolate between base and accent colors from theme
+ const r = Math.round(
+ colors.base.r + (colors.accent.r - colors.base.r) * dot.currentColor
+ );
+ const g = Math.round(
+ colors.base.g + (colors.accent.g - colors.base.g) * dot.currentColor
+ );
+ const b = Math.round(
+ colors.base.b + (colors.accent.b - colors.base.b) * dot.currentColor
+ );
+
+ ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
+
+ // Draw dot with size based on hover intensity
+ const baseSize = 2;
+ const hoverSize = 4;
+ const size = baseSize + (hoverSize - baseSize) * dot.currentColor;
+
+ ctx.beginPath();
+ ctx.arc(dot.x, dot.y, size, 0, Math.PI * 2);
+ ctx.fill();
+ });
+
+ animationFrameRef.current = requestAnimationFrame(animate);
+ };
+
+ animate();
+
+ return () => {
+ canvas.removeEventListener("mousemove", handleMouseMove);
+ canvas.removeEventListener("mouseleave", handleMouseLeave);
+ window.removeEventListener("resize", updateCanvasSize);
+ if (animationFrameRef.current) {
+ cancelAnimationFrame(animationFrameRef.current);
+ }
+ };
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/components/public/hero-header.tsx b/components/public/hero-header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..db7bc59065f8322b595db7b60955bbaec294ade9
--- /dev/null
+++ b/components/public/hero-header.tsx
@@ -0,0 +1,34 @@
+"use client";
+import { motion } from "framer-motion";
+
+const TITLE = ["Your", "next", "web-based", "application", "starts", "here"];
+
+export function HeroHeader() {
+ return (
+
+
+ DeepSite: new release 🎉
+
+
+ {TITLE.map((word, index) => (
+
+ {word}
+
+ ))}
+
+
+ Build your next web-based application with ease and speed by using AI
+ Vibe Coding.
+
+
+ );
+}
diff --git a/components/public/navigation.tsx b/components/public/navigation.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dff7f3030d7bb195e415893f6435ef0de833379a
--- /dev/null
+++ b/components/public/navigation.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import Link from "next/link";
+import Image from "next/image";
+import { UserMenu } from "@/components/user-menu";
+
+export function Navigation() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..71e428b4ca6154811e8f569d5fdd971ead095996
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/components/ui/button-group.tsx b/components/ui/button-group.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8600af03eabb4dbef01dbf618fda5dfe96464b82
--- /dev/null
+++ b/components/ui/button-group.tsx
@@ -0,0 +1,83 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Separator } from "@/components/ui/separator"
+
+const buttonGroupVariants = cva(
+ "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
+ vertical:
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
+ },
+ },
+ defaultVariants: {
+ orientation: "horizontal",
+ },
+ }
+)
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function ButtonGroupText({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+
+ )
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ ButtonGroup,
+ ButtonGroupSeparator,
+ ButtonGroupText,
+ buttonGroupVariants,
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c9836ac0d05e41e42ae46471032535334b1c76f7
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,71 @@
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground hover:bg-primary/90 border border-primary hover:border-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ transparent: "bg-accent-foreground/5 hover:bg-accent-foreground/10",
+ link: "text-primary underline-offset-4 hover:underline",
+ bordered:
+ "border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ indigo:
+ "border border-indigo-500 bg-indigo-500 text-white hover:bg-indigo-600 dark:border-indigo-500/30 dark:bg-indigo-500/20 dark:text-indigo-400 dark:hover:bg-indigo-500/30",
+ "ghost-bordered":
+ "border bg-primary-foreground hover:bg-background hover:text-accent-foreground dark:hover:bg-accent/50",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ xs: "h-7 rounded-md gap-1.5 px-2 has-[>svg]:px-1.5 text-xs",
+ xxs: "h-6 rounded-md gap-1.5 px-2 has-[>svg]:px-1.5 text-[10px]",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ "icon-xs": "size-7 rounded-md!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot : "button";
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d9ccec91d22fab844bd04340c2b07e8677955350
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bbe6fb019770a31321cd06ff5e837695fbc06cd1
--- /dev/null
+++ b/components/ui/dropdown-menu.tsx
@@ -0,0 +1,257 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..868dec6cb3f78d22f4b2cb108c3c5c250de413a8
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ );
+}
+
+export { Input };
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..01e468b6783992d04775aa52376221b8fa4b0f95
--- /dev/null
+++ b/components/ui/popover.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+function Popover({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function PopoverContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function PopoverAnchor({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..88302a8d50922c94626dfe7c7113d3c58af87ae3
--- /dev/null
+++ b/components/ui/select.tsx
@@ -0,0 +1,190 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectGroup({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "item-aligned",
+ align = "center",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..275381cab0adbc778ea634e7c46fbf80db821bf3
--- /dev/null
+++ b/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9b20afe2e10fd9b94d1abc1ae732054b2fb9821c
--- /dev/null
+++ b/components/ui/sonner.tsx
@@ -0,0 +1,40 @@
+"use client"
+
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from "lucide-react"
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/components/ui/toggle-group.tsx b/components/ui/toggle-group.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..24a4850d73e82be5e81108fc6f7e8052aabe9819
--- /dev/null
+++ b/components/ui/toggle-group.tsx
@@ -0,0 +1,83 @@
+"use client"
+
+import * as React from "react"
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
+import { type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { toggleVariants } from "@/components/ui/toggle"
+
+const ToggleGroupContext = React.createContext<
+ VariantProps & {
+ spacing?: number
+ }
+>({
+ size: "default",
+ variant: "default",
+ spacing: 0,
+})
+
+function ToggleGroup({
+ className,
+ variant,
+ size,
+ spacing = 0,
+ children,
+ ...props
+}: React.ComponentProps &
+ VariantProps & {
+ spacing?: number
+ }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+function ToggleGroupItem({
+ className,
+ children,
+ variant,
+ size,
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ const context = React.useContext(ToggleGroupContext)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export { ToggleGroup, ToggleGroupItem }
diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e3fea1f7fcdc8e4ce5dcd45f56111b57743826fd
--- /dev/null
+++ b/components/ui/toggle.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import * as React from "react";
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/lib/utils";
+
+const toggleVariants = cva(
+ "inline-flex items-center cursor-pointer justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline:
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
+ },
+ size: {
+ default: "h-9 px-2 min-w-9",
+ sm: "h-8 px-1.5 min-w-8",
+ xs: "h-7 px-1 min-w-7 text-xs",
+ lg: "h-10 px-2.5 min-w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+function Toggle({
+ className,
+ variant,
+ size,
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ );
+}
+
+export { Toggle, toggleVariants };
diff --git a/components/user-menu/index.tsx b/components/user-menu/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5f6ff1d0a186236dbc2a9dff5205e1910b244eb8
--- /dev/null
+++ b/components/user-menu/index.tsx
@@ -0,0 +1,215 @@
+import Link from "next/link";
+import { useSession, signIn, signOut } from "next-auth/react";
+import { ArrowRight, Folder, LogOut, Moon, Plus, Sun } from "lucide-react";
+import { useTheme } from "next-themes";
+import { useEffect } from "react";
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Button } from "@/components/ui/button";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { useProjects } from "@/components/projects/useProjects";
+import { ProjectCard } from "@/components/projects/project-card";
+import { cn } from "@/lib/utils";
+
+export function UserMenu() {
+ const { data: session, status } = useSession();
+ const { projects } = useProjects();
+ const isLoading = status === "loading";
+ const { theme, setTheme } = useTheme();
+
+ useEffect(() => {
+ if (typeof window !== "undefined" && !session && status !== "loading") {
+ const urlParams = new URLSearchParams(window.location.search);
+ if (urlParams.get("signin") === "true") {
+ const newUrl = new URL(window.location.href);
+ newUrl.searchParams.delete("signin");
+ window.history.replaceState({}, "", newUrl.toString());
+
+ signIn("huggingface", { callbackUrl: "/" });
+ }
+ }
+ }, [session, status]);
+
+ const handleSignIn = () => {
+ if (window.location.hostname === "localhost") {
+ signIn("huggingface", { callbackUrl: "/" });
+ return;
+ }
+ const targetUrl = "https://enzostvs-gradio-build.hf.space";
+
+ let isOnTargetPage = false;
+ if (typeof window !== "undefined") {
+ try {
+ const isInIframe = window !== window.parent;
+
+ if (isInIframe) {
+ try {
+ isOnTargetPage = window.parent.location.href.startsWith(targetUrl);
+ } catch {
+ isOnTargetPage = false;
+ }
+ } else {
+ isOnTargetPage = window.location.href.startsWith(targetUrl);
+ }
+ } catch {
+ isOnTargetPage = false;
+ }
+ }
+
+ if (!isOnTargetPage) {
+ window.open(`${targetUrl}?signin=true`, "_blank");
+ } else {
+ signIn("huggingface", { callbackUrl: "/" });
+ }
+ };
+
+ const handleSignOut = () => {
+ signOut({ callbackUrl: "/" });
+ };
+
+ if (isLoading) {
+ return (
+
+ Loading...
+
+ );
+ }
+ return session?.user ? (
+
+
+
+
+
+
+ {session.user.name?.charAt(0).toUpperCase() || "U"}
+
+
+ {session.user.name}
+
+
+
+ My Account
+
+
+
+ Profile
+
+
+
+
+ Settings
+
+
+ <>
+
+ Projects
+ {projects && projects?.length > 0 ? (
+
+ {projects?.slice(0, 3).map((project) => (
+
+ ))}
+
+ {projects?.length > 0 && (
+
+ View all projects
+
+
+ )}
+
+ New project
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ No projects found.
+ Create a new project to get started.
+
+
+
+ New project
+
+
+
+
+
+ )}
+ >
+
+ Theme
+
+ setTheme("light")}
+ className={cn(
+ "flex-1",
+ theme === "light" && "border-amber-300! bg-amber-500/10!"
+ )}
+ >
+
+
+ setTheme("dark")}
+ className={cn(
+ "flex-1",
+ theme === "dark" && "border-indigo-500/50! bg-indigo-500/20!"
+ )}
+ >
+
+
+
+
+
+
+ Sign out
+
+
+
+ ) : (
+ <>
+
+ Sign in
+
+ Get Started
+ >
+ );
+}
diff --git a/components/viewer/blank-page.tsx b/components/viewer/blank-page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..09025a0f305f5a07aad0095b632dd06831b27f3b
--- /dev/null
+++ b/components/viewer/blank-page.tsx
@@ -0,0 +1,68 @@
+"use client";
+import Image from "next/image";
+import { useMount } from "react-use";
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { EXAMPLES_OF_PROJECT_SUGGESTIONS } from "@/lib/prompts";
+
+interface ExampleOfProjectSuggestion {
+ short_value: string;
+ long_value: string;
+}
+
+export function BlankPage() {
+ const [suggestions, setSuggestions] = useState(
+ []
+ );
+ const router = useRouter();
+
+ useMount(() => {
+ const randomSuggestions = [...EXAMPLES_OF_PROJECT_SUGGESTIONS]
+ .sort(() => Math.random() - 0.5)
+ .slice(0, 3);
+ setSuggestions(randomSuggestions);
+ });
+ return (
+
+
+
+
+ Here are some suggestions for you:
+
+
+ {suggestions.map((suggestion) => (
+
{
+ const promptInput = document.getElementById("prompt-input");
+ if (promptInput) {
+ (promptInput as HTMLDivElement).innerHTML =
+ suggestion.long_value;
+ }
+ }}
+ >
+ {suggestion.short_value}
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/viewer/index.tsx b/components/viewer/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a2aafc06d2375aa03ddef235b39a930d33d6ac9
--- /dev/null
+++ b/components/viewer/index.tsx
@@ -0,0 +1,75 @@
+"use client";
+import { ArrowLeft } from "lucide-react";
+import { SandpackLayout, SandpackPreview } from "@codesandbox/sandpack-react";
+
+import { DeviceType, MobileTabType } from "@/lib/type";
+import { cn } from "@/lib/utils";
+import { GridPattern } from "@/components/grid-pattern";
+import { useProject } from "@/components/projects/useProject";
+import { BlankPage } from "./blank-page";
+import { Button } from "@/components/ui/button";
+
+export function AppViewer({
+ mobileTab,
+ onToggleMobileTab,
+ device,
+}: {
+ mobileTab: MobileTabType;
+ device: DeviceType;
+ onToggleMobileTab: (tab: MobileTabType) => void;
+}) {
+ const { project, files } = useProject();
+
+ return (
+ 0)
+ ? "lg:border lg:border-border-muted lg:dark:border-secondary"
+ : "bg-accent border border-border-muted dark:border-secondary"
+ )}
+ >
+
+ {project?.name || (files && files.length > 0) ? (
+
+
+
+ ) : (
+
+ )}
+
0)
+ ? "justify-start"
+ : "p-2 justify-center"
+ )}
+ >
+
onToggleMobileTab("left-sidebar")}
+ >
+ Go to Chat
+
+
+
+ );
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..05e726d1b4201bc8c7716d2b058279676582e8c0
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,18 @@
+import { defineConfig, globalIgnores } from "eslint/config";
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = defineConfig([
+ ...nextVitals,
+ ...nextTs,
+ // Override default ignores of eslint-config-next.
+ globalIgnores([
+ // Default ignores of eslint-config-next:
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ]),
+]);
+
+export default eslintConfig;
diff --git a/huggingface.d.ts b/huggingface.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..40754f5ee13b74d48459ce065ca45f873355cdfb
--- /dev/null
+++ b/huggingface.d.ts
@@ -0,0 +1,12 @@
+import { SpaceEntry } from "@huggingface/hub";
+
+declare module "@huggingface/hub" {
+ interface SpaceEntry {
+ cardData?: {
+ emoji?: string;
+ title?: string;
+ colorFrom?: string;
+ colorTo?: string;
+ };
+ }
+}
\ No newline at end of file
diff --git a/lib/auth.ts b/lib/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..78943fbf15eb35493dc622cc09d8e2f6027a269c
--- /dev/null
+++ b/lib/auth.ts
@@ -0,0 +1,76 @@
+import NextAuth from "next-auth";
+import type { NextAuthConfig } from "next-auth";
+
+export const authConfig = {
+ providers: [
+ {
+ id: "huggingface",
+ name: "Hugging Face",
+ type: "oauth",
+ clientId: process.env.AUTH_HUGGINGFACE_ID,
+ clientSecret: process.env.AUTH_HUGGINGFACE_SECRET,
+ issuer: "https://huggingface.co",
+ authorization: {
+ url: "https://huggingface.co/oauth/authorize",
+ params: {
+ scope: "openid profile read-repos manage-repos inference-api",
+ },
+ },
+ token: "https://huggingface.co/oauth/token",
+ userinfo: "https://huggingface.co/oauth/userinfo",
+ checks: ["state"],
+ profile(profile) {
+ return {
+ id: profile.sub,
+ name: profile.name || profile.preferred_username,
+ username: profile.preferred_username,
+ email: profile.email,
+ image: profile.picture,
+ };
+ },
+ },
+ ],
+ callbacks: {
+ async jwt({ token, account, user }) {
+ // Persist the OAuth access_token and user info to the token right after signin
+ if (account) {
+ token.accessToken = account.access_token;
+ }
+ if (user) {
+ token.username = user.username;
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ // Send properties to the client, like an access_token from a provider
+ session.accessToken = token.accessToken as string;
+ if (session.user) {
+ session.user.username = token.username as string;
+ }
+ return session;
+ },
+ authorized({ auth, request: { nextUrl } }) {
+ const isLoggedIn = !!auth?.user;
+ const isOnNew = nextUrl.pathname.startsWith("/new");
+
+ const pathSegments = nextUrl.pathname.split("/").filter(Boolean);
+ const isOnProjectPage = pathSegments.length >= 2 &&
+ !nextUrl.pathname.startsWith("/new") &&
+ !nextUrl.pathname.startsWith("/api");
+
+ if (isOnProjectPage || isOnNew) {
+ if (isLoggedIn) return true;
+ return false;
+ }
+
+ return true;
+ },
+ },
+ pages: {
+ signIn: "/",
+ },
+ trustHost: true,
+} satisfies NextAuthConfig;
+
+export const { handlers, auth, signIn, signOut } = NextAuth(authConfig);
+
diff --git a/lib/format.ts b/lib/format.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8691c23e1d23c829b9b978cd9de07c18f801f91f
--- /dev/null
+++ b/lib/format.ts
@@ -0,0 +1,333 @@
+import {
+ END_FILE_CONTENT,
+ END_PROJECT_NAME,
+ START_FILE_CONTENT,
+ START_PROJECT_NAME,
+ SEARCH_START,
+ DIVIDER,
+ REPLACE_END,
+} from "./prompts";
+import { File } from "./type";
+
+/**
+ * Validates that a filename has an extension.
+ * Returns the filename if valid, null otherwise.
+ * Allows dotfiles (like .gitignore) and regular files with extensions (like app.py).
+ */
+const validateFilename = (filename: string): string | null => {
+ if (!filename) return null;
+
+ const trimmed = filename.trim();
+ if (!trimmed) return null;
+
+ // Find the last dot in the filename
+ const lastDotIndex = trimmed.lastIndexOf(".");
+
+ // No dot found - invalid (no extension)
+ if (lastDotIndex === -1) return null;
+
+ // Dot at the end - invalid (no extension after dot)
+ if (lastDotIndex === trimmed.length - 1) return null;
+
+ // Dot at the start - this is a dotfile (like .gitignore), which is valid
+ // Dot in the middle - this is a regular file with extension (like app.py), which is valid
+ // Both cases are acceptable as long as there's something after the dot
+
+ return trimmed;
+};
+
+/**
+ * Check if the end of a string contains a partial match of a target string
+ */
+const hasPartialMarkerAtEnd = (text: string, marker: string): number => {
+ // Check for partial matches starting from length 3 (to avoid false positives with common substrings)
+ for (
+ let len = Math.max(3, Math.floor(marker.length / 2));
+ len < marker.length;
+ len++
+ ) {
+ const partialMarker = marker.substring(0, len);
+ if (text.endsWith(partialMarker)) {
+ return len; // Return the length of the partial match
+ }
+ }
+ return 0; // No partial match found
+};
+
+/**
+ * Extract message content by removing all special markers and their content
+ */
+const extractMessageContent = (message: string): string => {
+ let result = message;
+
+ // Remove all complete START_FILE_CONTENT...END_FILE_CONTENT blocks
+ const fileContentRegex = new RegExp(
+ `${START_FILE_CONTENT}[\\s\\S]*?${END_FILE_CONTENT}`,
+ "g"
+ );
+ result = result.replace(fileContentRegex, "");
+
+ // Remove incomplete START_FILE_CONTENT blocks (streaming - no END_FILE_CONTENT yet)
+ const incompleteFileIndex = result.indexOf(START_FILE_CONTENT);
+ if (incompleteFileIndex !== -1) {
+ result = result.substring(0, incompleteFileIndex);
+ }
+
+ // Remove all complete START_PROJECT_NAME...END_PROJECT_NAME blocks
+ const projectNameRegex = new RegExp(
+ `${START_PROJECT_NAME}[\\s\\S]*?${END_PROJECT_NAME}`,
+ "g"
+ );
+ result = result.replace(projectNameRegex, "");
+
+ // Remove incomplete START_PROJECT_NAME blocks (streaming - no END_PROJECT_NAME yet)
+ const incompleteProjectIndex = result.indexOf(START_PROJECT_NAME);
+ if (incompleteProjectIndex !== -1) {
+ result = result.substring(0, incompleteProjectIndex);
+ }
+
+ // Remove all complete SEARCH_START...REPLACE_END blocks
+ const searchReplaceRegex = new RegExp(
+ `${SEARCH_START}[\\s\\S]*?${REPLACE_END}`,
+ "g"
+ );
+ result = result.replace(searchReplaceRegex, "");
+
+ // Remove incomplete SEARCH_START blocks (streaming - no REPLACE_END yet)
+ const incompleteSearchIndex = result.indexOf(SEARCH_START);
+ if (incompleteSearchIndex !== -1) {
+ result = result.substring(0, incompleteSearchIndex);
+ }
+
+ // Handle incomplete/partial markers at the end (for streaming)
+ // This is critical to prevent showing marker text to users during streaming
+ const markers = [START_FILE_CONTENT, START_PROJECT_NAME, SEARCH_START];
+
+ for (const marker of markers) {
+ const partialLength = hasPartialMarkerAtEnd(result, marker);
+ if (partialLength > 0) {
+ result = result.substring(0, result.length - partialLength);
+ break; // Only one marker can be at the end
+ }
+ }
+
+ // Clean up extra whitespace
+ return result.trim();
+};
+
+export const formatResponse = (message: string, currentFiles: File[]) => {
+ // Track which files have been created or modified
+ const modifiedFiles = new Map();
+
+ // Extract message content (everything outside special markers)
+ const messageContent = extractMessageContent(message);
+
+ // Extract project title
+ let projectTitle =
+ message.split(START_PROJECT_NAME)[1]?.split(END_PROJECT_NAME)[0]?.trim() ??
+ "";
+
+ // Handle partial project title markers
+ if (projectTitle && !message.includes(END_PROJECT_NAME)) {
+ for (let i = END_PROJECT_NAME.length - 1; i > 0; i--) {
+ const partialMarker = END_PROJECT_NAME.substring(0, i);
+ if (projectTitle.endsWith(partialMarker)) {
+ projectTitle = projectTitle
+ .substring(0, projectTitle.length - partialMarker.length)
+ .trim();
+ break;
+ }
+ }
+ }
+
+ if (message.includes(SEARCH_START)) {
+ const searchSections = message.split(SEARCH_START).slice(1);
+
+ for (
+ let sectionIndex = 0;
+ sectionIndex < searchSections.length;
+ sectionIndex++
+ ) {
+ const section = searchSections[sectionIndex];
+ const isLastSection = sectionIndex === searchSections.length - 1;
+
+ // Check if this section has a complete REPLACE_END marker
+ const hasReplaceEnd = section.includes(REPLACE_END);
+
+ // For incomplete sections (last section without REPLACE_END), skip processing
+ // to avoid applying partial replacements that corrupt the file
+ if (isLastSection && !hasReplaceEnd) {
+ continue;
+ }
+
+ const searchPart = section.split(REPLACE_END)[0];
+ if (!searchPart) continue;
+
+ const dividerIndex = searchPart.indexOf(DIVIDER);
+ if (dividerIndex === -1) continue;
+
+ const searchContent = searchPart.substring(0, dividerIndex);
+ const replaceContent = searchPart.substring(
+ dividerIndex + DIVIDER.length
+ );
+
+ if (!searchContent || !replaceContent) continue;
+
+ // Extract filename from the first line (same line as SEARCH_START)
+ const firstNewlineIndex = searchContent.indexOf("\n");
+ let filename = "";
+ let searchContentAfterFilename = searchContent;
+
+ if (firstNewlineIndex !== -1) {
+ // Filename is on the first line (same line as SEARCH_START marker)
+ filename = searchContent.substring(0, firstNewlineIndex).trim();
+ searchContentAfterFilename = searchContent.substring(
+ firstNewlineIndex + 1
+ );
+ } else {
+ // No newline found, entire content might be just the filename
+ filename = searchContent.trim();
+ searchContentAfterFilename = "";
+ }
+
+ // Validate filename has extension
+ const validatedFilename = validateFilename(filename);
+ if (!validatedFilename) continue;
+ filename = validatedFilename;
+
+ const searchCodeStart = searchContentAfterFilename.indexOf("```");
+ if (searchCodeStart === -1) continue;
+
+ const searchCodeBlock =
+ searchContentAfterFilename.substring(searchCodeStart);
+ let searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/);
+ if (!searchMatch) {
+ searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*)/);
+ }
+ if (!searchMatch) continue;
+
+ const searchText = searchMatch[1].trim();
+
+ const replaceCodeStart = replaceContent.indexOf("```");
+ if (replaceCodeStart === -1) continue;
+
+ const replaceCodeBlock = replaceContent.substring(replaceCodeStart);
+ let replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/);
+ if (!replaceMatch) {
+ replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*)/);
+ }
+ if (!replaceMatch) continue;
+
+ let replaceText = replaceMatch[1];
+ for (let i = 1; i < 3; i++) {
+ const partialClosing = "`".repeat(i);
+ if (replaceText.endsWith(partialClosing)) {
+ replaceText = replaceText.substring(0, replaceText.length - i);
+ break;
+ }
+ }
+ replaceText = replaceText.trim();
+
+ // First check if we already have a modified version of this file in the current operation
+ const existingModifiedFile = modifiedFiles.get(filename);
+ const fileToModify =
+ existingModifiedFile || currentFiles.find((f) => f.path === filename);
+
+ if (fileToModify && fileToModify.content) {
+ const newContent = fileToModify.content.replace(
+ searchText,
+ replaceText
+ );
+ if (newContent !== fileToModify.content) {
+ modifiedFiles.set(filename, { path: filename, content: newContent });
+ }
+ } else if (!fileToModify) {
+ modifiedFiles.set(filename, { path: filename, content: replaceText });
+ }
+ }
+ }
+
+ // Handle new file creation blocks
+ if (message.includes(START_FILE_CONTENT)) {
+ const fileSections = message.split(START_FILE_CONTENT).slice(1);
+
+ for (const section of fileSections) {
+ const rawContent = section.split(END_FILE_CONTENT)[0] || "";
+
+ // Extract filename - it's on the same line as START_FILE_CONTENT (before first newline)
+ const firstNewlineIndex = rawContent.indexOf("\n");
+ let path = "";
+ let contentAfterFilename = rawContent;
+
+ if (firstNewlineIndex !== -1) {
+ // Filename is before the first newline (same line as START_FILE_CONTENT marker)
+ path = rawContent.substring(0, firstNewlineIndex).trim();
+ contentAfterFilename = rawContent.substring(firstNewlineIndex + 1);
+ } else {
+ // No newline found, entire content might be just the filename
+ path = rawContent.trim();
+ contentAfterFilename = "";
+ }
+
+ // Validate that path has an extension
+ const validatedPath = validateFilename(path);
+ if (!validatedPath) continue;
+ path = validatedPath;
+
+ // Find code block
+ const codeBlockStart = contentAfterFilename.indexOf("```");
+
+ if (codeBlockStart === -1) {
+ // No code block, use raw content after filename
+ const content = contentAfterFilename.trim();
+ if (content || !currentFiles.find((f) => f.path === path)) {
+ // Always add files from the message
+ modifiedFiles.set(path, { path, content });
+ }
+ } else {
+ // Has code block - extract content between ``` markers
+ const afterCodeBlockStart =
+ contentAfterFilename.substring(codeBlockStart);
+
+ // Try to match complete code block first
+ const completeMatch = afterCodeBlockStart.match(
+ /^```[\w]*\n?([\s\S]*?)```/
+ );
+
+ if (completeMatch) {
+ const content = completeMatch[1].trim();
+ modifiedFiles.set(path, { path, content });
+ } else {
+ // Incomplete code block (streaming) - match everything after opening ```
+ const incompleteMatch = afterCodeBlockStart.match(
+ /^```[\w]*\n?([\s\S]*)/
+ );
+ if (incompleteMatch) {
+ let content = incompleteMatch[1];
+ // Remove trailing partial closing marker if present
+ for (let i = 1; i < 3; i++) {
+ const partialClosing = "`".repeat(i);
+ if (content.endsWith(partialClosing)) {
+ content = content.substring(0, content.length - i);
+ break;
+ }
+ }
+ content = content.trim();
+ modifiedFiles.set(path, { path, content });
+ }
+ }
+ }
+ }
+ }
+
+ // Convert modified files map to array
+ const files = Array.from(modifiedFiles.values())?.filter(
+ (file) => file.path !== "README.md"
+ );
+
+ return {
+ messageContent,
+ files,
+ projectTitle,
+ };
+};
diff --git a/lib/prompts.ts b/lib/prompts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8dfa0313a501ecf46bf0fccba7985593df6faecf
--- /dev/null
+++ b/lib/prompts.ts
@@ -0,0 +1,555 @@
+export const START_FILE_CONTENT = "=== START_FILE_CONTENT";
+export const END_FILE_CONTENT = "=== END_FILE_CONTENT";
+export const START_PROJECT_NAME = "=== START_PROJECT_NAME";
+export const END_PROJECT_NAME = "=== END_PROJECT_NAME";
+export const SEARCH_START = "=== SEARCH";
+export const DIVIDER = "=======";
+export const REPLACE_END = "=== REPLACE";
+
+// todo: rework follow up prompt, take a look at what I did on the v3 version, was working better.
+
+export const PROMPT_FOR_IMAGE_GENERATION = `
+=== IMAGE PLACEHOLDER ===
+If you want to use image placeholder, http://Static.photos Usage:Format: http://static.photos/[category]/[dimensions]/[seed] where dimensions must be one of: 200x200, 320x240, 640x360, 1024x576, or 1200x630; seed can be any number (1-999+) for consistent images or omit for random; categories include: nature, office, people, technology, minimal, abstract, aerial, blurred, bokeh, gradient, monochrome, vintage, white, black, blue, red, green, yellow, cityscape, workspace, food, travel, textures, industry, indoor, outdoor, studio, finance, medical, season, holiday, event, sport, science, legal, estate, restaurant, retail, wellness, agriculture, construction, craft, cosmetic, automotive, gaming, or education.
+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).
+`;
+export const PROMPT_FOR_PROJECT_NAME = `
+## NAME OF THE PROJECT (REQUIRED WHEN CREATING A NEW PROJECT)
+Write the name of the project, based on the user's request. Add an emoji at the end of the name. It should be short, like 6 words. Be creative and unique.
+Examples:
+${START_PROJECT_NAME}
+My Awesome Project 🚀
+${END_PROJECT_NAME}
+${START_PROJECT_NAME}
+My Amazing Project 🤩
+${END_PROJECT_NAME}
+`;
+export const INITIAL_SYSTEM_PROMPT = `
+=== ABOUT YOU ===
+You are a helpful assistant and a developer with strong knowledge of HTML, CSS, JAVASCRIPT, and UI/UX Design.
+You are able to help with any questions about HTML, CSS, JAVASCRIPT, and UI/UX Design.
+You are also able to help with questions about projects the user has created that are related to HTML, CSS, JAVASCRIPT, and UI/UX Design.
+
+If the user asks you something not related to HTML, CSS, JAVASCRIPT, and UI/UX Design or their project, you must tell them that you are not able to help with that.
+The user will ask you questions and also ask you to modify their existing code. You don't need to ask for clarification, just code or answer the question directly.
+
+You are able to create multiple files to make the project more organized and easy to understand, like style.css for styling, script.js for JavaScript, etc.
+Regarding the code generation, you MUST create the best possible code for the user's request, with the best UI/UX design and the best performance. Make it always responsive and mobile-friendly. Try to be as close as possible to the user's request, but always with a modern and clean design.
+If the user asks you something without details, you must imagine the best possible solution for the user's request, and create the best possible code for it.
+
+🚨 IMPORTANT: If you want to show a code block, don't follow the rules bellow, just show the code block by using markdown code blocks. The rules bellow are only for real code generation asked by the user, not for demo purposes.
+
+=== PROJECT REQUIREMENTS ===
+- Always create at least a \`index.html\` file, this is the main file that will be used to display the project.
+- Do not create a \`README.md\` or any other markdown file, it will be automatically created by the API.
+- Use Tailwind CSS for styling, include it in the code by adding \`\` in the \`\` tag.
+- Use any library you need to use, just add it to the \`\` tag or \`\` tag, depending on the library.
+- For Icons, use the latest version of Lucide Icons, here is a demo on how to include it and use it:
+\`\`\`html
+
+
+
+
+
+
+
+
+- Use real public API, you can find good ones there: https://github.com/public-apis/public-apis, depending on the user's request, you can use the API to get the data and display it in the UI.
+
+${PROMPT_FOR_IMAGE_GENERATION}
+
+=== OUTPUT FORMAT ===
+🚨 CRITICAL: YOU MUST ALWAYS FOLLOW THIS EXACT FORMAT FOR EVERY RESPONSE. NO EXCEPTIONS.
+
+## RESPONSE FORMAT FOR EVERY RESPONSE
+You can write your human-readable response anywhere in your message. Markdown allowed, Emoji allowed to make it more engaging. This is your explanation or answer to the user.
+
+**IMPORTANT RULES:**
+1. ✅ ANY text that is NOT inside special markers (${START_FILE_CONTENT}, ${START_PROJECT_NAME}, ${SEARCH_START}) will be shown to the user as your message
+2. ✅ You can write text before, between, or after code blocks - it will all be combined as your message
+3. ✅ Use markdown formatting to make your responses clear and engaging
+
+**EXAMPLE:**
+I'll help you create that! 🚀
+
+[...then your code blocks if needed...]
+
+Here's what I've created for you...
+
+## RESPONSE FORMAT FOR CODE CREATION
+If the user asks you to code, follow the project requirements above, each file MUST be inside a markdown code block with the language identifier of the file, and the filename MUST be on the same line as ${START_FILE_CONTENT} and ${END_FILE_CONTENT}.
+Examples:
+${START_FILE_CONTENT} index.html
+\`\`\`html
+[Complete code for the file]
+\`\`\`
+${END_FILE_CONTENT}
+${START_FILE_CONTENT} style.css
+\`\`\`css
+[Complete code for the file]
+\`\`\`
+${END_FILE_CONTENT}
+${START_FILE_CONTENT} script.js
+\`\`\`javascript
+[Complete code for the file]
+\`\`\`
+${END_FILE_CONTENT}
+${START_FILE_CONTENT} components/header.js
+\`\`\`javascript
+[Complete code for the file]
+\`\`\`
+${END_FILE_CONTENT}
+
+${PROMPT_FOR_PROJECT_NAME}
+`;
+export const FOLLOW_UP_SYSTEM_PROMPT = `
+=== ABOUT YOU ===
+You are a helpful assistant and a developer with strong knowledge of HTML, CSS, JAVASCRIPT, and UI/UX Design.
+You are able to help with any questions about HTML, CSS, JAVASCRIPT, and UI/UX Design.
+You are also able to help with questions about projects the user has created that are related to HTML, CSS, JAVASCRIPT, and UI/UX Design.
+
+If the user asks you something not related to HTML, CSS, JAVASCRIPT, and UI/UX Design or their project, you must tell them that you are not able to help with that.
+The user will ask you questions and also ask you to modify their existing code. You don't need to ask for clarification, just code or answer the question directly.
+
+You are able to create/update multiple files to make the project more organized and easy to understand, like style.css for styling, script.js for JavaScript, etc.
+Regarding the code generation, you MUST create the best possible code for the user's request, with the best UI/UX design and the best performance. Make it always responsive and mobile-friendly. Try to be as close as possible to the user's request, but always with a modern and clean design.
+If the user asks you something without details, you must imagine the best possible solution for the user's request, and create the best possible code for it.
+
+🚨 IMPORTANT: If you want to show a code block for demonstration purposes, just show the code block using markdown code blocks. The rules below are ONLY for real code generation/modification requested by the user.
+
+🚨🚨🚨 CRITICAL UPDATE RULE: When the user asks for an update, you MUST:
+- ONLY MODIFY what the user specifically asked for
+- DO NOT rewrite entire files - use the SEARCH/REPLACE format to change only the necessary lines
+- Keep the original code structure and functionality intact
+- Only make additional changes if they are directly related and necessary for the requested change to work
+
+=== PROJECT REQUIREMENTS ===
+- Do not create a \`README.md\` or any other markdown file, it will be automatically created by the API.
+- Use Tailwind CSS for styling, include it in the code by adding \`\` in the \`\` tag.
+- Use any library you need to use, just add it to the \`\` tag or \`\` tag, depending on the library.
+- For Icons, use the latest version of Lucide Icons, here is a demo on how to include it and use it:
+\`\`\`html
+
+
+
+
+
+
+
+
+\`\`\`
+- Use real public API, you can find good ones there: https://github.com/public-apis/public-apis, depending on the user's request, you can use the API to get the data and display it in the UI.
+- When creating new pages or adding shared functionality, make sure to update navigation links in existing HTML files so users can navigate to the new pages.
+
+${PROMPT_FOR_IMAGE_GENERATION}
+
+=== OUTPUT FORMAT ===
+🚨 CRITICAL: YOU MUST ALWAYS FOLLOW THIS EXACT FORMAT FOR EVERY RESPONSE. NO EXCEPTIONS.
+
+## RESPONSE FORMAT FOR EVERY RESPONSE
+You can write your human-readable response anywhere in your message. Markdown allowed, Emoji allowed to make it more engaging. This is your explanation or answer to the user.
+
+**IMPORTANT RULES:**
+1. ✅ ANY text that is NOT inside special markers (${START_FILE_CONTENT}, ${SEARCH_START}) will be shown to the user as your message
+2. ✅ You can write text before, between, or after code blocks - it will all be combined as your message
+3. ✅ Use markdown formatting to make your responses clear and engaging
+
+**EXAMPLE:**
+I'll update that for you! 🚀
+
+[...then your SEARCH/REPLACE blocks or new file blocks if needed...]
+
+Done! Your changes are ready.
+
+## RESPONSE FORMAT FOR CODE UPDATES (MODIFYING EXISTING FILES)
+🚨 THIS IS THE PRIMARY FORMAT YOU SHOULD USE WHEN UPDATING EXISTING CODE.
+
+When the user asks you to update existing code, you MUST use the SEARCH/REPLACE format. DO NOT output entire files.
+You can do multiple updates in the same response, just follow the steps below for each update:
+
+**SEARCH/REPLACE FORMAT:**
+1. Start with ${SEARCH_START}
+2. Add a space and provide the filename of the file to update (e.g., index.html, style.css, script.js, about.html, etc.)
+3. Inside a markdown code block (with appropriate language: html, css, javascript), provide the EXACT lines from the current code that need to be replaced
+4. Use ${DIVIDER} to separate the search block from the replacement
+5. Inside a markdown code block (with same language), provide the new lines that should replace the original lines
+6. End with ${REPLACE_END}
+
+**IMPORTANT SEARCH/REPLACE RULES:**
+- You can use multiple SEARCH/REPLACE blocks if changes are needed in different parts of the same file or in different files
+- The SEARCH block must *exactly* match the current code, including indentation, whitespace, and line breaks
+- Choose enough context in your SEARCH block to make it unique within the file
+- To insert code at the beginning, use an empty SEARCH block (only ${SEARCH_START} filename, empty code block, ${DIVIDER}, then your code, ${REPLACE_END})
+- To insert code in the middle, include the line *before* the insertion point in SEARCH, then include that same line plus the new lines in REPLACE
+- To delete code, provide the lines to delete in SEARCH and leave REPLACE empty (empty code block between ${DIVIDER} and ${REPLACE_END})
+
+**EXAMPLES:**
+
+Example 1 - Updating HTML content:
+${SEARCH_START} index.html
+\`\`\`html
+ Old Title
+ Old description
+\`\`\`
+${DIVIDER}
+\`\`\`html
+ New Title
+ New amazing description with more details
+\`\`\`
+${REPLACE_END}
+
+Example 2 - Updating CSS:
+${SEARCH_START} style.css
+\`\`\`css
+body {
+ background: white;
+ color: black;
+}
+\`\`\`
+${DIVIDER}
+\`\`\`css
+body {
+ background: linear-gradient(to right, #667eea, #764ba2);
+ color: white;
+}
+\`\`\`
+${REPLACE_END}
+
+Example 3 - Inserting code (adding script tag before closing body):
+${SEARCH_START} index.html
+\`\`\`html
+
+
+\`\`\`
+${DIVIDER}
+\`\`\`html
+
+
+
+\`\`\`
+${REPLACE_END}
+
+Example 4 - Deleting code:
+${SEARCH_START} index.html
+\`\`\`html
+ This paragraph will be deleted.
+\`\`\`
+${DIVIDER}
+\`\`\`html
+\`\`\`
+${REPLACE_END}
+
+Example 5 - Multiple changes in different files:
+${SEARCH_START} index.html
+\`\`\`html
+ Welcome
+\`\`\`
+${DIVIDER}
+\`\`\`html
+ Welcome to our site!
+\`\`\`
+${REPLACE_END}
+
+${SEARCH_START} style.css
+\`\`\`css
+h1 {
+ font-size: 24px;
+}
+\`\`\`
+${DIVIDER}
+\`\`\`css
+h1 {
+ font-size: 36px;
+ font-weight: bold;
+}
+\`\`\`
+${REPLACE_END}
+
+## RESPONSE FORMAT FOR CREATING NEW FILES
+When the user asks you to create a NEW file (new HTML page, new CSS file, new JS file, etc.), use the following format:
+
+**NEW FILE FORMAT:**
+1. Start with ${START_FILE_CONTENT}
+2. Add a space and provide the filename (e.g., about.html, animation.js, components/header.js)
+3. Inside a markdown code block with appropriate language identifier (html, css, javascript), provide the COMPLETE code for the new file
+4. End with ${END_FILE_CONTENT}
+5. If creating a new page, make sure to ALSO UPDATE existing HTML files with navigation links using the SEARCH/REPLACE format
+
+**EXAMPLES:**
+
+Example 1 - Creating a new HTML page:
+${START_FILE_CONTENT} about.html
+\`\`\`html
+
+
+
+
+
+ About Us
+
+
+
+
+
+ Home
+ About
+
+
+ About Us
+ This is the about page.
+
+
+
+
+\`\`\`
+${END_FILE_CONTENT}
+
+Then update existing pages to link to it:
+${SEARCH_START} index.html
+\`\`\`html
+
+ Home
+
+\`\`\`
+${DIVIDER}
+\`\`\`html
+
+ Home
+ About
+
+\`\`\`
+${REPLACE_END}
+
+Example 2 - Creating a new JavaScript file:
+${START_FILE_CONTENT} animation.js
+\`\`\`javascript
+// Smooth scroll animation
+document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ e.preventDefault();
+ const target = document.querySelector(this.getAttribute('href'));
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth' });
+ }
+ });
+});
+\`\`\`
+${END_FILE_CONTENT}
+
+Then update HTML files to include it:
+${SEARCH_START} index.html
+\`\`\`html
+
+
+\`\`\`
+${DIVIDER}
+\`\`\`html
+
+
+
+\`\`\`
+${REPLACE_END}
+
+## SUMMARY OF WHEN TO USE EACH FORMAT
+- **SEARCH/REPLACE format** (${SEARCH_START}...${DIVIDER}...${REPLACE_END}): Use when UPDATING/MODIFYING existing files. This is your primary format for changes.
+- **NEW FILE format** (${START_FILE_CONTENT}...${END_FILE_CONTENT}): Use when CREATING brand new files that don't exist yet.
+- When creating new pages, ALWAYS update navigation in existing pages using SEARCH/REPLACE format.
+- When creating new CSS/JS files, ALWAYS update HTML files to include them using SEARCH/REPLACE format.
+
+Remember: DO NOT rewrite entire files when updating. Use SEARCH/REPLACE to change only what's necessary!
+`;
+
+export const EXAMPLES_OF_PROJECT_SUGGESTIONS = [
+ // Business & SaaS
+ {
+ short_value: "Modern SaaS Landing Page",
+ long_value:
+ "Create a modern SaaS landing page with a hero section featuring a product demo video or animation, benefits section with icons and descriptions, pricing plans comparison table with highlighted recommended tier, customer testimonials with company logos and photos, FAQ accordion section, integration showcase, and a prominent call-to-action footer with email signup.",
+ },
+ {
+ short_value: "Startup Landing Page",
+ long_value:
+ "Build a startup landing page with a bold hero section featuring the value proposition, problem-solution sections with visual illustrations, product features showcase with interactive elements, team section with photos and roles, investor logos, press mentions, early access signup form, and social proof metrics counter.",
+ },
+ {
+ short_value: "Business Consulting Website",
+ long_value:
+ "Design a professional business consulting website with a trust-building hero section, services overview with detailed descriptions, case studies with results and metrics, consultant team profiles with expertise areas, client testimonials carousel, consultation booking form, and a resources/blog section.",
+ },
+ // E-commerce & Retail
+ {
+ short_value: "E-commerce Product Page",
+ long_value:
+ "Create a detailed e-commerce product page with image gallery and zoom functionality, product specifications table, size/color selector with visual swatches, add-to-cart button with quantity selector, customer reviews and ratings section, related products carousel, shipping information, and return policy accordion.",
+ },
+ {
+ short_value: "Online Store Homepage",
+ long_value:
+ "Build an online store homepage with a hero banner featuring seasonal promotions, category navigation cards with images, featured products grid with quick view, bestsellers section, new arrivals carousel, customer testimonials, newsletter signup with discount offer, and trust badges for secure payment and shipping.",
+ },
+ {
+ short_value: "Fashion Brand Website",
+ long_value:
+ "Design a fashion brand website with full-screen hero slider showcasing latest collections, lookbook section with lifestyle photography, shop by category grid, featured collections with hover effects, Instagram feed integration, brand story section with video, styling tips blog section, and size guide with measurements.",
+ },
+ // Food & Restaurant
+ {
+ short_value: "Restaurant Website",
+ long_value:
+ "Create a restaurant website with appetizing hero section featuring signature dishes, online menu with categories and dish photos, reservation booking system with date/time picker, location map with directions, chef's special section, customer reviews with food ratings, gallery of restaurant ambiance, and contact form for events.",
+ },
+ {
+ short_value: "Coffee Shop Website",
+ long_value:
+ "Build a cozy coffee shop website with warm hero section showcasing coffee art, menu with drinks and pastries categorized by type, about section highlighting coffee sourcing and roasting process, location finder with multiple branches, loyalty program details, online ordering integration, barista spotlights, and events calendar.",
+ },
+ {
+ short_value: "Food Delivery Landing Page",
+ long_value:
+ "Design a food delivery landing page with eye-catching hero showing popular dishes, restaurant categories with cuisine filters, how it works section with step-by-step illustrations, delivery area checker with postal code input, app download buttons with QR code, partner restaurants showcase, promotional offers banner, and customer testimonials with ratings.",
+ },
+ // Real Estate & Property
+ {
+ short_value: "Real Estate Agency Website",
+ long_value:
+ "Create a real estate agency website with property search bar featuring filters for location/price/bedrooms, featured properties grid with key details and photos, virtual tour integration, neighborhood guides with amenities, agent profiles with contact information, mortgage calculator tool, market insights blog, and property valuation request form.",
+ },
+ {
+ short_value: "Luxury Property Showcase",
+ long_value:
+ "Build a luxury property showcase with full-screen hero image slider, exclusive property listings with high-quality photography, detailed property descriptions with amenities list, 360-degree virtual tours, location highlights with lifestyle benefits, appointment scheduling for private viewings, property comparison tool, and concierge services section.",
+ },
+ // Creative & Portfolio
+ {
+ short_value: "Photography Portfolio",
+ long_value:
+ "Design a photography portfolio with masonry grid layout showcasing best work, categorized galleries by photography type (wedding/portrait/landscape), lightbox image viewer with navigation, about the photographer section with bio and equipment, client testimonials, package pricing for different photo services, contact form for booking inquiries, and Instagram feed integration.",
+ },
+ {
+ short_value: "Creative Agency Portfolio",
+ long_value:
+ "Create a creative agency portfolio with bold hero section featuring latest campaign, services overview with icons and descriptions, case studies showcase with before/after comparisons and results metrics, client logos wall, team section with creative roles, design process timeline, awards and recognition section, and project inquiry form.",
+ },
+ {
+ short_value: "UX/UI Designer Portfolio",
+ long_value:
+ "Build a UX/UI designer portfolio with clean hero section introducing yourself, featured projects with case study breakdowns including problem/solution/results, interactive prototypes embedded, design process explanation with wireframes and mockups, skills and tools section with proficiency levels, client testimonials, Dribbble/Behance integration, and contact form.",
+ },
+ // Personal & Blog
+ {
+ short_value: "Personal Brand Website",
+ long_value:
+ "Design a personal brand website with impactful hero section with professional photo and tagline, about me section with story and values, services or expertise areas, portfolio or work samples, speaking engagements or media appearances, blog or articles section, newsletter signup, social media links, and contact form with calendar booking integration.",
+ },
+ {
+ short_value: "Modern Blog Website",
+ long_value:
+ "Create a modern blog website with featured post hero section, blog posts grid with thumbnails and excerpts, category filters and tags, author bio section with photo, related posts suggestions, comment section integration, email subscription form with popup, search functionality, popular posts sidebar, and social sharing buttons.",
+ },
+ {
+ short_value: "Travel Blog",
+ long_value:
+ "Build a travel blog with inspiring hero banner featuring destination photos, travel posts organized by country/region, interactive map showing visited places, travel guides with itineraries and tips, photo galleries from trips, travel essentials and gear recommendations, destination comparison tool, email newsletter for travel updates, and booking affiliate links.",
+ },
+ // Health & Fitness
+ {
+ short_value: "Fitness Gym Website",
+ long_value:
+ "Create a fitness gym website with motivating hero video background, class schedule calendar with booking system, membership plans comparison with benefits, trainer profiles with specializations, virtual gym tour with facility photos, success stories with before/after transformations, fitness blog with workout tips, free trial signup form, and location/hours information.",
+ },
+ {
+ short_value: "Yoga Studio Website",
+ long_value:
+ "Design a yoga studio website with calming hero section showcasing studio space, class types with descriptions and difficulty levels, live class schedule with online/in-person options, instructor bios with teaching philosophies, beginner's guide to yoga, wellness blog, workshop and retreat information, membership packages, and online booking system.",
+ },
+ {
+ short_value: "Health & Wellness Page",
+ long_value:
+ "Build a health and wellness page with hero section promoting holistic health, services offered including nutrition/fitness/mental health, practitioner profiles with credentials, appointment booking calendar, wellness resources and blog articles, patient testimonials with health journey stories, FAQ section, insurance information, and contact form with health assessment questionnaire.",
+ },
+ // Education & Learning
+ {
+ short_value: "Online Course Landing Page",
+ long_value:
+ "Create an online course landing page with compelling hero showcasing course outcomes, curriculum breakdown with module details, instructor credentials and biography, student testimonials with success stories, course preview video, pricing options with payment plans, enrollment countdown timer, skills you'll learn section with icons, and money-back guarantee badge.",
+ },
+ {
+ short_value: "University/School Website",
+ long_value:
+ "Design a university/school website with prominent hero featuring campus life, academic programs overview with departments, admissions information with requirements and deadlines, virtual campus tour with interactive map, faculty profiles with research areas, student resources and services, events calendar, news and announcements section, and application portal access.",
+ },
+ {
+ short_value: "Tutoring Service Website",
+ long_value:
+ "Build a tutoring service website with hero section highlighting student success rates, subjects offered with grade levels, tutor matching system with profiles and qualifications, flexible scheduling with online/in-person options, pricing packages and hourly rates, parent testimonials, free assessment offer, learning resources, and contact form with subject selector.",
+ },
+ // Events & Entertainment
+ {
+ short_value: "Event Conference Website",
+ long_value:
+ "Create an event conference website with countdown timer to event date, speaker lineup with bios and session topics, event schedule with multiple tracks, venue information with map and travel details, ticket types and pricing with early bird discounts, sponsor showcase with logo display, past event highlights with photos/videos, and registration form.",
+ },
+ {
+ short_value: "Music Festival Landing Page",
+ long_value:
+ "Design a music festival landing page with vibrant hero featuring headliner artists, full lineup with artist photos and genres, stage schedule with set times, festival map with venue layout, ticket tiers with VIP options, camping and accommodation info, festival rules and what to bring, photo gallery from previous years, and ticket purchase integration.",
+ },
+ {
+ short_value: "Wedding Website",
+ long_value:
+ "Build a wedding website with romantic hero featuring couple's photo, love story timeline, wedding day schedule with ceremony and reception details, venue information with directions, RSVP form with meal preferences, accommodation suggestions for guests, gift registry links, photo gallery from engagement, wedding party introductions, and FAQs section.",
+ },
+ // Professional Services
+ {
+ short_value: "Law Firm Website",
+ long_value:
+ "Create a law firm website with professional hero establishing trust and authority, practice areas with detailed service descriptions, attorney profiles with education and case experience, case results and settlements, legal resources and blog, client testimonials, consultation booking form, frequently asked legal questions, contact information with multiple office locations.",
+ },
+ {
+ short_value: "Dental Clinic Website",
+ long_value:
+ "Design a dental clinic website with welcoming hero showing modern facilities, dental services offered with procedure explanations, dentist and staff profiles with credentials, patient testimonials with smile transformations, new patient forms downloadable, insurance and payment options, dental health blog and tips, online appointment booking system, and emergency contact information.",
+ },
+ {
+ short_value: "Architecture Firm Website",
+ long_value:
+ "Build an architecture firm website with striking hero showcasing signature project, portfolio gallery organized by project type (residential/commercial/institutional), project case studies with design concepts and final photos, services from planning to construction, architect team profiles, design philosophy and approach, awards and publications, and project inquiry form.",
+ },
+ // Technology & Apps
+ {
+ short_value: "App Landing Page",
+ long_value:
+ "Create an app landing page with phone mockups showing app interface, key features section with animated icons, app store download buttons (iOS/Android), how it works explanation with screenshots, user testimonials and ratings, app demo video, subscription pricing if applicable, press mentions, FAQ section, and waitlist signup for beta access.",
+ },
+ {
+ short_value: "Software Product Page",
+ long_value:
+ "Design a software product page with hero demonstrating product in action, features breakdown with detailed descriptions and benefits, technical specifications and integrations, pricing tiers comparison table, customer logos and case studies, live product demo or free trial signup, API documentation link, security and compliance badges, and customer support information.",
+ },
+ // Non-profit & Community
+ {
+ short_value: "Non-profit Organization Website",
+ long_value:
+ "Create a non-profit website with mission-driven hero showing impact, about section explaining cause and history, programs and initiatives with detailed descriptions, impact metrics and success stories, donation form with different giving options, volunteer opportunities with signup, upcoming events and campaigns, transparency section with annual reports, and newsletter subscription.",
+ },
+ {
+ short_value: "Community Organization Website",
+ long_value:
+ "Build a community organization website with welcoming hero showcasing community members, about the organization with mission and values, programs and services offered, membership information and benefits, events calendar with community gatherings, volunteer opportunities, resources and support section, testimonials from community members, and contact form with location map.",
+ },
+ // Misc & Utility
+ {
+ short_value: "Interactive Weather Dashboard",
+ long_value:
+ "Design an interactive weather dashboard with current weather display showing temperature/conditions/humidity, location search with autocomplete, 7-day forecast with daily highs/lows, hourly forecast chart, weather map with radar overlay, severe weather alerts section, sunrise/sunset times, air quality index, UV index, and weather statistics using real weather API data.",
+ },
+ {
+ short_value: "Modern Calculator",
+ long_value:
+ "Create a modern calculator with clean interface featuring number pad and operation buttons, basic arithmetic operations (+,-,*,/), advanced functions (percentage, square root, power), calculation history log that can be cleared, keyboard input support, light/dark theme toggle, responsive design for mobile and desktop, and smooth animations for button presses.",
+ },
+];
diff --git a/lib/providers.ts b/lib/providers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1cf47dc628cad3ca5f8425123736e0a4c657fd4f
--- /dev/null
+++ b/lib/providers.ts
@@ -0,0 +1,67 @@
+import DeepSeekLogo from "@/assets/deepseek.svg";
+import QwenLogo from "@/assets/qwen.svg";
+import KimiLogo from "@/assets/kimi.svg";
+import ZaiLogo from "@/assets/zai.svg";
+import MiniMaxLogo from "@/assets/minimax.svg";
+
+export const MODELS = [
+ {
+ value: "deepseek-ai/DeepSeek-V3-0324",
+ label: "DeepSeek V3 O324",
+ providers: ["fireworks-ai", "nebius", "sambanova", "novita", "hyperbolic"],
+ autoProvider: "novita",
+ logo: DeepSeekLogo,
+ companyName: "DeepSeek",
+ isBestSeller: true,
+ },
+ {
+ value: "deepseek-ai/DeepSeek-V3.2",
+ label: "DeepSeek V3.2",
+ logo: DeepSeekLogo,
+ companyName: "DeepSeek",
+ },
+ {
+ value: "Qwen/Qwen3-Coder-30B-A3B-Instruct",
+ label: "Qwen3 Coder 30B A3B Instruct",
+ providers: ["novita", "hyperbolic"],
+ autoProvider: "novita",
+ logo: QwenLogo,
+ companyName: "Qwen",
+ },
+ {
+ value: "moonshotai/Kimi-K2-Instruct-0905",
+ label: "Kimi K2 Instruct",
+ providers: ["together", "novita", "groq"],
+ autoProvider: "groq",
+ logo: KimiLogo,
+ companyName: "Kimi",
+ },
+ {
+ value: "zai-org/GLM-4.7",
+ label: "GLM-4.7",
+ logo: ZaiLogo,
+ companyName: "Z.ai",
+ isNew: true,
+ isBestSeller: true,
+ },
+ {
+ value: "MiniMaxAI/MiniMax-M2.1",
+ label: "MiniMax M2.1",
+ logo: MiniMaxLogo,
+ companyName: "MiniMax",
+ top_k: 40,
+ temperature: 1.0,
+ top_p: 0.95,
+ isNew: true,
+ isBestSeller: true,
+ },
+];
+
+export const getProviders = async (model: string) => {
+ const response = await fetch(
+ `https://router.huggingface.co/v1/models/${model}`
+ );
+ const { data } = await response.json();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return data.providers.map((provider: any) => provider.provider);
+};
diff --git a/lib/type.ts b/lib/type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ffb9dff869ccd99f3d2806845ecbe53e4c7b1cdd
--- /dev/null
+++ b/lib/type.ts
@@ -0,0 +1,45 @@
+import { buttonVariants } from "@/components/ui/button";
+import { VariantProps } from "class-variance-authority";
+import { LucideIcon } from "lucide-react";
+
+export type DeviceType = "desktop" | "mobile";
+export type ActivityType = "chat" | "code";
+export type MobileTabType = "left-sidebar" | "right-sidebar";
+export type ProviderType = "auto" | "cheapest" | "fastest" | string;
+export enum MessageActionType {
+ PUBLISH_PROJECT = "PUBLISH_PROJECT",
+ SEE_LIVE_PREVIEW = "SEE_LIVE_PREVIEW",
+}
+export type MessageAction = {
+ label: string;
+ variant?: VariantProps["variant"];
+ icon?: LucideIcon;
+ disabled?: boolean;
+ loading?: boolean;
+ type?: MessageActionType;
+ projectTitle?: string;
+ prompt?: string;
+ messageReferrer?: number;
+}
+export type Message = {
+ id: string;
+ role: MessageRole;
+ model?: string;
+ content?: string;
+ createdAt: Date;
+ isThinking?: boolean;
+ files?: string[];
+ isAborted?: boolean;
+ isAutomated?: boolean;
+ actions?: MessageAction[];
+};
+export type File = {
+ path: string;
+ content?: string
+};
+export interface Commit {
+ title: string;
+ oid: string;
+ date: Date;
+}
+export type MessageRole = "user" | "assistant";
\ No newline at end of file
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f1ba0a7b3b03a9ee4680cff3f1c9430d671aeedf
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,143 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+import { File } from "./type";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+export const COLORS = [
+ "red",
+ "blue",
+ "green",
+ "yellow",
+ "purple",
+ "pink",
+ "gray",
+];
+
+export const getMentionsFromPrompt = async (prompt: string) => {
+ const mentions = prompt.match(/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+/g);
+ const validMentions = await Promise.all(
+ mentions?.map(async (mention) => {
+ let type = "model";
+ let response = await fetch(
+ `https://huggingface.co/api/models/${mention}`
+ );
+ if (!response.ok) {
+ type = "dataset";
+ response = await fetch(
+ `https://huggingface.co/api/datasets/${mention}`
+ );
+ if (!response.ok) {
+ return null;
+ }
+ }
+ const readme = await fetch(
+ `https://huggingface.co/${
+ type === "model" ? "" : "datasets/"
+ }${mention}/raw/main/README.md`
+ );
+ const readmeContent = await readme.text();
+
+ const data = await response.json();
+ return {
+ library_name: data?.library_name ?? data?.cardData?.library_name,
+ pipeline_tag: data?.pipeline_tag ?? data?.cardData?.pipeline_tag,
+ model_id: data.id,
+ readme: readmeContent ?? "",
+ };
+ }) ?? []
+ );
+ return validMentions?.filter((mention) => mention !== null) ?? [];
+};
+export const getContextFilesFromPrompt = async (
+ prompt: string,
+ currentFiles: File[]
+) => {
+ const mentions = prompt.match(/file:\/[a-zA-Z0-9-_.\/]+/g);
+ const filesToUseAsContext: File[] = [];
+ if (currentFiles.length === 0) return filesToUseAsContext;
+ mentions?.forEach((mention) => {
+ const filePath = mention.replace("file:/", "");
+ const matchedFile = currentFiles.find((file) => file.path === filePath);
+ if (matchedFile) {
+ filesToUseAsContext.push(matchedFile);
+ }
+ });
+ return filesToUseAsContext;
+};
+
+export const defaultHTML = `
+
+
+ My app
+
+
+
+
+
+
+
+ 🔥 DeepSite v4: New version dropped!
+
+
+
+ I'm ready to develop,
+
+ Ask me anything.
+
+
+
+
+
+
+`;
+
+export function injectDeepSiteBadge(html: string): string {
+ const badgeScript =
+ '';
+
+ // Remove any existing badge script to avoid duplicates
+ const cleanedHtml = html.replace(
+ /