enzostvs HF Staff commited on
Commit
61e6dfe
·
0 Parent(s):

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +4 -0
  2. .gitignore +41 -0
  3. Dockerfile +22 -0
  4. README.md +23 -0
  5. actions/mentions.ts +31 -0
  6. actions/projects.ts +156 -0
  7. app/(public)/layout.tsx +14 -0
  8. app/(public)/page.tsx +21 -0
  9. app/[owner]/[repoId]/page.tsx +25 -0
  10. app/api/ask/route.ts +144 -0
  11. app/api/auth/[...nextauth]/route.ts +4 -0
  12. app/api/projects/[repoId]/[commitId]/route.ts +49 -0
  13. app/api/projects/[repoId]/route.ts +97 -0
  14. app/api/projects/route.ts +97 -0
  15. app/api/redesign/route.ts +73 -0
  16. app/favicon.ico +0 -0
  17. app/globals.css +161 -0
  18. app/layout.tsx +60 -0
  19. app/new/page.tsx +10 -0
  20. app/not-found.tsx +17 -0
  21. assets/deepseek.svg +1 -0
  22. assets/kimi.svg +1 -0
  23. assets/minimax.svg +1 -0
  24. assets/qwen.svg +1 -0
  25. assets/space.svg +7 -0
  26. assets/zai.svg +13 -0
  27. components.json +22 -0
  28. components/ask-ai/ask-ai-landing.tsx +66 -0
  29. components/ask-ai/ask-ai.tsx +162 -0
  30. components/ask-ai/context.tsx +123 -0
  31. components/ask-ai/input-mentions.tsx +317 -0
  32. components/ask-ai/loading.tsx +35 -0
  33. components/ask-ai/models.tsx +210 -0
  34. components/ask-ai/redesign.tsx +157 -0
  35. components/ask-ai/uploader.tsx +39 -0
  36. components/ask-ai/useGeneration.ts +394 -0
  37. components/chat/index.tsx +315 -0
  38. components/chat/useChat.ts +121 -0
  39. components/code/index.tsx +215 -0
  40. components/code/monaco-editor.tsx +149 -0
  41. components/code/useEditor.ts +71 -0
  42. components/editor/commits.tsx +115 -0
  43. components/editor/header.tsx +208 -0
  44. components/editor/history-view.tsx +84 -0
  45. components/editor/index.tsx +148 -0
  46. components/editor/night-light.json +1728 -0
  47. components/editor/night.json +347 -0
  48. components/grid-pattern/index.tsx +70 -0
  49. components/loading/index.tsx +41 -0
  50. components/not-found/buttons.tsx +16 -0
.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ AUTH_HUGGINGFACE_ID=
2
+ AUTH_HUGGINGFACE_SECRET=
3
+ NEXTAUTH_URL=http://localhost:3001
4
+ AUTH_SECRET=
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+ USER root
3
+
4
+ # Install pnpm
5
+ RUN corepack enable && corepack prepare pnpm@latest --activate
6
+
7
+ USER 1000
8
+ WORKDIR /usr/src/app
9
+ # Copy package.json and pnpm-lock.yaml to the container
10
+ COPY --chown=1000 package.json pnpm-lock.yaml ./
11
+
12
+ # Copy the rest of the application files to the container
13
+ COPY --chown=1000 . .
14
+
15
+ RUN pnpm install
16
+ RUN pnpm run build
17
+
18
+ # Expose the application port (assuming your app runs on port 3000)
19
+ EXPOSE 3001
20
+
21
+ # Start the application
22
+ CMD ["pnpm", "start"]
README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: DeepSite v4
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 3001
9
+ license: mit
10
+ failure_strategy: rollback
11
+ short_description: Generate any application by Vibe Coding it
12
+ models:
13
+ - deepseek-ai/DeepSeek-V3-0324
14
+ - deepseek-ai/DeepSeek-V3.2
15
+ - Qwen/Qwen3-Coder-30B-A3B-Instruct
16
+ - moonshotai/Kimi-K2-Instruct-0905
17
+ - zai-org/GLM-4.7
18
+ - MiniMaxAI/MiniMax-M2.1
19
+ ---
20
+
21
+ # DeepSite 🏗️
22
+
23
+ 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.
actions/mentions.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { File } from "@/lib/type";
4
+
5
+ export const searchMentions = async (query: string) => {
6
+ const promises = [searchModels(query), searchDatasets(query)];
7
+ const results = await Promise.all(promises);
8
+ return { models: results[0], datasets: results[1] };
9
+ };
10
+
11
+ const searchModels = async (query: string) => {
12
+ const response = await fetch(
13
+ `https://huggingface.co/api/quicksearch?q=${query}&type=model&limit=3`
14
+ );
15
+ const data = await response.json();
16
+ return data?.models ?? [];
17
+ };
18
+
19
+ const searchDatasets = async (query: string) => {
20
+ const response = await fetch(
21
+ `https://huggingface.co/api/quicksearch?q=${query}&type=dataset&limit=3`
22
+ );
23
+ const data = await response.json();
24
+ return data?.datasets ?? [];
25
+ };
26
+
27
+ export const searchFilesMentions = async (query: string, files: File[]) => {
28
+ if (!query) return files;
29
+ const lowerQuery = query.toLowerCase();
30
+ return files.filter((file) => file.path.toLowerCase().includes(lowerQuery));
31
+ };
actions/projects.ts ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+ import {
3
+ downloadFile,
4
+ listCommits,
5
+ listFiles,
6
+ listSpaces,
7
+ RepoDesignation,
8
+ SpaceEntry,
9
+ spaceInfo,
10
+ } from "@huggingface/hub";
11
+
12
+ import { auth } from "@/lib/auth";
13
+ import { Commit, File } from "@/lib/type";
14
+
15
+ export interface ProjectWithCommits extends SpaceEntry {
16
+ commits?: Commit[];
17
+ }
18
+
19
+ const IGNORED_PATHS = ["README.md", ".gitignore", ".gitattributes"];
20
+
21
+ export const getProjects = async () => {
22
+ const projects: SpaceEntry[] = [];
23
+ const session = await auth();
24
+ if (!session?.user) {
25
+ return projects;
26
+ }
27
+ const token = session.accessToken;
28
+ for await (const space of listSpaces({
29
+ accessToken: token,
30
+ additionalFields: ["author", "cardData"],
31
+ search: {
32
+ owner: "enzostvs",
33
+ },
34
+ })) {
35
+ if (
36
+ space.sdk === "static" &&
37
+ Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
38
+ (space.cardData as { tags?: string[] })?.tags?.some((tag) =>
39
+ tag.includes("deepsite")
40
+ )
41
+ ) {
42
+ projects.push(space);
43
+ }
44
+ }
45
+ return projects;
46
+ };
47
+ export const getProject = async (id: string, commitId?: string) => {
48
+ const session = await auth();
49
+ if (!session?.user) {
50
+ return null;
51
+ }
52
+ const token = session.accessToken;
53
+ try {
54
+ const project: ProjectWithCommits | null = await spaceInfo({
55
+ name: id,
56
+ accessToken: token,
57
+ additionalFields: ["author", "cardData"],
58
+ });
59
+ const repo: RepoDesignation = {
60
+ type: "space",
61
+ name: id,
62
+ };
63
+ const files: File[] = [];
64
+ const params = { repo, accessToken: token };
65
+ if (commitId) {
66
+ Object.assign(params, { revision: commitId });
67
+ }
68
+ for await (const fileInfo of listFiles(params)) {
69
+ if (IGNORED_PATHS.includes(fileInfo.path)) continue;
70
+ if (
71
+ fileInfo.path.endsWith(".html") ||
72
+ fileInfo.path.endsWith(".css") ||
73
+ fileInfo.path.endsWith(".js") ||
74
+ fileInfo.path.endsWith(".json")
75
+ ) {
76
+ const blob = await downloadFile({
77
+ repo,
78
+ accessToken: token,
79
+ path: fileInfo.path,
80
+ raw: true,
81
+ ...(commitId ? { revision: commitId } : {}),
82
+ }).catch((_) => {
83
+ return null;
84
+ });
85
+ if (!blob) {
86
+ continue;
87
+ }
88
+ const html = await blob?.text();
89
+ if (!html) {
90
+ continue;
91
+ }
92
+ files[fileInfo.path === "index.html" ? "unshift" : "push"]({
93
+ path: fileInfo.path,
94
+ content: html,
95
+ });
96
+ }
97
+ if (fileInfo.type === "directory") {
98
+ for await (const subFile of listFiles({
99
+ repo,
100
+ accessToken: token,
101
+ path: fileInfo.path,
102
+ })) {
103
+ if (
104
+ subFile.path.endsWith(".html") ||
105
+ subFile.path.endsWith(".css") ||
106
+ subFile.path.endsWith(".js") ||
107
+ subFile.path.endsWith(".json")
108
+ ) {
109
+ const blob = await downloadFile({
110
+ repo,
111
+ accessToken: token,
112
+ path: subFile.path,
113
+ raw: true,
114
+ ...(commitId ? { revision: commitId } : {}),
115
+ }).catch((_) => {
116
+ return null;
117
+ });
118
+ if (!blob) {
119
+ continue;
120
+ }
121
+ const html = await blob?.text();
122
+ if (!html) {
123
+ continue;
124
+ }
125
+ files[subFile.path === "index.html" ? "unshift" : "push"]({
126
+ path: subFile.path,
127
+ content: html,
128
+ });
129
+ }
130
+ }
131
+ }
132
+ }
133
+ const commits: Commit[] = [];
134
+ const commitIterator = listCommits({ repo, accessToken: token });
135
+ for await (const commit of commitIterator) {
136
+ if (commit.title?.toLowerCase() === "initial commit") continue;
137
+ commits.push({
138
+ title: commit.title,
139
+ oid: commit.oid,
140
+ date: commit.date,
141
+ });
142
+ if (commits.length >= 20) {
143
+ break;
144
+ }
145
+ }
146
+
147
+ project.commits = commits;
148
+
149
+ return { project, files };
150
+ } catch (error) {
151
+ return {
152
+ project: null,
153
+ files: [],
154
+ };
155
+ }
156
+ };
app/(public)/layout.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Navigation } from "@/components/public/navigation";
2
+
3
+ export default function PublicLayout({
4
+ children,
5
+ }: Readonly<{
6
+ children: React.ReactNode;
7
+ }>) {
8
+ return (
9
+ <div className="min-h-screen font-sans">
10
+ <Navigation />
11
+ {children}
12
+ </div>
13
+ );
14
+ }
app/(public)/page.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AnimatedDotsBackground } from "@/components/public/animated-dots-background";
2
+ import { HeroHeader } from "@/components/public/hero-header";
3
+ import { UserProjects } from "@/components/projects/user-projects";
4
+ import { AskAiLanding } from "@/components/ask-ai/ask-ai-landing";
5
+
6
+ export default async function Homepage() {
7
+ return (
8
+ <>
9
+ <section className="container mx-auto relative z-10">
10
+ <HeroHeader />
11
+ <div className="absolute inset-0 -z-10">
12
+ <AnimatedDotsBackground />
13
+ </div>
14
+ <div className="max-w-xl mx-auto px-6">
15
+ <AskAiLanding />
16
+ </div>
17
+ </section>
18
+ <UserProjects />
19
+ </>
20
+ );
21
+ }
app/[owner]/[repoId]/page.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getProject } from "@/actions/projects";
2
+ import { AppEditor } from "@/components/editor";
3
+ import { notFound } from "next/navigation";
4
+
5
+ export default async function ProjectPage({
6
+ params,
7
+ searchParams,
8
+ }: {
9
+ params: Promise<{ owner: string; repoId: string }>;
10
+ searchParams: Promise<{ commit?: string }>;
11
+ }) {
12
+ const { owner, repoId } = await params;
13
+ const { commit } = await searchParams;
14
+ const datas = await getProject(`${owner}/${repoId}`, commit);
15
+ if (!datas?.project) {
16
+ return notFound();
17
+ }
18
+ return (
19
+ <AppEditor
20
+ project={datas.project}
21
+ files={datas.files ?? []}
22
+ isHistoryView={!!commit}
23
+ />
24
+ );
25
+ }
app/api/ask/route.ts ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { NextResponse } from "next/server";
3
+ import { InferenceClient } from "@huggingface/inference";
4
+
5
+ import { FOLLOW_UP_SYSTEM_PROMPT, INITIAL_SYSTEM_PROMPT } from "@/lib/prompts";
6
+ import { auth } from "@/lib/auth";
7
+ import { File, Message } from "@/lib/type";
8
+ import { MODELS } from "@/lib/providers";
9
+
10
+ export async function POST(request: Request) {
11
+ const session = await auth();
12
+ if (!session) {
13
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
14
+ }
15
+ const token = session.accessToken;
16
+
17
+ const body = await request.json();
18
+ const {
19
+ prompt,
20
+ previousMessages = [],
21
+ files = [],
22
+ provider: initialProvider,
23
+ mentions = [],
24
+ model,
25
+ redesignMd,
26
+ } = body;
27
+ const provider = initialProvider ?? "auto";
28
+
29
+ if (!prompt) {
30
+ return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
31
+ }
32
+ if (!model || !MODELS.find((m: (typeof MODELS)[0]) => m.value === model)) {
33
+ return NextResponse.json({ error: "Model is required" }, { status: 400 });
34
+ }
35
+
36
+ const client = new InferenceClient(token);
37
+
38
+ try {
39
+ const encoder = new TextEncoder();
40
+ const stream = new TransformStream();
41
+ const writer = stream.writable.getWriter();
42
+
43
+ const response = new NextResponse(stream.readable, {
44
+ headers: {
45
+ "Content-Type": "text/plain; charset=utf-8",
46
+ "Cache-Control": "no-cache",
47
+ Connection: "keep-alive",
48
+ },
49
+ });
50
+ (async () => {
51
+ try {
52
+ const chatCompletion = client.chatCompletionStream({
53
+ model: model + (provider !== "auto" ? `:${provider}` : ""),
54
+ messages: [
55
+ {
56
+ role: "system",
57
+ content:
58
+ files.length > 0
59
+ ? FOLLOW_UP_SYSTEM_PROMPT
60
+ : INITIAL_SYSTEM_PROMPT,
61
+ },
62
+ ...previousMessages.map((message: Message) => ({
63
+ role: message.role,
64
+ content: message.content,
65
+ })),
66
+ ...(files?.length > 0
67
+ ? [
68
+ {
69
+ role: "user",
70
+ content: `Here are the files that the user has provider:${files
71
+ .map(
72
+ (file: File) =>
73
+ `File: ${file.path}\nContent: ${file.content}`
74
+ )
75
+ .join("\n")}\n\n${prompt}`,
76
+ },
77
+ ]
78
+ : []),
79
+ {
80
+ role: "user",
81
+ content: `
82
+ ${prompt}
83
+ ${
84
+ mentions?.length > 0
85
+ ? `\n\nHere are the informations about the model or dataset that the user has mentioned to use: ${mentions
86
+ .map(
87
+ (mention: any) =>
88
+ `Library: ${mention.library_name}\nPipeline: ${mention.pipeline_tag}\nModel: ${mention.model_id}\nReadme for more information: \n${mention.readme}`
89
+ )
90
+ .join("\n")}`
91
+ : ""
92
+ }
93
+ `,
94
+ },
95
+ ],
96
+ stream: true,
97
+ max_tokens: 16_000,
98
+ });
99
+ while (true) {
100
+ const { done, value } = await chatCompletion.next();
101
+ if (done) {
102
+ break;
103
+ }
104
+
105
+ const chunk = value.choices[0]?.delta?.content;
106
+ if (chunk) {
107
+ await writer.write(encoder.encode(chunk));
108
+ }
109
+ }
110
+
111
+ await writer.close();
112
+ } catch (error) {
113
+ console.error(error);
114
+ try {
115
+ const errorMessage =
116
+ error instanceof Error
117
+ ? error.message
118
+ : "An error occurred while processing your request";
119
+ const errorPayload = JSON.stringify({
120
+ messageError: errorMessage,
121
+ isError: true,
122
+ });
123
+ await writer.write(encoder.encode(`\n\n__ERROR__:${errorPayload}`));
124
+ await writer.close();
125
+ } catch (closeError) {
126
+ console.error("Failed to send error message:", closeError);
127
+ try {
128
+ await writer.abort(error);
129
+ } catch (abortError) {
130
+ console.error("Failed to abort writer:", abortError);
131
+ }
132
+ }
133
+ }
134
+ })();
135
+
136
+ return response;
137
+ } catch (error) {
138
+ console.error(error);
139
+ return NextResponse.json(
140
+ { error: "Internal server error" },
141
+ { status: 500 }
142
+ );
143
+ }
144
+ }
app/api/auth/[...nextauth]/route.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import { handlers } from "@/lib/auth";
2
+
3
+ export const { GET, POST } = handlers;
4
+
app/api/projects/[repoId]/[commitId]/route.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from "@/lib/auth";
2
+ import { createBranch, RepoDesignation } from "@huggingface/hub";
3
+ import { format } from "date-fns";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export async function POST(
7
+ request: Request,
8
+ { params }: { params: Promise<{ repoId: string; commitId: string }> }
9
+ ) {
10
+ const { repoId, commitId }: { repoId: string; commitId: string } =
11
+ await params;
12
+ const session = await auth();
13
+ if (!session) {
14
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
15
+ }
16
+ const token = session.accessToken;
17
+
18
+ const repo: RepoDesignation = {
19
+ type: "space",
20
+ name: session.user?.username + "/" + repoId,
21
+ };
22
+
23
+ const commitTitle = `🔖 ${format(new Date(), "dd/MM")} - ${format(
24
+ new Date(),
25
+ "HH:mm"
26
+ )} - Set commit ${commitId} as default.`;
27
+
28
+ await fetch(
29
+ `https://huggingface.co/api/spaces/${session.user?.username}/${repoId}/branch/main`,
30
+ {
31
+ method: "POST",
32
+ headers: {
33
+ Authorization: `Bearer ${token}`,
34
+ "Content-Type": "application/json",
35
+ },
36
+ body: JSON.stringify({
37
+ startingPoint: commitId,
38
+ overwrite: true,
39
+ }),
40
+ }
41
+ ).catch((error) => {
42
+ return NextResponse.json(
43
+ { error: error ?? "Failed to create branch" },
44
+ { status: 500 }
45
+ );
46
+ });
47
+
48
+ return NextResponse.json({ success: true }, { status: 200 });
49
+ }
app/api/projects/[repoId]/route.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth } from "@/lib/auth";
2
+ import { RepoDesignation, deleteRepo, uploadFiles } from "@huggingface/hub";
3
+ import { format } from "date-fns";
4
+ import { NextResponse } from "next/server";
5
+
6
+ export async function PUT(
7
+ request: Request,
8
+ { params }: { params: Promise<{ repoId: string }> }
9
+ ) {
10
+ const { repoId }: { repoId: string } = await params;
11
+ const session = await auth();
12
+ if (!session) {
13
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
14
+ }
15
+ const token = session.accessToken;
16
+
17
+ const body = await request.json();
18
+ const { files, prompt, isManualChanges } = body;
19
+
20
+ if (!files) {
21
+ return NextResponse.json({ error: "Files are required" }, { status: 400 });
22
+ }
23
+
24
+ if (!prompt) {
25
+ return NextResponse.json({ error: "Prompt is required" }, { status: 400 });
26
+ }
27
+
28
+ const repo: RepoDesignation = {
29
+ type: "space",
30
+ name: session.user?.username + "/" + repoId,
31
+ };
32
+
33
+ const filesToUpload: File[] = [];
34
+ for (const file of files) {
35
+ let mimeType = "text/x-python";
36
+ if (file.path.endsWith(".txt")) {
37
+ mimeType = "text/plain";
38
+ } else if (file.path.endsWith(".md")) {
39
+ mimeType = "text/markdown";
40
+ } else if (file.path.endsWith(".json")) {
41
+ mimeType = "application/json";
42
+ }
43
+ filesToUpload.push(new File([file.content], file.path, { type: mimeType }));
44
+ }
45
+ const baseTitle = isManualChanges
46
+ ? ""
47
+ : `🐳 ${format(new Date(), "dd/MM")} - ${format(new Date(), "HH:mm")} - `;
48
+ const commitTitle = baseTitle + (prompt ?? "Follow-up DeepSite commit");
49
+ const response = await uploadFiles({
50
+ repo,
51
+ files: filesToUpload,
52
+ accessToken: token,
53
+ commitTitle,
54
+ });
55
+
56
+ return NextResponse.json(
57
+ {
58
+ success: true,
59
+ commit: {
60
+ oid: response.commit,
61
+ title: commitTitle,
62
+ date: new Date(),
63
+ },
64
+ },
65
+ { status: 200 }
66
+ );
67
+ }
68
+
69
+ export async function DELETE(
70
+ request: Request,
71
+ { params }: { params: Promise<{ repoId: string }> }
72
+ ) {
73
+ const { repoId }: { repoId: string } = await params;
74
+ const session = await auth();
75
+ if (!session) {
76
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
77
+ }
78
+ const token = session.accessToken;
79
+
80
+ const repo: RepoDesignation = {
81
+ type: "space",
82
+ name: session.user?.username + "/" + repoId,
83
+ };
84
+
85
+ try {
86
+ await deleteRepo({
87
+ repo,
88
+ accessToken: token as string,
89
+ });
90
+
91
+ return NextResponse.json({ success: true }, { status: 200 });
92
+ } catch (error) {
93
+ const errMsg =
94
+ error instanceof Error ? error.message : "Failed to delete project";
95
+ return NextResponse.json({ error: errMsg }, { status: 500 });
96
+ }
97
+ }
app/api/projects/route.ts ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { RepoDesignation, createRepo, uploadFiles } from "@huggingface/hub";
3
+
4
+ import { auth } from "@/lib/auth";
5
+ import { COLORS, injectDeepSiteBadge, isIndexPage } from "@/lib/utils";
6
+
7
+ export async function POST(request: Request) {
8
+ const session = await auth();
9
+ if (!session) {
10
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11
+ }
12
+ const token = session.accessToken;
13
+
14
+ const body = await request.json();
15
+ const { projectTitle, files, prompt } = body;
16
+
17
+ if (!files) {
18
+ return NextResponse.json(
19
+ { error: "Project title and files are required" },
20
+ { status: 400 }
21
+ );
22
+ }
23
+
24
+ const title =
25
+ projectTitle || projectTitle !== "" ? projectTitle : "DeepSite Project";
26
+
27
+ const formattedTitle = title
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, "-")
30
+ .split("-")
31
+ .filter(Boolean)
32
+ .join("-")
33
+ .slice(0, 96);
34
+
35
+ const repo: RepoDesignation = {
36
+ type: "space",
37
+ name: session.user?.username + "/" + formattedTitle,
38
+ };
39
+
40
+ const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
41
+ const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
42
+ const README = `---
43
+ title: ${projectTitle}
44
+ colorFrom: ${colorFrom}
45
+ colorTo: ${colorTo}
46
+ sdk: static
47
+ tags:
48
+ - deepsite-v4
49
+ ---
50
+
51
+ # ${title}
52
+
53
+ This project has been created with [DeepSite](https://huggingface.co/deepsite) AI Vibe Coding.
54
+ `;
55
+
56
+ const filesToUpload: File[] = [
57
+ new File([README], "README.md", { type: "text/markdown" }),
58
+ ];
59
+ for (const file of files) {
60
+ let mimeType = "text/html";
61
+ if (file.path.endsWith(".css")) {
62
+ mimeType = "text/css";
63
+ } else if (file.path.endsWith(".js")) {
64
+ mimeType = "text/javascript";
65
+ }
66
+ const content =
67
+ mimeType === "text/html" && isIndexPage(file.path)
68
+ ? injectDeepSiteBadge(file.content)
69
+ : file.content;
70
+
71
+ filesToUpload.push(new File([content], file.path, { type: mimeType }));
72
+ }
73
+
74
+ try {
75
+ const { repoUrl } = await createRepo({
76
+ accessToken: token as string,
77
+ repo: repo,
78
+ sdk: "static",
79
+ });
80
+
81
+ const commitTitle = prompt ?? "Initial DeepSite commit";
82
+ await uploadFiles({
83
+ repo,
84
+ files: filesToUpload,
85
+ accessToken: token as string,
86
+ commitTitle,
87
+ });
88
+
89
+ const path = repoUrl.split("/").slice(-2).join("/");
90
+
91
+ return NextResponse.json({ repoUrl: path }, { status: 200 });
92
+ } catch (error) {
93
+ const errMsg =
94
+ error instanceof Error ? error.message : "Failed to upload files";
95
+ return NextResponse.json({ error: errMsg }, { status: 500 });
96
+ }
97
+ }
app/api/redesign/route.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ const FETCH_TIMEOUT = 30_000;
5
+ export const maxDuration = 60;
6
+
7
+ export async function PUT(request: NextRequest) {
8
+ const body = await request.json();
9
+ const { url } = body;
10
+
11
+ if (!url) {
12
+ return NextResponse.json({ error: "URL is required" }, { status: 400 });
13
+ }
14
+
15
+ try {
16
+ const controller = new AbortController();
17
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
18
+
19
+ try {
20
+ const response = await fetch(
21
+ `https://r.jina.ai/${encodeURIComponent(url)}`,
22
+ {
23
+ method: "POST",
24
+ signal: controller.signal,
25
+ }
26
+ );
27
+
28
+ clearTimeout(timeoutId);
29
+
30
+ if (!response.ok) {
31
+ return NextResponse.json(
32
+ { error: "Failed to fetch redesign" },
33
+ { status: 500 }
34
+ );
35
+ }
36
+ const markdown = await response.text();
37
+ return NextResponse.json(
38
+ {
39
+ ok: true,
40
+ markdown,
41
+ },
42
+ { status: 200 }
43
+ );
44
+ } catch (fetchError: any) {
45
+ clearTimeout(timeoutId);
46
+
47
+ if (fetchError.name === "AbortError") {
48
+ return NextResponse.json(
49
+ {
50
+ error:
51
+ "Request timeout: The external service took too long to respond. Please try again.",
52
+ },
53
+ { status: 504 }
54
+ );
55
+ }
56
+ throw fetchError;
57
+ }
58
+ } catch (error: any) {
59
+ if (error.name === "AbortError" || error.message?.includes("timeout")) {
60
+ return NextResponse.json(
61
+ {
62
+ error:
63
+ "Request timeout: The external service took too long to respond. Please try again.",
64
+ },
65
+ { status: 504 }
66
+ );
67
+ }
68
+ return NextResponse.json(
69
+ { error: error.message || "An error occurred" },
70
+ { status: 500 }
71
+ );
72
+ }
73
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --font-sans: var(--font-geist-sans);
10
+ --font-mono: var(--font-geist-mono);
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --radius-sm: calc(var(--radius) - 4px);
41
+ --radius-md: calc(var(--radius) - 2px);
42
+ --radius-lg: var(--radius);
43
+ --radius-xl: calc(var(--radius) + 4px);
44
+ }
45
+
46
+ :root {
47
+ --radius: 0.65rem;
48
+ --background: oklch(1 0 0);
49
+ --foreground: oklch(0.145 0 0);
50
+ --card: oklch(1 0 0);
51
+ --card-foreground: oklch(0.145 0 0);
52
+ --popover: oklch(1 0 0);
53
+ --popover-foreground: oklch(0.145 0 0);
54
+ --primary: oklch(0.205 0 0);
55
+ --primary-foreground: oklch(0.985 0 0);
56
+ --secondary: oklch(0.97 0 0);
57
+ --secondary-foreground: oklch(0.205 0 0);
58
+ --muted: oklch(0.97 0 0);
59
+ --muted-foreground: oklch(0.556 0 0);
60
+ --accent: oklch(0.97 0 0);
61
+ --accent-foreground: oklch(0.205 0 0);
62
+ --destructive: oklch(0.577 0.245 27.325);
63
+ --border: oklch(0.922 0 0);
64
+ --input: oklch(0.922 0 0);
65
+ --ring: oklch(0.708 0 0);
66
+ --chart-1: oklch(0.646 0.222 41.116);
67
+ --chart-2: oklch(0.6 0.118 184.704);
68
+ --chart-3: oklch(0.398 0.07 227.392);
69
+ --chart-4: oklch(0.828 0.189 84.429);
70
+ --chart-5: oklch(0.769 0.188 70.08);
71
+ --radius: 0.625rem;
72
+ --sidebar: oklch(0.985 0 0);
73
+ --sidebar-foreground: oklch(0.145 0 0);
74
+ --sidebar-primary: oklch(0.205 0 0);
75
+ --sidebar-primary-foreground: oklch(0.985 0 0);
76
+ --sidebar-accent: oklch(0.97 0 0);
77
+ --sidebar-accent-foreground: oklch(0.205 0 0);
78
+ --sidebar-border: oklch(0.922 0 0);
79
+ --sidebar-ring: oklch(0.708 0 0);
80
+ }
81
+
82
+ .dark {
83
+ --background: oklch(0.145 0 0);
84
+ --foreground: oklch(0.985 0 0);
85
+ --card: oklch(0.205 0 0);
86
+ --card-foreground: oklch(0.985 0 0);
87
+ --popover: oklch(0.205 0 0);
88
+ --popover-foreground: oklch(0.985 0 0);
89
+ --primary: oklch(0.922 0 0);
90
+ --primary-foreground: oklch(0.205 0 0);
91
+ --secondary: oklch(0.269 0 0);
92
+ --secondary-foreground: oklch(0.985 0 0);
93
+ --muted: oklch(0.269 0 0);
94
+ --muted-foreground: oklch(0.708 0 0);
95
+ --accent: oklch(0.269 0 0);
96
+ --accent-foreground: oklch(0.985 0 0);
97
+ --destructive: oklch(0.704 0.191 22.216);
98
+ --border: oklch(1 0 0 / 10%);
99
+ --input: oklch(1 0 0 / 15%);
100
+ --ring: oklch(0.556 0 0);
101
+ --chart-1: oklch(0.488 0.243 264.376);
102
+ --chart-2: oklch(0.696 0.17 162.48);
103
+ --chart-3: oklch(0.769 0.188 70.08);
104
+ --chart-4: oklch(0.627 0.265 303.9);
105
+ --chart-5: oklch(0.645 0.246 16.439);
106
+ --sidebar: oklch(0.205 0 0);
107
+ --sidebar-foreground: oklch(0.985 0 0);
108
+ --sidebar-primary: oklch(0.488 0.243 264.376);
109
+ --sidebar-primary-foreground: oklch(0.985 0 0);
110
+ --sidebar-accent: oklch(0.269 0 0);
111
+ --sidebar-accent-foreground: oklch(0.985 0 0);
112
+ --sidebar-border: oklch(1 0 0 / 10%);
113
+ --sidebar-ring: oklch(0.556 0 0);
114
+ }
115
+
116
+ @layer base {
117
+ * {
118
+ @apply border-border outline-ring/50;
119
+ }
120
+ body {
121
+ @apply bg-background text-foreground;
122
+ }
123
+ }
124
+
125
+ .monaco-editor .margin {
126
+ @apply bg-background!;
127
+ }
128
+ .monaco-editor .monaco-editor-background {
129
+ @apply bg-background!;
130
+ }
131
+ .monaco-editor .decorationsOverviewRuler {
132
+ @apply opacity-0!;
133
+ }
134
+ .monaco-editor .view-line {
135
+ /* @apply bg-primary/50!; */
136
+ }
137
+ .monaco-editor .scroll-decoration {
138
+ @apply opacity-0!;
139
+ }
140
+ .monaco-editor .cursors-layer .cursor {
141
+ @apply bg-primary!;
142
+ }
143
+
144
+ .content-placeholder::before {
145
+ content: attr(data-placeholder);
146
+ position: absolute;
147
+ pointer-events: none;
148
+ opacity: 0.5;
149
+ @apply top-5 left-6;
150
+ }
151
+
152
+ .sp-layout .sp-file-explorer .sp-file-explorer-list .sp-explorer[data-active="true"] {
153
+ @apply text-indigo-500!;
154
+ }
155
+
156
+ .sp-layout .sp-stack .sp-tabs .sp-tab-container[aria-selected="true"] .sp-tab-button {
157
+ @apply text-indigo-500!;
158
+ }
159
+ .sp-layout .sp-stack .sp-tabs .sp-tab-container:has(button:focus) {
160
+ @apply outline-none! border-none!;
161
+ }
app/layout.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+ import { ThemeProvider } from "@/components/providers/theme";
5
+ import { AuthProvider } from "@/components/providers/session";
6
+ import { Toaster } from "@/components/ui/sonner";
7
+ import { ReactQueryProvider } from "@/components/providers/react-query";
8
+
9
+ const geistSans = Geist({
10
+ variable: "--font-geist-sans",
11
+ subsets: ["latin"],
12
+ });
13
+
14
+ const geistMono = Geist_Mono({
15
+ variable: "--font-geist-mono",
16
+ subsets: ["latin"],
17
+ });
18
+
19
+ export const metadata: Metadata = {
20
+ title: "Build your next application | DeepSite",
21
+ description:
22
+ "Build your next application with ease and speed by using AI Vibe Coding.",
23
+ icons: {
24
+ icon: "/logo.svg",
25
+ shortcut: "/logo.svg",
26
+ apple: "/logo.svg",
27
+ other: {
28
+ rel: "icon",
29
+ url: "/logo.svg",
30
+ },
31
+ },
32
+ };
33
+
34
+ export default function RootLayout({
35
+ children,
36
+ }: Readonly<{
37
+ children: React.ReactNode;
38
+ }>) {
39
+ return (
40
+ <html lang="en" suppressHydrationWarning>
41
+ <body
42
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
43
+ >
44
+ <Toaster richColors />
45
+ <AuthProvider>
46
+ <ReactQueryProvider>
47
+ <ThemeProvider
48
+ attribute="class"
49
+ defaultTheme="system"
50
+ enableSystem
51
+ disableTransitionOnChange
52
+ >
53
+ {children}
54
+ </ThemeProvider>
55
+ </ReactQueryProvider>
56
+ </AuthProvider>
57
+ </body>
58
+ </html>
59
+ );
60
+ }
app/new/page.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AppEditor } from "@/components/editor";
2
+
3
+ export default async function NewProjectPage({
4
+ searchParams,
5
+ }: {
6
+ searchParams: Promise<{ prompt: string }>;
7
+ }) {
8
+ const { prompt } = await searchParams;
9
+ return <AppEditor isNew={true} initialPrompt={prompt} />;
10
+ }
app/not-found.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NotFoundButtons } from "@/components/not-found/buttons";
2
+ import { Navigation } from "@/components/public/navigation";
3
+
4
+ export default function NotFound() {
5
+ return (
6
+ <div className="min-h-screen font-sans">
7
+ <Navigation />
8
+ <div className="px-6 py-16 max-w-5xl mx-auto text-center">
9
+ <h1 className="text-5xl font-bold mb-5">Oh no! Page not found.</h1>
10
+ <p className="text-lg text-muted-foreground mb-8">
11
+ The page you are looking for does not exist.
12
+ </p>
13
+ <NotFoundButtons />
14
+ </div>
15
+ </div>
16
+ );
17
+ }
assets/deepseek.svg ADDED
assets/kimi.svg ADDED
assets/minimax.svg ADDED
assets/qwen.svg ADDED
assets/space.svg ADDED
assets/zai.svg ADDED
components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
components/ask-ai/ask-ai-landing.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ArrowUp } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { useInterval, useLocalStorage } from "react-use";
5
+ import { useRouter } from "next/navigation";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+ import { ProviderType } from "@/lib/type";
9
+ import { Models } from "./models";
10
+ import { MODELS } from "@/lib/providers";
11
+ import { cn } from "@/lib/utils";
12
+ import { AiLoading } from "./loading";
13
+ import { EXAMPLES_OF_PROJECT_SUGGESTIONS } from "@/lib/prompts";
14
+
15
+ export function AskAiLanding({ className }: { className?: string }) {
16
+ const [model, setModel] = useState<string>(MODELS[0].value);
17
+ const [provider, setProvider] = useLocalStorage<ProviderType>(
18
+ "provider",
19
+ "auto" as ProviderType
20
+ );
21
+ const router = useRouter();
22
+ const [prompt, setPrompt] = useState<string>("");
23
+ return (
24
+ <div
25
+ className={cn(
26
+ "dark:bg-[#222222] bg-accent border border-border-muted rounded-xl p-3 block relative",
27
+ className
28
+ )}
29
+ >
30
+ <textarea
31
+ id="prompt-input"
32
+ className="w-full h-full resize-none outline-none text-primary text-sm"
33
+ placeholder="Ask me anything..."
34
+ value={prompt}
35
+ onChange={(e) => setPrompt(e.target.value)}
36
+ onKeyDown={(e) => {
37
+ if (e.key === "Enter" && !e.shiftKey) {
38
+ e.preventDefault();
39
+ router.push(`/new?prompt=${prompt}`);
40
+ }
41
+ }}
42
+ />
43
+ <footer className="flex items-center justify-between">
44
+ <div className="flex items-center gap-2">
45
+ <Models
46
+ model={model}
47
+ setModel={setModel}
48
+ provider={provider as ProviderType}
49
+ setProvider={setProvider}
50
+ />
51
+ </div>
52
+ <div>
53
+ <Button
54
+ size="icon-sm"
55
+ className="rounded-full!"
56
+ onClick={() => {
57
+ router.push(`/new?prompt=${prompt}`);
58
+ }}
59
+ >
60
+ <ArrowUp />
61
+ </Button>
62
+ </div>
63
+ </footer>
64
+ </div>
65
+ );
66
+ }
components/ask-ai/ask-ai.tsx ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ArrowUp, Paintbrush, X } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { HiStop } from "react-icons/hi2";
5
+ import { useLocalStorage, useMount } from "react-use";
6
+ import { useRouter } from "next/navigation";
7
+
8
+ import { Button } from "@/components/ui/button";
9
+ import { cn } from "@/lib/utils";
10
+ import { useGeneration } from "./useGeneration";
11
+ import { File, MobileTabType, ProviderType } from "@/lib/type";
12
+ import { Models } from "./models";
13
+ import { MODELS } from "@/lib/providers";
14
+ import { Redesign } from "./redesign";
15
+ import { Uploader } from "./uploader";
16
+ import { InputMentions } from "./input-mentions";
17
+
18
+ // todo: send redesignMd to callAi + add it to the prompt
19
+
20
+ export function AskAI({
21
+ initialPrompt,
22
+ className,
23
+ onToggleMobileTab,
24
+ files,
25
+ isNew = false,
26
+ isHistoryView,
27
+ projectName = "new",
28
+ }: {
29
+ initialPrompt?: string;
30
+ className?: string;
31
+ files?: File[] | null;
32
+ onToggleMobileTab?: (tab: MobileTabType) => void;
33
+ isNew?: boolean;
34
+ isHistoryView?: boolean;
35
+ projectName?: string;
36
+ }) {
37
+ const [prompt, setPrompt] = useState(initialPrompt ?? "");
38
+ const [model = MODELS[0].value, setModel] = useLocalStorage<string>(
39
+ "model",
40
+ MODELS[0].value
41
+ );
42
+ const [provider, setProvider] = useLocalStorage<ProviderType>(
43
+ "provider",
44
+ "auto" as ProviderType
45
+ );
46
+ const [redesignMd, setRedesignMd] = useState<{
47
+ md: string;
48
+ url: string;
49
+ } | null>(null);
50
+
51
+ const router = useRouter();
52
+ const { callAi, isLoading, stopGeneration, audio } =
53
+ useGeneration(projectName);
54
+
55
+ // const messages =
56
+ // queryClient.getQueryData<Message[]>(MESSAGES_QUERY_KEY(projectName)) ?? [];
57
+
58
+ // const clearMessages = () => {
59
+ // queryClient.setQueryData<Message[]>(MESSAGES_QUERY_KEY(projectName), []);
60
+ // localStorage.removeItem(`messages-${projectName}`);
61
+ // };
62
+
63
+ const onComplete = () => {
64
+ onToggleMobileTab?.("right-sidebar");
65
+ };
66
+
67
+ useMount(() => {
68
+ if (initialPrompt && initialPrompt.trim() !== "" && isNew) {
69
+ setTimeout(() => {
70
+ if (isHistoryView) return;
71
+ callAi({
72
+ prompt: initialPrompt,
73
+ model,
74
+ onComplete,
75
+ provider,
76
+ });
77
+ router.replace("/new");
78
+ }, 200);
79
+ }
80
+ });
81
+
82
+ const onSubmit = () => {
83
+ if (isHistoryView) return;
84
+ callAi({ prompt, model, onComplete, provider, redesignMd });
85
+ };
86
+
87
+ return (
88
+ <div
89
+ className={cn(
90
+ "dark:bg-[#222222] bg-accent border border-border-muted rounded-xl p-2.5 block relative",
91
+ className
92
+ )}
93
+ >
94
+ <InputMentions
95
+ files={files}
96
+ prompt={prompt}
97
+ setPrompt={setPrompt}
98
+ redesignMdUrl={redesignMd?.url?.replace(/(^\w+:|^)\/\//, "")}
99
+ onSubmit={onSubmit}
100
+ />
101
+ <footer className="flex items-center justify-between mt-0">
102
+ <div className="flex items-center gap-1.5">
103
+ <Uploader />
104
+ <Models
105
+ model={model}
106
+ setModel={setModel}
107
+ provider={provider as ProviderType}
108
+ setProvider={setProvider}
109
+ />
110
+ {!files ||
111
+ (files?.length === 0 &&
112
+ (redesignMd ? (
113
+ <Button
114
+ size="xs"
115
+ variant="indigo"
116
+ className="rounded-full! px-2.5!"
117
+ onClick={() => setRedesignMd(null)}
118
+ >
119
+ <Paintbrush className="size-3" />
120
+ {redesignMd.url?.replace(/(^\w+:|^)\/\//, "")}
121
+ <X className="size-3.5" onClick={() => setRedesignMd(null)} />
122
+ </Button>
123
+ ) : (
124
+ <Redesign
125
+ onRedesign={(md, url) => {
126
+ setRedesignMd({
127
+ md,
128
+ url,
129
+ });
130
+ }}
131
+ />
132
+ )))}
133
+ </div>
134
+ <div>
135
+ {isLoading ? (
136
+ <Button
137
+ size="icon-sm"
138
+ className="rounded-full!"
139
+ variant="bordered"
140
+ onClick={stopGeneration}
141
+ >
142
+ <HiStop />
143
+ </Button>
144
+ ) : (
145
+ <Button
146
+ size="icon-sm"
147
+ className="rounded-full!"
148
+ disabled={!prompt.trim() || isLoading || isHistoryView}
149
+ onClick={onSubmit}
150
+ >
151
+ <ArrowUp />
152
+ </Button>
153
+ )}
154
+ </div>
155
+ </footer>
156
+ <audio ref={audio} id="audio" className="hidden">
157
+ <source src="/ding.mp3" type="audio/mpeg" />
158
+ Your browser does not support the audio element.
159
+ </audio>
160
+ </div>
161
+ );
162
+ }
components/ask-ai/context.tsx ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AtSign, Braces, FileCode, FileText, X } from "lucide-react";
2
+ import { useMemo, useState } from "react";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverTrigger,
9
+ } from "@/components/ui/popover";
10
+ import { Button } from "@/components/ui/button";
11
+ import { File } from "@/lib/type";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ export const Context = ({
15
+ files,
16
+ setFiles,
17
+ }: {
18
+ files: File[];
19
+ setFiles: (files: File[]) => void;
20
+ }) => {
21
+ const queryClient = useQueryClient();
22
+ const [open, setOpen] = useState(false);
23
+
24
+ const getFileIcon = (filePath: string, size = "size-3.5") => {
25
+ if (filePath.endsWith(".css")) {
26
+ return <Braces className={size} />;
27
+ } else if (filePath.endsWith(".js")) {
28
+ return <FileCode className={size} />;
29
+ } else if (filePath.endsWith(".json")) {
30
+ return <Braces className={size} />;
31
+ } else {
32
+ return <FileText className={size} />;
33
+ }
34
+ };
35
+
36
+ const getFiles = () => queryClient.getQueryData<File[]>(["files"]) ?? [];
37
+
38
+ return (
39
+ <div className="flex items-center justify-start gap-1 flex-wrap">
40
+ <Popover open={open} onOpenChange={setOpen}>
41
+ <PopoverTrigger asChild>
42
+ <Button size="xxs" variant={open ? "default" : "bordered"}>
43
+ <AtSign className="size-3" />
44
+ Add Context...
45
+ </Button>
46
+ </PopoverTrigger>
47
+ <PopoverContent
48
+ align="start"
49
+ className="translate-x-6 space-y-4 min-w-fit rounded-2xl! p-0!"
50
+ >
51
+ <main className="p-4">
52
+ <p className="text-xs text-muted-foreground mb-2.5">
53
+ Select a file to send as context
54
+ </p>
55
+ <div className="max-h-[200px] overflow-y-auto space-y-0.5">
56
+ {getFiles().length === 0 ? (
57
+ <div className="text-xs text-muted-foreground">
58
+ No files available
59
+ </div>
60
+ ) : (
61
+ <>
62
+ <button
63
+ onClick={() => {
64
+ setFiles([]);
65
+ setOpen(false);
66
+ }}
67
+ 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 ${
68
+ files.length === 0
69
+ ? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
70
+ : "text-muted-foreground"
71
+ }`}
72
+ >
73
+ All files (default)
74
+ </button>
75
+ {getFiles()?.map((page) => (
76
+ <button
77
+ key={page.path}
78
+ onClick={() => {
79
+ if (files.some((f) => f.path === page.path))
80
+ setFiles(files.filter((f) => f.path !== page.path));
81
+ else setFiles(files ? [...files, page] : [page]);
82
+ setOpen(false);
83
+ }}
84
+ 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 ${
85
+ files?.some((f) => f.path === page.path)
86
+ ? "bg-linear-to-r from-indigo-500/20 to-indigo-500/5 text-primary font-medium"
87
+ : "text-muted-foreground"
88
+ }`}
89
+ >
90
+ <span className="shrink-0">
91
+ {getFileIcon(page.path, "size-3")}
92
+ </span>
93
+ <span className="truncate flex-1">{page.path}</span>
94
+ </button>
95
+ ))}
96
+ </>
97
+ )}
98
+ </div>
99
+ </main>
100
+ </PopoverContent>
101
+ </Popover>
102
+ {files?.map((file) => (
103
+ <Button
104
+ key={file.path}
105
+ size="xxs"
106
+ variant="bordered"
107
+ className="cursor-default!"
108
+ >
109
+ {getFileIcon(file.path, "size-3")}
110
+ {file.path}
111
+ <span
112
+ className="opacity-50 hover:opacity-80 cursor-pointer"
113
+ onClick={() => {
114
+ setFiles(files.filter((f) => f.path !== file.path));
115
+ }}
116
+ >
117
+ <X className="size-3.5 opacity-50 hover:opacity-80 shrink-0" />
118
+ </span>
119
+ </Button>
120
+ ))}
121
+ </div>
122
+ );
123
+ };
components/ask-ai/input-mentions.tsx ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState, useEffect } from "react";
2
+ import { useClickAway } from "react-use";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+
5
+ import { searchFilesMentions } from "@/actions/mentions";
6
+ import { File } from "@/lib/type";
7
+ import { Braces, FileCode, FileText } from "lucide-react";
8
+
9
+ export function InputMentions({
10
+ prompt,
11
+ files,
12
+ setPrompt,
13
+ redesignMdUrl,
14
+ onSubmit,
15
+ }: {
16
+ prompt: string;
17
+ files?: File[] | null;
18
+ redesignMdUrl?: string;
19
+ setPrompt: (prompt: string) => void;
20
+ onSubmit: () => void;
21
+ }) {
22
+ const queryClient = useQueryClient();
23
+ const [showMentionDropdown, setShowMentionDropdown] = useState(false);
24
+ const [, setMentionSearch] = useState("");
25
+ const contentEditableRef = useRef<HTMLDivElement>(null);
26
+ const dropdownRef = useRef<HTMLDivElement>(null);
27
+ const [results, setResults] = useState<File[]>([]);
28
+
29
+ useClickAway(dropdownRef, () => {
30
+ setShowMentionDropdown(false);
31
+ });
32
+
33
+ const getTextContent = (element: HTMLElement): string => {
34
+ let text = "";
35
+ const childNodes = element.childNodes;
36
+
37
+ for (let i = 0; i < childNodes.length; i++) {
38
+ const node = childNodes[i];
39
+ if (node.nodeType === Node.TEXT_NODE) {
40
+ text += node.textContent || "";
41
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
42
+ const el = node as HTMLElement;
43
+ if (el.classList.contains("mention-chip")) {
44
+ text += el.getAttribute("data-mention-id") || "";
45
+ } else {
46
+ text += el.textContent || "";
47
+ }
48
+ }
49
+ }
50
+ return text + "\u0020";
51
+ };
52
+
53
+ const extractPromptWithIds = (): string => {
54
+ if (!contentEditableRef.current) return "";
55
+
56
+ let text = "";
57
+ const childNodes = contentEditableRef.current.childNodes;
58
+
59
+ for (let i = 0; i < childNodes.length; i++) {
60
+ const node = childNodes[i];
61
+ if (node.nodeType === Node.TEXT_NODE) {
62
+ text += node.textContent || "";
63
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
64
+ const el = node as HTMLElement;
65
+ if (el.classList.contains("mention-chip")) {
66
+ text += el.getAttribute("data-mention-id") || "";
67
+ } else {
68
+ text += el.textContent || "";
69
+ }
70
+ }
71
+ }
72
+ return text;
73
+ };
74
+
75
+ const shouldDetectMention = (): {
76
+ detect: boolean;
77
+ textBeforeCursor: string;
78
+ } => {
79
+ const selection = window.getSelection();
80
+ if (!selection || !contentEditableRef.current) {
81
+ return { detect: false, textBeforeCursor: "" };
82
+ }
83
+
84
+ const range = selection.getRangeAt(0);
85
+ const node = range.startContainer;
86
+
87
+ if (node.nodeType === Node.ELEMENT_NODE) {
88
+ const element = node as HTMLElement;
89
+ if (element.classList?.contains("mention-chip")) {
90
+ return { detect: false, textBeforeCursor: "" };
91
+ }
92
+ }
93
+
94
+ if (node.parentElement?.classList?.contains("mention-chip")) {
95
+ return { detect: false, textBeforeCursor: "" };
96
+ }
97
+
98
+ if (node.nodeType === Node.TEXT_NODE) {
99
+ const textContent = node.textContent || "";
100
+ const cursorOffset = range.startOffset;
101
+ const textBeforeCursor = textContent.substring(0, cursorOffset);
102
+
103
+ const lastAtIndex = textBeforeCursor.lastIndexOf("@");
104
+ if (lastAtIndex !== -1) {
105
+ const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
106
+ if (!textAfterAt.includes(" ")) {
107
+ return { detect: true, textBeforeCursor: textAfterAt };
108
+ }
109
+ }
110
+ }
111
+
112
+ return { detect: false, textBeforeCursor: "" };
113
+ };
114
+
115
+ const handleInput = async () => {
116
+ if (!contentEditableRef.current) return;
117
+ const text = getTextContent(contentEditableRef.current);
118
+ if (text.trim() === "") {
119
+ contentEditableRef.current.innerHTML = "";
120
+ }
121
+ setPrompt(text);
122
+
123
+ const { detect, textBeforeCursor } = shouldDetectMention();
124
+
125
+ if (detect && files && files?.length > 0) {
126
+ setMentionSearch(textBeforeCursor);
127
+ setShowMentionDropdown(true);
128
+ const files = queryClient.getQueryData<File[]>(["files"]) ?? [];
129
+ const results = await searchFilesMentions(textBeforeCursor, files);
130
+ setResults(results);
131
+ } else {
132
+ setShowMentionDropdown(false);
133
+ }
134
+ };
135
+
136
+ const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
137
+ const mentionChip = document.createElement("span");
138
+
139
+ const baseClasses =
140
+ "mention-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium";
141
+ const typeClasses =
142
+ "bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
143
+
144
+ mentionChip.className = `${baseClasses} ${typeClasses}`;
145
+ mentionChip.contentEditable = "false";
146
+ mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
147
+ mentionChip.textContent = `@${mentionId}`;
148
+
149
+ return mentionChip;
150
+ };
151
+
152
+ const insertMention = (mentionId: string) => {
153
+ if (!contentEditableRef.current) return;
154
+
155
+ const selection = window.getSelection();
156
+ if (!selection || selection.rangeCount === 0) return;
157
+
158
+ const range = selection.getRangeAt(0);
159
+ const textNode = range.startContainer;
160
+
161
+ if (textNode.nodeType !== Node.TEXT_NODE) return;
162
+
163
+ const textContent = textNode.textContent || "";
164
+ const cursorOffset = range.startOffset;
165
+
166
+ const textBeforeCursor = textContent.substring(0, cursorOffset);
167
+ const lastAtIndex = textBeforeCursor.lastIndexOf("@");
168
+
169
+ if (lastAtIndex !== -1) {
170
+ const mentionChip = createMentionChipElement(mentionId);
171
+
172
+ const beforeText = textContent.substring(0, lastAtIndex);
173
+ const afterText = textContent.substring(cursorOffset);
174
+ const parent = textNode.parentNode;
175
+ if (!parent) return;
176
+
177
+ const beforeNode = beforeText
178
+ ? document.createTextNode(beforeText)
179
+ : null;
180
+ const spaceNode = document.createTextNode("\u0020");
181
+ const afterNode = afterText ? document.createTextNode(afterText) : null;
182
+
183
+ if (beforeNode) {
184
+ parent.insertBefore(beforeNode, textNode);
185
+ }
186
+ parent.insertBefore(mentionChip, textNode);
187
+ parent.insertBefore(spaceNode, textNode);
188
+ if (afterNode) {
189
+ parent.insertBefore(afterNode, textNode);
190
+ }
191
+
192
+ parent.removeChild(textNode);
193
+
194
+ const newRange = document.createRange();
195
+ if (afterNode) {
196
+ newRange.setStart(afterNode, 0);
197
+ } else {
198
+ newRange.setStartAfter(spaceNode);
199
+ }
200
+ newRange.collapse(true);
201
+ selection.removeAllRanges();
202
+ selection.addRange(newRange);
203
+
204
+ const newText = getTextContent(contentEditableRef.current);
205
+ setPrompt(newText);
206
+ setShowMentionDropdown(false);
207
+ setMentionSearch("");
208
+ }
209
+ };
210
+
211
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
212
+ if (!prompt || prompt.trim() === "") return;
213
+
214
+ if (e.key === "Enter" && !e.shiftKey) {
215
+ e.preventDefault();
216
+ const promptWithIds = extractPromptWithIds();
217
+ setPrompt(promptWithIds);
218
+ onSubmit();
219
+
220
+ if (contentEditableRef.current) {
221
+ contentEditableRef.current.innerHTML = "";
222
+ }
223
+ setPrompt("");
224
+ setShowMentionDropdown(false);
225
+ } else if (e.key === "Escape") {
226
+ setShowMentionDropdown(false);
227
+ }
228
+ };
229
+
230
+ useEffect(() => {
231
+ if (
232
+ contentEditableRef.current &&
233
+ prompt === "" &&
234
+ contentEditableRef.current.innerHTML !== ""
235
+ ) {
236
+ contentEditableRef.current.innerHTML = "";
237
+ }
238
+ }, [prompt]);
239
+
240
+ const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
241
+ e.preventDefault();
242
+ const text = e.clipboardData.getData("text/plain");
243
+ document.execCommand("insertText", false, text);
244
+ };
245
+
246
+ return (
247
+ <div className="relative">
248
+ <div
249
+ id="prompt-input"
250
+ ref={contentEditableRef}
251
+ contentEditable
252
+ className="pb-2 min-h-10 max-h-[130px] overflow-y-auto w-full h-full resize-none outline-none text-primary text-sm bg-transparent empty:before:block empty:before:content-[attr(data-placeholder)] empty:before:text-muted-foreground empty:before:pointer-events-none"
253
+ data-placeholder={
254
+ redesignMdUrl
255
+ ? `Ask me anything about ${redesignMdUrl}...`
256
+ : files && files.length > 0
257
+ ? "Ask me anything. Type @ to mention a file..."
258
+ : "Ask me anything..."
259
+ }
260
+ onInput={handleInput}
261
+ onKeyDown={handleKeyDown}
262
+ onPaste={handlePaste}
263
+ suppressContentEditableWarning
264
+ ></div>
265
+ {showMentionDropdown && (
266
+ <div
267
+ ref={dropdownRef}
268
+ className="absolute bottom-full mb-2 left-0 z-50 bg-background border border-border rounded-lg shadow-lg min-w-[250px] animate-in fade-in slide-in-from-bottom-2 duration-200"
269
+ >
270
+ <div className="text-xs text-muted-foreground/60 px-2 py-2">
271
+ {results?.length > 0 && (
272
+ <ul>
273
+ {results.map((file) => (
274
+ <MentionResult
275
+ key={file.path}
276
+ file={file}
277
+ onSelect={() => insertMention(file.path)}
278
+ />
279
+ ))}
280
+ </ul>
281
+ )}
282
+ </div>
283
+ </div>
284
+ )}
285
+ </div>
286
+ );
287
+ }
288
+
289
+ export const getFileIcon = (filePath: string, size = "size-3.5") => {
290
+ if (filePath.endsWith(".css")) {
291
+ return <Braces className={size} />;
292
+ } else if (filePath.endsWith(".js")) {
293
+ return <FileCode className={size} />;
294
+ } else if (filePath.endsWith(".json")) {
295
+ return <Braces className={size} />;
296
+ } else {
297
+ return <FileText className={size} />;
298
+ }
299
+ };
300
+
301
+ function MentionResult({
302
+ file,
303
+ onSelect,
304
+ }: {
305
+ file: File;
306
+ onSelect: () => void;
307
+ }) {
308
+ return (
309
+ <li
310
+ className="flex items-center justify-start gap-2 transition-all duration-200 hover:bg-linear-to-r from-indigo-500/40 to-indigo-500/5 text-primary font-medium rounded-lg px-2 py-2 cursor-pointer select-none"
311
+ onClick={onSelect}
312
+ >
313
+ {getFileIcon(file.path, "size-3")}
314
+ {file.path}
315
+ </li>
316
+ );
317
+ }
components/ask-ai/loading.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import Loading from "@/components/loading";
3
+
4
+ export const AiLoading = ({
5
+ text = "AI Assistant is thinking...",
6
+ showCircle = true,
7
+ className,
8
+ }: {
9
+ text?: string;
10
+ showCircle?: boolean;
11
+ className?: string;
12
+ }) => {
13
+ return (
14
+ <div className={`flex items-center justify-start gap-2 ${className}`}>
15
+ {showCircle && <Loading overlay={false} className="size-4! opacity-50" />}
16
+ <p className="text-muted-foreground text-sm">
17
+ <span className="inline-flex">
18
+ {text.split("").map((char, index) => (
19
+ <span
20
+ key={index}
21
+ className="bg-linear-to-r from-muted-foreground to-primary bg-clip-text text-transparent animate-pulse"
22
+ style={{
23
+ animationDelay: `${index * 0.1}s`,
24
+ animationDuration: "1.3s",
25
+ animationIterationCount: "infinite",
26
+ }}
27
+ >
28
+ {char === " " ? "\u00A0" : char}
29
+ </span>
30
+ ))}
31
+ </span>
32
+ </p>
33
+ </div>
34
+ );
35
+ };
components/ask-ai/models.tsx ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ BrainIcon,
3
+ ChevronDown,
4
+ DollarSign,
5
+ StarsIcon,
6
+ Zap,
7
+ } from "lucide-react";
8
+ import { useMemo, useState } from "react";
9
+
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "@/components/ui/popover";
15
+ import { Button } from "@/components/ui/button";
16
+ import { cn } from "@/lib/utils";
17
+ import { ProviderType } from "@/lib/type";
18
+ import { MODELS } from "@/lib/providers";
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectGroup,
23
+ SelectItem,
24
+ SelectLabel,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from "@/components/ui/select";
28
+
29
+ export function Models({
30
+ model,
31
+ setModel,
32
+ provider,
33
+ setProvider,
34
+ }: {
35
+ model: string;
36
+ setModel: (model: string) => void;
37
+ provider: ProviderType;
38
+ setProvider: (provider: ProviderType) => void;
39
+ }) {
40
+ const [open, setOpen] = useState(false);
41
+
42
+ const formattedModels = useMemo(() => {
43
+ const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
44
+ [];
45
+ const keys = new Set<string>();
46
+ MODELS.forEach((model) => {
47
+ if (!keys.has(model.companyName)) {
48
+ lists.push({
49
+ isCategory: true,
50
+ name: model.companyName,
51
+ logo: model.logo,
52
+ });
53
+ keys.add(model.companyName);
54
+ }
55
+ lists.push(model);
56
+ });
57
+ return lists;
58
+ }, []);
59
+
60
+ return (
61
+ <Popover open={open} onOpenChange={setOpen}>
62
+ <PopoverTrigger asChild>
63
+ <Button
64
+ variant={open ? "default" : "bordered"}
65
+ size="xs"
66
+ className="flex items-center gap-1 rounded-full! px-2.5!"
67
+ >
68
+ <span className="max-w-48 truncate">
69
+ {model.split("/").pop()?.toLowerCase()}
70
+ </span>
71
+ <ChevronDown className="size-3" />
72
+ </Button>
73
+ </PopoverTrigger>
74
+ <PopoverContent className="translate-x-6 rounded-2xl! p-0! bg-accent! border-border-muted! min-w-fit text-center overflow-hidden">
75
+ <header className="bg-linear-to-b from-indigo-500/15 dark:from-indigo-500/40 to-accent p-6">
76
+ <div className="flex items-center justify-center -space-x-4 mb-3">
77
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
78
+ 💬
79
+ </div>
80
+ <div className="size-11 rounded-full bg-yellow-200 shadow-2xl flex items-center justify-center text-2xl z-2">
81
+ 🧠
82
+ </div>
83
+ <div className="size-9 rounded-full bg-green-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
84
+ 🤖
85
+ </div>
86
+ </div>
87
+ <p className="text-xl font-semibold text-primary">
88
+ Choose your AI model
89
+ </p>
90
+ <p className="text-sm text-muted-foreground mt-1.5">
91
+ Select the AI model that best fits your needs.
92
+ </p>
93
+ </header>
94
+ <main className="space-y-4 px-6 pb-6">
95
+ <div>
96
+ <Select defaultValue={model} onValueChange={setModel}>
97
+ <SelectTrigger className="w-full">
98
+ <SelectValue placeholder="Select a model" />
99
+ </SelectTrigger>
100
+ <SelectContent>
101
+ <SelectGroup>
102
+ {formattedModels.map(
103
+ (
104
+ item:
105
+ | (typeof MODELS)[0]
106
+ | { isCategory: true; name: string }
107
+ ) => {
108
+ if ("isCategory" in item) {
109
+ return (
110
+ <SelectLabel
111
+ key={item.name}
112
+ className="flex items-center gap-1"
113
+ >
114
+ {item.name}
115
+ </SelectLabel>
116
+ );
117
+ }
118
+ const {
119
+ value,
120
+ label,
121
+ isNew = false,
122
+ isBestSeller = false,
123
+ } = item;
124
+ return (
125
+ <SelectItem key={value} value={value} className="">
126
+ {value.split("/").pop() || label}
127
+ {isNew && (
128
+ <span className="text-xs bg-indigo-500 dark:bg-indigo-500/20 text-primary-foreground dark:text-indigo-500 rounded-full px-1.5 py-0.5">
129
+ New
130
+ </span>
131
+ )}
132
+ {isBestSeller && (
133
+ <StarsIcon className="size-3.5 text-yellow-500 fill-yellow-500" />
134
+ )}
135
+ </SelectItem>
136
+ );
137
+ }
138
+ )}
139
+ </SelectGroup>
140
+ </SelectContent>
141
+ </Select>
142
+ </div>
143
+ <div>
144
+ <p className="text-sm text-muted-foreground mb-2">Provider mode:</p>
145
+ <div
146
+ role="radiogroup"
147
+ aria-label="Provider mode"
148
+ className="flex w-fit items-center gap-1.5"
149
+ >
150
+ {(
151
+ [
152
+ { value: "cheapest", icon: DollarSign, color: "emerald" },
153
+ {
154
+ value: "auto",
155
+ icon: BrainIcon,
156
+ color: "indigo",
157
+ name: "Smartest",
158
+ },
159
+ { value: "fastest", icon: Zap, color: "amber" },
160
+ ] as const
161
+ ).map(
162
+ ({
163
+ value,
164
+ icon: Icon,
165
+ color,
166
+ name,
167
+ }: {
168
+ value: string;
169
+ icon: React.ElementType;
170
+ color: string;
171
+ name?: string;
172
+ }) => (
173
+ <div
174
+ key={value}
175
+ role="radio"
176
+ aria-checked={provider === value}
177
+ tabIndex={0}
178
+ onClick={() => setProvider(value)}
179
+ onKeyDown={(e) => {
180
+ if (e.key === "Enter" || e.key === " ") {
181
+ e.preventDefault();
182
+ setProvider(value);
183
+ }
184
+ }}
185
+ className={cn(
186
+ "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",
187
+ "hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
188
+ provider === value && [
189
+ "bg-transparent",
190
+ color === "emerald" &&
191
+ "[&_svg]:fill-emerald-500 [&_svg]:stroke-emerald-500 bg-emerald-500/10! border-emerald-500/10! text-emerald-500!",
192
+ color === "indigo" &&
193
+ "[&_svg]:fill-indigo-500 [&_svg]:stroke-indigo-500 bg-indigo-500/10! border-indigo-500/10! text-indigo-500!",
194
+ color === "amber" &&
195
+ "[&_svg]:fill-amber-500 [&_svg]:stroke-amber-500 bg-amber-500/10! border-amber-500/10! text-amber-500!",
196
+ ]
197
+ )}
198
+ >
199
+ <Icon className="size-3.5" />
200
+ {name ?? value.charAt(0).toUpperCase() + value.slice(1)}
201
+ </div>
202
+ )
203
+ )}
204
+ </div>
205
+ </div>
206
+ </main>
207
+ </PopoverContent>
208
+ </Popover>
209
+ );
210
+ }
components/ask-ai/redesign.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Paintbrush } from "lucide-react";
3
+ import { toast } from "sonner";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Popover,
8
+ PopoverContent,
9
+ PopoverTrigger,
10
+ } from "@/components/ui/popover";
11
+ import { Input } from "@/components/ui/input";
12
+ import Loading from "@/components/loading";
13
+
14
+ export function Redesign({
15
+ onRedesign,
16
+ }: {
17
+ onRedesign: (md: string, url: string) => void;
18
+ }) {
19
+ const [url, setUrl] = useState<string>("");
20
+ const [open, setOpen] = useState(false);
21
+ const [isLoading, setIsLoading] = useState(false);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ const checkIfUrlIsValid = (url: string) => {
25
+ const urlPattern = new RegExp(
26
+ /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
27
+ "i"
28
+ );
29
+ return urlPattern.test(url);
30
+ };
31
+
32
+ const handleClick = async () => {
33
+ setError(null);
34
+ if (isLoading) return;
35
+ if (!url) {
36
+ toast.error("Please enter a URL.");
37
+ return;
38
+ }
39
+ if (!checkIfUrlIsValid(url)) {
40
+ toast.error("Please enter a valid URL.");
41
+ return;
42
+ }
43
+ setIsLoading(true);
44
+ const response = await fetch("/api/redesign", {
45
+ method: "PUT",
46
+ body: JSON.stringify({ url: url.trim() }),
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ },
50
+ })
51
+ .then(async (response) => {
52
+ if (response?.ok) {
53
+ const data = await response.json();
54
+ return data;
55
+ }
56
+ })
57
+ .catch((error) => {
58
+ setError(error.message);
59
+ });
60
+ if (response.ok) {
61
+ onRedesign(response.markdown, url);
62
+ }
63
+ setIsLoading(false);
64
+ };
65
+
66
+ return (
67
+ <Popover open={open} onOpenChange={setOpen}>
68
+ <form>
69
+ <PopoverTrigger asChild>
70
+ <Button
71
+ size="xs"
72
+ variant={open ? "default" : "bordered"}
73
+ className="rounded-full! px-2.5!"
74
+ >
75
+ <Paintbrush className="size-3.5" />
76
+ Redesign
77
+ </Button>
78
+ </PopoverTrigger>
79
+ <PopoverContent
80
+ align="start"
81
+ className="rounded-2xl! p-0! bg-accent! border-border-muted! min-w-xs text-center overflow-hidden"
82
+ >
83
+ <header className="bg-linear-to-b from-indigo-500/15 dark:from-indigo-500/40 to-accent p-6">
84
+ <div className="flex items-center justify-center -space-x-4 mb-3">
85
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
86
+ 🎨
87
+ </div>
88
+ <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
89
+ 🥳
90
+ </div>
91
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
92
+ 💎
93
+ </div>
94
+ </div>
95
+ <p className="text-xl font-semibold text-primary">
96
+ Redesign your Site!
97
+ </p>
98
+ <p className="text-sm text-muted-foreground mt-1.5">
99
+ Try our new Redesign feature to give your site a fresh look.
100
+ </p>
101
+ </header>
102
+ <main className="space-y-4 p-6">
103
+ {error && <p className="text-sm text-red-500">{error}</p>}
104
+ <div>
105
+ <p className="text-sm text-muted-foreground mb-2">
106
+ Enter your website URL to get started:
107
+ </p>
108
+ <Input
109
+ type="text"
110
+ placeholder="https://example.com"
111
+ value={url}
112
+ onChange={(e) => setUrl(e.target.value)}
113
+ onBlur={(e) => {
114
+ const inputUrl = e.target.value.trim();
115
+ if (!inputUrl) {
116
+ setUrl("");
117
+ return;
118
+ }
119
+ if (!checkIfUrlIsValid(inputUrl)) {
120
+ toast.error("Please enter a valid URL.");
121
+ return;
122
+ }
123
+ setUrl(inputUrl);
124
+ }}
125
+ onKeyDown={(e) => {
126
+ if (e.key === "Enter") {
127
+ handleClick();
128
+ }
129
+ }}
130
+ />
131
+ </div>
132
+ <div>
133
+ <p className="text-sm text-muted-foreground mb-2">
134
+ Then, let&apos;s redesign it!
135
+ </p>
136
+ <Button onClick={handleClick} className="relative w-full">
137
+ {isLoading ? (
138
+ <>
139
+ <Loading
140
+ overlay={false}
141
+ className="ml-2 size-4 animate-spin text-primary-foreground"
142
+ />
143
+ Fetching your site...
144
+ </>
145
+ ) : (
146
+ <>
147
+ Redesign <Paintbrush className="size-4" />
148
+ </>
149
+ )}
150
+ </Button>
151
+ </div>
152
+ </main>
153
+ </PopoverContent>
154
+ </form>
155
+ </Popover>
156
+ );
157
+ }
components/ask-ai/uploader.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Paperclip } from "lucide-react";
2
+ import { useState } from "react";
3
+
4
+ import {
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from "@/components/ui/popover";
9
+ import { Button } from "@/components/ui/button";
10
+ // todo: implement the uploader component
11
+
12
+ export const Uploader = () => {
13
+ const [open, setOpen] = useState(false);
14
+ return (
15
+ <div className="flex items-center justify-start gap-1 flex-wrap">
16
+ <Popover open={open} onOpenChange={setOpen}>
17
+ <PopoverTrigger asChild>
18
+ <Button
19
+ size="icon-xs"
20
+ variant={open ? "default" : "bordered"}
21
+ className="rounded-full!"
22
+ >
23
+ <Paperclip className="size-3" />
24
+ </Button>
25
+ </PopoverTrigger>
26
+ <PopoverContent
27
+ align="start"
28
+ className="space-y-4 min-w-fit rounded-2xl! p-0!"
29
+ >
30
+ <main className="p-4">
31
+ <p className="text-xs text-muted-foreground mb-2.5">
32
+ Upload a file
33
+ </p>
34
+ </main>
35
+ </PopoverContent>
36
+ </Popover>
37
+ </div>
38
+ );
39
+ };
components/ask-ai/useGeneration.ts ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { useRef } from "react";
4
+ import { toast } from "sonner";
5
+ import { useRouter } from "next/navigation";
6
+ import { v4 as uuidv4 } from "uuid";
7
+
8
+ import { formatResponse } from "@/lib/format";
9
+ import { File, Message, MessageActionType, ProviderType } from "@/lib/type";
10
+ import { getContextFilesFromPrompt } from "@/lib/utils";
11
+ import { useLocalStorage } from "react-use";
12
+
13
+ const MESSAGES_QUERY_KEY = (projectName: string) =>
14
+ ["messages", projectName] as const;
15
+
16
+ export const useGeneration = (projectName: string) => {
17
+ const router = useRouter();
18
+ const audio = useRef<HTMLAudioElement>(null);
19
+ const queryClient = useQueryClient();
20
+ const abortController = useRef<AbortController | null>(null);
21
+ const [, setStoredMessages, clearStoredMessages] = useLocalStorage<Message[]>(
22
+ `messages-${projectName}`,
23
+ []
24
+ );
25
+
26
+ const { data: isLoading } = useQuery({
27
+ queryKey: ["ai.generation.isLoading"],
28
+ queryFn: () => false,
29
+ refetchOnWindowFocus: false,
30
+ refetchOnReconnect: false,
31
+ refetchOnMount: false,
32
+ staleTime: Infinity,
33
+ });
34
+
35
+ const setIsLoading = (isLoading: boolean) => {
36
+ queryClient.setQueryData(["ai.generation.isLoading"], isLoading);
37
+ };
38
+
39
+ const getFiles = () => queryClient.getQueryData<File[]>(["files"]) ?? [];
40
+ const setFiles = (newFiles: File[]) => {
41
+ queryClient.setQueryData<File[]>(["files"], (oldFiles: File[] = []) => {
42
+ const currentFiles = oldFiles.filter(
43
+ (file) => !newFiles.some((f) => f.path === file.path)
44
+ );
45
+ return [...currentFiles, ...newFiles];
46
+ });
47
+ };
48
+
49
+ const getMessages = () =>
50
+ queryClient.getQueryData<Message[]>(
51
+ MESSAGES_QUERY_KEY(projectName ?? "new")
52
+ ) ?? [];
53
+ const addMessage = (message: Omit<Message, "id">) => {
54
+ const id = uuidv4();
55
+ const key = MESSAGES_QUERY_KEY(projectName ?? "new");
56
+ queryClient.setQueryData<Message[]>(key, (oldMessages = []) => {
57
+ const newMessages = [...oldMessages, { ...message, id }];
58
+ if (projectName !== "new") {
59
+ localStorage.setItem(
60
+ `messages-${projectName}`,
61
+ JSON.stringify(newMessages)
62
+ );
63
+ }
64
+ return newMessages;
65
+ });
66
+ return id;
67
+ };
68
+
69
+ const updateLastMessage = (content: string, files?: File[]) => {
70
+ queryClient.setQueryData<Message[]>(
71
+ MESSAGES_QUERY_KEY(projectName),
72
+ (oldMessages = []) => {
73
+ const newMessages = [
74
+ ...oldMessages.slice(0, -1),
75
+ {
76
+ ...oldMessages[oldMessages.length - 1],
77
+ content,
78
+ isThinking: false,
79
+ files: files?.map((file) => file.path),
80
+ },
81
+ ];
82
+ if (projectName !== "new") {
83
+ setStoredMessages(newMessages);
84
+ }
85
+ return newMessages;
86
+ }
87
+ );
88
+ };
89
+
90
+ const updateMessage = (messageId: string, message: Partial<Message>) => {
91
+ const key = MESSAGES_QUERY_KEY(projectName ?? "new");
92
+ const currentMessages = queryClient.getQueryData<Message[]>(key);
93
+ if (!currentMessages) return;
94
+ const index = currentMessages.findIndex((m) => m.id === messageId);
95
+ if (index === -1) return;
96
+ const newMessages = [
97
+ ...currentMessages.slice(0, index),
98
+ { ...currentMessages[index], ...message },
99
+ ...currentMessages.slice(index + 1),
100
+ ];
101
+ if (projectName !== "new") {
102
+ setStoredMessages(newMessages);
103
+ }
104
+ queryClient.setQueryData<Message[]>(key, newMessages);
105
+ };
106
+
107
+ const storeMessages = async (newProjectName: string) => {
108
+ return new Promise((resolve) => {
109
+ const currentMessages = queryClient.getQueryData<Message[]>(
110
+ MESSAGES_QUERY_KEY("new")
111
+ );
112
+ localStorage.setItem(
113
+ `messages-${newProjectName}`,
114
+ JSON.stringify(currentMessages)
115
+ );
116
+ queryClient.setQueryData<Message[]>(
117
+ MESSAGES_QUERY_KEY(newProjectName),
118
+ currentMessages
119
+ );
120
+ setTimeout(() => resolve(true), 100);
121
+ });
122
+ };
123
+
124
+ const createProject = async (
125
+ files: File[],
126
+ projectTitle: string,
127
+ indexMessage: string,
128
+ prompt: string
129
+ ) => {
130
+ updateMessage(indexMessage, {
131
+ actions: [
132
+ {
133
+ label: "Publishing on Hugging Face...",
134
+ variant: "default",
135
+ loading: true,
136
+ type: MessageActionType.PUBLISH_PROJECT,
137
+ },
138
+ ],
139
+ });
140
+ try {
141
+ const response = await fetch("/api/projects", {
142
+ method: "POST",
143
+ body: JSON.stringify({
144
+ projectTitle,
145
+ files,
146
+ prompt,
147
+ }),
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ },
151
+ }).then(async (response) => {
152
+ if (response.ok) {
153
+ const data = await response.json();
154
+ return data;
155
+ }
156
+ throw new Error("Failed to publish project");
157
+ });
158
+
159
+ if (response.repoUrl) {
160
+ toast.success("Project has been published, build in progress...");
161
+ updateMessage(indexMessage, {
162
+ actions: [
163
+ {
164
+ label: "See Live preview",
165
+ variant: "default",
166
+ type: MessageActionType.SEE_LIVE_PREVIEW,
167
+ },
168
+ ],
169
+ });
170
+ storeMessages(response.repoUrl).then(() => {
171
+ router.push(`/${response.repoUrl}`);
172
+ });
173
+ }
174
+ } catch (error) {
175
+ toast.error("Failed to publish project");
176
+ updateMessage(indexMessage, {
177
+ actions: [
178
+ {
179
+ label: "Publish on Hugging Face",
180
+ variant: "default",
181
+ type: MessageActionType.PUBLISH_PROJECT,
182
+ projectTitle,
183
+ prompt,
184
+ },
185
+ ],
186
+ });
187
+ }
188
+ };
189
+
190
+ const callAi = async ({
191
+ prompt,
192
+ model,
193
+ onComplete,
194
+ provider = "auto",
195
+ redesignMd,
196
+ }: {
197
+ prompt: string;
198
+ model: string;
199
+ redesignMd?: {
200
+ url: string;
201
+ md: string;
202
+ } | null;
203
+ onComplete: () => void;
204
+ provider?: ProviderType;
205
+ }) => {
206
+ setIsLoading(true);
207
+ const messages = getMessages();
208
+ const files = getFiles();
209
+ const filesToUse = await getContextFilesFromPrompt(prompt, files);
210
+ const previousMessages = [...messages]?.filter(
211
+ (message) => !message.isAutomated || !message.isAborted
212
+ );
213
+ addMessage({
214
+ role: "user",
215
+ content: prompt,
216
+ createdAt: new Date(),
217
+ });
218
+ addMessage({
219
+ role: "assistant",
220
+ isThinking: true,
221
+ createdAt: new Date(),
222
+ model,
223
+ });
224
+
225
+ const isFollowUp = files?.length > 0;
226
+ abortController.current = new AbortController();
227
+
228
+ const request = await fetch("/api/ask", {
229
+ method: "POST",
230
+ body: JSON.stringify({
231
+ prompt,
232
+ model,
233
+ files: filesToUse,
234
+ previousMessages,
235
+ provider,
236
+ redesignMd,
237
+ }),
238
+ headers: {
239
+ "Content-Type": "application/json",
240
+ },
241
+ ...(abortController.current
242
+ ? { signal: abortController.current.signal }
243
+ : {}),
244
+ });
245
+
246
+ const currentMessages = getMessages();
247
+
248
+ if (!request.ok) {
249
+ const lastMessageId = currentMessages[currentMessages.length - 1].id;
250
+ updateMessage(lastMessageId, {
251
+ isThinking: false,
252
+ isAborted: true,
253
+ content: `Error: Failed to connect to AI service`,
254
+ });
255
+ setIsLoading(false);
256
+ return;
257
+ }
258
+
259
+ if (request && request.body) {
260
+ const reader = request.body.getReader();
261
+ const decoder = new TextDecoder();
262
+ let completeResponse = "";
263
+ const read = async () => {
264
+ const { done, value } = await reader.read();
265
+ if (done) {
266
+ audio.current?.play();
267
+ const files = getFiles();
268
+ const {
269
+ messageContent,
270
+ files: newFiles,
271
+ projectTitle,
272
+ } = formatResponse(completeResponse, files ?? []);
273
+ updateLastMessage(messageContent, newFiles);
274
+ if (newFiles && newFiles.length > 0) {
275
+ setFiles(newFiles);
276
+ onComplete();
277
+ if (projectName === "new") {
278
+ addMessage({
279
+ role: "assistant",
280
+ content:
281
+ "I've finished the generation. Now you can decide to publish the project on Hugging Face to share it!",
282
+ createdAt: new Date(),
283
+ isAutomated: true,
284
+ actions: [
285
+ {
286
+ label: "Publish on Hugging Face",
287
+ variant: "default",
288
+ type: MessageActionType.PUBLISH_PROJECT,
289
+ prompt,
290
+ projectTitle,
291
+ },
292
+ ],
293
+ });
294
+ } else {
295
+ const response = await fetch(
296
+ `/api/projects/${projectName.split("/")[1]}`,
297
+ {
298
+ method: "PUT",
299
+ body: JSON.stringify({
300
+ files: newFiles,
301
+ prompt,
302
+ }),
303
+ headers: {
304
+ "Content-Type": "application/json",
305
+ },
306
+ }
307
+ ).then(async (response) => {
308
+ if (response.ok) {
309
+ const data = await response.json();
310
+ return data;
311
+ }
312
+ });
313
+ if (response.success) {
314
+ toast.success("Project has been updated");
315
+ } else {
316
+ toast.error("Failed to update project");
317
+ }
318
+ }
319
+ }
320
+
321
+ setIsLoading(false);
322
+ return;
323
+ }
324
+ // if (abortController.current?.signal.aborted) {
325
+ // toast.error("Generation aborted");
326
+ // setIsLoading(false);
327
+ // return;
328
+ // }
329
+ const chunk = decoder.decode(value, { stream: true });
330
+ completeResponse += chunk;
331
+
332
+ if (completeResponse.includes("__ERROR__:")) {
333
+ const errorMatch = completeResponse.match(/__ERROR__:(.+)/);
334
+ if (errorMatch) {
335
+ try {
336
+ const errorData = JSON.parse(errorMatch[1]);
337
+ if (errorData.isError) {
338
+ const lastMessageId =
339
+ currentMessages[currentMessages.length - 1].id;
340
+ updateMessage(lastMessageId, {
341
+ isThinking: false,
342
+ isAborted: true,
343
+ content: `Error: ${errorData.messageError}`,
344
+ });
345
+ setIsLoading(false);
346
+ return;
347
+ }
348
+ } catch (e) {
349
+ console.error("Failed to parse error message:", e);
350
+ }
351
+ }
352
+ }
353
+
354
+ const files = getFiles();
355
+ const { messageContent, files: newFiles } = formatResponse(
356
+ completeResponse,
357
+ files ?? []
358
+ );
359
+ if (messageContent) updateLastMessage(messageContent);
360
+ if (newFiles && newFiles.length > 0) {
361
+ if (!isFollowUp) {
362
+ setFiles(newFiles);
363
+ }
364
+ updateLastMessage(messageContent, newFiles);
365
+ }
366
+ read();
367
+ };
368
+ return await read();
369
+ }
370
+ };
371
+
372
+ const stopGeneration = () => {
373
+ if (abortController.current) {
374
+ abortController.current.abort();
375
+ abortController.current = null;
376
+ setIsLoading(false);
377
+ const currentMessages = getMessages();
378
+ const lastMessageId = currentMessages[currentMessages.length - 1].id;
379
+ updateMessage(lastMessageId, {
380
+ isAborted: true,
381
+ isThinking: false,
382
+ });
383
+ }
384
+ };
385
+
386
+ return {
387
+ callAi,
388
+ isLoading,
389
+ stopGeneration,
390
+ files: getFiles(),
391
+ createProject,
392
+ audio,
393
+ };
394
+ };
components/chat/index.tsx ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
2
+ import { useSession } from "next-auth/react";
3
+ import { cn } from "@/lib/utils";
4
+ import { ChevronRight, ExternalLink } from "lucide-react";
5
+ import { useEffect, useRef } from "react";
6
+ import Markdown from "react-markdown";
7
+ import { useQueryClient } from "@tanstack/react-query";
8
+ import { formatDistanceToNow } from "date-fns";
9
+ import { SpaceEntry } from "@huggingface/hub";
10
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
11
+ import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
12
+
13
+ import { useChat } from "./useChat";
14
+ import { AiLoading } from "../ask-ai/loading";
15
+ import Loading from "../loading";
16
+ import { useGeneration } from "../ask-ai/useGeneration";
17
+ import { MessageAction, MessageActionType, File } from "@/lib/type";
18
+ import { Button } from "../ui/button";
19
+ import { getFileIcon } from "../ask-ai/input-mentions";
20
+
21
+ export function AppEditorChat({
22
+ isNew,
23
+ projectName,
24
+ onSelectFile,
25
+ }: {
26
+ isNew?: boolean;
27
+ projectName?: string;
28
+ onSelectFile: (file: string) => void;
29
+ }) {
30
+ const chatContainerRef = useRef<HTMLDivElement>(null);
31
+ const { data: session } = useSession();
32
+ const queryClient = useQueryClient();
33
+ const chatProjectName = isNew ? "new" : projectName ?? "new";
34
+ const { messages } = useChat(chatProjectName);
35
+ const { isLoading, createProject } = useGeneration(chatProjectName);
36
+
37
+ const project = queryClient.getQueryData<SpaceEntry>(["project"]);
38
+ const files = queryClient.getQueryData<File[]>(["files"]) ?? [];
39
+
40
+ const handleShowLastFile = (files?: string[]) => {
41
+ if (files && files.length > 0) {
42
+ const lastGeneratedFile = files[files.length - 1];
43
+ if (lastGeneratedFile) {
44
+ onSelectFile?.(lastGeneratedFile);
45
+ }
46
+ }
47
+ };
48
+ const handleActions = (action: MessageAction, messageId: string) => {
49
+ if (!action) return;
50
+ switch (action.type) {
51
+ case MessageActionType.PUBLISH_PROJECT:
52
+ return createProject(
53
+ files ?? [],
54
+ action.projectTitle ?? "",
55
+ messageId,
56
+ action.prompt ?? ""
57
+ );
58
+ case MessageActionType.SEE_LIVE_PREVIEW:
59
+ return window.open(
60
+ `https://huggingface.co/spaces/${project?.name}`,
61
+ "_blank"
62
+ );
63
+ }
64
+ };
65
+
66
+ useEffect(() => {
67
+ if (chatContainerRef.current) {
68
+ chatContainerRef.current.scrollTop =
69
+ chatContainerRef.current.scrollHeight;
70
+ }
71
+ }, [messages]);
72
+
73
+ return (
74
+ <div ref={chatContainerRef} className="grow px-2 overflow-y-auto pb-3">
75
+ {isNew ? (
76
+ <p className="text-xs text-muted-foreground text-center mb-3 bg-accent rounded-md mx-auto w-fit px-2 py-1">
77
+ Start a new conversation with the AI
78
+ </p>
79
+ ) : (
80
+ <p className="text-xs text-muted-foreground text-center mb-3 bg-accent rounded-md mx-auto w-fit px-2 py-1">
81
+ Your last conversation has not been restored.
82
+ </p>
83
+ )}
84
+ <div className="grid grid-cols-1 gap-6 w-full">
85
+ {messages.map((message, id) => (
86
+ <div
87
+ key={id}
88
+ className={cn(
89
+ "flex items-center justify-start gap-2 flex-1",
90
+ message.role === "user"
91
+ ? "flex-row-reverse text-right"
92
+ : "flex-row"
93
+ )}
94
+ >
95
+ <Avatar className="size-4">
96
+ <AvatarImage
97
+ src={message.role === "user" ? session?.user?.image ?? "" : ""}
98
+ alt="DeepSite"
99
+ className="size-4"
100
+ />
101
+ <AvatarFallback
102
+ className={cn(
103
+ message.role === "assistant"
104
+ ? "bg-linear-to-br from-indigo-500 to-emerald-500"
105
+ : ""
106
+ )}
107
+ >
108
+ {message.role === "user"
109
+ ? session?.user?.name?.charAt(0) ?? ""
110
+ : ""}
111
+ </AvatarFallback>
112
+ </Avatar>
113
+ <div className="flex flex-col max-w-[90%]">
114
+ <div
115
+ className={cn(
116
+ "whitespace-pre-line text-sm wrap-break-word text-muted-foreground",
117
+ message.role === "assistant"
118
+ ? "-space-y-2 min-w-[60px] border border-border bg-linear-to-br from-muted px-4 py-3.5 rounded-xl"
119
+ : ""
120
+ )}
121
+ >
122
+ <Markdown
123
+ components={{
124
+ strong: ({ children }) => (
125
+ <b className="font-medium text-primary">{children}</b>
126
+ ),
127
+ code: ({ className, children }) => {
128
+ const isCodeBlock = className?.startsWith("language-");
129
+ const language = className?.replace("language-", "");
130
+ if (isCodeBlock) {
131
+ return (
132
+ <SyntaxHighlighter
133
+ language={language}
134
+ style={dracula}
135
+ className="m-0! text-xs! rounded-md!"
136
+ >
137
+ {String(children).trim()}
138
+ </SyntaxHighlighter>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <code className="font-mono bg-indigo-500/10 text-indigo-500 text-[10px] px-1.5 py-0.5 rounded-md">
144
+ {children}
145
+ </code>
146
+ );
147
+ },
148
+ ul: ({ children }) => (
149
+ <ul className="list-inside m-0! p-0! flex flex-col gap-1 list-disc">
150
+ {children}
151
+ </ul>
152
+ ),
153
+ ol: ({ children }) => (
154
+ <ol className="list-inside m-0! p-0! flex flex-col gap-1 list-decimal">
155
+ {children}
156
+ </ol>
157
+ ),
158
+ li: ({ children }) => (
159
+ <li className="text-muted-foreground">{children}</li>
160
+ ),
161
+ a: ({ children, href }) => (
162
+ <a
163
+ href={href}
164
+ target="_blank"
165
+ rel="noopener noreferrer"
166
+ className="hover:text-blue-500 underline inline-flex items-center gap-0.5"
167
+ >
168
+ <ExternalLink className="size-3" />
169
+ {children}
170
+ </a>
171
+ ),
172
+ pre: ({ children }) => <>{children}</>,
173
+ p: ({ children }) => {
174
+ if (
175
+ typeof children === "string" &&
176
+ String(children).includes("file:/")
177
+ ) {
178
+ const parts = children.split(/(file:\/\S+)/g);
179
+ return (
180
+ <p>
181
+ {parts.map((part, index) =>
182
+ part.startsWith("file:/") ? (
183
+ <span
184
+ key={index}
185
+ className="inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400"
186
+ >
187
+ {getFileIcon(part, "size-2.5")}
188
+ {part.replace("file:/", "")}
189
+ </span>
190
+ ) : (
191
+ part
192
+ )
193
+ )}
194
+ </p>
195
+ );
196
+ }
197
+ return <p>{children}</p>;
198
+ },
199
+ }}
200
+ >
201
+ {message.content}
202
+ </Markdown>
203
+ {message.isThinking && (
204
+ <AiLoading showCircle={false} className="opacity-70" />
205
+ )}
206
+ {message.isAborted && (
207
+ <p
208
+ className={cn(
209
+ "text-[10px] text-muted-foreground font-mono bg-muted-foreground/10 rounded-md px-2 py-1 w-fit",
210
+ message.content ? "mt-5" : ""
211
+ )}
212
+ >
213
+ The request has been aborted due to an error OR the user has
214
+ stopped the generation.
215
+ </p>
216
+ )}
217
+ </div>
218
+ {message.model && message.createdAt && (
219
+ <div className="flex items-center justify-between">
220
+ <p className="text-xs text-muted-foreground mt-1.5 pl-1">
221
+ via{" "}
222
+ <code className="font-mono bg-accent text-muted-foreground text-[10px] px-1.5 py-0.5 rounded-md">
223
+ {message.model}
224
+ </code>
225
+ </p>
226
+ {!message.isThinking && (
227
+ <p className="text-[10px] text-muted-foreground/70 mt-1.5 pl-1">
228
+ {formatDistanceToNow(message.createdAt, {
229
+ addSuffix: true,
230
+ })}
231
+ </p>
232
+ )}
233
+ </div>
234
+ )}
235
+ {message.files && message.files.length > 0 && (
236
+ <div className="bg-accent px-3 py-2 rounded-lg w-fit mt-2.5">
237
+ <div className="flex items-center gap-2">
238
+ {isLoading &&
239
+ messages[messages.length - 1].id === message.id ? (
240
+ <>
241
+ <Loading
242
+ overlay={false}
243
+ className="size-3! opacity-50"
244
+ />
245
+ <p className="font-medium text-xs text-muted-foreground">
246
+ {isNew ? "Creating" : "Editing"}{" "}
247
+ <FileCode
248
+ file={message.files[message.files.length - 1]}
249
+ />
250
+ </p>
251
+ </>
252
+ ) : (
253
+ <>
254
+ <p className="font-medium text-xs text-muted-foreground">
255
+ {isNew ? "Created" : "Edited"}{" "}
256
+ <FileCode
257
+ file={message.files[message.files.length - 1]}
258
+ />
259
+ {message.files.length > 1 && (
260
+ <span className="text-[10px] text-muted-foreground ml-1 font-normal">
261
+ (+ {message.files.length - 1} files)
262
+ </span>
263
+ )}
264
+ </p>
265
+ <Button
266
+ size="icon-xs"
267
+ variant="transparent"
268
+ className="cursor-pointer size-4"
269
+ onClick={() => handleShowLastFile(message.files)}
270
+ >
271
+ <ChevronRight className="size-3.5 text-muted-foreground/60" />
272
+ </Button>
273
+ </>
274
+ )}
275
+ </div>
276
+ </div>
277
+ )}
278
+ {message.actions &&
279
+ message.actions.length > 0 &&
280
+ messages.length - 1 === id && (
281
+ <div className="flex items-center gap-2 mt-2.5">
282
+ {message.actions.map((action, id) => (
283
+ <Button
284
+ key={id}
285
+ size="xs"
286
+ disabled={action.disabled ?? action.loading}
287
+ variant={action.variant ?? "secondary"}
288
+ onClick={() => handleActions(action, message.id)}
289
+ >
290
+ {action.loading && (
291
+ <Loading
292
+ className={`size-3! opacity-50 ${
293
+ action.variant === "default" ? "text-white!" : ""
294
+ }`}
295
+ overlay={false}
296
+ />
297
+ )}
298
+ {action.label}
299
+ </Button>
300
+ ))}
301
+ </div>
302
+ )}
303
+ </div>
304
+ </div>
305
+ ))}
306
+ </div>
307
+ </div>
308
+ );
309
+ }
310
+
311
+ const FileCode = ({ file }: { file: string }) => (
312
+ <code className="font-mono bg-accent text-muted-foreground border border-border-muted rounded text-[10px] px-1 py-0.5">
313
+ {file}
314
+ </code>
315
+ );
components/chat/useChat.ts ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { useLocalStorage } from "react-use";
3
+ import { useEffect } from "react";
4
+ import { v4 as uuidv4 } from "uuid";
5
+
6
+ import { File, Message } from "@/lib/type";
7
+
8
+ const MESSAGES_QUERY_KEY = (projectName: string) =>
9
+ ["messages", projectName] as const;
10
+ const DEFAULT_FIRST_MESSAGE = (projectName: string): Message[] => {
11
+ if (projectName === "new") {
12
+ return [
13
+ {
14
+ id: uuidv4(),
15
+ role: "assistant",
16
+ content: `Hello! Welcome to DeepSite!
17
+ Ask me anything about DeepSite and AI, and I'll help you build it.`,
18
+ createdAt: new Date(),
19
+ },
20
+ ];
21
+ }
22
+ return [
23
+ {
24
+ id: uuidv4(),
25
+ role: "assistant",
26
+ content: "Welcome back to your project! 🤩",
27
+ createdAt: new Date(),
28
+ },
29
+ ];
30
+ };
31
+
32
+ export function useChat(projectName: string) {
33
+ const queryClient = useQueryClient();
34
+ const [, setStoredMessages, clearStoredMessages] = useLocalStorage<Message[]>(
35
+ `messages-${projectName}`,
36
+ []
37
+ );
38
+
39
+ useEffect(() => {
40
+ if (projectName !== "new") {
41
+ queryClient.invalidateQueries({
42
+ queryKey: MESSAGES_QUERY_KEY(projectName),
43
+ });
44
+ }
45
+ }, [projectName, queryClient]);
46
+
47
+ const { data: messages = [] } = useQuery<Message[]>({
48
+ queryKey: MESSAGES_QUERY_KEY(projectName),
49
+ queryFn: () => {
50
+ if (projectName === "new") {
51
+ return DEFAULT_FIRST_MESSAGE(projectName);
52
+ }
53
+ const storedData = localStorage.getItem(`messages-${projectName}`);
54
+ if (storedData) {
55
+ try {
56
+ const parsedMessages = JSON.parse(storedData);
57
+ if (parsedMessages && parsedMessages.length > 0) {
58
+ return parsedMessages;
59
+ }
60
+ } catch (error) {
61
+ console.error("Failed to parse stored messages:", error);
62
+ }
63
+ }
64
+ return DEFAULT_FIRST_MESSAGE(projectName);
65
+ },
66
+ refetchOnMount: false,
67
+ refetchOnWindowFocus: false,
68
+ refetchOnReconnect: false,
69
+ refetchInterval: false,
70
+ refetchIntervalInBackground: false,
71
+ });
72
+
73
+ const addMessage = (message: Omit<Message, "id">) => {
74
+ const id = uuidv4();
75
+ queryClient.setQueryData<Message[]>(
76
+ MESSAGES_QUERY_KEY(projectName),
77
+ (oldMessages = []) => {
78
+ const newMessages = [
79
+ ...oldMessages,
80
+ {
81
+ ...message,
82
+ id,
83
+ },
84
+ ];
85
+ if (projectName !== "new") {
86
+ setStoredMessages(newMessages);
87
+ }
88
+ return newMessages;
89
+ }
90
+ );
91
+ return id;
92
+ };
93
+ const clearMessages = () => {
94
+ queryClient.setQueryData<Message[]>(MESSAGES_QUERY_KEY(projectName), []);
95
+ clearStoredMessages();
96
+ };
97
+
98
+ const storeMessages = async (newProjectName: string) => {
99
+ return new Promise((resolve) => {
100
+ const currentMessages = queryClient.getQueryData<Message[]>(
101
+ MESSAGES_QUERY_KEY("new")
102
+ );
103
+ localStorage.setItem(
104
+ `messages-${newProjectName}`,
105
+ JSON.stringify(currentMessages)
106
+ );
107
+ queryClient.setQueryData<Message[]>(
108
+ MESSAGES_QUERY_KEY(newProjectName),
109
+ currentMessages
110
+ );
111
+ setTimeout(() => resolve(true), 100);
112
+ });
113
+ };
114
+
115
+ return {
116
+ messages,
117
+ addMessage,
118
+ clearMessages,
119
+ storeMessages,
120
+ };
121
+ }
components/code/index.tsx ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import {
3
+ SandpackLayout,
4
+ SandpackFileExplorer,
5
+ } from "@codesandbox/sandpack-react";
6
+ import { ChevronRight, ChevronLeft } from "lucide-react";
7
+ import { useParams } from "next/navigation";
8
+ import { useQueryClient } from "@tanstack/react-query";
9
+ import { format } from "date-fns";
10
+ import { useUpdateEffect } from "react-use";
11
+
12
+ import { AppEditorMonacoEditor } from "./monaco-editor";
13
+ import { Button } from "@/components/ui/button";
14
+ import { cn } from "@/lib/utils";
15
+ import Loading from "../loading";
16
+ import { useManualUpdates } from "../projects/useManualUpdates";
17
+ import { ProjectWithCommits } from "@/actions/projects";
18
+
19
+ export function AppEditorCode() {
20
+ const queryClient = useQueryClient();
21
+ const { isManuallyUpdatedSameAsFiles } = useManualUpdates();
22
+ const { repoId } = useParams<{ repoId: string }>();
23
+
24
+ const [isFileExplorerCollapsed, setIsFileExplorerCollapsed] = useState(true);
25
+ const [isSavingChanges, setIsSavingChanges] = useState(false);
26
+ const [isSavingChangesSuccess, setIsSavingChangesSuccess] = useState(false);
27
+ const [isSavingChangesError, setIsSavingChangesError] = useState(false);
28
+ const [isClosing, setIsClosing] = useState(false);
29
+ const [showSaveChanges, setShowSaveChanges] = useState(false);
30
+
31
+ const handleSaveChanges = async () => {
32
+ if (repoId === "new") return;
33
+ setIsSavingChanges(true);
34
+ const manuallyUpdatedFiles =
35
+ queryClient.getQueryData<File[]>(["manuallyUpdatedFiles"]) ?? [];
36
+ const response = await fetch(`/api/projects/${repoId}`, {
37
+ method: "PUT",
38
+ body: JSON.stringify({
39
+ files: manuallyUpdatedFiles,
40
+ prompt: `✍️ ${format(new Date(), "dd/MM")} - ${format(
41
+ new Date(),
42
+ "HH:mm"
43
+ )} - Manual changes.`,
44
+ isManualChanges: true,
45
+ }),
46
+ }).then(async (response) => {
47
+ if (response.ok) {
48
+ const data = await response.json();
49
+ return data;
50
+ }
51
+ throw new Error("Failed to save changes");
52
+ });
53
+ if (response.success) {
54
+ setIsSavingChangesSuccess(true);
55
+ queryClient.invalidateQueries({ queryKey: ["manuallyUpdatedFiles"] });
56
+ queryClient.setQueryData(
57
+ ["project"],
58
+ (oldProject: ProjectWithCommits) => ({
59
+ ...oldProject,
60
+ commits: [response.commit, ...(oldProject?.commits ?? [])],
61
+ })
62
+ );
63
+ } else {
64
+ setIsSavingChangesError(true);
65
+ }
66
+ setIsSavingChanges(false);
67
+ setShowSaveChanges(false);
68
+ };
69
+
70
+ const undoChanges = () => {
71
+ queryClient.setQueryData<File[]>(["manuallyUpdatedFiles"], []);
72
+ setShowSaveChanges(false);
73
+ };
74
+
75
+ useUpdateEffect(() => {
76
+ if (isSavingChangesSuccess) {
77
+ setTimeout(() => setIsSavingChangesSuccess(false), 3000);
78
+ }
79
+ }, [isSavingChangesSuccess]);
80
+
81
+ useUpdateEffect(() => {
82
+ if (isClosing) {
83
+ setTimeout(() => {
84
+ setIsSavingChangesSuccess(false);
85
+ setIsSavingChangesError(false);
86
+ setIsClosing(false);
87
+ }, 300);
88
+ }
89
+ }, [isClosing]);
90
+
91
+ return (
92
+ <div className="flex flex-col h-[calc(100%-0px)] w-full relative overflow-hidden">
93
+ <SandpackLayout className="h-full! flex-row! flex-! rounded-2xl! border! border-border-muted! dark:border-secondary!">
94
+ <Button
95
+ variant="bordered"
96
+ size="icon-xs"
97
+ className="absolute top-2 left-2 z-10"
98
+ onClick={() => setIsFileExplorerCollapsed(!isFileExplorerCollapsed)}
99
+ title={
100
+ isFileExplorerCollapsed
101
+ ? "Expand file explorer"
102
+ : "Collapse file explorer"
103
+ }
104
+ >
105
+ {isFileExplorerCollapsed ? (
106
+ <ChevronRight className="h-4 w-4" />
107
+ ) : (
108
+ <ChevronLeft className="h-4 w-4" />
109
+ )}
110
+ </Button>
111
+ <SandpackFileExplorer
112
+ className={cn(
113
+ "flex-1 h-full! pt-8!",
114
+ isFileExplorerCollapsed
115
+ ? "w-[45px]! min-w-[45px]! flex-[0_1_0%]!"
116
+ : ""
117
+ )}
118
+ autoHiddenFiles={true}
119
+ />
120
+ <AppEditorMonacoEditor setShowSaveChanges={setShowSaveChanges} />
121
+ </SandpackLayout>
122
+ {repoId && (
123
+ <div
124
+ className={cn(
125
+ "absolute bottom-0 left-0 w-full py-3 px-4 transition-all duration-300",
126
+ (!showSaveChanges &&
127
+ !isSavingChangesSuccess &&
128
+ !isSavingChangesError) ||
129
+ isClosing
130
+ ? "translate-y-full opacity-0 pointer-events-none"
131
+ : ""
132
+ )}
133
+ >
134
+ <div
135
+ className={cn(
136
+ "bg-accent/50 backdrop-blur-sm rounded-xl border border-border-muted p-3 flex items-center justify-between",
137
+ isSavingChangesError ? "border-red-500/20 bg-red-500/10!" : "",
138
+ isSavingChangesSuccess
139
+ ? "border-emerald-500/20 bg-emerald-500/10!"
140
+ : ""
141
+ )}
142
+ >
143
+ <div>
144
+ <p
145
+ className={cn(
146
+ "text-sm font-medium",
147
+ isSavingChangesSuccess
148
+ ? "text-emerald-500"
149
+ : isSavingChangesError
150
+ ? "text-red-500"
151
+ : "text-primary"
152
+ )}
153
+ >
154
+ {isSavingChangesSuccess
155
+ ? "Changes saved"
156
+ : isSavingChangesError
157
+ ? "Failed to save changes"
158
+ : "Save Changes"}
159
+ </p>
160
+ <p
161
+ className={cn(
162
+ "text-xs text-muted-foreground",
163
+ isSavingChangesSuccess
164
+ ? "text-emerald-500"
165
+ : isSavingChangesError
166
+ ? "text-red-500"
167
+ : "text-muted-foreground"
168
+ )}
169
+ >
170
+ {isSavingChangesSuccess
171
+ ? "Changes saved successfully"
172
+ : isSavingChangesError
173
+ ? "Something went wrong, please try again."
174
+ : "You have unsaved manual changes. Click the button to save your changes."}
175
+ </p>
176
+ </div>
177
+ <div className="flex items-center justify-end gap-2">
178
+ {!isSavingChangesSuccess ||
179
+ (!isSavingChanges && (
180
+ <Button size="xs" variant="secondary" onClick={undoChanges}>
181
+ Undo
182
+ </Button>
183
+ ))}
184
+
185
+ {isSavingChangesSuccess || isSavingChangesError ? (
186
+ <Button
187
+ variant={isSavingChangesError ? "destructive" : "default"}
188
+ size="xs"
189
+ onClick={() => setIsClosing(true)}
190
+ >
191
+ Close
192
+ </Button>
193
+ ) : (
194
+ <Button
195
+ variant="default"
196
+ size="xs"
197
+ onClick={handleSaveChanges}
198
+ disabled={isSavingChanges}
199
+ >
200
+ Save Changes
201
+ {isSavingChanges && (
202
+ <Loading
203
+ overlay={false}
204
+ className="text-primary-foreground! size-3.5!"
205
+ />
206
+ )}
207
+ </Button>
208
+ )}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+ }
components/code/monaco-editor.tsx ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo, useRef, useCallback, useEffect } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { Monaco } from "@monaco-editor/react";
4
+ import {
5
+ useActiveCode,
6
+ SandpackStack,
7
+ FileTabs,
8
+ useSandpack,
9
+ } from "@codesandbox/sandpack-react";
10
+ import { useTheme } from "next-themes";
11
+
12
+ import NightLight from "@/components/editor/night-light.json";
13
+ import Night from "@/components/editor/night.json";
14
+ import { File } from "@/lib/type";
15
+ import dynamic from "next/dynamic";
16
+
17
+ const Editor = dynamic(
18
+ () => import("@monaco-editor/react").then((mod) => mod.Editor),
19
+ { ssr: false }
20
+ );
21
+
22
+ const LANGUAGE_MAP = {
23
+ js: "javascript",
24
+ ts: "typescript",
25
+ html: "html",
26
+ css: "css",
27
+ json: "json",
28
+ txt: "text",
29
+ };
30
+
31
+ export function AppEditorMonacoEditor({
32
+ setShowSaveChanges,
33
+ }: {
34
+ setShowSaveChanges: (show: boolean) => void;
35
+ }) {
36
+ const { theme } = useTheme();
37
+ const { code, updateCode } = useActiveCode();
38
+ const { sandpack } = useSandpack();
39
+ const queryClient = useQueryClient();
40
+ const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
41
+
42
+ const handleEditorDidMount = (monaco: Monaco) => {
43
+ monaco.editor.defineTheme("NightLight", {
44
+ base: "vs",
45
+ inherit: true,
46
+ ...NightLight,
47
+ rules: [],
48
+ });
49
+ monaco.editor.defineTheme("Night", {
50
+ base: "vs-dark",
51
+ ...Night,
52
+ rules: [],
53
+ });
54
+ };
55
+
56
+ const language = useMemo(() => {
57
+ const extension = sandpack.activeFile
58
+ .split(".")
59
+ .pop()
60
+ ?.toLowerCase() as string;
61
+ return LANGUAGE_MAP[extension as keyof typeof LANGUAGE_MAP] ?? "text";
62
+ }, [sandpack.activeFile]);
63
+
64
+ const updateFile = useCallback(
65
+ (newValue: string, activeFile: string) => {
66
+ if (updateTimeoutRef.current) {
67
+ clearTimeout(updateTimeoutRef.current);
68
+ }
69
+
70
+ updateTimeoutRef.current = setTimeout(() => {
71
+ const manuallyUpdatedFiles =
72
+ queryClient.getQueryData<File[]>(["manuallyUpdatedFiles"]) ?? [];
73
+ const fileIndex = manuallyUpdatedFiles.findIndex(
74
+ (file) => file.path === activeFile
75
+ );
76
+ if (fileIndex !== -1) {
77
+ manuallyUpdatedFiles[fileIndex].content = newValue;
78
+ } else {
79
+ manuallyUpdatedFiles.push({
80
+ path: activeFile,
81
+ content: newValue,
82
+ });
83
+ }
84
+ queryClient.setQueryData<File[]>(
85
+ ["manuallyUpdatedFiles"],
86
+ manuallyUpdatedFiles
87
+ );
88
+ }, 100);
89
+ },
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ [queryClient]
92
+ );
93
+
94
+ const handleEditorChange = (value: string | undefined) => {
95
+ setShowSaveChanges(true);
96
+ const newValue = value || "";
97
+ updateCode(newValue);
98
+ const activeFile = sandpack.activeFile?.replace(/^\//, "");
99
+ updateFile(newValue, activeFile);
100
+ };
101
+
102
+ useEffect(() => {
103
+ return () => {
104
+ if (updateTimeoutRef.current) {
105
+ clearTimeout(updateTimeoutRef.current);
106
+ }
107
+ };
108
+ }, []);
109
+
110
+ return (
111
+ <SandpackStack className="h-full!">
112
+ <FileTabs />
113
+ <div style={{ flex: 1 }}>
114
+ <Editor
115
+ width="100%"
116
+ height="100%"
117
+ language={language}
118
+ theme={theme === "dark" ? "Night" : "NightLight"}
119
+ key={sandpack.activeFile}
120
+ options={{
121
+ fontSize: 14,
122
+ fontFamily: "Jetbrains-Mono",
123
+ fontLigatures: true,
124
+ wordWrap: "on",
125
+ minimap: {
126
+ enabled: false,
127
+ },
128
+ bracketPairColorization: {
129
+ enabled: true,
130
+ },
131
+ cursorBlinking: "smooth",
132
+ formatOnPaste: true,
133
+ suggest: {
134
+ showFields: false,
135
+ showFunctions: false,
136
+ },
137
+ stickyTabStops: false,
138
+ stickyScroll: {
139
+ enabled: false,
140
+ },
141
+ }}
142
+ beforeMount={handleEditorDidMount}
143
+ value={code}
144
+ onChange={handleEditorChange}
145
+ />
146
+ </div>
147
+ </SandpackStack>
148
+ );
149
+ }
components/code/useEditor.ts ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { useProject } from "@/components/projects/useProject";
3
+ import { useMemo } from "react";
4
+
5
+ const LANGUAGE_MAP = {
6
+ "py": "python",
7
+ "js": "javascript",
8
+ "ts": "typescript",
9
+ "html": "html",
10
+ "css": "css",
11
+ "json": "json",
12
+ "txt": "text",
13
+ }
14
+
15
+ export const useEditor = () => {
16
+ const queryClient = useQueryClient();
17
+ const { files } = useProject();
18
+
19
+ const { data: isFileListOpen } = useQuery<boolean>({
20
+ queryKey: ["isFileListOpen"],
21
+ queryFn: () => {
22
+ return false;
23
+ },
24
+ initialData: false,
25
+ refetchInterval: false,
26
+ refetchOnWindowFocus: false,
27
+ });
28
+
29
+ const setIsFileListOpen = (isOpen: boolean) => {
30
+ queryClient.setQueryData<boolean>(["isFileListOpen"], () => isOpen);
31
+ }
32
+
33
+ const { data: editorFilePath } = useQuery<string>({
34
+ queryKey: ["editorFile"],
35
+ queryFn: () => {
36
+ return "app.py";
37
+ },
38
+ initialData: "app.py",
39
+ refetchInterval: false,
40
+ refetchOnWindowFocus: false,
41
+ refetchOnMount: false,
42
+ refetchOnReconnect: false,
43
+ refetchIntervalInBackground: false,
44
+ });
45
+
46
+ const setEditorFilePath = (path: string) => {
47
+ queryClient.setQueryData<string>(["editorFile"], () => path);
48
+ }
49
+
50
+ const editorFileData = useMemo(() => {
51
+ const finalFile = { path: "", content: "", language: "" };
52
+ if (editorFilePath) {
53
+ const file = files?.find((file) => file.path === editorFilePath);
54
+ if (file) {
55
+ finalFile.path = file.path;
56
+ finalFile.content = file.content ?? "";
57
+ finalFile.language = LANGUAGE_MAP[file.path.split(".").pop()?.toLowerCase() as keyof typeof LANGUAGE_MAP] ?? "text";
58
+ }
59
+ }
60
+
61
+ return finalFile
62
+ }, [editorFilePath, files])
63
+
64
+ return {
65
+ editorFilePath,
66
+ editorFileData,
67
+ setEditorFilePath,
68
+ isFileListOpen,
69
+ setIsFileListOpen,
70
+ }
71
+ }
components/editor/commits.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState } from "react";
2
+ import { HistoryIcon } from "lucide-react";
3
+ import { useSearchParams } from "next/navigation";
4
+ import { useUpdateEffect } from "react-use";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from "@/components/ui/popover";
12
+ import { Commit } from "@/lib/type";
13
+ import { cn, humanizeNumber } from "@/lib/utils";
14
+
15
+ export const Commits = function ({ commits }: { commits?: Commit[] }) {
16
+ const searchParams = useSearchParams();
17
+ const commitParam = searchParams.get("commit");
18
+
19
+ const commitsContainer = useRef<HTMLUListElement>(null);
20
+ const commitSelected = useRef<HTMLLIElement>(null);
21
+ const [open, setOpen] = useState(false);
22
+ const [selectedCommit, setSelectedCommit] = useState<string | null>(
23
+ commitParam
24
+ ? commitParam
25
+ : commits && commits.length > 0
26
+ ? commits[0]?.oid
27
+ : null
28
+ );
29
+
30
+ const handleViewCommit = (commitId: string) => {
31
+ let url = window.location.pathname;
32
+ if (commitId !== commits?.[0].oid) {
33
+ url += `?commit=${commitId}`;
34
+ }
35
+ window.location.replace(url);
36
+ };
37
+
38
+ useUpdateEffect(() => {
39
+ if (open) {
40
+ setTimeout(() => {
41
+ if (commitSelected.current && commitsContainer.current) {
42
+ const container = commitsContainer.current;
43
+ const selected = commitSelected.current;
44
+ if (!container || !selected) return;
45
+ const offsetTop = selected.offsetTop;
46
+ const offsetHeight = selected.offsetHeight;
47
+ const containerHeight = container.offsetHeight;
48
+ const scrollTop = offsetTop - containerHeight / 2 + offsetHeight / 2;
49
+ container.scrollTo({
50
+ top: scrollTop,
51
+ behavior: "smooth",
52
+ });
53
+ }
54
+ }, 200);
55
+ }
56
+ }, [open]);
57
+
58
+ useUpdateEffect(() => {
59
+ if (selectedCommit !== commits?.[0].oid) {
60
+ setSelectedCommit(commits ? commits[0]?.oid : null);
61
+ }
62
+ }, [commits]);
63
+
64
+ return (
65
+ <Popover open={open} onOpenChange={setOpen}>
66
+ <form>
67
+ <PopoverTrigger asChild>
68
+ <Button variant={open ? "default" : "bordered"} size="xs">
69
+ <HistoryIcon className="size-3.5" />
70
+ <span>
71
+ History <span className="max-lg:hidden">version</span>
72
+ </span>
73
+ <span className="py-0.5 px-1 text-[10px] flex items-center justify-center rounded font-semibold bg-accent text-primary font-mono">
74
+ {humanizeNumber(commits ? commits.length : 0)}
75
+ </span>
76
+ </Button>
77
+ </PopoverTrigger>
78
+ <PopoverContent
79
+ align="start"
80
+ className="rounded-2xl! p-0! overflow-hidden!"
81
+ >
82
+ <header className="bg-background px-3 py-2 border-b border-background-foreground text-xs text-muted-foreground font-medium flex items-center justify-center gap-1.5">
83
+ Historical Versions
84
+ <span className="py-0.5 px-1 text-[10px] inline-flex items-center justify-center rounded font-semibold bg-accent text-primary font-mono">
85
+ {humanizeNumber(commits ? commits.length : 0)}
86
+ </span>
87
+ </header>
88
+ <ul
89
+ ref={commitsContainer}
90
+ className="p-1 space-y-1 max-h-80 overflow-y-auto"
91
+ >
92
+ {commits?.map((commit) => (
93
+ <li
94
+ key={commit.oid}
95
+ className={cn(
96
+ "overflow-hidden group relative px-3 py-2 text-xs cursor-pointer rounded-lg",
97
+ selectedCommit === commit.oid
98
+ ? "bg-indigo-500/10 text-indigo-600"
99
+ : "hover:bg-accent"
100
+ )}
101
+ ref={selectedCommit === commit.oid ? commitSelected : undefined}
102
+ onClick={() => handleViewCommit(commit.oid)}
103
+ >
104
+ <p className="truncate break-all">{commit.title}</p>
105
+ <p className="text-[10px] text-muted-foreground mt-0.5">
106
+ {new Date(commit.date).toLocaleString()}
107
+ </p>
108
+ </li>
109
+ ))}
110
+ </ul>
111
+ </PopoverContent>
112
+ </form>
113
+ </Popover>
114
+ );
115
+ };
components/editor/header.tsx ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Earth,
6
+ RefreshCw,
7
+ Monitor,
8
+ Smartphone,
9
+ Code,
10
+ ChevronDown,
11
+ MessageCircle,
12
+ ExternalLink,
13
+ } from "lucide-react";
14
+ import Image from "next/image";
15
+ import Link from "next/link";
16
+ import { motion } from "framer-motion";
17
+ import { useSandpackNavigation } from "@codesandbox/sandpack-react";
18
+
19
+ import { Button } from "@/components/ui/button";
20
+ import { ActivityType, DeviceType, MobileTabType } from "@/lib/type";
21
+ import { UserMenu } from "@/components/user-menu";
22
+ import { useProject } from "@/components/projects/useProject";
23
+ import { cn } from "@/lib/utils";
24
+ import { Commits } from "./commits";
25
+
26
+ export function AppEditorHeader({
27
+ currentActivity,
28
+ onToggleActivity,
29
+ onToggleDevice,
30
+ device,
31
+ mobileTab,
32
+ isHistoryView,
33
+ }: {
34
+ isNew?: boolean;
35
+ isHistoryView?: boolean;
36
+ currentActivity: ActivityType;
37
+ onToggleActivity: (activity: ActivityType) => void;
38
+ onToggleDevice: () => void;
39
+ device: DeviceType;
40
+ mobileTab: MobileTabType;
41
+ onToggleMobileTab: (tab: MobileTabType) => void;
42
+ }) {
43
+ const { project, files } = useProject();
44
+ const { refresh } = useSandpackNavigation();
45
+ const [isRefreshing, setIsRefreshing] = useState(false);
46
+ const [isHovered, setIsHovered] = useState(false);
47
+
48
+ const handleRefresh = () => {
49
+ setIsRefreshing(true);
50
+ setTimeout(() => {
51
+ refresh();
52
+ setIsRefreshing(false);
53
+ }, 1000);
54
+ };
55
+
56
+ const handleToggleActivity = () => {
57
+ onToggleActivity(currentActivity === "chat" ? "code" : "chat");
58
+ };
59
+
60
+ return (
61
+ <header className="px-3 lg:px-2 py-1.5 flex items-center gap-3">
62
+ <div
63
+ className={cn(
64
+ "w-1/3 flex items-center gap-1.5 justify-between",
65
+ mobileTab !== "left-sidebar" ? "max-lg:hidden" : "max-lg:w-full!"
66
+ )}
67
+ >
68
+ <Button variant="ghost" className="pl-2.5! pr-3! py-1.5! h-auto!">
69
+ <Image
70
+ src="/logo.svg"
71
+ alt="DeepSite"
72
+ width={100}
73
+ height={100}
74
+ className="size-8"
75
+ />
76
+ <div className="flex flex-col -space-y-1 items-start">
77
+ <p className="text-sm font-bold text-primary">
78
+ {project?.cardData?.title ?? "New DeepSite website"}{" "}
79
+ {project?.cardData?.emoji}
80
+ </p>
81
+ <p className="text-xs text-muted-foreground">
82
+ Live preview of your app
83
+ </p>
84
+ </div>
85
+ <ChevronDown />
86
+ </Button>
87
+ <div className="flex items-center justify-end gap-2 max-lg:hidden">
88
+ {(project?.commits?.length ?? 0) > 0 && (
89
+ <Commits commits={project?.commits} />
90
+ )}
91
+ </div>
92
+ </div>
93
+ <div className="grow flex items-center justify-end lg:grid lg:grid-cols-3">
94
+ <div className="flex items-center justify-start gap-1.5 max-lg:hidden">
95
+ {files && files.length > 0 && (
96
+ <>
97
+ {!isHistoryView && (
98
+ <Button
99
+ variant="indigo"
100
+ size="xs"
101
+ onClick={handleToggleActivity}
102
+ >
103
+ {currentActivity === "chat" ? <Code /> : <MessageCircle />}
104
+ {currentActivity === "chat" ? "Code" : "Chat"}
105
+ </Button>
106
+ )}
107
+ <Button
108
+ variant="bordered"
109
+ size="xs"
110
+ onClick={onToggleDevice}
111
+ className="gap-2"
112
+ >
113
+ {device === "desktop" ? (
114
+ <>
115
+ <Monitor className="size-4" />
116
+ Desktop
117
+ </>
118
+ ) : (
119
+ <>
120
+ <Smartphone className="size-4" />
121
+ Mobile
122
+ </>
123
+ )}
124
+ </Button>
125
+ </>
126
+ )}
127
+ {!isHistoryView && project?.name && (
128
+ <Link
129
+ href={`https://${project.name.replaceAll(
130
+ "/",
131
+ "-"
132
+ )}.static.hf.space`}
133
+ target="_blank"
134
+ onMouseEnter={() => setIsHovered(true)}
135
+ onMouseLeave={() => setIsHovered(false)}
136
+ >
137
+ <Button
138
+ variant="bordered"
139
+ className="h-7 px-2! overflow-hidden gap-0"
140
+ >
141
+ <ExternalLink className="size-4 shrink-0" />
142
+ <motion.span
143
+ initial={false}
144
+ animate={{
145
+ width: isHovered ? "auto" : 0,
146
+ opacity: isHovered ? 1 : 0,
147
+ marginLeft: isHovered ? 6 : 0,
148
+ }}
149
+ transition={{ duration: 0.2, ease: "linear" }}
150
+ className="overflow-hidden whitespace-nowrap text-xs"
151
+ style={{ display: "inline-block" }}
152
+ >
153
+ See Live preview
154
+ </motion.span>
155
+ </Button>
156
+ </Link>
157
+ )}
158
+ {isHistoryView && (
159
+ <Button
160
+ variant="indigo"
161
+ size="xs"
162
+ className="cursor-default! border-transparent!"
163
+ >
164
+ Viewing historical version
165
+ </Button>
166
+ )}
167
+ </div>
168
+ <div
169
+ className={cn(
170
+ "flex items-center justify-center",
171
+ mobileTab !== "right-sidebar" && "max-lg:hidden"
172
+ )}
173
+ >
174
+ <div className="flex items-center justify-between gap-3 pl-3 pr-2 py-1 bg-secondary rounded-md text-xs">
175
+ <div className="flex items-center text-muted-foreground">
176
+ <Earth className="size-3 shrink-0 mr-2" />
177
+ <span className="font-mono max-w-26 truncate">
178
+ {project?.name
179
+ ? `${project.name.replaceAll("/", "-")}`
180
+ : "localhost:3000"}
181
+ </span>
182
+ <span className="font-mono">
183
+ {project?.name && ".static.hf.space"}
184
+ </span>
185
+ </div>
186
+ <div className="flex items-center gap-0.5">
187
+ <Button
188
+ variant="ghost"
189
+ size="icon-xs"
190
+ onClick={handleRefresh}
191
+ aria-label="Refresh preview"
192
+ disabled={isRefreshing}
193
+ className="hover:bg-background/60"
194
+ >
195
+ <RefreshCw
196
+ className={`size-3 ${isRefreshing ? "animate-spin" : ""}`}
197
+ />
198
+ </Button>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ <div className="flex items-center justify-end gap-2 w-full">
203
+ <UserMenu />
204
+ </div>
205
+ </div>
206
+ </header>
207
+ );
208
+ }
components/editor/history-view.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { ArrowLeft, ArrowRight } from "lucide-react";
3
+ import { useParams, useSearchParams } from "next/navigation";
4
+ import { toast } from "sonner";
5
+
6
+ export const HistoryView = function () {
7
+ const { repoId } = useParams<{ repoId: string }>();
8
+ const searchParams = useSearchParams();
9
+ const commitId = searchParams.get("commit");
10
+
11
+ const handleSetAsDefault = async () => {
12
+ const confirmation = confirm(
13
+ "This action will set this historical version as the default version of the project. Are you sure you want to proceed?"
14
+ );
15
+ if (!confirmation) return;
16
+ const response = await fetch(`/api/projects/${repoId}/${commitId}`, {
17
+ method: "POST",
18
+ }).then(async (response) => {
19
+ if (response.ok) {
20
+ const data = await response.json();
21
+ return data;
22
+ }
23
+ throw new Error("Failed to save changes");
24
+ });
25
+ if (response.success) {
26
+ toast.success("Set as default version successfully!");
27
+ setTimeout(() => {
28
+ close();
29
+ }, 500);
30
+ } else {
31
+ alert("Failed to set as default version, try again later.");
32
+ }
33
+ };
34
+
35
+ const close = () => {
36
+ window.location.replace(window.location.pathname);
37
+ };
38
+
39
+ return (
40
+ <div className="absolute inset-0 bg-background/10 rounded-2xl backdrop-blur-xs z-10 flex items-center justify-center p-4 border">
41
+ <div className="max-w-sm rounded-2xl overflow-hidden border border-indigo-500/20 text-center bg-linear-to-b from-accent/50 to-accent/30">
42
+ <header className="bg-linear-to-b from-indigo-500/15 dark:from-indigo-500/40 to-accent p-6">
43
+ <div className="flex items-center justify-center -space-x-4 mb-3">
44
+ <div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
45
+ ✏️
46
+ </div>
47
+ <div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
48
+ 👀
49
+ </div>
50
+ <div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
51
+ 🔒
52
+ </div>
53
+ </div>
54
+ <p className="text-xl font-semibold text-primary">
55
+ History View Enabled
56
+ </p>
57
+ <p className="text-sm text-muted-foreground mt-1.5">
58
+ You are currently viewing a historical version of this project.
59
+ Editing is disabled in this mode.
60
+ </p>
61
+ </header>
62
+ <div className="p-5 bg-accent flex items-center justify-center gap-2">
63
+ <Button
64
+ variant="transparent"
65
+ size="sm"
66
+ className="flex-1 cyrsor"
67
+ onClick={() => close()}
68
+ >
69
+ <ArrowLeft className="" />
70
+ Go Back
71
+ </Button>
72
+ <Button
73
+ variant="default"
74
+ size="sm"
75
+ className="flex-1"
76
+ onClick={handleSetAsDefault}
77
+ >
78
+ Set as Default Version
79
+ </Button>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
components/editor/index.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useState, useMemo } from "react";
3
+ import dynamic from "next/dynamic";
4
+ import { useBeforeUnload } from "react-use";
5
+ import { ArrowRight } from "lucide-react";
6
+ import { useTheme } from "next-themes";
7
+ import { amethyst } from "@codesandbox/sandpack-themes";
8
+
9
+ import { AppEditorHeader } from "./header";
10
+ import { AppViewer } from "@/components/viewer";
11
+ import { ActivityType, DeviceType, File, MobileTabType } from "@/lib/type";
12
+ import { useProject } from "@/components/projects/useProject";
13
+ import { AskAI } from "@/components/ask-ai/ask-ai";
14
+ import { AppEditorChat } from "@/components/chat";
15
+ import { cn, defaultHTML } from "@/lib/utils";
16
+ import { Button } from "@/components/ui/button";
17
+ import { AppEditorCode } from "@/components/code";
18
+ import { ProjectWithCommits } from "@/actions/projects";
19
+ import { HistoryView } from "./history-view";
20
+
21
+ const SandpackProvider = dynamic(
22
+ () =>
23
+ import("@codesandbox/sandpack-react").then((mod) => mod.SandpackProvider),
24
+ { ssr: false }
25
+ );
26
+
27
+ export function AppEditor({
28
+ project: initialProject,
29
+ files: initialFiles,
30
+ initialPrompt,
31
+ isNew = false,
32
+ isHistoryView = false,
33
+ }: {
34
+ project?: ProjectWithCommits | null;
35
+ files?: File[];
36
+ initialPrompt?: string;
37
+ isNew?: boolean;
38
+ isHistoryView?: boolean;
39
+ }) {
40
+ const [currentActivity, setCurrentActivity] = useState<ActivityType>("chat");
41
+ const [device, setDevice] = useState<DeviceType>("desktop");
42
+ const [mobileTab, setMobileTab] = useState<MobileTabType>("left-sidebar");
43
+ const { project, files } = useProject(initialProject, initialFiles, isNew);
44
+ const { resolvedTheme } = useTheme();
45
+ const projectName = isNew ? "new" : project?.name ?? "new";
46
+
47
+ const toggleDevice = () => {
48
+ setDevice((prev) => (prev === "desktop" ? "mobile" : "desktop"));
49
+ };
50
+
51
+ useBeforeUnload(
52
+ !project && (files?.length ?? 0) > 0,
53
+ "You have unsaved changes, are you sure?"
54
+ );
55
+
56
+ const sandpackFiles = useMemo(() => {
57
+ if (files && files.length > 0) {
58
+ return files.reduce(
59
+ (acc, file) => ({
60
+ ...acc,
61
+ [file.path]: { code: file.content ?? "" },
62
+ }),
63
+ {}
64
+ );
65
+ } else {
66
+ return {
67
+ "/index.html": {
68
+ code: defaultHTML,
69
+ },
70
+ };
71
+ }
72
+ }, [files]);
73
+
74
+ return (
75
+ <SandpackProvider
76
+ template="static"
77
+ // options={{
78
+ // initMode: "immediate",
79
+ // autoReload: false,
80
+ // recompileDelay: 3000,
81
+ // recompileMode: "immediate",
82
+ // }}
83
+ files={sandpackFiles}
84
+ // id={projectName}
85
+ // key={projectName}
86
+ theme={resolvedTheme === "dark" ? amethyst : undefined}
87
+ className="h-screen! w-full! flex! flex-col! justify-between! lg:overflow-hidden!"
88
+ >
89
+ <AppEditorHeader
90
+ currentActivity={currentActivity}
91
+ onToggleActivity={setCurrentActivity}
92
+ onToggleDevice={toggleDevice}
93
+ device={device}
94
+ mobileTab={mobileTab}
95
+ onToggleMobileTab={setMobileTab}
96
+ isNew={isNew}
97
+ isHistoryView={isHistoryView}
98
+ />
99
+ <main className="flex-1! flex! items-center! pb-3! gap-3! px-3!">
100
+ <div
101
+ className={cn(
102
+ "w-1/3 flex flex-col justify-between h-full relative",
103
+ mobileTab !== "left-sidebar" ? "max-lg:hidden" : "max-lg:w-full!"
104
+ )}
105
+ >
106
+ {currentActivity === "chat" ? (
107
+ <div className="justify-between flex flex-col h-[calc(100vh-70px)] grow">
108
+ <AppEditorChat
109
+ isNew={isNew}
110
+ projectName={projectName}
111
+ onSelectFile={() => {
112
+ setCurrentActivity("code");
113
+ }}
114
+ />
115
+ <div className="">
116
+ <div className="lg:hidden flex items-center justify-end pb-2">
117
+ <Button
118
+ variant="bordered"
119
+ size="xs"
120
+ onClick={() => setMobileTab("right-sidebar")}
121
+ >
122
+ Go to Preview <ArrowRight className="size-3" />
123
+ </Button>
124
+ </div>
125
+ <AskAI
126
+ isHistoryView={isHistoryView}
127
+ isNew={isNew}
128
+ projectName={projectName}
129
+ initialPrompt={initialPrompt}
130
+ files={files}
131
+ onToggleMobileTab={setMobileTab}
132
+ />
133
+ </div>
134
+ </div>
135
+ ) : (
136
+ <AppEditorCode />
137
+ )}
138
+ {isHistoryView && <HistoryView />}
139
+ </div>
140
+ <AppViewer
141
+ mobileTab={mobileTab}
142
+ onToggleMobileTab={setMobileTab}
143
+ device={device}
144
+ />
145
+ </main>
146
+ </SandpackProvider>
147
+ );
148
+ }
components/editor/night-light.json ADDED
@@ -0,0 +1,1728 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Night Owl Light",
3
+ "type": "light",
4
+ "semanticHighlighting": true,
5
+ "colors": {
6
+ "foreground": "#403f53",
7
+ "focusBorder": "#93A1A1",
8
+ "errorForeground": "#403f53",
9
+ "selection.background": "#7a8181ad",
10
+ "descriptionForeground": "#403f53",
11
+ "widget.shadow": "#d9d9d9",
12
+ "titleBar.activeBackground": "#F0F0F0",
13
+ "notifications.background": "#F0F0F0",
14
+ "notifications.foreground": "#403f53",
15
+ "notificationLink.foreground": "#994cc3",
16
+ "notifications.border": "#CCCCCC",
17
+ "notificationCenter.border": "#CCCCCC",
18
+ "notificationToast.border": "#CCCCCC",
19
+ "notificationCenterHeader.foreground": "#403f53",
20
+ "notificationCenterHeader.background": "#F0F0F0",
21
+ "button.background": "#2AA298",
22
+ "button.foreground": "#F0F0F0",
23
+ "dropdown.background": "#F0F0F0",
24
+ "dropdown.foreground": "#403f53",
25
+ "dropdown.border": "#d9d9d9",
26
+ "input.background": "#F0F0F0",
27
+ "input.foreground": "#403f53",
28
+ "input.border": "#d9d9d9",
29
+ "input.placeholderForeground": "#93A1A1",
30
+ "inputOption.activeBorder": "#2AA298",
31
+ "inputValidation.infoBorder": "#D0D0D0",
32
+ "inputValidation.infoBackground": "#F0F0F0",
33
+ "inputValidation.warningBackground": "#daaa01",
34
+ "inputValidation.warningBorder": "#E0AF02",
35
+ "inputValidation.errorBackground": "#f76e6e",
36
+ "inputValidation.errorBorder": "#de3d3b",
37
+ "badge.background": "#2AA298",
38
+ "badge.foreground": "#F0F0F0",
39
+ "progressBar.background": "#2AA298",
40
+ "list.activeSelectionBackground": "#d3e8f8",
41
+ "list.activeSelectionForeground": "#403f53",
42
+ "list.inactiveSelectionBackground": "#E0E7EA",
43
+ "list.inactiveSelectionForeground": "#403f53",
44
+ "list.focusBackground": "#d3e8f8",
45
+ "list.hoverBackground": "#d3e8f8",
46
+ "list.focusForeground": "#403f53",
47
+ "list.hoverForeground": "#403f53",
48
+ "list.highlightForeground": "#403f53",
49
+ "list.errorForeground": "#E64D49",
50
+ "list.warningForeground": "#daaa01",
51
+ "activityBar.background": "#F0F0F0",
52
+ "activityBar.foreground": "#403f53",
53
+ "activityBar.dropBackground": "#D0D0D0",
54
+ "activityBarBadge.background": "#403f53",
55
+ "activityBarBadge.foreground": "#F0F0F0",
56
+ "activityBar.border": "#F0F0F0",
57
+ "sideBar.background": "#F0F0F0",
58
+ "sideBar.foreground": "#403f53",
59
+ "sideBarTitle.foreground": "#403f53",
60
+ "sideBar.border": "#F0F0F0",
61
+ "scrollbar.shadow": "#CCCCCC",
62
+ "tab.border": "#F0F0F0",
63
+ "tab.activeBackground": "#F6F6F6",
64
+ "tab.activeForeground": "#403f53",
65
+ "tab.inactiveForeground": "#403f53",
66
+ "tab.inactiveBackground": "#F0F0F0",
67
+ "editorGroup.border": "#F0F0F0",
68
+ "editorGroup.background": "#F6F6F6",
69
+ "editorGroupHeader.tabsBackground": "#F0F0F0",
70
+ "editorGroupHeader.tabsBorder": "#F0F0F0",
71
+ "editorGroupHeader.noTabsBackground": "#F0F0F0",
72
+ "tab.activeModifiedBorder": "#2AA298",
73
+ "tab.inactiveModifiedBorder": "#93A1A1",
74
+ "tab.unfocusedActiveModifiedBorder": "#93A1A1",
75
+ "tab.unfocusedInactiveModifiedBorder": "#93A1A1",
76
+ "editor.background": "#FBFBFB",
77
+ "editor.foreground": "#403f53",
78
+ "editorCursor.foreground": "#90A7B2",
79
+ "editorLineNumber.foreground": "#90A7B2",
80
+ "editorLineNumber.activeForeground": "#403f53",
81
+ "editor.selectionBackground": "#E0E0E0",
82
+ "editor.selectionHighlightBackground": "#339cec33",
83
+ "editor.wordHighlightBackground": "#339cec33",
84
+ "editor.wordHighlightStrongBackground": "#007dd659",
85
+ "editor.findMatchBackground": "#93A1A16c",
86
+ "editor.findMatchHighlightBackground": "#93a1a16c",
87
+ "editor.findRangeHighlightBackground": "#7497a633",
88
+ "editor.hoverHighlightBackground": "#339cec33",
89
+ "editor.lineHighlightBackground": "#F0F0F0",
90
+ "editor.rangeHighlightBackground": "#000000",
91
+ "editorWhitespace.foreground": "#d9d9d9",
92
+ "editorIndentGuide.background": "#d9d9d9",
93
+ "editorCodeLens.foreground": "#403f53",
94
+ "editorError.foreground": "#E64D49",
95
+ "editorError.border": "#FBFBFB",
96
+ "editorWarning.foreground": "#daaa01",
97
+ "editorWarning.border": "#daaa01",
98
+ "editorGutter.addedBackground": "#49d0c5",
99
+ "editorGutter.modifiedBackground": "#6fbef6",
100
+ "editorGutter.deletedBackground": "#f76e6e",
101
+ "editorRuler.foreground": "#d9d9d9",
102
+ "editorOverviewRuler.errorForeground": "#E64D49",
103
+ "editorOverviewRuler.warningForeground": "#daaa01",
104
+ "editorInlayHint.background": "#F0F0F0",
105
+ "editorInlayHint.foreground": "#403f53",
106
+ "editorWidget.background": "#F0F0F0",
107
+ "editorWidget.border": "#d9d9d9",
108
+ "editorSuggestWidget.background": "#F0F0F0",
109
+ "editorSuggestWidget.foreground": "#403f53",
110
+ "editorSuggestWidget.highlightForeground": "#403f53",
111
+ "editorSuggestWidget.selectedBackground": "#d3e8f8",
112
+ "editorSuggestWidget.border": "#d9d9d9",
113
+ "editorHoverWidget.background": "#F0F0F0",
114
+ "editorHoverWidget.border": "#d9d9d9",
115
+ "debugExceptionWidget.background": "#F0F0F0",
116
+ "debugExceptionWidget.border": "#d9d9d9",
117
+ "editorMarkerNavigation.background": "#D0D0D0",
118
+ "editorMarkerNavigationError.background": "#f76e6e",
119
+ "editorMarkerNavigationWarning.background": "#daaa01",
120
+ "debugToolBar.background": "#F0F0F0",
121
+ "pickerGroup.border": "#d9d9d9",
122
+ "pickerGroup.foreground": "#403f53",
123
+ "extensionButton.prominentBackground": "#2AA298",
124
+ "extensionButton.prominentForeground": "#F0F0F0",
125
+ "statusBar.background": "#F0F0F0",
126
+ "statusBar.border": "#F0F0F0",
127
+ "statusBar.debuggingBackground": "#F0F0F0",
128
+ "statusBar.debuggingForeground": "#403f53",
129
+ "statusBar.foreground": "#403f53",
130
+ "statusBar.noFolderBackground": "#F0F0F0",
131
+ "statusBar.noFolderForeground": "#403f53",
132
+ "panel.background": "#F0F0F0",
133
+ "panel.border": "#d9d9d9",
134
+ "peekView.border": "#d9d9d9",
135
+ "peekViewEditor.background": "#F6F6F6",
136
+ "peekViewEditorGutter.background": "#F6F6F6",
137
+ "peekViewEditor.matchHighlightBackground": "#49d0c5",
138
+ "peekViewResult.background": "#F0F0F0",
139
+ "peekViewResult.fileForeground": "#403f53",
140
+ "peekViewResult.lineForeground": "#403f53",
141
+ "peekViewResult.matchHighlightBackground": "#49d0c5",
142
+ "peekViewResult.selectionBackground": "#E0E7EA",
143
+ "peekViewResult.selectionForeground": "#403f53",
144
+ "peekViewTitle.background": "#F0F0F0",
145
+ "peekViewTitleLabel.foreground": "#403f53",
146
+ "peekViewTitleDescription.foreground": "#403f53",
147
+ "terminal.ansiBrightBlack": "#403f53",
148
+ "terminal.ansiBlack": "#403f53",
149
+ "terminal.ansiBrightBlue": "#288ed7",
150
+ "terminal.ansiBlue": "#288ed7",
151
+ "terminal.ansiBrightCyan": "#2AA298",
152
+ "terminal.ansiCyan": "#2AA298",
153
+ "terminal.ansiBrightGreen": "#08916a",
154
+ "terminal.ansiGreen": "#08916a",
155
+ "terminal.ansiBrightMagenta": "#d6438a",
156
+ "terminal.ansiMagenta": "#d6438a",
157
+ "terminal.ansiBrightRed": "#de3d3b",
158
+ "terminal.ansiRed": "#de3d3b",
159
+ "terminal.ansiBrightWhite": "#93A1A1",
160
+ "terminal.ansiWhite": "#93A1A1",
161
+ "terminal.ansiBrightYellow": "#daaa01",
162
+ "terminal.ansiYellow": "#E0AF02",
163
+ "terminal.background": "#F6F6F6",
164
+ "terminal.foreground": "#403f53"
165
+ },
166
+ "tokenColors": [
167
+ {
168
+ "name": "Changed",
169
+ "scope": [
170
+ "markup.changed",
171
+ "meta.diff.header.git",
172
+ "meta.diff.header.from-file",
173
+ "meta.diff.header.to-file"
174
+ ],
175
+ "settings": {
176
+ "foreground": "#a2bffc",
177
+ "fontStyle": "italic"
178
+ }
179
+ },
180
+ {
181
+ "name": "Deleted",
182
+ "scope": "markup.deleted.diff",
183
+ "settings": {
184
+ "foreground": "#EF535090",
185
+ "fontStyle": "italic"
186
+ }
187
+ },
188
+ {
189
+ "name": "Inserted",
190
+ "scope": "markup.inserted.diff",
191
+ "settings": {
192
+ "foreground": "#4876d6ff",
193
+ "fontStyle": "italic"
194
+ }
195
+ },
196
+ {
197
+ "name": "Global settings",
198
+ "settings": {
199
+ "background": "#011627",
200
+ "foreground": "#403f53"
201
+ }
202
+ },
203
+ {
204
+ "name": "Comment",
205
+ "scope": ["comment", "punctuation.definition.comment"],
206
+ "settings": {
207
+ "foreground": "#989fb1",
208
+ "fontStyle": "italic"
209
+ }
210
+ },
211
+ {
212
+ "name": "String",
213
+ "scope": "string",
214
+ "settings": {
215
+ "foreground": "#4876d6"
216
+ }
217
+ },
218
+ {
219
+ "name": "String Quoted",
220
+ "scope": ["string.quoted", "variable.other.readwrite.js"],
221
+ "settings": {
222
+ "foreground": "#c96765"
223
+ }
224
+ },
225
+ {
226
+ "name": "Support Constant Math",
227
+ "scope": "support.constant.math",
228
+ "settings": {
229
+ "foreground": "#4876d6"
230
+ }
231
+ },
232
+ {
233
+ "name": "Number",
234
+ "scope": ["constant.numeric", "constant.character.numeric"],
235
+ "settings": {
236
+ "foreground": "#aa0982",
237
+ "fontStyle": ""
238
+ }
239
+ },
240
+ {
241
+ "name": "Built-in constant",
242
+ "scope": [
243
+ "constant.language",
244
+ "punctuation.definition.constant",
245
+ "variable.other.constant"
246
+ ],
247
+ "settings": {
248
+ "foreground": "#4876d6"
249
+ }
250
+ },
251
+ {
252
+ "name": "User-defined constant",
253
+ "scope": ["constant.character", "constant.other"],
254
+ "settings": {
255
+ "foreground": "#4876d6"
256
+ }
257
+ },
258
+ {
259
+ "name": "Constant Character Escape",
260
+ "scope": "constant.character.escape",
261
+ "settings": {
262
+ "foreground": "#aa0982"
263
+ }
264
+ },
265
+ {
266
+ "name": "RegExp String",
267
+ "scope": ["string.regexp", "string.regexp keyword.other"],
268
+ "settings": {
269
+ "foreground": "#5ca7e4"
270
+ }
271
+ },
272
+ {
273
+ "name": "Comma in functions",
274
+ "scope": "meta.function punctuation.separator.comma",
275
+ "settings": {
276
+ "foreground": "#5f7e97"
277
+ }
278
+ },
279
+ {
280
+ "name": "Variable",
281
+ "scope": "variable",
282
+ "settings": {
283
+ "foreground": "#4876d6"
284
+ }
285
+ },
286
+ {
287
+ "name": "Keyword",
288
+ "scope": ["punctuation.accessor", "keyword"],
289
+ "settings": {
290
+ "foreground": "#994cc3",
291
+ "fontStyle": "italic"
292
+ }
293
+ },
294
+ {
295
+ "name": "Storage",
296
+ "scope": [
297
+ "storage",
298
+ "meta.var.expr",
299
+ "meta.class meta.method.declaration meta.var.expr storage.type.js",
300
+ "storage.type.property.js",
301
+ "storage.type.property.ts",
302
+ "storage.type.property.tsx"
303
+ ],
304
+ "settings": {
305
+ "foreground": "#994cc3",
306
+ "fontStyle": "italic"
307
+ }
308
+ },
309
+ {
310
+ "name": "Storage type",
311
+ "scope": "storage.type",
312
+ "settings": {
313
+ "foreground": "#994cc3"
314
+ }
315
+ },
316
+ {
317
+ "name": "Storage type",
318
+ "scope": "storage.type.function.arrow.js",
319
+ "settings": {
320
+ "fontStyle": ""
321
+ }
322
+ },
323
+ {
324
+ "name": "Class name",
325
+ "scope": ["entity.name.class", "meta.class entity.name.type.class"],
326
+ "settings": {
327
+ "foreground": "#111111"
328
+ }
329
+ },
330
+ {
331
+ "name": "Inherited class",
332
+ "scope": "entity.other.inherited-class",
333
+ "settings": {
334
+ "foreground": "#4876d6"
335
+ }
336
+ },
337
+ {
338
+ "name": "Function name",
339
+ "scope": "entity.name.function",
340
+ "settings": {
341
+ "foreground": "#994cc3",
342
+ "fontStyle": "italic"
343
+ }
344
+ },
345
+ {
346
+ "name": "Meta Tag",
347
+ "scope": ["punctuation.definition.tag", "meta.tag"],
348
+ "settings": {
349
+ "foreground": "#994cc3"
350
+ }
351
+ },
352
+ {
353
+ "name": "HTML Tag names",
354
+ "scope": [
355
+ "entity.name.tag",
356
+ "meta.tag.other.html",
357
+ "meta.tag.other.js",
358
+ "meta.tag.other.tsx",
359
+ "entity.name.tag.tsx",
360
+ "entity.name.tag.js",
361
+ "entity.name.tag",
362
+ "meta.tag.js",
363
+ "meta.tag.tsx",
364
+ "meta.tag.html"
365
+ ],
366
+ "settings": {
367
+ "foreground": "#994cc3",
368
+ "fontStyle": ""
369
+ }
370
+ },
371
+ {
372
+ "name": "Tag attribute",
373
+ "scope": "entity.other.attribute-name",
374
+ "settings": {
375
+ "fontStyle": "italic",
376
+ "foreground": "#4876d6"
377
+ }
378
+ },
379
+ {
380
+ "name": "Entity Name Tag Custom",
381
+ "scope": "entity.name.tag.custom",
382
+ "settings": {
383
+ "foreground": "#4876d6"
384
+ }
385
+ },
386
+ {
387
+ "name": "Library (function & constant)",
388
+ "scope": ["support.function", "support.constant"],
389
+ "settings": {
390
+ "foreground": "#4876d6"
391
+ }
392
+ },
393
+ {
394
+ "name": "Support Constant Property Value meta",
395
+ "scope": "support.constant.meta.property-value",
396
+ "settings": {
397
+ "foreground": "#0c969b"
398
+ }
399
+ },
400
+ {
401
+ "name": "Library class/type",
402
+ "scope": ["support.type", "support.class"],
403
+ "settings": {
404
+ "foreground": "#4876d6"
405
+ }
406
+ },
407
+ {
408
+ "name": "Support Variable DOM",
409
+ "scope": "support.variable.dom",
410
+ "settings": {
411
+ "foreground": "#4876d6"
412
+ }
413
+ },
414
+ {
415
+ "name": "Invalid",
416
+ "scope": "invalid",
417
+ "settings": {
418
+ "foreground": "#ff2c83"
419
+ }
420
+ },
421
+ {
422
+ "name": "Invalid deprecated",
423
+ "scope": "invalid.deprecated",
424
+ "settings": {
425
+ "foreground": "#d3423e"
426
+ }
427
+ },
428
+ {
429
+ "name": "Keyword Operator",
430
+ "scope": "keyword.operator",
431
+ "settings": {
432
+ "foreground": "#0c969b",
433
+ "fontStyle": ""
434
+ }
435
+ },
436
+ {
437
+ "name": "Keyword Operator Relational",
438
+ "scope": "keyword.operator.relational",
439
+ "settings": {
440
+ "foreground": "#994cc3",
441
+ "fontStyle": "italic"
442
+ }
443
+ },
444
+ {
445
+ "name": "Keyword Operator Assignment",
446
+ "scope": "keyword.operator.assignment",
447
+ "settings": {
448
+ "foreground": "#994cc3"
449
+ }
450
+ },
451
+ {
452
+ "name": "Keyword Operator Arithmetic",
453
+ "scope": "keyword.operator.arithmetic",
454
+ "settings": {
455
+ "foreground": "#994cc3"
456
+ }
457
+ },
458
+ {
459
+ "name": "Keyword Operator Bitwise",
460
+ "scope": "keyword.operator.bitwise",
461
+ "settings": {
462
+ "foreground": "#994cc3"
463
+ }
464
+ },
465
+ {
466
+ "name": "Keyword Operator Increment",
467
+ "scope": "keyword.operator.increment",
468
+ "settings": {
469
+ "foreground": "#994cc3"
470
+ }
471
+ },
472
+ {
473
+ "name": "Keyword Operator Ternary",
474
+ "scope": "keyword.operator.ternary",
475
+ "settings": {
476
+ "foreground": "#994cc3"
477
+ }
478
+ },
479
+ {
480
+ "name": "Double-Slashed Comment",
481
+ "scope": "comment.line.double-slash",
482
+ "settings": {
483
+ "foreground": "#939dbb"
484
+ }
485
+ },
486
+ {
487
+ "name": "Object",
488
+ "scope": "object",
489
+ "settings": {
490
+ "foreground": "#cdebf7"
491
+ }
492
+ },
493
+ {
494
+ "name": "Null",
495
+ "scope": "constant.language.null",
496
+ "settings": {
497
+ "foreground": "#bc5454"
498
+ }
499
+ },
500
+ {
501
+ "name": "Meta Brace",
502
+ "scope": "meta.brace",
503
+ "settings": {
504
+ "foreground": "#403f53"
505
+ }
506
+ },
507
+ {
508
+ "name": "Meta Delimiter Period",
509
+ "scope": "meta.delimiter.period",
510
+ "settings": {
511
+ "foreground": "#994cc3",
512
+ "fontStyle": "italic"
513
+ }
514
+ },
515
+ {
516
+ "name": "Punctuation Definition String",
517
+ "scope": "punctuation.definition.string",
518
+ "settings": {
519
+ "foreground": "#111111"
520
+ }
521
+ },
522
+ {
523
+ "name": "Punctuation Definition String Markdown",
524
+ "scope": "punctuation.definition.string.begin.markdown",
525
+ "settings": {
526
+ "foreground": "#bc5454"
527
+ }
528
+ },
529
+ {
530
+ "name": "Boolean",
531
+ "scope": "constant.language.boolean",
532
+ "settings": {
533
+ "foreground": "#bc5454"
534
+ }
535
+ },
536
+ {
537
+ "name": "Object Comma",
538
+ "scope": "object.comma",
539
+ "settings": {
540
+ "foreground": "#ffffff"
541
+ }
542
+ },
543
+ {
544
+ "name": "Variable Parameter Function",
545
+ "scope": "variable.parameter.function",
546
+ "settings": {
547
+ "foreground": "#0c969b",
548
+ "fontStyle": ""
549
+ }
550
+ },
551
+ {
552
+ "name": "Support Type Property Name & entity name tags",
553
+ "scope": [
554
+ "support.type.vendor.property-name",
555
+ "support.constant.vendor.property-value",
556
+ "support.type.property-name",
557
+ "meta.property-list entity.name.tag"
558
+ ],
559
+ "settings": {
560
+ "foreground": "#0c969b",
561
+ "fontStyle": ""
562
+ }
563
+ },
564
+ {
565
+ "name": "Entity Name tag reference in stylesheets",
566
+ "scope": "meta.property-list entity.name.tag.reference",
567
+ "settings": {
568
+ "foreground": "#57eaf1"
569
+ }
570
+ },
571
+ {
572
+ "name": "Constant Other Color RGB Value Punctuation Definition Constant",
573
+ "scope": "constant.other.color.rgb-value punctuation.definition.constant",
574
+ "settings": {
575
+ "foreground": "#aa0982"
576
+ }
577
+ },
578
+ {
579
+ "name": "Constant Other Color",
580
+ "scope": "constant.other.color",
581
+ "settings": {
582
+ "foreground": "#aa0982"
583
+ }
584
+ },
585
+ {
586
+ "name": "Keyword Other Unit",
587
+ "scope": "keyword.other.unit",
588
+ "settings": {
589
+ "foreground": "#aa0982"
590
+ }
591
+ },
592
+ {
593
+ "name": "Meta Selector",
594
+ "scope": "meta.selector",
595
+ "settings": {
596
+ "foreground": "#994cc3",
597
+ "fontStyle": "italic"
598
+ }
599
+ },
600
+ {
601
+ "name": "Entity Other Attribute Name Id",
602
+ "scope": "entity.other.attribute-name.id",
603
+ "settings": {
604
+ "foreground": "#aa0982"
605
+ }
606
+ },
607
+ {
608
+ "name": "Meta Property Name",
609
+ "scope": "meta.property-name",
610
+ "settings": {
611
+ "foreground": "#0c969b"
612
+ }
613
+ },
614
+ {
615
+ "name": "Doctypes",
616
+ "scope": ["entity.name.tag.doctype", "meta.tag.sgml.doctype"],
617
+ "settings": {
618
+ "foreground": "#994cc3",
619
+ "fontStyle": "italic"
620
+ }
621
+ },
622
+ {
623
+ "name": "Punctuation Definition Parameters",
624
+ "scope": "punctuation.definition.parameters",
625
+ "settings": {
626
+ "foreground": "#111111"
627
+ }
628
+ },
629
+ {
630
+ "name": "Keyword Control Operator",
631
+ "scope": "keyword.control.operator",
632
+ "settings": {
633
+ "foreground": "#0c969b"
634
+ }
635
+ },
636
+ {
637
+ "name": "Keyword Operator Logical",
638
+ "scope": "keyword.operator.logical",
639
+ "settings": {
640
+ "foreground": "#994cc3",
641
+ "fontStyle": ""
642
+ }
643
+ },
644
+ {
645
+ "name": "Variable Instances",
646
+ "scope": [
647
+ "variable.instance",
648
+ "variable.other.instance",
649
+ "variable.readwrite.instance",
650
+ "variable.other.readwrite.instance",
651
+ "variable.other.property"
652
+ ],
653
+ "settings": {
654
+ "foreground": "#0c969b"
655
+ }
656
+ },
657
+ {
658
+ "name": "Variable Property Other object property",
659
+ "scope": ["variable.other.object.property"],
660
+ "settings": {
661
+ "foreground": "#111111",
662
+ "fontStyle": "italic"
663
+ }
664
+ },
665
+ {
666
+ "name": "Variable Property Other object",
667
+ "scope": ["variable.other.object.js"],
668
+ "settings": {
669
+ "fontStyle": ""
670
+ }
671
+ },
672
+ {
673
+ "name": "Entity Name Function",
674
+ "scope": ["entity.name.function"],
675
+ "settings": {
676
+ "foreground": "#4876d6",
677
+ "fontStyle": "italic"
678
+ }
679
+ },
680
+ {
681
+ "name": "Keyword Operator Comparison, imports, returns and Keyword Operator Ruby",
682
+ "scope": [
683
+ "keyword.operator.comparison",
684
+ "keyword.control.flow.js",
685
+ "keyword.control.flow.ts",
686
+ "keyword.control.flow.tsx",
687
+ "keyword.control.ruby",
688
+ "keyword.control.module.ruby",
689
+ "keyword.control.class.ruby",
690
+ "keyword.control.def.ruby",
691
+ "keyword.control.loop.js",
692
+ "keyword.control.loop.ts",
693
+ "keyword.control.import.js",
694
+ "keyword.control.import.ts",
695
+ "keyword.control.import.tsx",
696
+ "keyword.control.from.js",
697
+ "keyword.control.from.ts",
698
+ "keyword.control.from.tsx",
699
+ "keyword.operator.instanceof.js",
700
+ "keyword.operator.expression.instanceof.ts",
701
+ "keyword.operator.expression.instanceof.tsx"
702
+ ],
703
+ "settings": {
704
+ "foreground": "#994cc3",
705
+ "fontStyle": "italic"
706
+ }
707
+ },
708
+ {
709
+ "name": "Keyword Control Conditional",
710
+ "scope": [
711
+ "keyword.control.conditional.js",
712
+ "keyword.control.conditional.ts",
713
+ "keyword.control.switch.js",
714
+ "keyword.control.switch.ts"
715
+ ],
716
+ "settings": {
717
+ "foreground": "#994cc3",
718
+ "fontStyle": ""
719
+ }
720
+ },
721
+ {
722
+ "name": "Support Constant, `new` keyword, Special Method Keyword, `debugger`, other keywords",
723
+ "scope": [
724
+ "support.constant",
725
+ "keyword.other.special-method",
726
+ "keyword.other.new",
727
+ "keyword.other.debugger",
728
+ "keyword.control"
729
+ ],
730
+ "settings": {
731
+ "foreground": "#0c969b"
732
+ }
733
+ },
734
+ {
735
+ "name": "Support Function",
736
+ "scope": "support.function",
737
+ "settings": {
738
+ "foreground": "#4876d6"
739
+ }
740
+ },
741
+ {
742
+ "name": "Invalid Broken",
743
+ "scope": "invalid.broken",
744
+ "settings": {
745
+ "foreground": "#aa0982"
746
+ }
747
+ },
748
+ {
749
+ "name": "Invalid Unimplemented",
750
+ "scope": "invalid.unimplemented",
751
+ "settings": {
752
+ "foreground": "#8BD649"
753
+ }
754
+ },
755
+ {
756
+ "name": "Invalid Illegal",
757
+ "scope": "invalid.illegal",
758
+ "settings": {
759
+ "foreground": "#c96765"
760
+ }
761
+ },
762
+ {
763
+ "name": "Language Variable",
764
+ "scope": "variable.language",
765
+ "settings": {
766
+ "foreground": "#0c969b"
767
+ }
768
+ },
769
+ {
770
+ "name": "Support Variable Property",
771
+ "scope": "support.variable.property",
772
+ "settings": {
773
+ "foreground": "#0c969b"
774
+ }
775
+ },
776
+ {
777
+ "name": "Variable Function",
778
+ "scope": "variable.function",
779
+ "settings": {
780
+ "foreground": "#4876d6"
781
+ }
782
+ },
783
+ {
784
+ "name": "Variable Interpolation",
785
+ "scope": "variable.interpolation",
786
+ "settings": {
787
+ "foreground": "#ec5f67"
788
+ }
789
+ },
790
+ {
791
+ "name": "Meta Function Call",
792
+ "scope": "meta.function-call",
793
+ "settings": {
794
+ "foreground": "#4876d6"
795
+ }
796
+ },
797
+ {
798
+ "name": "Punctuation Section Embedded",
799
+ "scope": "punctuation.section.embedded",
800
+ "settings": {
801
+ "foreground": "#d3423e"
802
+ }
803
+ },
804
+ {
805
+ "name": "Punctuation Tweaks",
806
+ "scope": [
807
+ "punctuation.terminator.expression",
808
+ "punctuation.definition.arguments",
809
+ "punctuation.definition.array",
810
+ "punctuation.section.array",
811
+ "meta.array"
812
+ ],
813
+ "settings": {
814
+ "foreground": "#403f53"
815
+ }
816
+ },
817
+ {
818
+ "name": "More Punctuation Tweaks",
819
+ "scope": [
820
+ "punctuation.definition.list.begin",
821
+ "punctuation.definition.list.end",
822
+ "punctuation.separator.arguments",
823
+ "punctuation.definition.list"
824
+ ],
825
+ "settings": {
826
+ "foreground": "#111111"
827
+ }
828
+ },
829
+ {
830
+ "name": "Template Strings",
831
+ "scope": "string.template meta.template.expression",
832
+ "settings": {
833
+ "foreground": "#d3423e"
834
+ }
835
+ },
836
+ {
837
+ "name": "Backtics(``) in Template Strings",
838
+ "scope": "string.template punctuation.definition.string",
839
+ "settings": {
840
+ "foreground": "#403f53"
841
+ }
842
+ },
843
+ {
844
+ "name": "Italics",
845
+ "scope": "italic",
846
+ "settings": {
847
+ "foreground": "#994cc3",
848
+ "fontStyle": "italic"
849
+ }
850
+ },
851
+ {
852
+ "name": "Bold",
853
+ "scope": "bold",
854
+ "settings": {
855
+ "foreground": "#4876d6",
856
+ "fontStyle": "bold"
857
+ }
858
+ },
859
+ {
860
+ "name": "Quote",
861
+ "scope": "quote",
862
+ "settings": {
863
+ "foreground": "#697098",
864
+ "fontStyle": "italic"
865
+ }
866
+ },
867
+ {
868
+ "name": "Raw Code",
869
+ "scope": "raw",
870
+ "settings": {
871
+ "foreground": "#0c969b"
872
+ }
873
+ },
874
+ {
875
+ "name": "CoffeScript Variable Assignment",
876
+ "scope": "variable.assignment.coffee",
877
+ "settings": {
878
+ "foreground": "#31e1eb"
879
+ }
880
+ },
881
+ {
882
+ "name": "CoffeScript Parameter Function",
883
+ "scope": "variable.parameter.function.coffee",
884
+ "settings": {
885
+ "foreground": "#403f53"
886
+ }
887
+ },
888
+ {
889
+ "name": "CoffeeScript Assignments",
890
+ "scope": "variable.assignment.coffee",
891
+ "settings": {
892
+ "foreground": "#0c969b"
893
+ }
894
+ },
895
+ {
896
+ "name": "C# Readwrite Variables",
897
+ "scope": "variable.other.readwrite.cs",
898
+ "settings": {
899
+ "foreground": "#403f53"
900
+ }
901
+ },
902
+ {
903
+ "name": "C# Classes & Storage types",
904
+ "scope": ["entity.name.type.class.cs", "storage.type.cs"],
905
+ "settings": {
906
+ "foreground": "#4876d6"
907
+ }
908
+ },
909
+ {
910
+ "name": "C# Namespaces",
911
+ "scope": "entity.name.type.namespace.cs",
912
+ "settings": {
913
+ "foreground": "#0c969b"
914
+ }
915
+ },
916
+ {
917
+ "name": "Tag names in Stylesheets",
918
+ "scope": [
919
+ "entity.name.tag.css",
920
+ "entity.name.tag.less",
921
+ "entity.name.tag.custom.css",
922
+ "support.constant.property-value.css"
923
+ ],
924
+ "settings": {
925
+ "foreground": "#c96765",
926
+ "fontStyle": ""
927
+ }
928
+ },
929
+ {
930
+ "name": "Wildcard(*) selector in Stylesheets",
931
+ "scope": [
932
+ "entity.name.tag.wildcard.css",
933
+ "entity.name.tag.wildcard.less",
934
+ "entity.name.tag.wildcard.scss",
935
+ "entity.name.tag.wildcard.sass"
936
+ ],
937
+ "settings": {
938
+ "foreground": "#0c969b"
939
+ }
940
+ },
941
+ {
942
+ "name": "CSS Keyword Other Unit",
943
+ "scope": "keyword.other.unit.css",
944
+ "settings": {
945
+ "foreground": "#4876d6"
946
+ }
947
+ },
948
+ {
949
+ "name": "Attribute Name for CSS",
950
+ "scope": [
951
+ "meta.attribute-selector.css entity.other.attribute-name.attribute",
952
+ "variable.other.readwrite.js"
953
+ ],
954
+ "settings": {
955
+ "foreground": "#aa0982"
956
+ }
957
+ },
958
+ {
959
+ "name": "Elixir Classes",
960
+ "scope": [
961
+ "source.elixir support.type.elixir",
962
+ "source.elixir meta.module.elixir entity.name.class.elixir"
963
+ ],
964
+ "settings": {
965
+ "foreground": "#4876d6"
966
+ }
967
+ },
968
+ {
969
+ "name": "Elixir Functions",
970
+ "scope": "source.elixir entity.name.function",
971
+ "settings": {
972
+ "foreground": "#4876d6"
973
+ }
974
+ },
975
+ {
976
+ "name": "Elixir Constants",
977
+ "scope": [
978
+ "source.elixir constant.other.symbol.elixir",
979
+ "source.elixir constant.other.keywords.elixir"
980
+ ],
981
+ "settings": {
982
+ "foreground": "#4876d6"
983
+ }
984
+ },
985
+ {
986
+ "name": "Elixir String Punctuations",
987
+ "scope": "source.elixir punctuation.definition.string",
988
+ "settings": {
989
+ "foreground": "#4876d6"
990
+ }
991
+ },
992
+ {
993
+ "name": "Elixir",
994
+ "scope": [
995
+ "source.elixir variable.other.readwrite.module.elixir",
996
+ "source.elixir variable.other.readwrite.module.elixir punctuation.definition.variable.elixir"
997
+ ],
998
+ "settings": {
999
+ "foreground": "#4876d6"
1000
+ }
1001
+ },
1002
+ {
1003
+ "name": "Elixir Binary Punctuations",
1004
+ "scope": "source.elixir .punctuation.binary.elixir",
1005
+ "settings": {
1006
+ "foreground": "#994cc3",
1007
+ "fontStyle": "italic"
1008
+ }
1009
+ },
1010
+ {
1011
+ "name": "Closure Constant Keyword",
1012
+ "scope": "constant.keyword.clojure",
1013
+ "settings": {
1014
+ "foreground": "#0c969b"
1015
+ }
1016
+ },
1017
+ {
1018
+ "name": "Go Function Calls",
1019
+ "scope": "source.go meta.function-call.go",
1020
+ "settings": {
1021
+ "foreground": "#0c969b"
1022
+ }
1023
+ },
1024
+ {
1025
+ "name": "Go Keywords",
1026
+ "scope": [
1027
+ "source.go keyword.package.go",
1028
+ "source.go keyword.import.go",
1029
+ "source.go keyword.function.go",
1030
+ "source.go keyword.type.go",
1031
+ "source.go keyword.struct.go",
1032
+ "source.go keyword.interface.go",
1033
+ "source.go keyword.const.go",
1034
+ "source.go keyword.var.go",
1035
+ "source.go keyword.map.go",
1036
+ "source.go keyword.channel.go",
1037
+ "source.go keyword.control.go"
1038
+ ],
1039
+ "settings": {
1040
+ "foreground": "#994cc3",
1041
+ "fontStyle": "italic"
1042
+ }
1043
+ },
1044
+ {
1045
+ "name": "Go Constants e.g. nil, string format (%s, %d, etc.)",
1046
+ "scope": [
1047
+ "source.go constant.language.go",
1048
+ "source.go constant.other.placeholder.go"
1049
+ ],
1050
+ "settings": {
1051
+ "foreground": "#bc5454"
1052
+ }
1053
+ },
1054
+ {
1055
+ "name": "C++ Functions",
1056
+ "scope": [
1057
+ "entity.name.function.preprocessor.cpp",
1058
+ "entity.scope.name.cpp"
1059
+ ],
1060
+ "settings": {
1061
+ "foreground": "#0c969bff"
1062
+ }
1063
+ },
1064
+ {
1065
+ "name": "C++ Meta Namespace",
1066
+ "scope": ["meta.namespace-block.cpp"],
1067
+ "settings": {
1068
+ "foreground": "#111111"
1069
+ }
1070
+ },
1071
+ {
1072
+ "name": "C++ Language Primitive Storage",
1073
+ "scope": ["storage.type.language.primitive.cpp"],
1074
+ "settings": {
1075
+ "foreground": "#bc5454"
1076
+ }
1077
+ },
1078
+ {
1079
+ "name": "C++ Preprocessor Macro",
1080
+ "scope": ["meta.preprocessor.macro.cpp"],
1081
+ "settings": {
1082
+ "foreground": "#403f53"
1083
+ }
1084
+ },
1085
+ {
1086
+ "name": "C++ Variable Parameter",
1087
+ "scope": ["variable.parameter"],
1088
+ "settings": {
1089
+ "foreground": "#111111"
1090
+ }
1091
+ },
1092
+ {
1093
+ "name": "Powershell Variables",
1094
+ "scope": ["variable.other.readwrite.powershell"],
1095
+ "settings": {
1096
+ "foreground": "#4876d6"
1097
+ }
1098
+ },
1099
+ {
1100
+ "name": "Powershell Function",
1101
+ "scope": ["support.function.powershell"],
1102
+ "settings": {
1103
+ "foreground": "#0c969bff"
1104
+ }
1105
+ },
1106
+ {
1107
+ "name": "ID Attribute Name in HTML",
1108
+ "scope": "entity.other.attribute-name.id.html",
1109
+ "settings": {
1110
+ "foreground": "#4876d6"
1111
+ }
1112
+ },
1113
+ {
1114
+ "name": "HTML Punctuation Definition Tag",
1115
+ "scope": "punctuation.definition.tag.html",
1116
+ "settings": {
1117
+ "foreground": "#994cc3"
1118
+ }
1119
+ },
1120
+ {
1121
+ "name": "HTML Doctype",
1122
+ "scope": "meta.tag.sgml.doctype.html",
1123
+ "settings": {
1124
+ "foreground": "#994cc3",
1125
+ "fontStyle": "italic"
1126
+ }
1127
+ },
1128
+ {
1129
+ "name": "JavaScript Classes",
1130
+ "scope": "meta.class entity.name.type.class.js",
1131
+ "settings": {
1132
+ "foreground": "#111111"
1133
+ }
1134
+ },
1135
+ {
1136
+ "name": "JavaScript Method Declaration e.g. `constructor`",
1137
+ "scope": "meta.method.declaration storage.type.js",
1138
+ "settings": {
1139
+ "foreground": "#4876d6"
1140
+ }
1141
+ },
1142
+ {
1143
+ "name": "JavaScript Terminator",
1144
+ "scope": "terminator.js",
1145
+ "settings": {
1146
+ "foreground": "#403f53"
1147
+ }
1148
+ },
1149
+ {
1150
+ "name": "JavaScript Meta Punctuation Definition",
1151
+ "scope": "meta.js punctuation.definition.js",
1152
+ "settings": {
1153
+ "foreground": "#403f53"
1154
+ }
1155
+ },
1156
+ {
1157
+ "name": "Entity Names in Code Documentations",
1158
+ "scope": [
1159
+ "entity.name.type.instance.jsdoc",
1160
+ "entity.name.type.instance.phpdoc"
1161
+ ],
1162
+ "settings": {
1163
+ "foreground": "#5f7e97"
1164
+ }
1165
+ },
1166
+ {
1167
+ "name": "Other Variables in Code Documentations",
1168
+ "scope": ["variable.other.jsdoc", "variable.other.phpdoc"],
1169
+ "settings": {
1170
+ "foreground": "#78ccf0"
1171
+ }
1172
+ },
1173
+ {
1174
+ "name": "JavaScript module imports and exports",
1175
+ "scope": [
1176
+ "variable.other.meta.import.js",
1177
+ "meta.import.js variable.other",
1178
+ "variable.other.meta.export.js",
1179
+ "meta.export.js variable.other"
1180
+ ],
1181
+ "settings": {
1182
+ "foreground": "#403f53"
1183
+ }
1184
+ },
1185
+ {
1186
+ "name": "JavaScript Variable Parameter Function",
1187
+ "scope": "variable.parameter.function.js",
1188
+ "settings": {
1189
+ "foreground": "#7986E7"
1190
+ }
1191
+ },
1192
+ {
1193
+ "name": "JavaScript[React] Variable Other Object",
1194
+ "scope": [
1195
+ "variable.other.object.js",
1196
+ "variable.other.object.jsx",
1197
+ "variable.object.property.js",
1198
+ "variable.object.property.jsx"
1199
+ ],
1200
+ "settings": {
1201
+ "foreground": "#403f53"
1202
+ }
1203
+ },
1204
+ {
1205
+ "name": "JavaScript Variables",
1206
+ "scope": ["variable.js", "variable.other.js"],
1207
+ "settings": {
1208
+ "foreground": "#403f53"
1209
+ }
1210
+ },
1211
+ {
1212
+ "name": "JavaScript Entity Name Type",
1213
+ "scope": ["entity.name.type.js", "entity.name.type.module.js"],
1214
+ "settings": {
1215
+ "foreground": "#111111",
1216
+ "fontStyle": ""
1217
+ }
1218
+ },
1219
+ {
1220
+ "name": "JavaScript Support Classes",
1221
+ "scope": "support.class.js",
1222
+ "settings": {
1223
+ "foreground": "#403f53"
1224
+ }
1225
+ },
1226
+ {
1227
+ "name": "JSON Property Names",
1228
+ "scope": "support.type.property-name.json",
1229
+ "settings": {
1230
+ "foreground": "#0c969b"
1231
+ }
1232
+ },
1233
+ {
1234
+ "name": "JSON Support Constants",
1235
+ "scope": "support.constant.json",
1236
+ "settings": {
1237
+ "foreground": "#4876d6"
1238
+ }
1239
+ },
1240
+ {
1241
+ "name": "JSON Property values (string)",
1242
+ "scope": "meta.structure.dictionary.value.json string.quoted.double",
1243
+ "settings": {
1244
+ "foreground": "#c789d6"
1245
+ }
1246
+ },
1247
+ {
1248
+ "name": "Strings in JSON values",
1249
+ "scope": "string.quoted.double.json punctuation.definition.string.json",
1250
+ "settings": {
1251
+ "foreground": "#0c969b"
1252
+ }
1253
+ },
1254
+ {
1255
+ "name": "Specific JSON Property values like null",
1256
+ "scope": "meta.structure.dictionary.json meta.structure.dictionary.value constant.language",
1257
+ "settings": {
1258
+ "foreground": "#bc5454"
1259
+ }
1260
+ },
1261
+ {
1262
+ "name": "JavaScript Other Variable",
1263
+ "scope": "variable.other.object.js",
1264
+ "settings": {
1265
+ "foreground": "#0c969b",
1266
+ "fontStyle": "italic"
1267
+ }
1268
+ },
1269
+ {
1270
+ "name": "Ruby Variables",
1271
+ "scope": ["variable.other.ruby"],
1272
+ "settings": {
1273
+ "foreground": "#403f53"
1274
+ }
1275
+ },
1276
+ {
1277
+ "name": "Ruby Class",
1278
+ "scope": ["entity.name.type.class.ruby"],
1279
+ "settings": {
1280
+ "foreground": "#c96765"
1281
+ }
1282
+ },
1283
+ {
1284
+ "name": "Ruby Hashkeys",
1285
+ "scope": "constant.language.symbol.hashkey.ruby",
1286
+ "settings": {
1287
+ "foreground": "#0c969b"
1288
+ }
1289
+ },
1290
+ {
1291
+ "name": "Ruby Symbols",
1292
+ "scope": "constant.language.symbol.ruby",
1293
+ "settings": {
1294
+ "foreground": "#0c969b"
1295
+ }
1296
+ },
1297
+ {
1298
+ "name": "LESS Tag names",
1299
+ "scope": "entity.name.tag.less",
1300
+ "settings": {
1301
+ "foreground": "#994cc3"
1302
+ }
1303
+ },
1304
+ {
1305
+ "name": "LESS Keyword Other Unit",
1306
+ "scope": "keyword.other.unit.css",
1307
+ "settings": {
1308
+ "foreground": "#0c969b"
1309
+ }
1310
+ },
1311
+ {
1312
+ "name": "Attribute Name for LESS",
1313
+ "scope": "meta.attribute-selector.less entity.other.attribute-name.attribute",
1314
+ "settings": {
1315
+ "foreground": "#aa0982"
1316
+ }
1317
+ },
1318
+ {
1319
+ "name": "Markdown Headings",
1320
+ "scope": [
1321
+ "markup.heading",
1322
+ "markup.heading.setext.1",
1323
+ "markup.heading.setext.2"
1324
+ ],
1325
+ "settings": {
1326
+ "foreground": "#4876d6"
1327
+ }
1328
+ },
1329
+ {
1330
+ "name": "Markdown Italics",
1331
+ "scope": "markup.italic",
1332
+ "settings": {
1333
+ "foreground": "#994cc3",
1334
+ "fontStyle": "italic"
1335
+ }
1336
+ },
1337
+ {
1338
+ "name": "Markdown Bold",
1339
+ "scope": "markup.bold",
1340
+ "settings": {
1341
+ "foreground": "#4876d6",
1342
+ "fontStyle": "bold"
1343
+ }
1344
+ },
1345
+ {
1346
+ "name": "Markdown Quote + others",
1347
+ "scope": "markup.quote",
1348
+ "settings": {
1349
+ "foreground": "#697098",
1350
+ "fontStyle": "italic"
1351
+ }
1352
+ },
1353
+ {
1354
+ "name": "Markdown Raw Code + others",
1355
+ "scope": "markup.inline.raw",
1356
+ "settings": {
1357
+ "foreground": "#0c969b"
1358
+ }
1359
+ },
1360
+ {
1361
+ "name": "Markdown Links",
1362
+ "scope": [
1363
+ "markup.underline.link",
1364
+ "markup.underline.link.image"
1365
+ ],
1366
+ "settings": {
1367
+ "foreground": "#ff869a"
1368
+ }
1369
+ },
1370
+ {
1371
+ "name": "Markdown Link Title and Description",
1372
+ "scope": [
1373
+ "string.other.link.title.markdown",
1374
+ "string.other.link.description.markdown"
1375
+ ],
1376
+ "settings": {
1377
+ "foreground": "#403f53"
1378
+ }
1379
+ },
1380
+ {
1381
+ "name": "Markdown Punctuation",
1382
+ "scope": [
1383
+ "punctuation.definition.string.markdown",
1384
+ "punctuation.definition.string.begin.markdown",
1385
+ "punctuation.definition.string.end.markdown",
1386
+ "meta.link.inline.markdown punctuation.definition.string"
1387
+ ],
1388
+ "settings": {
1389
+ "foreground": "#4876d6"
1390
+ }
1391
+ },
1392
+ {
1393
+ "name": "Markdown MetaData Punctuation",
1394
+ "scope": ["punctuation.definition.metadata.markdown"],
1395
+ "settings": {
1396
+ "foreground": "#0c969b"
1397
+ }
1398
+ },
1399
+ {
1400
+ "name": "Markdown List Punctuation",
1401
+ "scope": ["beginning.punctuation.definition.list.markdown"],
1402
+ "settings": {
1403
+ "foreground": "#4876d6"
1404
+ }
1405
+ },
1406
+ {
1407
+ "name": "Markdown Inline Raw String",
1408
+ "scope": "markup.inline.raw.string.markdown",
1409
+ "settings": {
1410
+ "foreground": "#4876d6"
1411
+ }
1412
+ },
1413
+ {
1414
+ "name": "PHP Variables",
1415
+ "scope": ["variable.other.php", "variable.other.property.php"],
1416
+ "settings": {
1417
+ "foreground": "#111111"
1418
+ }
1419
+ },
1420
+ {
1421
+ "name": "Support Classes in PHP",
1422
+ "scope": "support.class.php",
1423
+ "settings": {
1424
+ "foreground": "#111111"
1425
+ }
1426
+ },
1427
+ {
1428
+ "name": "Punctuations in PHP function calls",
1429
+ "scope": "meta.function-call.php punctuation",
1430
+ "settings": {
1431
+ "foreground": "#403f53"
1432
+ }
1433
+ },
1434
+ {
1435
+ "name": "PHP Global Variables",
1436
+ "scope": "variable.other.global.php",
1437
+ "settings": {
1438
+ "foreground": "#4876d6"
1439
+ }
1440
+ },
1441
+ {
1442
+ "name": "Declaration Punctuation in PHP Global Variables",
1443
+ "scope": "variable.other.global.php punctuation.definition.variable",
1444
+ "settings": {
1445
+ "foreground": "#4876d6"
1446
+ }
1447
+ },
1448
+ {
1449
+ "name": "Language Constants in Python",
1450
+ "scope": "constant.language.python",
1451
+ "settings": {
1452
+ "foreground": "#bc5454"
1453
+ }
1454
+ },
1455
+ {
1456
+ "name": "Python Function Parameter and Arguments",
1457
+ "scope": [
1458
+ "variable.parameter.function.python",
1459
+ "meta.function-call.arguments.python"
1460
+ ],
1461
+ "settings": {
1462
+ "foreground": "#4876d6"
1463
+ }
1464
+ },
1465
+ {
1466
+ "name": "Python Function Call",
1467
+ "scope": [
1468
+ "meta.function-call.python",
1469
+ "meta.function-call.generic.python"
1470
+ ],
1471
+ "settings": {
1472
+ "foreground": "#0c969b"
1473
+ }
1474
+ },
1475
+ {
1476
+ "name": "Punctuations in Python",
1477
+ "scope": "punctuation.python",
1478
+ "settings": {
1479
+ "foreground": "#403f53"
1480
+ }
1481
+ },
1482
+ {
1483
+ "name": "Decorator Functions in Python",
1484
+ "scope": "entity.name.function.decorator.python",
1485
+ "settings": {
1486
+ "foreground": "#4876d6"
1487
+ }
1488
+ },
1489
+ {
1490
+ "name": "Python Language Variable",
1491
+ "scope": "source.python variable.language.special",
1492
+ "settings": {
1493
+ "foreground": "#aa0982"
1494
+ }
1495
+ },
1496
+ {
1497
+ "name": "Python import control keyword",
1498
+ "scope": "keyword.control",
1499
+ "settings": {
1500
+ "foreground": "#994cc3",
1501
+ "fontStyle": "italic"
1502
+ }
1503
+ },
1504
+ {
1505
+ "name": "SCSS Variable",
1506
+ "scope": [
1507
+ "variable.scss",
1508
+ "variable.sass",
1509
+ "variable.parameter.url.scss",
1510
+ "variable.parameter.url.sass"
1511
+ ],
1512
+ "settings": {
1513
+ "foreground": "#4876d6"
1514
+ }
1515
+ },
1516
+ {
1517
+ "name": "Variables in SASS At-Rules",
1518
+ "scope": [
1519
+ "source.css.scss meta.at-rule variable",
1520
+ "source.css.sass meta.at-rule variable"
1521
+ ],
1522
+ "settings": {
1523
+ "foreground": "#4876d6"
1524
+ }
1525
+ },
1526
+ {
1527
+ "name": "Variables in SASS At-Rules",
1528
+ "scope": [
1529
+ "source.css.scss meta.at-rule variable",
1530
+ "source.css.sass meta.at-rule variable"
1531
+ ],
1532
+ "settings": {
1533
+ "foreground": "#111111"
1534
+ }
1535
+ },
1536
+ {
1537
+ "name": "Attribute Name for SASS",
1538
+ "scope": [
1539
+ "meta.attribute-selector.scss entity.other.attribute-name.attribute",
1540
+ "meta.attribute-selector.sass entity.other.attribute-name.attribute"
1541
+ ],
1542
+ "settings": {
1543
+ "foreground": "#aa0982"
1544
+ }
1545
+ },
1546
+ {
1547
+ "name": "Tag names in SASS",
1548
+ "scope": ["entity.name.tag.scss", "entity.name.tag.sass"],
1549
+ "settings": {
1550
+ "foreground": "#0c969b"
1551
+ }
1552
+ },
1553
+ {
1554
+ "name": "SASS Keyword Other Unit",
1555
+ "scope": ["keyword.other.unit.scss", "keyword.other.unit.sass"],
1556
+ "settings": {
1557
+ "foreground": "#994cc3"
1558
+ }
1559
+ },
1560
+ {
1561
+ "name": "TypeScript[React] Variables and Object Properties",
1562
+ "scope": [
1563
+ "variable.other.readwrite.alias.ts",
1564
+ "variable.other.readwrite.alias.tsx",
1565
+ "variable.other.readwrite.ts",
1566
+ "variable.other.readwrite.tsx",
1567
+ "variable.other.object.ts",
1568
+ "variable.other.object.tsx",
1569
+ "variable.object.property.ts",
1570
+ "variable.object.property.tsx",
1571
+ "variable.other.ts",
1572
+ "variable.other.tsx",
1573
+ "variable.tsx",
1574
+ "variable.ts"
1575
+ ],
1576
+ "settings": {
1577
+ "foreground": "#403f53"
1578
+ }
1579
+ },
1580
+ {
1581
+ "name": "TypeScript[React] Entity Name Types",
1582
+ "scope": ["entity.name.type.ts", "entity.name.type.tsx"],
1583
+ "settings": {
1584
+ "foreground": "#111111"
1585
+ }
1586
+ },
1587
+ {
1588
+ "name": "TypeScript[React] Node Classes",
1589
+ "scope": ["support.class.node.ts", "support.class.node.tsx"],
1590
+ "settings": {
1591
+ "foreground": "#4876d6"
1592
+ }
1593
+ },
1594
+ {
1595
+ "name": "TypeScript[React] Entity Name Types as Parameters",
1596
+ "scope": [
1597
+ "meta.type.parameters.ts entity.name.type",
1598
+ "meta.type.parameters.tsx entity.name.type"
1599
+ ],
1600
+ "settings": {
1601
+ "foreground": "#5f7e97"
1602
+ }
1603
+ },
1604
+ {
1605
+ "name": "TypeScript[React] Import/Export Punctuations",
1606
+ "scope": [
1607
+ "meta.import.ts punctuation.definition.block",
1608
+ "meta.import.tsx punctuation.definition.block",
1609
+ "meta.export.ts punctuation.definition.block",
1610
+ "meta.export.tsx punctuation.definition.block"
1611
+ ],
1612
+ "settings": {
1613
+ "foreground": "#403f53"
1614
+ }
1615
+ },
1616
+ {
1617
+ "name": "TypeScript[React] Punctuation Decorators",
1618
+ "scope": [
1619
+ "meta.decorator punctuation.decorator.ts",
1620
+ "meta.decorator punctuation.decorator.tsx"
1621
+ ],
1622
+ "settings": {
1623
+ "foreground": "#4876d6"
1624
+ }
1625
+ },
1626
+ {
1627
+ "name": "TypeScript[React] Punctuation Decorators",
1628
+ "scope": "meta.tag.js meta.jsx.children.tsx",
1629
+ "settings": {
1630
+ "foreground": "#4876d6"
1631
+ }
1632
+ },
1633
+ {
1634
+ "name": "YAML Entity Name Tags",
1635
+ "scope": "entity.name.tag.yaml",
1636
+ "settings": {
1637
+ "foreground": "#111111"
1638
+ }
1639
+ },
1640
+ {
1641
+ "name": "JavaScript Variable Other ReadWrite",
1642
+ "scope": ["variable.other.readwrite.js", "variable.parameter"],
1643
+ "settings": {
1644
+ "foreground": "#403f53"
1645
+ }
1646
+ },
1647
+ {
1648
+ "name": "Support Class Component",
1649
+ "scope": ["support.class.component.js", "support.class.component.tsx"],
1650
+ "settings": {
1651
+ "foreground": "#aa0982",
1652
+ "fontStyle": ""
1653
+ }
1654
+ },
1655
+ {
1656
+ "name": "Text nested in React tags",
1657
+ "scope": [
1658
+ "meta.jsx.children",
1659
+ "meta.jsx.children.js",
1660
+ "meta.jsx.children.tsx"
1661
+ ],
1662
+ "settings": {
1663
+ "foreground": "#403f53"
1664
+ }
1665
+ },
1666
+ {
1667
+ "name": "TypeScript Classes",
1668
+ "scope": "meta.class entity.name.type.class.tsx",
1669
+ "settings": {
1670
+ "foreground": "#111111"
1671
+ }
1672
+ },
1673
+ {
1674
+ "name": "TypeScript Entity Name Type",
1675
+ "scope": ["entity.name.type.tsx", "entity.name.type.module.tsx"],
1676
+ "settings": {
1677
+ "foreground": "#111111"
1678
+ }
1679
+ },
1680
+ {
1681
+ "name": "TypeScript Class Variable Keyword",
1682
+ "scope": [
1683
+ "meta.class.ts meta.var.expr.ts storage.type.ts",
1684
+ "meta.class.tsx meta.var.expr.tsx storage.type.tsx"
1685
+ ],
1686
+ "settings": {
1687
+ "foreground": "#994CC3"
1688
+ }
1689
+ },
1690
+ {
1691
+ "name": "TypeScript Method Declaration e.g. `constructor`",
1692
+ "scope": [
1693
+ "meta.method.declaration storage.type.ts",
1694
+ "meta.method.declaration storage.type.tsx"
1695
+ ],
1696
+ "settings": {
1697
+ "foreground": "#4876d6"
1698
+ }
1699
+ },
1700
+ {
1701
+ "name": "normalize font style of certain components",
1702
+ "scope": [
1703
+ "meta.property-list.css meta.property-value.css variable.other.less",
1704
+ "meta.property-list.scss variable.scss",
1705
+ "meta.property-list.sass variable.sass",
1706
+ "meta.brace",
1707
+ "keyword.operator.operator",
1708
+ "keyword.operator.or.regexp",
1709
+ "keyword.operator.expression.in",
1710
+ "keyword.operator.relational",
1711
+ "keyword.operator.assignment",
1712
+ "keyword.operator.comparison",
1713
+ "keyword.operator.type",
1714
+ "keyword.operator",
1715
+ "keyword",
1716
+ "punctuation.definintion.string",
1717
+ "punctuation",
1718
+ "variable.other.readwrite.js",
1719
+ "storage.type",
1720
+ "source.css",
1721
+ "string.quoted"
1722
+ ],
1723
+ "settings": {
1724
+ "fontStyle": ""
1725
+ }
1726
+ }
1727
+ ]
1728
+ }
components/editor/night.json ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "inherit": true,
3
+ "rules": [
4
+ {
5
+ "background": "24292e",
6
+ "token": ""
7
+ },
8
+ {
9
+ "foreground": "959da5",
10
+ "token": "comment"
11
+ },
12
+ {
13
+ "foreground": "959da5",
14
+ "token": "punctuation.definition.comment"
15
+ },
16
+ {
17
+ "foreground": "959da5",
18
+ "token": "string.comment"
19
+ },
20
+ {
21
+ "foreground": "c8e1ff",
22
+ "token": "constant"
23
+ },
24
+ {
25
+ "foreground": "c8e1ff",
26
+ "token": "entity.name.constant"
27
+ },
28
+ {
29
+ "foreground": "c8e1ff",
30
+ "token": "variable.other.constant"
31
+ },
32
+ {
33
+ "foreground": "c8e1ff",
34
+ "token": "variable.language"
35
+ },
36
+ {
37
+ "foreground": "b392f0",
38
+ "token": "entity"
39
+ },
40
+ {
41
+ "foreground": "b392f0",
42
+ "token": "entity.name"
43
+ },
44
+ {
45
+ "foreground": "f6f8fa",
46
+ "token": "variable.parameter.function"
47
+ },
48
+ {
49
+ "foreground": "7bcc72",
50
+ "token": "entity.name.tag"
51
+ },
52
+ {
53
+ "foreground": "ea4a5a",
54
+ "token": "keyword"
55
+ },
56
+ {
57
+ "foreground": "ea4a5a",
58
+ "token": "storage"
59
+ },
60
+ {
61
+ "foreground": "ea4a5a",
62
+ "token": "storage.type"
63
+ },
64
+ {
65
+ "foreground": "f6f8fa",
66
+ "token": "storage.modifier.package"
67
+ },
68
+ {
69
+ "foreground": "f6f8fa",
70
+ "token": "storage.modifier.import"
71
+ },
72
+ {
73
+ "foreground": "f6f8fa",
74
+ "token": "storage.type.java"
75
+ },
76
+ {
77
+ "foreground": "79b8ff",
78
+ "token": "string"
79
+ },
80
+ {
81
+ "foreground": "79b8ff",
82
+ "token": "punctuation.definition.string"
83
+ },
84
+ {
85
+ "foreground": "79b8ff",
86
+ "token": "string punctuation.section.embedded source"
87
+ },
88
+ {
89
+ "foreground": "c8e1ff",
90
+ "token": "support"
91
+ },
92
+ {
93
+ "foreground": "c8e1ff",
94
+ "token": "meta.property-name"
95
+ },
96
+ {
97
+ "foreground": "fb8532",
98
+ "token": "variable"
99
+ },
100
+ {
101
+ "foreground": "f6f8fa",
102
+ "token": "variable.other"
103
+ },
104
+ {
105
+ "foreground": "d73a49",
106
+ "fontStyle": "bold italic underline",
107
+ "token": "invalid.broken"
108
+ },
109
+ {
110
+ "foreground": "d73a49",
111
+ "fontStyle": "bold italic underline",
112
+ "token": "invalid.deprecated"
113
+ },
114
+ {
115
+ "foreground": "fafbfc",
116
+ "background": "d73a49",
117
+ "fontStyle": "italic underline",
118
+ "token": "invalid.illegal"
119
+ },
120
+ {
121
+ "foreground": "fafbfc",
122
+ "background": "d73a49",
123
+ "fontStyle": "italic underline",
124
+ "token": "carriage-return"
125
+ },
126
+ {
127
+ "foreground": "d73a49",
128
+ "fontStyle": "bold italic underline",
129
+ "token": "invalid.unimplemented"
130
+ },
131
+ {
132
+ "foreground": "d73a49",
133
+ "token": "message.error"
134
+ },
135
+ {
136
+ "foreground": "f6f8fa",
137
+ "token": "string source"
138
+ },
139
+ {
140
+ "foreground": "c8e1ff",
141
+ "token": "string variable"
142
+ },
143
+ {
144
+ "foreground": "79b8ff",
145
+ "token": "source.regexp"
146
+ },
147
+ {
148
+ "foreground": "79b8ff",
149
+ "token": "string.regexp"
150
+ },
151
+ {
152
+ "foreground": "79b8ff",
153
+ "token": "string.regexp.character-class"
154
+ },
155
+ {
156
+ "foreground": "79b8ff",
157
+ "token": "string.regexp constant.character.escape"
158
+ },
159
+ {
160
+ "foreground": "79b8ff",
161
+ "token": "string.regexp source.ruby.embedded"
162
+ },
163
+ {
164
+ "foreground": "79b8ff",
165
+ "token": "string.regexp string.regexp.arbitrary-repitition"
166
+ },
167
+ {
168
+ "foreground": "7bcc72",
169
+ "fontStyle": "bold",
170
+ "token": "string.regexp constant.character.escape"
171
+ },
172
+ {
173
+ "foreground": "c8e1ff",
174
+ "token": "support.constant"
175
+ },
176
+ {
177
+ "foreground": "c8e1ff",
178
+ "token": "support.variable"
179
+ },
180
+ {
181
+ "foreground": "c8e1ff",
182
+ "token": "meta.module-reference"
183
+ },
184
+ {
185
+ "foreground": "fb8532",
186
+ "token": "markup.list"
187
+ },
188
+ {
189
+ "foreground": "0366d6",
190
+ "fontStyle": "bold",
191
+ "token": "markup.heading"
192
+ },
193
+ {
194
+ "foreground": "0366d6",
195
+ "fontStyle": "bold",
196
+ "token": "markup.heading entity.name"
197
+ },
198
+ {
199
+ "foreground": "c8e1ff",
200
+ "token": "markup.quote"
201
+ },
202
+ {
203
+ "foreground": "f6f8fa",
204
+ "fontStyle": "italic",
205
+ "token": "markup.italic"
206
+ },
207
+ {
208
+ "foreground": "f6f8fa",
209
+ "fontStyle": "bold",
210
+ "token": "markup.bold"
211
+ },
212
+ {
213
+ "foreground": "c8e1ff",
214
+ "token": "markup.raw"
215
+ },
216
+ {
217
+ "foreground": "b31d28",
218
+ "background": "ffeef0",
219
+ "token": "markup.deleted"
220
+ },
221
+ {
222
+ "foreground": "b31d28",
223
+ "background": "ffeef0",
224
+ "token": "meta.diff.header.from-file"
225
+ },
226
+ {
227
+ "foreground": "b31d28",
228
+ "background": "ffeef0",
229
+ "token": "punctuation.definition.deleted"
230
+ },
231
+ {
232
+ "foreground": "176f2c",
233
+ "background": "f0fff4",
234
+ "token": "markup.inserted"
235
+ },
236
+ {
237
+ "foreground": "176f2c",
238
+ "background": "f0fff4",
239
+ "token": "meta.diff.header.to-file"
240
+ },
241
+ {
242
+ "foreground": "176f2c",
243
+ "background": "f0fff4",
244
+ "token": "punctuation.definition.inserted"
245
+ },
246
+ {
247
+ "foreground": "b08800",
248
+ "background": "fffdef",
249
+ "token": "markup.changed"
250
+ },
251
+ {
252
+ "foreground": "b08800",
253
+ "background": "fffdef",
254
+ "token": "punctuation.definition.changed"
255
+ },
256
+ {
257
+ "foreground": "2f363d",
258
+ "background": "959da5",
259
+ "token": "markup.ignored"
260
+ },
261
+ {
262
+ "foreground": "2f363d",
263
+ "background": "959da5",
264
+ "token": "markup.untracked"
265
+ },
266
+ {
267
+ "foreground": "b392f0",
268
+ "fontStyle": "bold",
269
+ "token": "meta.diff.range"
270
+ },
271
+ {
272
+ "foreground": "c8e1ff",
273
+ "token": "meta.diff.header"
274
+ },
275
+ {
276
+ "foreground": "0366d6",
277
+ "fontStyle": "bold",
278
+ "token": "meta.separator"
279
+ },
280
+ {
281
+ "foreground": "0366d6",
282
+ "token": "meta.output"
283
+ },
284
+ {
285
+ "foreground": "ffeef0",
286
+ "token": "brackethighlighter.tag"
287
+ },
288
+ {
289
+ "foreground": "ffeef0",
290
+ "token": "brackethighlighter.curly"
291
+ },
292
+ {
293
+ "foreground": "ffeef0",
294
+ "token": "brackethighlighter.round"
295
+ },
296
+ {
297
+ "foreground": "ffeef0",
298
+ "token": "brackethighlighter.square"
299
+ },
300
+ {
301
+ "foreground": "ffeef0",
302
+ "token": "brackethighlighter.angle"
303
+ },
304
+ {
305
+ "foreground": "ffeef0",
306
+ "token": "brackethighlighter.quote"
307
+ },
308
+ {
309
+ "foreground": "d73a49",
310
+ "token": "brackethighlighter.unmatched"
311
+ },
312
+ {
313
+ "foreground": "d73a49",
314
+ "token": "sublimelinter.mark.error"
315
+ },
316
+ {
317
+ "foreground": "fb8532",
318
+ "token": "sublimelinter.mark.warning"
319
+ },
320
+ {
321
+ "foreground": "6a737d",
322
+ "token": "sublimelinter.gutter-mark"
323
+ },
324
+ {
325
+ "foreground": "79b8ff",
326
+ "fontStyle": "underline",
327
+ "token": "constant.other.reference.link"
328
+ },
329
+ {
330
+ "foreground": "79b8ff",
331
+ "fontStyle": "underline",
332
+ "token": "string.other.link"
333
+ }
334
+ ],
335
+ "colors": {
336
+ "editor.foreground": "#f6f8fa",
337
+ "editor.background": "#24292e",
338
+ "editor.selectionBackground": "#4c2889",
339
+ "editor.inactiveSelectionBackground": "#444d56",
340
+ "editor.lineHighlightBackground": "#444d56",
341
+ "editorCursor.foreground": "#ffffff",
342
+ "editorWhitespace.foreground": "#6a737d",
343
+ "editorIndentGuide.background": "#6a737d",
344
+ "editorIndentGuide.activeBackground": "#f6f8fa",
345
+ "editor.selectionHighlightBorder": "#444d56"
346
+ }
347
+ }
components/grid-pattern/index.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useId } from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface GridPatternProps extends React.SVGProps<SVGSVGElement> {
6
+ width?: number;
7
+ height?: number;
8
+ x?: number;
9
+ y?: number;
10
+ squares?: Array<[x: number, y: number]>;
11
+ strokeDasharray?: string;
12
+ className?: string;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ export function GridPattern({
17
+ width = 40,
18
+ height = 40,
19
+ x = -1,
20
+ y = -1,
21
+ strokeDasharray = "0",
22
+ squares,
23
+ className,
24
+ ...props
25
+ }: GridPatternProps) {
26
+ const id = useId();
27
+
28
+ return (
29
+ <svg
30
+ aria-hidden="true"
31
+ className={cn(
32
+ "pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400 -z-1 dark:fill-gray-600/30 dark:stroke-gray-600",
33
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ <defs>
38
+ <pattern
39
+ id={id}
40
+ width={width}
41
+ height={height}
42
+ patternUnits="userSpaceOnUse"
43
+ x={x}
44
+ y={y}
45
+ >
46
+ <path
47
+ d={`M.5 ${height}V.5H${width}`}
48
+ fill="none"
49
+ strokeDasharray={strokeDasharray}
50
+ />
51
+ </pattern>
52
+ </defs>
53
+ <rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
54
+ {squares && (
55
+ <svg x={x} y={y} className="overflow-visible">
56
+ {squares.map(([x, y]) => (
57
+ <rect
58
+ strokeWidth="0"
59
+ key={`${x}-${y}`}
60
+ width={width - 1}
61
+ height={height - 1}
62
+ x={x * width + 1}
63
+ y={y * height + 1}
64
+ />
65
+ ))}
66
+ </svg>
67
+ )}
68
+ </svg>
69
+ );
70
+ }
components/loading/index.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils";
2
+
3
+ function Loading({
4
+ overlay = true,
5
+ className,
6
+ }: {
7
+ overlay?: boolean;
8
+ className?: string;
9
+ }) {
10
+ return (
11
+ <div
12
+ className={cn("", {
13
+ "absolute left-0 top-0 h-full w-full flex items-center justify-center z-20 bg-black/50 rounded-full":
14
+ overlay,
15
+ })}
16
+ >
17
+ <svg
18
+ className={`size-5 animate-spin text-primary ${className}`}
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ fill="none"
21
+ viewBox="0 0 24 24"
22
+ >
23
+ <circle
24
+ className="opacity-25"
25
+ cx="12"
26
+ cy="12"
27
+ r="10"
28
+ stroke="currentColor"
29
+ strokeWidth="4"
30
+ ></circle>
31
+ <path
32
+ className="opacity-75"
33
+ fill="currentColor"
34
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
35
+ ></path>
36
+ </svg>
37
+ </div>
38
+ );
39
+ }
40
+
41
+ export default Loading;
components/not-found/buttons.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import Link from "next/link";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ export function NotFoundButtons() {
6
+ return (
7
+ <div className="flex items-center justify-center gap-3">
8
+ <Link href="/">
9
+ <Button size="lg">Return Home</Button>
10
+ </Link>
11
+ <Button size="lg" variant="outline" onClick={() => window.history.back()}>
12
+ Back to previous page
13
+ </Button>
14
+ </div>
15
+ );
16
+ }