Spaces:
Running
Running
Upload ChatGPT Image 14 сент. 2025 г., 14_27_00.png
#415
by
Antonovish
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .gitattributes +1 -0
- README.md +4 -11
- app/(public)/layout.tsx +1 -1
- app/(public)/page.tsx +43 -4
- app/(public)/projects/page.tsx +13 -0
- app/[namespace]/[repoId]/page.tsx +0 -28
- app/actions/projects.ts +40 -24
- app/actions/rewrite-prompt.ts +35 -0
- app/api/{ask → ask-ai}/route.ts +71 -217
- app/api/auth/login-url/route.ts +0 -23
- app/api/auth/logout/route.ts +0 -25
- app/api/auth/route.ts +1 -21
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +14 -16
- app/api/me/projects/[namespace]/[repoId]/route.ts +179 -90
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
- app/api/me/projects/route.ts +95 -75
- app/api/me/route.ts +1 -22
- app/auth/callback/page.tsx +42 -67
- app/layout.tsx +33 -49
- app/new/page.tsx +0 -14
- app/projects/[namespace]/[repoId]/page.tsx +42 -0
- app/projects/new/page.tsx +5 -0
- app/sitemap.ts +0 -28
- assets/deepseek.svg +0 -1
- assets/globals.css +0 -225
- assets/kimi.svg +0 -1
- assets/qwen.svg +0 -1
- assets/zai.svg +0 -13
- components.json +1 -1
- components/animated-blobs/index.tsx +0 -34
- components/animated-text/index.tsx +0 -123
- components/contexts/app-context.tsx +10 -6
- components/contexts/login-context.tsx +0 -62
- components/contexts/pro-context.tsx +0 -48
- components/editor/ask-ai/fake-ask.tsx +0 -97
- components/editor/ask-ai/follow-up-tooltip.tsx +36 -0
- components/editor/ask-ai/index.tsx +320 -142
- components/editor/ask-ai/loading.tsx +0 -68
- components/editor/ask-ai/prompt-builder/content-modal.tsx +0 -196
- components/editor/ask-ai/prompt-builder/index.tsx +0 -68
- components/editor/ask-ai/prompt-builder/tailwind-colors.tsx +0 -58
- components/editor/ask-ai/prompt-builder/themes.tsx +0 -48
- components/editor/ask-ai/re-imagine.tsx +4 -10
- components/editor/ask-ai/selector.tsx +0 -41
- components/editor/ask-ai/settings.tsx +143 -230
- components/editor/ask-ai/uploader.tsx +167 -129
- components/editor/deploy-button/content.tsx +111 -0
- components/editor/deploy-button/index.tsx +79 -0
- components/editor/footer/index.tsx +150 -0
.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
public/ChatGPT[[:space:]]Image[[:space:]]14[[:space:]]сент.[[:space:]]2025[[:space:]]г.,[[:space:]]14_27_00.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title: DeepSite
|
3 |
emoji: 🐳
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
@@ -7,23 +7,16 @@ sdk: docker
|
|
7 |
pinned: true
|
8 |
app_port: 3000
|
9 |
license: mit
|
10 |
-
short_description: Generate any application
|
11 |
models:
|
12 |
- deepseek-ai/DeepSeek-V3-0324
|
13 |
- deepseek-ai/DeepSeek-R1-0528
|
14 |
-
- deepseek-ai/DeepSeek-V3.1
|
15 |
-
- deepseek-ai/DeepSeek-V3.1-Terminus
|
16 |
-
- deepseek-ai/DeepSeek-V3.2-Exp
|
17 |
-
- Qwen/Qwen3-Coder-480B-A35B-Instruct
|
18 |
-
- moonshotai/Kimi-K2-Instruct
|
19 |
-
- moonshotai/Kimi-K2-Instruct-0905
|
20 |
-
- zai-org/GLM-4.6
|
21 |
---
|
22 |
|
23 |
# DeepSite 🐳
|
24 |
|
25 |
-
DeepSite is a
|
26 |
|
27 |
## How to use it locally
|
28 |
|
29 |
-
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
|
|
1 |
---
|
2 |
+
title: DeepSite v2
|
3 |
emoji: 🐳
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
|
|
7 |
pinned: true
|
8 |
app_port: 3000
|
9 |
license: mit
|
10 |
+
short_description: Generate any application with DeepSeek
|
11 |
models:
|
12 |
- deepseek-ai/DeepSeek-V3-0324
|
13 |
- deepseek-ai/DeepSeek-R1-0528
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
---
|
15 |
|
16 |
# DeepSite 🐳
|
17 |
|
18 |
+
DeepSite is a coding platform powered by DeepSeek AI, 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.
|
19 |
|
20 |
## How to use it locally
|
21 |
|
22 |
+
Follow [this discussion](https://huggingface.co/spaces/enzostvs/deepsite/discussions/74)
|
app/(public)/layout.tsx
CHANGED
@@ -6,7 +6,7 @@ export default async function PublicLayout({
|
|
6 |
children: React.ReactNode;
|
7 |
}>) {
|
8 |
return (
|
9 |
-
<div className="h-screen bg-
|
10 |
<div className="background__noisy" />
|
11 |
<Navigation />
|
12 |
{children}
|
|
|
6 |
children: React.ReactNode;
|
7 |
}>) {
|
8 |
return (
|
9 |
+
<div className="min-h-screen bg-black z-1 relative">
|
10 |
<div className="background__noisy" />
|
11 |
<Navigation />
|
12 |
{children}
|
app/(public)/page.tsx
CHANGED
@@ -1,5 +1,44 @@
|
|
1 |
-
import {
|
2 |
-
|
3 |
-
export default
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
}
|
|
|
1 |
+
import { AskAi } from "@/components/space/ask-ai";
|
2 |
+
import { redirect } from "next/navigation";
|
3 |
+
export default function Home() {
|
4 |
+
redirect("/projects/new");
|
5 |
+
return (
|
6 |
+
<>
|
7 |
+
<header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
|
8 |
+
<div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-xs text-neutral-300 px-3 py-1 max-w-max mx-auto mb-2">
|
9 |
+
✨ DeepSite Public Beta
|
10 |
+
</div>
|
11 |
+
<h1 className="text-8xl font-semibold text-white font-mono max-w-4xl">
|
12 |
+
Code your website with AI in seconds
|
13 |
+
</h1>
|
14 |
+
<p className="text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl">
|
15 |
+
Vibe Coding has never been so easy.
|
16 |
+
</p>
|
17 |
+
<div className="mt-14 max-w-2xl w-full mx-auto">
|
18 |
+
<AskAi />
|
19 |
+
</div>
|
20 |
+
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
21 |
+
<div className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl rounded-full" />
|
22 |
+
<div className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10 transform rotate-12" />
|
23 |
+
<div className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10 rounded-3xl" />
|
24 |
+
<div className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3 rounded-lg transform -rotate-15" />
|
25 |
+
</div>
|
26 |
+
</header>
|
27 |
+
<div id="community" className="h-screen flex items-center justify-center">
|
28 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
29 |
+
Community Driven
|
30 |
+
</h1>
|
31 |
+
</div>
|
32 |
+
<div id="deploy" className="h-screen flex items-center justify-center">
|
33 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
34 |
+
Deploy your website in seconds
|
35 |
+
</h1>
|
36 |
+
</div>
|
37 |
+
<div id="features" className="h-screen flex items-center justify-center">
|
38 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
39 |
+
Features that make you smile
|
40 |
+
</h1>
|
41 |
+
</div>
|
42 |
+
</>
|
43 |
+
);
|
44 |
}
|
app/(public)/projects/page.tsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { redirect } from "next/navigation";
|
2 |
+
|
3 |
+
import { MyProjects } from "@/components/my-projects";
|
4 |
+
import { getProjects } from "@/app/actions/projects";
|
5 |
+
|
6 |
+
export default async function ProjectsPage() {
|
7 |
+
const { ok, projects } = await getProjects();
|
8 |
+
if (!ok) {
|
9 |
+
redirect("/");
|
10 |
+
}
|
11 |
+
|
12 |
+
return <MyProjects projects={projects} />;
|
13 |
+
}
|
app/[namespace]/[repoId]/page.tsx
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
import { AppEditor } from "@/components/editor";
|
2 |
-
import { generateSEO } from "@/lib/seo";
|
3 |
-
import { Metadata } from "next";
|
4 |
-
|
5 |
-
export async function generateMetadata({
|
6 |
-
params,
|
7 |
-
}: {
|
8 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
9 |
-
}): Promise<Metadata> {
|
10 |
-
const { namespace, repoId } = await params;
|
11 |
-
|
12 |
-
return generateSEO({
|
13 |
-
title: `${namespace}/${repoId} - DeepSite Editor`,
|
14 |
-
description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
|
15 |
-
path: `/${namespace}/${repoId}`,
|
16 |
-
// Prevent indexing of individual project editor pages if they contain sensitive content
|
17 |
-
noIndex: false, // Set to true if you want to keep project pages private
|
18 |
-
});
|
19 |
-
}
|
20 |
-
|
21 |
-
export default async function ProjectNamespacePage({
|
22 |
-
params,
|
23 |
-
}: {
|
24 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
25 |
-
}) {
|
26 |
-
const { namespace, repoId } = await params;
|
27 |
-
return <AppEditor namespace={namespace} repoId={repoId} />;
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/actions/projects.ts
CHANGED
@@ -2,13 +2,13 @@
|
|
2 |
|
3 |
import { isAuthenticated } from "@/lib/auth";
|
4 |
import { NextResponse } from "next/server";
|
5 |
-
import
|
6 |
-
import
|
|
|
7 |
|
8 |
export async function getProjects(): Promise<{
|
9 |
ok: boolean;
|
10 |
projects: ProjectType[];
|
11 |
-
isEmpty?: boolean;
|
12 |
}> {
|
13 |
const user = await isAuthenticated();
|
14 |
|
@@ -19,29 +19,45 @@ export async function getProjects(): Promise<{
|
|
19 |
};
|
20 |
}
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
(
|
35 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
36 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
37 |
-
)
|
38 |
-
) {
|
39 |
-
projects.push(space);
|
40 |
-
}
|
41 |
}
|
42 |
-
|
43 |
return {
|
44 |
ok: true,
|
45 |
-
projects,
|
46 |
};
|
47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
import { isAuthenticated } from "@/lib/auth";
|
4 |
import { NextResponse } from "next/server";
|
5 |
+
import dbConnect from "@/lib/mongodb";
|
6 |
+
import Project from "@/models/Project";
|
7 |
+
import { Project as ProjectType } from "@/types";
|
8 |
|
9 |
export async function getProjects(): Promise<{
|
10 |
ok: boolean;
|
11 |
projects: ProjectType[];
|
|
|
12 |
}> {
|
13 |
const user = await isAuthenticated();
|
14 |
|
|
|
19 |
};
|
20 |
}
|
21 |
|
22 |
+
await dbConnect();
|
23 |
+
const projects = await Project.find({
|
24 |
+
user_id: user?.id,
|
25 |
+
})
|
26 |
+
.sort({ _createdAt: -1 })
|
27 |
+
.limit(100)
|
28 |
+
.lean();
|
29 |
+
if (!projects) {
|
30 |
+
return {
|
31 |
+
ok: false,
|
32 |
+
projects: [],
|
33 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
}
|
|
|
35 |
return {
|
36 |
ok: true,
|
37 |
+
projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
|
38 |
};
|
39 |
}
|
40 |
+
|
41 |
+
export async function getProject(
|
42 |
+
namespace: string,
|
43 |
+
repoId: string
|
44 |
+
): Promise<ProjectType | null> {
|
45 |
+
const user = await isAuthenticated();
|
46 |
+
|
47 |
+
if (user instanceof NextResponse || !user) {
|
48 |
+
return null;
|
49 |
+
}
|
50 |
+
|
51 |
+
await dbConnect();
|
52 |
+
const project = await Project.findOne({
|
53 |
+
user_id: user.id,
|
54 |
+
namespace,
|
55 |
+
repoId,
|
56 |
+
}).lean();
|
57 |
+
|
58 |
+
if (!project) {
|
59 |
+
return null;
|
60 |
+
}
|
61 |
+
|
62 |
+
return JSON.parse(JSON.stringify(project)) as ProjectType;
|
63 |
+
}
|
app/actions/rewrite-prompt.ts
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { InferenceClient } from "@huggingface/inference";
|
2 |
+
|
3 |
+
const START_REWRITE_PROMPT = ">>>>>>> START PROMPT >>>>>>";
|
4 |
+
const END_REWRITE_PROMPT = ">>>>>>> END PROMPT >>>>>>";
|
5 |
+
|
6 |
+
export const callAiRewritePrompt = async (prompt: string, { token, billTo }: { token: string, billTo?: string | null }) => {
|
7 |
+
const client = new InferenceClient(token);
|
8 |
+
const response = await client.chatCompletion(
|
9 |
+
{
|
10 |
+
model: "deepseek-ai/DeepSeek-V3.1",
|
11 |
+
provider: "novita",
|
12 |
+
messages: [{
|
13 |
+
role: "system",
|
14 |
+
content: `You are a helpful assistant that rewrites prompts to make them better. All the prompts will be about creating a website or app.
|
15 |
+
Try to make the prompt more detailed and specific to create a good UI/UX Design and good code.
|
16 |
+
Format the result by following this format:
|
17 |
+
${START_REWRITE_PROMPT}
|
18 |
+
new prompt here
|
19 |
+
${END_REWRITE_PROMPT}
|
20 |
+
If you don't rewrite the prompt, return the original prompt.
|
21 |
+
Make sure to return the prompt in the same language as the prompt you are given. Also IMPORTANT: Make sure to keep the original intent of the prompt. Improve it it needed, but don't change the original intent.
|
22 |
+
`
|
23 |
+
},{ role: "user", content: prompt }],
|
24 |
+
},
|
25 |
+
billTo ? { billTo } : {}
|
26 |
+
);
|
27 |
+
|
28 |
+
const responseContent = response.choices[0]?.message?.content;
|
29 |
+
if (!responseContent) {
|
30 |
+
return prompt;
|
31 |
+
}
|
32 |
+
const startIndex = responseContent.indexOf(START_REWRITE_PROMPT);
|
33 |
+
const endIndex = responseContent.indexOf(END_REWRITE_PROMPT);
|
34 |
+
return responseContent.substring(startIndex + START_REWRITE_PROMPT.length, endIndex);
|
35 |
+
};
|
app/api/{ask → ask-ai}/route.ts
RENAMED
@@ -4,7 +4,7 @@ import { NextResponse } from "next/server";
|
|
4 |
import { headers } from "next/headers";
|
5 |
import { InferenceClient } from "@huggingface/inference";
|
6 |
|
7 |
-
import { MODELS } from "@/lib/providers";
|
8 |
import {
|
9 |
DIVIDER,
|
10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
@@ -16,17 +16,9 @@ import {
|
|
16 |
SEARCH_START,
|
17 |
UPDATE_PAGE_START,
|
18 |
UPDATE_PAGE_END,
|
19 |
-
PROMPT_FOR_PROJECT_NAME,
|
20 |
} from "@/lib/prompts";
|
21 |
-
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
22 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
23 |
import { Page } from "@/types";
|
24 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
25 |
-
import { isAuthenticated } from "@/lib/auth";
|
26 |
-
import { getBestProvider } from "@/lib/best-provider";
|
27 |
-
// import { rewritePrompt } from "@/lib/rewrite-prompt";
|
28 |
-
import { COLORS } from "@/lib/utils";
|
29 |
-
import { templates } from "@/lib/templates";
|
30 |
|
31 |
const ipAddresses = new Map();
|
32 |
|
@@ -35,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|
35 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
36 |
|
37 |
const body = await request.json();
|
38 |
-
const { prompt, provider, model, redesignMarkdown,
|
39 |
|
40 |
if (!model || (!prompt && !redesignMarkdown)) {
|
41 |
return NextResponse.json(
|
@@ -55,8 +47,18 @@ export async function POST(request: NextRequest) {
|
|
55 |
);
|
56 |
}
|
57 |
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
let billTo: string | null = null;
|
61 |
|
62 |
/**
|
@@ -89,13 +91,18 @@ export async function POST(request: NextRequest) {
|
|
89 |
billTo = "huggingface";
|
90 |
}
|
91 |
|
92 |
-
const
|
|
|
|
|
|
|
|
|
93 |
|
94 |
-
|
95 |
|
96 |
-
if (
|
97 |
-
|
98 |
-
|
|
|
99 |
|
100 |
try {
|
101 |
const encoder = new TextEncoder();
|
@@ -114,37 +121,33 @@ export async function POST(request: NextRequest) {
|
|
114 |
// let completeResponse = "";
|
115 |
try {
|
116 |
const client = new InferenceClient(token);
|
117 |
-
|
118 |
-
const systemPrompt = INITIAL_SYSTEM_PROMPT;
|
119 |
-
|
120 |
-
const userPrompt = rewrittenPrompt;
|
121 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
|
122 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
|
123 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
124 |
-
|
125 |
const chatCompletion = client.chatCompletionStream(
|
126 |
{
|
127 |
model: selectedModel.value,
|
128 |
-
provider: selectedProvider.
|
129 |
messages: [
|
130 |
{
|
131 |
role: "system",
|
132 |
-
content:
|
133 |
},
|
|
|
|
|
|
|
|
|
134 |
{
|
135 |
role: "user",
|
136 |
-
content:
|
137 |
-
|
138 |
-
|
139 |
},
|
140 |
],
|
141 |
-
|
142 |
},
|
143 |
billTo ? { billTo } : {}
|
144 |
);
|
145 |
|
146 |
while (true) {
|
147 |
-
const { done, value } = await chatCompletion.next()
|
148 |
if (done) {
|
149 |
break;
|
150 |
}
|
@@ -154,9 +157,6 @@ export async function POST(request: NextRequest) {
|
|
154 |
await writer.write(encoder.encode(chunk));
|
155 |
}
|
156 |
}
|
157 |
-
|
158 |
-
// Explicitly close the writer after successful completion
|
159 |
-
await writer.close();
|
160 |
} catch (error: any) {
|
161 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
162 |
await writer.write(
|
@@ -168,18 +168,7 @@ export async function POST(request: NextRequest) {
|
|
168 |
})
|
169 |
)
|
170 |
);
|
171 |
-
} else
|
172 |
-
await writer.write(
|
173 |
-
encoder.encode(
|
174 |
-
JSON.stringify({
|
175 |
-
ok: false,
|
176 |
-
openSelectProvider: true,
|
177 |
-
message: error.message,
|
178 |
-
})
|
179 |
-
)
|
180 |
-
);
|
181 |
-
}
|
182 |
-
else {
|
183 |
await writer.write(
|
184 |
encoder.encode(
|
185 |
JSON.stringify({
|
@@ -192,12 +181,7 @@ export async function POST(request: NextRequest) {
|
|
192 |
);
|
193 |
}
|
194 |
} finally {
|
195 |
-
|
196 |
-
try {
|
197 |
-
await writer?.close();
|
198 |
-
} catch {
|
199 |
-
// Ignore errors when closing the writer as it might already be closed
|
200 |
-
}
|
201 |
}
|
202 |
})();
|
203 |
|
@@ -216,19 +200,13 @@ export async function POST(request: NextRequest) {
|
|
216 |
}
|
217 |
|
218 |
export async function PUT(request: NextRequest) {
|
219 |
-
const user = await isAuthenticated();
|
220 |
-
if (user instanceof NextResponse || !user) {
|
221 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
222 |
-
}
|
223 |
-
|
224 |
const authHeaders = await headers();
|
|
|
225 |
|
226 |
const body = await request.json();
|
227 |
-
const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files,
|
228 |
body;
|
229 |
|
230 |
-
let repoId = repoIdFromBody;
|
231 |
-
|
232 |
if (!prompt || pages.length === 0) {
|
233 |
return NextResponse.json(
|
234 |
{ ok: false, error: "Missing required fields" },
|
@@ -246,7 +224,7 @@ export async function PUT(request: NextRequest) {
|
|
246 |
);
|
247 |
}
|
248 |
|
249 |
-
let token =
|
250 |
let billTo: string | null = null;
|
251 |
|
252 |
/**
|
@@ -281,112 +259,52 @@ export async function PUT(request: NextRequest) {
|
|
281 |
|
282 |
const client = new InferenceClient(token);
|
283 |
|
284 |
-
const
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
let searchRegex = escapeRegExp(searchBlock)
|
290 |
-
.replace(/\s+/g, '\\s*')
|
291 |
-
.replace(/>\s*</g, '>\\s*<')
|
292 |
-
.replace(/\s*>/g, '\\s*>');
|
293 |
-
|
294 |
-
return new RegExp(searchRegex, 'g');
|
295 |
-
};
|
296 |
-
|
297 |
-
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
298 |
|
299 |
try {
|
300 |
-
const
|
301 |
-
const userContext = "You are modifying the HTML file based on the user's request.";
|
302 |
-
|
303 |
-
const getRelevantPages = (pages: Page[], prompt: string, maxPages: number = 2): Page[] => {
|
304 |
-
if (pages.length <= maxPages) return pages;
|
305 |
-
|
306 |
-
const indexPage = pages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
307 |
-
const otherPages = pages.filter(p => p !== indexPage);
|
308 |
-
|
309 |
-
if (selectedElementHtml) {
|
310 |
-
const elementKeywords = selectedElementHtml.toLowerCase().match(/class=["']([^"']*)["']|id=["']([^"']*)["']/g) || [];
|
311 |
-
const relevantPages = otherPages.filter(page => {
|
312 |
-
const pageContent = page.html.toLowerCase();
|
313 |
-
return elementKeywords.some((keyword: string) => pageContent.includes(keyword.toLowerCase()));
|
314 |
-
});
|
315 |
-
|
316 |
-
return indexPage ? [indexPage, ...relevantPages.slice(0, maxPages - 1)] : relevantPages.slice(0, maxPages);
|
317 |
-
}
|
318 |
-
|
319 |
-
const keywords = prompt.toLowerCase().split(/\s+/).filter(word => word.length > 3);
|
320 |
-
const scoredPages = otherPages.map(page => {
|
321 |
-
const pageContent = (page.path + ' ' + page.html).toLowerCase();
|
322 |
-
const score = keywords.reduce((acc, keyword) => {
|
323 |
-
return acc + (pageContent.includes(keyword) ? 1 : 0);
|
324 |
-
}, 0);
|
325 |
-
return { page, score };
|
326 |
-
});
|
327 |
-
|
328 |
-
const topPages = scoredPages
|
329 |
-
.sort((a, b) => b.score - a.score)
|
330 |
-
.slice(0, maxPages - (indexPage ? 1 : 0))
|
331 |
-
.map(item => item.page);
|
332 |
-
|
333 |
-
return indexPage ? [indexPage, ...topPages] : topPages;
|
334 |
-
};
|
335 |
-
|
336 |
-
const relevantPages = getRelevantPages(pages || [], prompt);
|
337 |
-
const pagesContext = relevantPages
|
338 |
-
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
339 |
-
.join("\n\n");
|
340 |
-
|
341 |
-
const assistantContext = `${
|
342 |
-
selectedElementHtml
|
343 |
-
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
|
344 |
-
: ""
|
345 |
-
}. Current pages (${relevantPages.length}/${pages?.length || 0} shown): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
346 |
-
|
347 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
348 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
349 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
350 |
-
|
351 |
-
const chatCompletion = client.chatCompletionStream(
|
352 |
{
|
353 |
model: selectedModel.value,
|
354 |
-
provider: selectedProvider.
|
355 |
messages: [
|
356 |
{
|
357 |
role: "system",
|
358 |
-
content:
|
359 |
},
|
360 |
{
|
361 |
role: "user",
|
362 |
-
content:
|
|
|
|
|
363 |
},
|
364 |
{
|
365 |
role: "assistant",
|
366 |
-
|
|
|
|
|
|
|
|
|
|
|
367 |
},
|
368 |
{
|
369 |
role: "user",
|
370 |
content: prompt,
|
371 |
},
|
372 |
],
|
373 |
-
...
|
|
|
|
|
|
|
|
|
374 |
},
|
375 |
billTo ? { billTo } : {}
|
376 |
);
|
377 |
|
378 |
-
|
379 |
-
while (true) {
|
380 |
-
const { done, value } = await chatCompletion.next();
|
381 |
-
if (done) {
|
382 |
-
break;
|
383 |
-
}
|
384 |
-
|
385 |
-
const deltaContent = value.choices[0]?.delta?.content;
|
386 |
-
if (deltaContent) {
|
387 |
-
chunk += deltaContent;
|
388 |
-
}
|
389 |
-
}
|
390 |
if (!chunk) {
|
391 |
return NextResponse.json(
|
392 |
{ ok: false, message: "No content returned from the model" },
|
@@ -449,18 +367,15 @@ export async function PUT(request: NextRequest) {
|
|
449 |
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
450 |
updatedLines.push([1, replaceBlock.split("\n").length]);
|
451 |
} else {
|
452 |
-
const
|
453 |
-
|
454 |
-
|
455 |
-
if (match) {
|
456 |
-
const matchedText = match[0];
|
457 |
-
const beforeText = pageHtml.substring(0, match.index);
|
458 |
const startLineNumber = beforeText.split("\n").length;
|
459 |
const replaceLines = replaceBlock.split("\n").length;
|
460 |
const endLineNumber = startLineNumber + replaceLines - 1;
|
461 |
|
462 |
updatedLines.push([startLineNumber, endLineNumber]);
|
463 |
-
pageHtml = pageHtml.replace(
|
464 |
}
|
465 |
}
|
466 |
|
@@ -538,18 +453,15 @@ export async function PUT(request: NextRequest) {
|
|
538 |
newHtml = `${replaceBlock}\n${newHtml}`;
|
539 |
updatedLines.push([1, replaceBlock.split("\n").length]);
|
540 |
} else {
|
541 |
-
const
|
542 |
-
|
543 |
-
|
544 |
-
if (match) {
|
545 |
-
const matchedText = match[0];
|
546 |
-
const beforeText = newHtml.substring(0, match.index);
|
547 |
const startLineNumber = beforeText.split("\n").length;
|
548 |
const replaceLines = replaceBlock.split("\n").length;
|
549 |
const endLineNumber = startLineNumber + replaceLines - 1;
|
550 |
|
551 |
updatedLines.push([startLineNumber, endLineNumber]);
|
552 |
-
newHtml = newHtml.replace(
|
553 |
}
|
554 |
}
|
555 |
|
@@ -563,67 +475,10 @@ export async function PUT(request: NextRequest) {
|
|
563 |
}
|
564 |
}
|
565 |
|
566 |
-
const files: File[] = [];
|
567 |
-
updatedPages.forEach((page: Page) => {
|
568 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
569 |
-
files.push(file);
|
570 |
-
});
|
571 |
-
|
572 |
-
if (isNew) {
|
573 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
574 |
-
const formattedTitle = projectName?.toLowerCase()
|
575 |
-
.replace(/[^a-z0-9]+/g, "-")
|
576 |
-
.split("-")
|
577 |
-
.filter(Boolean)
|
578 |
-
.join("-")
|
579 |
-
.slice(0, 96);
|
580 |
-
const repo: RepoDesignation = {
|
581 |
-
type: "space",
|
582 |
-
name: `${user.name}/${formattedTitle}`,
|
583 |
-
};
|
584 |
-
const { repoUrl} = await createRepo({
|
585 |
-
repo,
|
586 |
-
accessToken: user.token as string,
|
587 |
-
});
|
588 |
-
repoId = repoUrl.split("/").slice(-2).join("/");
|
589 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
590 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
591 |
-
const README = `---
|
592 |
-
title: ${projectName}
|
593 |
-
colorFrom: ${colorFrom}
|
594 |
-
colorTo: ${colorTo}
|
595 |
-
emoji: 🐳
|
596 |
-
sdk: static
|
597 |
-
pinned: false
|
598 |
-
tags:
|
599 |
-
- deepsite-v3
|
600 |
-
---
|
601 |
-
|
602 |
-
# Welcome to your new DeepSite project!
|
603 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
604 |
-
`;
|
605 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
606 |
-
}
|
607 |
-
|
608 |
-
const response = await uploadFiles({
|
609 |
-
repo: {
|
610 |
-
type: "space",
|
611 |
-
name: repoId,
|
612 |
-
},
|
613 |
-
files,
|
614 |
-
commitTitle: prompt,
|
615 |
-
accessToken: user.token as string,
|
616 |
-
});
|
617 |
-
|
618 |
return NextResponse.json({
|
619 |
ok: true,
|
620 |
updatedLines,
|
621 |
pages: updatedPages,
|
622 |
-
repoId,
|
623 |
-
commit: {
|
624 |
-
...response.commit,
|
625 |
-
title: prompt,
|
626 |
-
}
|
627 |
});
|
628 |
} else {
|
629 |
return NextResponse.json(
|
@@ -653,4 +508,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
|
|
653 |
);
|
654 |
}
|
655 |
}
|
656 |
-
|
|
|
4 |
import { headers } from "next/headers";
|
5 |
import { InferenceClient } from "@huggingface/inference";
|
6 |
|
7 |
+
import { MODELS, PROVIDERS } from "@/lib/providers";
|
8 |
import {
|
9 |
DIVIDER,
|
10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
|
|
16 |
SEARCH_START,
|
17 |
UPDATE_PAGE_START,
|
18 |
UPDATE_PAGE_END,
|
|
|
19 |
} from "@/lib/prompts";
|
|
|
20 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
21 |
import { Page } from "@/types";
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
const ipAddresses = new Map();
|
24 |
|
|
|
27 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
28 |
|
29 |
const body = await request.json();
|
30 |
+
const { prompt, provider, model, redesignMarkdown, previousPrompts, pages } = body;
|
31 |
|
32 |
if (!model || (!prompt && !redesignMarkdown)) {
|
33 |
return NextResponse.json(
|
|
|
47 |
);
|
48 |
}
|
49 |
|
50 |
+
if (!selectedModel.providers.includes(provider) && provider !== "auto") {
|
51 |
+
return NextResponse.json(
|
52 |
+
{
|
53 |
+
ok: false,
|
54 |
+
error: `The selected model does not support the ${provider} provider.`,
|
55 |
+
openSelectProvider: true,
|
56 |
+
},
|
57 |
+
{ status: 400 }
|
58 |
+
);
|
59 |
+
}
|
60 |
+
|
61 |
+
let token = userToken;
|
62 |
let billTo: string | null = null;
|
63 |
|
64 |
/**
|
|
|
91 |
billTo = "huggingface";
|
92 |
}
|
93 |
|
94 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
95 |
+
const selectedProvider =
|
96 |
+
provider === "auto"
|
97 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
98 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
99 |
|
100 |
+
const rewrittenPrompt = prompt;
|
101 |
|
102 |
+
// if (prompt?.length < 240) {
|
103 |
+
|
104 |
+
//rewrittenPrompt = await callAiRewritePrompt(prompt, { token, billTo });
|
105 |
+
// }
|
106 |
|
107 |
try {
|
108 |
const encoder = new TextEncoder();
|
|
|
121 |
// let completeResponse = "";
|
122 |
try {
|
123 |
const client = new InferenceClient(token);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
const chatCompletion = client.chatCompletionStream(
|
125 |
{
|
126 |
model: selectedModel.value,
|
127 |
+
provider: selectedProvider.id as any,
|
128 |
messages: [
|
129 |
{
|
130 |
role: "system",
|
131 |
+
content: INITIAL_SYSTEM_PROMPT,
|
132 |
},
|
133 |
+
...(pages?.length > 1 ? [{
|
134 |
+
role: "assistant",
|
135 |
+
content: `Here are the current pages:\n\n${pages.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}\n\nNow, please create a new page based on this code. Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
|
136 |
+
}] : []),
|
137 |
{
|
138 |
role: "user",
|
139 |
+
content: redesignMarkdown
|
140 |
+
? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
|
141 |
+
: rewrittenPrompt,
|
142 |
},
|
143 |
],
|
144 |
+
max_tokens: selectedProvider.max_tokens,
|
145 |
},
|
146 |
billTo ? { billTo } : {}
|
147 |
);
|
148 |
|
149 |
while (true) {
|
150 |
+
const { done, value } = await chatCompletion.next();
|
151 |
if (done) {
|
152 |
break;
|
153 |
}
|
|
|
157 |
await writer.write(encoder.encode(chunk));
|
158 |
}
|
159 |
}
|
|
|
|
|
|
|
160 |
} catch (error: any) {
|
161 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
162 |
await writer.write(
|
|
|
168 |
})
|
169 |
)
|
170 |
);
|
171 |
+
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
await writer.write(
|
173 |
encoder.encode(
|
174 |
JSON.stringify({
|
|
|
181 |
);
|
182 |
}
|
183 |
} finally {
|
184 |
+
await writer?.close();
|
|
|
|
|
|
|
|
|
|
|
185 |
}
|
186 |
})();
|
187 |
|
|
|
200 |
}
|
201 |
|
202 |
export async function PUT(request: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
203 |
const authHeaders = await headers();
|
204 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
205 |
|
206 |
const body = await request.json();
|
207 |
+
const { prompt, previousPrompts, provider, selectedElementHtml, model, pages, files, } =
|
208 |
body;
|
209 |
|
|
|
|
|
210 |
if (!prompt || pages.length === 0) {
|
211 |
return NextResponse.json(
|
212 |
{ ok: false, error: "Missing required fields" },
|
|
|
224 |
);
|
225 |
}
|
226 |
|
227 |
+
let token = userToken;
|
228 |
let billTo: string | null = null;
|
229 |
|
230 |
/**
|
|
|
259 |
|
260 |
const client = new InferenceClient(token);
|
261 |
|
262 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
263 |
+
const selectedProvider =
|
264 |
+
provider === "auto"
|
265 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
266 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
|
268 |
try {
|
269 |
+
const response = await client.chatCompletion(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
270 |
{
|
271 |
model: selectedModel.value,
|
272 |
+
provider: selectedProvider.id as any,
|
273 |
messages: [
|
274 |
{
|
275 |
role: "system",
|
276 |
+
content: FOLLOW_UP_SYSTEM_PROMPT,
|
277 |
},
|
278 |
{
|
279 |
role: "user",
|
280 |
+
content: previousPrompts
|
281 |
+
? `Also here are the previous prompts:\n\n${previousPrompts.map((p: string) => `- ${p}`).join("\n")}`
|
282 |
+
: "You are modifying the HTML file based on the user's request.",
|
283 |
},
|
284 |
{
|
285 |
role: "assistant",
|
286 |
+
|
287 |
+
content: `${
|
288 |
+
selectedElementHtml
|
289 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
290 |
+
: ""
|
291 |
+
}. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
|
292 |
},
|
293 |
{
|
294 |
role: "user",
|
295 |
content: prompt,
|
296 |
},
|
297 |
],
|
298 |
+
...(selectedProvider.id !== "sambanova"
|
299 |
+
? {
|
300 |
+
max_tokens: selectedProvider.max_tokens,
|
301 |
+
}
|
302 |
+
: {}),
|
303 |
},
|
304 |
billTo ? { billTo } : {}
|
305 |
);
|
306 |
|
307 |
+
const chunk = response.choices[0]?.message?.content;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
308 |
if (!chunk) {
|
309 |
return NextResponse.json(
|
310 |
{ ok: false, message: "No content returned from the model" },
|
|
|
367 |
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
368 |
updatedLines.push([1, replaceBlock.split("\n").length]);
|
369 |
} else {
|
370 |
+
const blockPosition = pageHtml.indexOf(searchBlock);
|
371 |
+
if (blockPosition !== -1) {
|
372 |
+
const beforeText = pageHtml.substring(0, blockPosition);
|
|
|
|
|
|
|
373 |
const startLineNumber = beforeText.split("\n").length;
|
374 |
const replaceLines = replaceBlock.split("\n").length;
|
375 |
const endLineNumber = startLineNumber + replaceLines - 1;
|
376 |
|
377 |
updatedLines.push([startLineNumber, endLineNumber]);
|
378 |
+
pageHtml = pageHtml.replace(searchBlock, replaceBlock);
|
379 |
}
|
380 |
}
|
381 |
|
|
|
453 |
newHtml = `${replaceBlock}\n${newHtml}`;
|
454 |
updatedLines.push([1, replaceBlock.split("\n").length]);
|
455 |
} else {
|
456 |
+
const blockPosition = newHtml.indexOf(searchBlock);
|
457 |
+
if (blockPosition !== -1) {
|
458 |
+
const beforeText = newHtml.substring(0, blockPosition);
|
|
|
|
|
|
|
459 |
const startLineNumber = beforeText.split("\n").length;
|
460 |
const replaceLines = replaceBlock.split("\n").length;
|
461 |
const endLineNumber = startLineNumber + replaceLines - 1;
|
462 |
|
463 |
updatedLines.push([startLineNumber, endLineNumber]);
|
464 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
465 |
}
|
466 |
}
|
467 |
|
|
|
475 |
}
|
476 |
}
|
477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
478 |
return NextResponse.json({
|
479 |
ok: true,
|
480 |
updatedLines,
|
481 |
pages: updatedPages,
|
|
|
|
|
|
|
|
|
|
|
482 |
});
|
483 |
} else {
|
484 |
return NextResponse.json(
|
|
|
508 |
);
|
509 |
}
|
510 |
}
|
|
app/api/auth/login-url/route.ts
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
|
3 |
-
export async function GET(req: NextRequest) {
|
4 |
-
const host = req.headers.get("host") ?? "localhost:3000";
|
5 |
-
|
6 |
-
let url: string;
|
7 |
-
if (host.includes("localhost")) {
|
8 |
-
url = host;
|
9 |
-
} else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
|
10 |
-
url = "enzostvs-deepsite.hf.space";
|
11 |
-
} else {
|
12 |
-
url = "deepsite.hf.co";
|
13 |
-
}
|
14 |
-
|
15 |
-
const redirect_uri =
|
16 |
-
`${host.includes("localhost") ? "http://" : "https://"}` +
|
17 |
-
url +
|
18 |
-
"/auth/callback";
|
19 |
-
|
20 |
-
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
21 |
-
|
22 |
-
return NextResponse.json({ loginUrl: loginRedirectUrl });
|
23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/logout/route.ts
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
import { NextResponse } from "next/server";
|
2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
3 |
-
|
4 |
-
export async function POST() {
|
5 |
-
const cookieName = MY_TOKEN_KEY();
|
6 |
-
const isProduction = process.env.NODE_ENV === "production";
|
7 |
-
|
8 |
-
const response = NextResponse.json(
|
9 |
-
{ message: "Logged out successfully" },
|
10 |
-
{ status: 200 }
|
11 |
-
);
|
12 |
-
|
13 |
-
// Clear the HTTP-only cookie
|
14 |
-
const cookieOptions = [
|
15 |
-
`${cookieName}=`,
|
16 |
-
"Max-Age=0",
|
17 |
-
"Path=/",
|
18 |
-
"HttpOnly",
|
19 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
20 |
-
].join("; ");
|
21 |
-
|
22 |
-
response.headers.set("Set-Cookie", cookieOptions);
|
23 |
-
|
24 |
-
return response;
|
25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/route.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
3 |
|
4 |
export async function POST(req: NextRequest) {
|
5 |
const body = await req.json();
|
@@ -71,17 +70,11 @@ export async function POST(req: NextRequest) {
|
|
71 |
}
|
72 |
const user = await userResponse.json();
|
73 |
|
74 |
-
|
75 |
-
const isProduction = process.env.NODE_ENV === "production";
|
76 |
-
|
77 |
-
// Create response with user data
|
78 |
-
const nextResponse = NextResponse.json(
|
79 |
{
|
80 |
access_token: response.access_token,
|
81 |
expires_in: response.expires_in,
|
82 |
user,
|
83 |
-
// Include fallback flag for iframe contexts
|
84 |
-
useLocalStorageFallback: true,
|
85 |
},
|
86 |
{
|
87 |
status: 200,
|
@@ -90,17 +83,4 @@ export async function POST(req: NextRequest) {
|
|
90 |
},
|
91 |
}
|
92 |
);
|
93 |
-
|
94 |
-
// Set HTTP-only cookie with proper attributes for iframe support
|
95 |
-
const cookieOptions = [
|
96 |
-
`${cookieName}=${response.access_token}`,
|
97 |
-
`Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
|
98 |
-
"Path=/",
|
99 |
-
"HttpOnly",
|
100 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
101 |
-
].join("; ");
|
102 |
-
|
103 |
-
nextResponse.headers.set("Set-Cookie", cookieOptions);
|
104 |
-
|
105 |
-
return nextResponse;
|
106 |
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
2 |
|
3 |
export async function POST(req: NextRequest) {
|
4 |
const body = await req.json();
|
|
|
70 |
}
|
71 |
const user = await userResponse.json();
|
72 |
|
73 |
+
return NextResponse.json(
|
|
|
|
|
|
|
|
|
74 |
{
|
75 |
access_token: response.access_token,
|
76 |
expires_in: response.expires_in,
|
77 |
user,
|
|
|
|
|
78 |
},
|
79 |
{
|
80 |
status: 200,
|
|
|
83 |
},
|
84 |
}
|
85 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
}
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
DELETED
@@ -1,190 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export async function POST(
|
8 |
-
req: NextRequest,
|
9 |
-
{ params }: {
|
10 |
-
params: Promise<{
|
11 |
-
namespace: string;
|
12 |
-
repoId: string;
|
13 |
-
commitId: string;
|
14 |
-
}>
|
15 |
-
}
|
16 |
-
) {
|
17 |
-
const user = await isAuthenticated();
|
18 |
-
|
19 |
-
if (user instanceof NextResponse || !user) {
|
20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
21 |
-
}
|
22 |
-
|
23 |
-
const param = await params;
|
24 |
-
const { namespace, repoId, commitId } = param;
|
25 |
-
|
26 |
-
try {
|
27 |
-
const repo: RepoDesignation = {
|
28 |
-
type: "space",
|
29 |
-
name: `${namespace}/${repoId}`,
|
30 |
-
};
|
31 |
-
|
32 |
-
const space = await spaceInfo({
|
33 |
-
name: `${namespace}/${repoId}`,
|
34 |
-
accessToken: user.token as string,
|
35 |
-
additionalFields: ["author"],
|
36 |
-
});
|
37 |
-
|
38 |
-
if (!space || space.sdk !== "static") {
|
39 |
-
return NextResponse.json(
|
40 |
-
{ ok: false, error: "Space is not a static space." },
|
41 |
-
{ status: 404 }
|
42 |
-
);
|
43 |
-
}
|
44 |
-
|
45 |
-
if (space.author !== user.name) {
|
46 |
-
return NextResponse.json(
|
47 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
48 |
-
{ status: 403 }
|
49 |
-
);
|
50 |
-
}
|
51 |
-
|
52 |
-
// Fetch files from the specific commit
|
53 |
-
const files: File[] = [];
|
54 |
-
const pages: Page[] = [];
|
55 |
-
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
56 |
-
const commitFilePaths: Set<string> = new Set();
|
57 |
-
|
58 |
-
// Get all files from the specific commit
|
59 |
-
for await (const fileInfo of listFiles({
|
60 |
-
repo,
|
61 |
-
accessToken: user.token as string,
|
62 |
-
revision: commitId,
|
63 |
-
})) {
|
64 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
65 |
-
|
66 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
67 |
-
commitFilePaths.add(fileInfo.path);
|
68 |
-
|
69 |
-
// Fetch the file content from the specific commit
|
70 |
-
const response = await fetch(
|
71 |
-
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
72 |
-
);
|
73 |
-
|
74 |
-
if (response.ok) {
|
75 |
-
const content = await response.text();
|
76 |
-
let mimeType = "text/plain";
|
77 |
-
|
78 |
-
switch (fileExtension) {
|
79 |
-
case "html":
|
80 |
-
mimeType = "text/html";
|
81 |
-
// Add HTML files to pages array for client-side setPages
|
82 |
-
pages.push({
|
83 |
-
path: fileInfo.path,
|
84 |
-
html: content,
|
85 |
-
});
|
86 |
-
break;
|
87 |
-
case "css":
|
88 |
-
mimeType = "text/css";
|
89 |
-
break;
|
90 |
-
case "js":
|
91 |
-
mimeType = "application/javascript";
|
92 |
-
break;
|
93 |
-
case "json":
|
94 |
-
mimeType = "application/json";
|
95 |
-
break;
|
96 |
-
case "md":
|
97 |
-
mimeType = "text/markdown";
|
98 |
-
break;
|
99 |
-
}
|
100 |
-
|
101 |
-
const file = new File([content], fileInfo.path, { type: mimeType });
|
102 |
-
files.push(file);
|
103 |
-
}
|
104 |
-
}
|
105 |
-
}
|
106 |
-
|
107 |
-
// Get files currently in main branch to identify files to delete
|
108 |
-
const mainBranchFilePaths: Set<string> = new Set();
|
109 |
-
for await (const fileInfo of listFiles({
|
110 |
-
repo,
|
111 |
-
accessToken: user.token as string,
|
112 |
-
revision: "main",
|
113 |
-
})) {
|
114 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
115 |
-
|
116 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
117 |
-
mainBranchFilePaths.add(fileInfo.path);
|
118 |
-
}
|
119 |
-
}
|
120 |
-
|
121 |
-
// Identify files to delete (exist in main but not in commit)
|
122 |
-
const filesToDelete: string[] = [];
|
123 |
-
for (const mainFilePath of mainBranchFilePaths) {
|
124 |
-
if (!commitFilePaths.has(mainFilePath)) {
|
125 |
-
filesToDelete.push(mainFilePath);
|
126 |
-
}
|
127 |
-
}
|
128 |
-
|
129 |
-
if (files.length === 0 && filesToDelete.length === 0) {
|
130 |
-
return NextResponse.json(
|
131 |
-
{ ok: false, error: "No files found in the specified commit and no files to delete" },
|
132 |
-
{ status: 404 }
|
133 |
-
);
|
134 |
-
}
|
135 |
-
|
136 |
-
// Delete files that exist in main but not in the commit being promoted
|
137 |
-
if (filesToDelete.length > 0) {
|
138 |
-
await deleteFiles({
|
139 |
-
repo,
|
140 |
-
paths: filesToDelete,
|
141 |
-
accessToken: user.token as string,
|
142 |
-
commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
|
143 |
-
commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
|
144 |
-
});
|
145 |
-
}
|
146 |
-
|
147 |
-
// Upload the files to the main branch with a promotion commit message
|
148 |
-
if (files.length > 0) {
|
149 |
-
await uploadFiles({
|
150 |
-
repo,
|
151 |
-
files,
|
152 |
-
accessToken: user.token as string,
|
153 |
-
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
154 |
-
commitDescription: `Promoted commit ${commitId} to main branch`,
|
155 |
-
});
|
156 |
-
}
|
157 |
-
|
158 |
-
return NextResponse.json(
|
159 |
-
{
|
160 |
-
ok: true,
|
161 |
-
message: "Version promoted successfully",
|
162 |
-
promotedCommit: commitId,
|
163 |
-
pages: pages,
|
164 |
-
},
|
165 |
-
{ status: 200 }
|
166 |
-
);
|
167 |
-
|
168 |
-
} catch (error: any) {
|
169 |
-
|
170 |
-
// Handle specific HuggingFace API errors
|
171 |
-
if (error.statusCode === 404) {
|
172 |
-
return NextResponse.json(
|
173 |
-
{ ok: false, error: "Commit not found" },
|
174 |
-
{ status: 404 }
|
175 |
-
);
|
176 |
-
}
|
177 |
-
|
178 |
-
if (error.statusCode === 403) {
|
179 |
-
return NextResponse.json(
|
180 |
-
{ ok: false, error: "Access denied to repository" },
|
181 |
-
{ status: 403 }
|
182 |
-
);
|
183 |
-
}
|
184 |
-
|
185 |
-
return NextResponse.json(
|
186 |
-
{ ok: false, error: error.message || "Failed to promote version" },
|
187 |
-
{ status: 500 }
|
188 |
-
);
|
189 |
-
}
|
190 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation,
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
import Project from "@/models/Project";
|
6 |
import dbConnect from "@/lib/mongodb";
|
7 |
|
|
|
|
|
8 |
export async function POST(
|
9 |
req: NextRequest,
|
10 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
@@ -16,26 +18,22 @@ export async function POST(
|
|
16 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
}
|
18 |
|
|
|
19 |
const param = await params;
|
20 |
const { namespace, repoId } = param;
|
21 |
|
22 |
-
const
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
});
|
27 |
-
|
28 |
-
if (!space || space.sdk !== "static") {
|
29 |
-
return NextResponse.json(
|
30 |
-
{ ok: false, error: "Space is not a static space." },
|
31 |
-
{ status: 404 }
|
32 |
-
);
|
33 |
-
}
|
34 |
|
35 |
-
if (
|
36 |
return NextResponse.json(
|
37 |
-
{
|
38 |
-
|
|
|
|
|
|
|
39 |
);
|
40 |
}
|
41 |
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { RepoDesignation, uploadFiles } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
import Project from "@/models/Project";
|
6 |
import dbConnect from "@/lib/mongodb";
|
7 |
|
8 |
+
// No longer need the ImageUpload interface since we're handling FormData with File objects
|
9 |
+
|
10 |
export async function POST(
|
11 |
req: NextRequest,
|
12 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
|
|
18 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
19 |
}
|
20 |
|
21 |
+
await dbConnect();
|
22 |
const param = await params;
|
23 |
const { namespace, repoId } = param;
|
24 |
|
25 |
+
const project = await Project.findOne({
|
26 |
+
user_id: user.id,
|
27 |
+
space_id: `${namespace}/${repoId}`,
|
28 |
+
}).lean();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
+
if (!project) {
|
31 |
return NextResponse.json(
|
32 |
+
{
|
33 |
+
ok: false,
|
34 |
+
error: "Project not found",
|
35 |
+
},
|
36 |
+
{ status: 404 }
|
37 |
);
|
38 |
}
|
39 |
|
app/api/me/projects/[namespace]/[repoId]/route.ts
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, spaceInfo,
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
|
|
|
|
6 |
|
7 |
-
export async function
|
8 |
req: NextRequest,
|
9 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
10 |
) {
|
@@ -14,63 +16,23 @@ export async function DELETE(
|
|
14 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
15 |
}
|
16 |
|
|
|
17 |
const param = await params;
|
18 |
const { namespace, repoId } = param;
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
});
|
26 |
-
|
27 |
-
if (!space || space.sdk !== "static") {
|
28 |
-
return NextResponse.json(
|
29 |
-
{ ok: false, error: "Space is not a static space." },
|
30 |
-
{ status: 404 }
|
31 |
-
);
|
32 |
-
}
|
33 |
-
|
34 |
-
if (space.author !== user.name) {
|
35 |
-
return NextResponse.json(
|
36 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
37 |
-
{ status: 403 }
|
38 |
-
);
|
39 |
-
}
|
40 |
-
|
41 |
-
const repo: RepoDesignation = {
|
42 |
-
type: "space",
|
43 |
-
name: `${namespace}/${repoId}`,
|
44 |
-
};
|
45 |
-
|
46 |
-
await deleteRepo({
|
47 |
-
repo,
|
48 |
-
accessToken: user.token as string,
|
49 |
-
});
|
50 |
-
|
51 |
-
|
52 |
-
return NextResponse.json({ ok: true }, { status: 200 });
|
53 |
-
} catch (error: any) {
|
54 |
return NextResponse.json(
|
55 |
-
{
|
56 |
-
|
|
|
|
|
|
|
57 |
);
|
58 |
}
|
59 |
-
}
|
60 |
-
|
61 |
-
export async function GET(
|
62 |
-
req: NextRequest,
|
63 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
64 |
-
) {
|
65 |
-
const user = await isAuthenticated();
|
66 |
-
|
67 |
-
if (user instanceof NextResponse || !user) {
|
68 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
69 |
-
}
|
70 |
-
|
71 |
-
const param = await params;
|
72 |
-
const { namespace, repoId } = param;
|
73 |
-
|
74 |
try {
|
75 |
const space = await spaceInfo({
|
76 |
name: namespace + "/" + repoId,
|
@@ -103,49 +65,37 @@ export async function GET(
|
|
103 |
};
|
104 |
|
105 |
const htmlFiles: Page[] = [];
|
106 |
-
const
|
107 |
|
108 |
-
const
|
109 |
|
110 |
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
111 |
if (fileInfo.path.endsWith(".html")) {
|
112 |
-
const
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
});
|
122 |
-
} else {
|
123 |
htmlFiles.push({
|
124 |
path: fileInfo.path,
|
125 |
-
|
126 |
-
|
|
|
127 |
}
|
128 |
}
|
129 |
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
130 |
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
131 |
-
if (
|
132 |
-
|
133 |
}
|
134 |
}
|
135 |
}
|
136 |
}
|
137 |
-
|
138 |
-
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
139 |
-
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
|
140 |
-
continue;
|
141 |
-
}
|
142 |
-
commits.push({
|
143 |
-
title: commit.title,
|
144 |
-
oid: commit.oid,
|
145 |
-
date: commit.date,
|
146 |
-
});
|
147 |
-
}
|
148 |
-
|
149 |
if (htmlFiles.length === 0) {
|
150 |
return NextResponse.json(
|
151 |
{
|
@@ -155,17 +105,14 @@ export async function GET(
|
|
155 |
{ status: 404 }
|
156 |
);
|
157 |
}
|
|
|
158 |
return NextResponse.json(
|
159 |
{
|
160 |
project: {
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
_updatedAt: space.updatedAt,
|
165 |
},
|
166 |
-
pages: htmlFiles,
|
167 |
-
files,
|
168 |
-
commits,
|
169 |
ok: true,
|
170 |
},
|
171 |
{ status: 200 }
|
@@ -174,6 +121,10 @@ export async function GET(
|
|
174 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
175 |
} catch (error: any) {
|
176 |
if (error.statusCode === 404) {
|
|
|
|
|
|
|
|
|
177 |
return NextResponse.json(
|
178 |
{ error: "Space not found", ok: false },
|
179 |
{ status: 404 }
|
@@ -185,3 +136,141 @@ export async function GET(
|
|
185 |
);
|
186 |
}
|
187 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { RepoDesignation, spaceInfo, uploadFiles, listFiles } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
import { Page } from "@/types";
|
8 |
|
9 |
+
export async function GET(
|
10 |
req: NextRequest,
|
11 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
12 |
) {
|
|
|
16 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
}
|
18 |
|
19 |
+
await dbConnect();
|
20 |
const param = await params;
|
21 |
const { namespace, repoId } = param;
|
22 |
|
23 |
+
const project = await Project.findOne({
|
24 |
+
user_id: user.id,
|
25 |
+
space_id: `${namespace}/${repoId}`,
|
26 |
+
}).lean();
|
27 |
+
if (!project) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
return NextResponse.json(
|
29 |
+
{
|
30 |
+
ok: false,
|
31 |
+
error: "Project not found",
|
32 |
+
},
|
33 |
+
{ status: 404 }
|
34 |
);
|
35 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
try {
|
37 |
const space = await spaceInfo({
|
38 |
name: namespace + "/" + repoId,
|
|
|
65 |
};
|
66 |
|
67 |
const htmlFiles: Page[] = [];
|
68 |
+
const images: string[] = [];
|
69 |
|
70 |
+
const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
71 |
|
72 |
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
73 |
if (fileInfo.path.endsWith(".html")) {
|
74 |
+
const res = await fetch(`https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/${fileInfo.path}`);
|
75 |
+
if (res.ok) {
|
76 |
+
const html = await res.text();
|
77 |
+
if (fileInfo.path === "index.html") {
|
78 |
+
htmlFiles.unshift({
|
79 |
+
path: fileInfo.path,
|
80 |
+
html,
|
81 |
+
});
|
82 |
+
} else {
|
|
|
|
|
83 |
htmlFiles.push({
|
84 |
path: fileInfo.path,
|
85 |
+
html,
|
86 |
+
});
|
87 |
+
}
|
88 |
}
|
89 |
}
|
90 |
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
91 |
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
92 |
+
if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
93 |
+
images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
94 |
}
|
95 |
}
|
96 |
}
|
97 |
}
|
98 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
if (htmlFiles.length === 0) {
|
100 |
return NextResponse.json(
|
101 |
{
|
|
|
105 |
{ status: 404 }
|
106 |
);
|
107 |
}
|
108 |
+
|
109 |
return NextResponse.json(
|
110 |
{
|
111 |
project: {
|
112 |
+
...project,
|
113 |
+
pages: htmlFiles,
|
114 |
+
images,
|
|
|
115 |
},
|
|
|
|
|
|
|
116 |
ok: true,
|
117 |
},
|
118 |
{ status: 200 }
|
|
|
121 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
122 |
} catch (error: any) {
|
123 |
if (error.statusCode === 404) {
|
124 |
+
await Project.deleteOne({
|
125 |
+
user_id: user.id,
|
126 |
+
space_id: `${namespace}/${repoId}`,
|
127 |
+
});
|
128 |
return NextResponse.json(
|
129 |
{ error: "Space not found", ok: false },
|
130 |
{ status: 404 }
|
|
|
136 |
);
|
137 |
}
|
138 |
}
|
139 |
+
|
140 |
+
export async function PUT(
|
141 |
+
req: NextRequest,
|
142 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
143 |
+
) {
|
144 |
+
const user = await isAuthenticated();
|
145 |
+
|
146 |
+
if (user instanceof NextResponse || !user) {
|
147 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
148 |
+
}
|
149 |
+
|
150 |
+
await dbConnect();
|
151 |
+
const param = await params;
|
152 |
+
const { namespace, repoId } = param;
|
153 |
+
const { pages, prompts } = await req.json();
|
154 |
+
|
155 |
+
const project = await Project.findOne({
|
156 |
+
user_id: user.id,
|
157 |
+
space_id: `${namespace}/${repoId}`,
|
158 |
+
}).lean();
|
159 |
+
if (!project) {
|
160 |
+
return NextResponse.json(
|
161 |
+
{
|
162 |
+
ok: false,
|
163 |
+
error: "Project not found",
|
164 |
+
},
|
165 |
+
{ status: 404 }
|
166 |
+
);
|
167 |
+
}
|
168 |
+
|
169 |
+
const repo: RepoDesignation = {
|
170 |
+
type: "space",
|
171 |
+
name: `${namespace}/${repoId}`,
|
172 |
+
};
|
173 |
+
|
174 |
+
const files: File[] = [];
|
175 |
+
const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
|
176 |
+
type: "text/plain",
|
177 |
+
});
|
178 |
+
files.push(promptsFile);
|
179 |
+
pages.forEach((page: Page) => {
|
180 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
181 |
+
files.push(file);
|
182 |
+
});
|
183 |
+
await uploadFiles({
|
184 |
+
repo,
|
185 |
+
files,
|
186 |
+
accessToken: user.token as string,
|
187 |
+
commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
|
188 |
+
});
|
189 |
+
|
190 |
+
await Project.updateOne(
|
191 |
+
{ user_id: user.id, space_id: `${namespace}/${repoId}` },
|
192 |
+
{
|
193 |
+
$set: {
|
194 |
+
prompts: [
|
195 |
+
...prompts,
|
196 |
+
],
|
197 |
+
},
|
198 |
+
}
|
199 |
+
);
|
200 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
201 |
+
}
|
202 |
+
|
203 |
+
export async function POST(
|
204 |
+
req: NextRequest,
|
205 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
206 |
+
) {
|
207 |
+
const user = await isAuthenticated();
|
208 |
+
|
209 |
+
if (user instanceof NextResponse || !user) {
|
210 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
211 |
+
}
|
212 |
+
|
213 |
+
await dbConnect();
|
214 |
+
const param = await params;
|
215 |
+
const { namespace, repoId } = param;
|
216 |
+
|
217 |
+
const space = await spaceInfo({
|
218 |
+
name: namespace + "/" + repoId,
|
219 |
+
accessToken: user.token as string,
|
220 |
+
additionalFields: ["author"],
|
221 |
+
});
|
222 |
+
|
223 |
+
if (!space || space.sdk !== "static") {
|
224 |
+
return NextResponse.json(
|
225 |
+
{
|
226 |
+
ok: false,
|
227 |
+
error: "Space is not a static space",
|
228 |
+
},
|
229 |
+
{ status: 404 }
|
230 |
+
);
|
231 |
+
}
|
232 |
+
if (space.author !== user.name) {
|
233 |
+
return NextResponse.json(
|
234 |
+
{
|
235 |
+
ok: false,
|
236 |
+
error: "Space does not belong to the authenticated user",
|
237 |
+
},
|
238 |
+
{ status: 403 }
|
239 |
+
);
|
240 |
+
}
|
241 |
+
|
242 |
+
const project = await Project.findOne({
|
243 |
+
user_id: user.id,
|
244 |
+
space_id: `${namespace}/${repoId}`,
|
245 |
+
}).lean();
|
246 |
+
if (project) {
|
247 |
+
// redirect to the project page if it already exists
|
248 |
+
return NextResponse.json(
|
249 |
+
{
|
250 |
+
ok: false,
|
251 |
+
error: "Project already exists",
|
252 |
+
redirect: `/projects/${namespace}/${repoId}`,
|
253 |
+
},
|
254 |
+
{ status: 400 }
|
255 |
+
);
|
256 |
+
}
|
257 |
+
|
258 |
+
const newProject = new Project({
|
259 |
+
user_id: user.id,
|
260 |
+
space_id: `${namespace}/${repoId}`,
|
261 |
+
prompts: [],
|
262 |
+
});
|
263 |
+
|
264 |
+
await newProject.save();
|
265 |
+
return NextResponse.json(
|
266 |
+
{
|
267 |
+
ok: true,
|
268 |
+
project: {
|
269 |
+
id: newProject._id,
|
270 |
+
space_id: newProject.space_id,
|
271 |
+
prompts: newProject.prompts,
|
272 |
+
},
|
273 |
+
},
|
274 |
+
{ status: 201 }
|
275 |
+
);
|
276 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
DELETED
@@ -1,64 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { uploadFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export async function PUT(
|
8 |
-
req: NextRequest,
|
9 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
10 |
-
) {
|
11 |
-
const user = await isAuthenticated();
|
12 |
-
if (user instanceof NextResponse || !user) {
|
13 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
-
}
|
15 |
-
|
16 |
-
const param = await params;
|
17 |
-
const { namespace, repoId } = param;
|
18 |
-
const { pages, commitTitle = "Manual changes saved" } = await req.json();
|
19 |
-
|
20 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
21 |
-
return NextResponse.json(
|
22 |
-
{ ok: false, error: "Pages are required" },
|
23 |
-
{ status: 400 }
|
24 |
-
);
|
25 |
-
}
|
26 |
-
|
27 |
-
try {
|
28 |
-
// Prepare files for upload
|
29 |
-
const files: File[] = [];
|
30 |
-
pages.forEach((page: Page) => {
|
31 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
32 |
-
files.push(file);
|
33 |
-
});
|
34 |
-
|
35 |
-
// Upload files to HuggingFace Hub
|
36 |
-
const response = await uploadFiles({
|
37 |
-
repo: {
|
38 |
-
type: "space",
|
39 |
-
name: `${namespace}/${repoId}`,
|
40 |
-
},
|
41 |
-
files,
|
42 |
-
commitTitle,
|
43 |
-
accessToken: user.token as string,
|
44 |
-
});
|
45 |
-
|
46 |
-
return NextResponse.json({
|
47 |
-
ok: true,
|
48 |
-
pages,
|
49 |
-
commit: {
|
50 |
-
...response.commit,
|
51 |
-
title: commitTitle,
|
52 |
-
}
|
53 |
-
});
|
54 |
-
} catch (error: any) {
|
55 |
-
console.error("Error saving manual changes:", error);
|
56 |
-
return NextResponse.json(
|
57 |
-
{
|
58 |
-
ok: false,
|
59 |
-
error: error.message || "Failed to save changes",
|
60 |
-
},
|
61 |
-
{ status: 500 }
|
62 |
-
);
|
63 |
-
}
|
64 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/route.ts
CHANGED
@@ -1,107 +1,127 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import {
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
|
|
6 |
import { COLORS } from "@/lib/utils";
|
|
|
7 |
|
8 |
-
export async function
|
9 |
-
req: NextRequest,
|
10 |
-
) {
|
11 |
const user = await isAuthenticated();
|
|
|
12 |
if (user instanceof NextResponse || !user) {
|
13 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
}
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
const
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
colorFrom: ${colorFrom}
|
37 |
colorTo: ${colorTo}
|
38 |
-
emoji: 🐳
|
39 |
sdk: static
|
40 |
pinned: false
|
41 |
tags:
|
42 |
-
- deepsite
|
43 |
---
|
44 |
|
45 |
-
|
46 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
47 |
-
`;
|
48 |
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
repo,
|
60 |
-
accessToken: user.token as string,
|
61 |
});
|
62 |
-
const commitTitle = !prompt || prompt.trim() === "" ? "Redesign my website" : prompt;
|
63 |
await uploadFiles({
|
64 |
repo,
|
65 |
files,
|
66 |
accessToken: user.token as string,
|
67 |
-
commitTitle
|
68 |
});
|
69 |
-
|
70 |
const path = repoUrl.split("/").slice(-2).join("/");
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
continue;
|
76 |
-
}
|
77 |
-
commits.push({
|
78 |
-
title: commit.title,
|
79 |
-
oid: commit.oid,
|
80 |
-
date: commit.date,
|
81 |
-
});
|
82 |
-
}
|
83 |
-
|
84 |
-
const space = await spaceInfo({
|
85 |
-
name: repo.name,
|
86 |
-
accessToken: user.token as string,
|
87 |
});
|
88 |
-
|
89 |
-
|
90 |
-
files,
|
91 |
-
pages,
|
92 |
-
commits,
|
93 |
-
project: {
|
94 |
-
id: space.id,
|
95 |
-
space_id: space.name,
|
96 |
-
_updatedAt: space.updatedAt,
|
97 |
-
}
|
98 |
-
}
|
99 |
-
|
100 |
-
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
101 |
} catch (err: any) {
|
102 |
return NextResponse.json(
|
103 |
{ error: err.message, ok: false },
|
104 |
{ status: 500 }
|
105 |
);
|
106 |
}
|
107 |
-
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
import { COLORS } from "@/lib/utils";
|
8 |
+
import { Page } from "@/types";
|
9 |
|
10 |
+
export async function GET() {
|
|
|
|
|
11 |
const user = await isAuthenticated();
|
12 |
+
|
13 |
if (user instanceof NextResponse || !user) {
|
14 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
15 |
}
|
16 |
|
17 |
+
await dbConnect();
|
18 |
+
|
19 |
+
const projects = await Project.find({
|
20 |
+
user_id: user?.id,
|
21 |
+
})
|
22 |
+
.sort({ _createdAt: -1 })
|
23 |
+
.limit(100)
|
24 |
+
.lean();
|
25 |
+
if (!projects) {
|
26 |
+
return NextResponse.json(
|
27 |
+
{
|
28 |
+
ok: false,
|
29 |
+
projects: [],
|
30 |
+
},
|
31 |
+
{ status: 404 }
|
32 |
+
);
|
33 |
+
}
|
34 |
+
return NextResponse.json(
|
35 |
+
{
|
36 |
+
ok: true,
|
37 |
+
projects,
|
38 |
+
},
|
39 |
+
{ status: 200 }
|
40 |
+
);
|
41 |
+
}
|
42 |
+
|
43 |
+
export async function POST(request: NextRequest) {
|
44 |
+
const user = await isAuthenticated();
|
45 |
+
|
46 |
+
if (user instanceof NextResponse || !user) {
|
47 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
48 |
+
}
|
49 |
+
|
50 |
+
const { title, pages, prompts } = await request.json();
|
51 |
+
|
52 |
+
if (!title || !pages || pages.length === 0) {
|
53 |
+
return NextResponse.json(
|
54 |
+
{ message: "Title and HTML content are required.", ok: false },
|
55 |
+
{ status: 400 }
|
56 |
+
);
|
57 |
+
}
|
58 |
+
|
59 |
+
await dbConnect();
|
60 |
+
|
61 |
+
try {
|
62 |
+
let readme = "";
|
63 |
+
|
64 |
+
const newTitle = title
|
65 |
+
.toLowerCase()
|
66 |
+
.replace(/[^a-z0-9]+/g, "-")
|
67 |
+
.split("-")
|
68 |
+
.filter(Boolean)
|
69 |
+
.join("-")
|
70 |
+
.slice(0, 96);
|
71 |
+
|
72 |
+
const repo: RepoDesignation = {
|
73 |
+
type: "space",
|
74 |
+
name: `${user.name}/${newTitle}`,
|
75 |
+
};
|
76 |
+
|
77 |
+
const { repoUrl } = await createRepo({
|
78 |
+
repo,
|
79 |
+
accessToken: user.token as string,
|
80 |
+
});
|
81 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
82 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
83 |
+
readme = `---
|
84 |
+
title: ${newTitle}
|
85 |
+
emoji: 🐳
|
86 |
colorFrom: ${colorFrom}
|
87 |
colorTo: ${colorTo}
|
|
|
88 |
sdk: static
|
89 |
pinned: false
|
90 |
tags:
|
91 |
+
- deepsite
|
92 |
---
|
93 |
|
94 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
|
|
|
|
|
95 |
|
96 |
+
const readmeFile = new File([readme], "README.md", {
|
97 |
+
type: "text/markdown",
|
98 |
+
});
|
99 |
+
const promptsFile = new File([prompts.join("\n")], "prompts.txt", {
|
100 |
+
type: "text/plain",
|
101 |
+
});
|
102 |
+
const files = [readmeFile, promptsFile];
|
103 |
+
pages.forEach((page: Page) => {
|
104 |
+
const file = new File([page.html], page.path, { type: "text/html" });
|
105 |
+
files.push(file);
|
|
|
|
|
106 |
});
|
|
|
107 |
await uploadFiles({
|
108 |
repo,
|
109 |
files,
|
110 |
accessToken: user.token as string,
|
111 |
+
commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
|
112 |
});
|
|
|
113 |
const path = repoUrl.split("/").slice(-2).join("/");
|
114 |
+
const project = await Project.create({
|
115 |
+
user_id: user.id,
|
116 |
+
space_id: path,
|
117 |
+
prompts,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
});
|
119 |
+
return NextResponse.json({ project, path, ok: true }, { status: 201 });
|
120 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
} catch (err: any) {
|
122 |
return NextResponse.json(
|
123 |
{ error: err.message, ok: false },
|
124 |
{ status: 500 }
|
125 |
);
|
126 |
}
|
127 |
+
}
|
app/api/me/route.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import { listSpaces } from "@huggingface/hub";
|
2 |
import { headers } from "next/headers";
|
3 |
import { NextResponse } from "next/server";
|
4 |
|
@@ -22,25 +21,5 @@ export async function GET() {
|
|
22 |
);
|
23 |
}
|
24 |
const user = await userResponse.json();
|
25 |
-
|
26 |
-
for await (const space of listSpaces({
|
27 |
-
accessToken: token.replace("Bearer ", "") as string,
|
28 |
-
additionalFields: ["author", "cardData"],
|
29 |
-
search: {
|
30 |
-
owner: user.name,
|
31 |
-
}
|
32 |
-
})) {
|
33 |
-
if (
|
34 |
-
space.sdk === "static" &&
|
35 |
-
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
36 |
-
(
|
37 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
38 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
39 |
-
)
|
40 |
-
) {
|
41 |
-
projects.push(space);
|
42 |
-
}
|
43 |
-
}
|
44 |
-
|
45 |
-
return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
|
46 |
}
|
|
|
|
|
1 |
import { headers } from "next/headers";
|
2 |
import { NextResponse } from "next/server";
|
3 |
|
|
|
21 |
);
|
22 |
}
|
23 |
const user = await userResponse.json();
|
24 |
+
return NextResponse.json({ user, errCode: null }, { status: 200 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
app/auth/callback/page.tsx
CHANGED
@@ -5,92 +5,67 @@ import { use, useState } from "react";
|
|
5 |
import { useMount, useTimeoutFn } from "react-use";
|
6 |
|
7 |
import { Button } from "@/components/ui/button";
|
8 |
-
import { AnimatedBlobs } from "@/components/animated-blobs";
|
9 |
-
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
10 |
export default function AuthCallback({
|
11 |
searchParams,
|
12 |
}: {
|
13 |
searchParams: Promise<{ code: string }>;
|
14 |
}) {
|
15 |
const [showButton, setShowButton] = useState(false);
|
16 |
-
const [isPopupAuth, setIsPopupAuth] = useState(false);
|
17 |
const { code } = use(searchParams);
|
18 |
const { loginFromCode } = useUser();
|
19 |
-
const { postMessage } = useBroadcastChannel("auth", () => {});
|
20 |
|
21 |
useMount(async () => {
|
22 |
if (code) {
|
23 |
-
|
24 |
-
setIsPopupAuth(isPopup);
|
25 |
-
|
26 |
-
if (isPopup) {
|
27 |
-
postMessage({
|
28 |
-
type: "user-oauth",
|
29 |
-
code: code,
|
30 |
-
});
|
31 |
-
|
32 |
-
setTimeout(() => {
|
33 |
-
if (window.opener) {
|
34 |
-
window.close();
|
35 |
-
}
|
36 |
-
}, 1000);
|
37 |
-
} else {
|
38 |
-
await loginFromCode(code);
|
39 |
-
}
|
40 |
}
|
41 |
});
|
42 |
|
43 |
-
useTimeoutFn(
|
|
|
|
|
|
|
44 |
|
45 |
return (
|
46 |
-
<div className="h-screen flex flex-col justify-center items-center
|
47 |
-
<div className="
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
53 |
-
🚀
|
54 |
-
</div>
|
55 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
56 |
-
👋
|
57 |
-
</div>
|
58 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
59 |
-
🙌
|
60 |
-
</div>
|
61 |
</div>
|
62 |
-
<
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
</p>
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
78 |
</p>
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
Go to Home
|
83 |
-
</Button>
|
84 |
-
</Link>
|
85 |
-
) : (
|
86 |
-
<p className="text-xs text-neutral-500">
|
87 |
-
Please wait, we are logging you in...
|
88 |
-
</p>
|
89 |
-
)}
|
90 |
-
</div>
|
91 |
-
</main>
|
92 |
-
</div>
|
93 |
-
<AnimatedBlobs />
|
94 |
</div>
|
95 |
</div>
|
96 |
);
|
|
|
5 |
import { useMount, useTimeoutFn } from "react-use";
|
6 |
|
7 |
import { Button } from "@/components/ui/button";
|
|
|
|
|
8 |
export default function AuthCallback({
|
9 |
searchParams,
|
10 |
}: {
|
11 |
searchParams: Promise<{ code: string }>;
|
12 |
}) {
|
13 |
const [showButton, setShowButton] = useState(false);
|
|
|
14 |
const { code } = use(searchParams);
|
15 |
const { loginFromCode } = useUser();
|
|
|
16 |
|
17 |
useMount(async () => {
|
18 |
if (code) {
|
19 |
+
await loginFromCode(code);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
});
|
22 |
|
23 |
+
useTimeoutFn(
|
24 |
+
() => setShowButton(true),
|
25 |
+
7000 // Show button after 5 seconds
|
26 |
+
);
|
27 |
|
28 |
return (
|
29 |
+
<div className="h-screen flex flex-col justify-center items-center">
|
30 |
+
<div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
31 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
32 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
33 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
34 |
+
🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
</div>
|
36 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
37 |
+
👋
|
38 |
+
</div>
|
39 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
40 |
+
🙌
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
<p className="text-xl font-semibold text-neutral-950">
|
44 |
+
Login In Progress...
|
45 |
+
</p>
|
46 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
47 |
+
Wait a moment while we log you in with your code.
|
48 |
+
</p>
|
49 |
+
</header>
|
50 |
+
<main className="space-y-4 p-6">
|
51 |
+
<div>
|
52 |
+
<p className="text-sm text-neutral-700 mb-4 max-w-xs">
|
53 |
+
If you are not redirected automatically in the next 5 seconds,
|
54 |
+
please click the button below
|
55 |
</p>
|
56 |
+
{showButton ? (
|
57 |
+
<Link href="/">
|
58 |
+
<Button variant="black" className="relative">
|
59 |
+
Go to Home
|
60 |
+
</Button>
|
61 |
+
</Link>
|
62 |
+
) : (
|
63 |
+
<p className="text-xs text-neutral-500">
|
64 |
+
Please wait, we are logging you in...
|
65 |
</p>
|
66 |
+
)}
|
67 |
+
</div>
|
68 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
</div>
|
70 |
</div>
|
71 |
);
|
app/layout.tsx
CHANGED
@@ -2,18 +2,15 @@
|
|
2 |
import type { Metadata, Viewport } from "next";
|
3 |
import { Inter, PT_Sans } from "next/font/google";
|
4 |
import { cookies } from "next/headers";
|
5 |
-
import Script from "next/script";
|
6 |
|
|
|
7 |
import "@/assets/globals.css";
|
8 |
import { Toaster } from "@/components/ui/sonner";
|
9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
import { apiServer } from "@/lib/api";
|
11 |
-
import IframeDetector from "@/components/iframe-detector";
|
12 |
import AppContext from "@/components/contexts/app-context";
|
13 |
-
import
|
14 |
-
import
|
15 |
-
import { ProProvider } from "@/components/contexts/pro-context";
|
16 |
-
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
17 |
|
18 |
const inter = Inter({
|
19 |
variable: "--font-inter-sans",
|
@@ -27,12 +24,31 @@ const ptSans = PT_Sans({
|
|
27 |
});
|
28 |
|
29 |
export const metadata: Metadata = {
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
title: "DeepSite | Build with AI ✨",
|
32 |
description:
|
33 |
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
34 |
-
|
35 |
-
}
|
36 |
appleWebApp: {
|
37 |
capable: true,
|
38 |
title: "DeepSite",
|
@@ -43,9 +59,6 @@ export const metadata: Metadata = {
|
|
43 |
shortcut: "/logo.svg",
|
44 |
apple: "/logo.svg",
|
45 |
},
|
46 |
-
verification: {
|
47 |
-
google: process.env.GOOGLE_SITE_VERIFICATION,
|
48 |
-
},
|
49 |
};
|
50 |
|
51 |
export const viewport: Viewport = {
|
@@ -57,54 +70,29 @@ export const viewport: Viewport = {
|
|
57 |
async function getMe() {
|
58 |
const cookieStore = await cookies();
|
59 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
60 |
-
if (!token) return { user: null,
|
61 |
try {
|
62 |
const res = await apiServer.get("/me", {
|
63 |
headers: {
|
64 |
Authorization: `Bearer ${token}`,
|
65 |
},
|
66 |
});
|
67 |
-
return { user: res.data.user,
|
68 |
} catch (err: any) {
|
69 |
-
return { user: null,
|
70 |
}
|
71 |
}
|
72 |
|
|
|
|
|
73 |
export default async function RootLayout({
|
74 |
children,
|
75 |
}: Readonly<{
|
76 |
children: React.ReactNode;
|
77 |
}>) {
|
78 |
const data = await getMe();
|
79 |
-
|
80 |
-
// Generate structured data
|
81 |
-
const structuredData = generateStructuredData("WebApplication", {
|
82 |
-
name: "DeepSite",
|
83 |
-
description: "Build websites with AI, no code required",
|
84 |
-
url: "https://deepsite.hf.co",
|
85 |
-
});
|
86 |
-
|
87 |
-
const organizationData = generateStructuredData("Organization", {
|
88 |
-
name: "DeepSite",
|
89 |
-
url: "https://deepsite.hf.co",
|
90 |
-
});
|
91 |
-
|
92 |
return (
|
93 |
<html lang="en">
|
94 |
-
<head>
|
95 |
-
<script
|
96 |
-
type="application/ld+json"
|
97 |
-
dangerouslySetInnerHTML={{
|
98 |
-
__html: JSON.stringify(structuredData),
|
99 |
-
}}
|
100 |
-
/>
|
101 |
-
<script
|
102 |
-
type="application/ld+json"
|
103 |
-
dangerouslySetInnerHTML={{
|
104 |
-
__html: JSON.stringify(organizationData),
|
105 |
-
}}
|
106 |
-
/>
|
107 |
-
</head>
|
108 |
<Script
|
109 |
defer
|
110 |
data-domain="deepsite.hf.co"
|
@@ -115,13 +103,9 @@ export default async function RootLayout({
|
|
115 |
>
|
116 |
<IframeDetector />
|
117 |
<Toaster richColors position="bottom-center" />
|
118 |
-
<
|
119 |
-
<AppContext me={data}>
|
120 |
-
|
121 |
-
<ProProvider>{children}</ProProvider>
|
122 |
-
</LoginProvider>
|
123 |
-
</AppContext>
|
124 |
-
</TanstackContext>
|
125 |
</body>
|
126 |
</html>
|
127 |
);
|
|
|
2 |
import type { Metadata, Viewport } from "next";
|
3 |
import { Inter, PT_Sans } from "next/font/google";
|
4 |
import { cookies } from "next/headers";
|
|
|
5 |
|
6 |
+
import TanstackProvider from "@/components/providers/tanstack-query-provider";
|
7 |
import "@/assets/globals.css";
|
8 |
import { Toaster } from "@/components/ui/sonner";
|
9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
import { apiServer } from "@/lib/api";
|
|
|
11 |
import AppContext from "@/components/contexts/app-context";
|
12 |
+
import Script from "next/script";
|
13 |
+
import IframeDetector from "@/components/iframe-detector";
|
|
|
|
|
14 |
|
15 |
const inter = Inter({
|
16 |
variable: "--font-inter-sans",
|
|
|
24 |
});
|
25 |
|
26 |
export const metadata: Metadata = {
|
27 |
+
title: "DeepSite | Build with AI ✨",
|
28 |
+
description:
|
29 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
30 |
+
openGraph: {
|
31 |
+
title: "DeepSite | Build with AI ✨",
|
32 |
+
description:
|
33 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
34 |
+
url: "https://deepsite.hf.co",
|
35 |
+
siteName: "DeepSite",
|
36 |
+
images: [
|
37 |
+
{
|
38 |
+
url: "https://deepsite.hf.co/banner.png",
|
39 |
+
width: 1200,
|
40 |
+
height: 630,
|
41 |
+
alt: "DeepSite Open Graph Image",
|
42 |
+
},
|
43 |
+
],
|
44 |
+
},
|
45 |
+
twitter: {
|
46 |
+
card: "summary_large_image",
|
47 |
title: "DeepSite | Build with AI ✨",
|
48 |
description:
|
49 |
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
50 |
+
images: ["https://deepsite.hf.co/banner.png"],
|
51 |
+
},
|
52 |
appleWebApp: {
|
53 |
capable: true,
|
54 |
title: "DeepSite",
|
|
|
59 |
shortcut: "/logo.svg",
|
60 |
apple: "/logo.svg",
|
61 |
},
|
|
|
|
|
|
|
62 |
};
|
63 |
|
64 |
export const viewport: Viewport = {
|
|
|
70 |
async function getMe() {
|
71 |
const cookieStore = await cookies();
|
72 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
73 |
+
if (!token) return { user: null, errCode: null };
|
74 |
try {
|
75 |
const res = await apiServer.get("/me", {
|
76 |
headers: {
|
77 |
Authorization: `Bearer ${token}`,
|
78 |
},
|
79 |
});
|
80 |
+
return { user: res.data.user, errCode: null };
|
81 |
} catch (err: any) {
|
82 |
+
return { user: null, errCode: err.status };
|
83 |
}
|
84 |
}
|
85 |
|
86 |
+
// if domain isn't deepsite.hf.co or enzostvs-deepsite.hf.space redirect to deepsite.hf.co
|
87 |
+
|
88 |
export default async function RootLayout({
|
89 |
children,
|
90 |
}: Readonly<{
|
91 |
children: React.ReactNode;
|
92 |
}>) {
|
93 |
const data = await getMe();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
return (
|
95 |
<html lang="en">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
<Script
|
97 |
defer
|
98 |
data-domain="deepsite.hf.co"
|
|
|
103 |
>
|
104 |
<IframeDetector />
|
105 |
<Toaster richColors position="bottom-center" />
|
106 |
+
<TanstackProvider>
|
107 |
+
<AppContext me={data}>{children}</AppContext>
|
108 |
+
</TanstackProvider>
|
|
|
|
|
|
|
|
|
109 |
</body>
|
110 |
</html>
|
111 |
);
|
app/new/page.tsx
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
import { AppEditor } from "@/components/editor";
|
2 |
-
import { Metadata } from "next";
|
3 |
-
import { generateSEO } from "@/lib/seo";
|
4 |
-
|
5 |
-
export const metadata: Metadata = generateSEO({
|
6 |
-
title: "Create New Project - DeepSite",
|
7 |
-
description:
|
8 |
-
"Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.",
|
9 |
-
path: "/new",
|
10 |
-
});
|
11 |
-
|
12 |
-
export default function NewProjectPage() {
|
13 |
-
return <AppEditor isNew />;
|
14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/projects/[namespace]/[repoId]/page.tsx
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cookies } from "next/headers";
|
2 |
+
import { redirect } from "next/navigation";
|
3 |
+
|
4 |
+
import { apiServer } from "@/lib/api";
|
5 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
6 |
+
import { AppEditor } from "@/components/editor";
|
7 |
+
|
8 |
+
async function getProject(namespace: string, repoId: string) {
|
9 |
+
// TODO replace with a server action
|
10 |
+
const cookieStore = await cookies();
|
11 |
+
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
12 |
+
if (!token) return {};
|
13 |
+
try {
|
14 |
+
const { data } = await apiServer.get(
|
15 |
+
`/me/projects/${namespace}/${repoId}`,
|
16 |
+
{
|
17 |
+
headers: {
|
18 |
+
Authorization: `Bearer ${token}`,
|
19 |
+
},
|
20 |
+
}
|
21 |
+
);
|
22 |
+
|
23 |
+
return data.project;
|
24 |
+
} catch {
|
25 |
+
return {};
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
export default async function ProjectNamespacePage({
|
30 |
+
params,
|
31 |
+
}: {
|
32 |
+
params: Promise<{ namespace: string; repoId: string }>;
|
33 |
+
}) {
|
34 |
+
const { namespace, repoId } = await params;
|
35 |
+
const data = await getProject(namespace, repoId);
|
36 |
+
if (!data?.pages) {
|
37 |
+
redirect("/projects");
|
38 |
+
}
|
39 |
+
return (
|
40 |
+
<AppEditor project={data} pages={data.pages} images={data.images ?? []} />
|
41 |
+
);
|
42 |
+
}
|
app/projects/new/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { AppEditor } from "@/components/editor";
|
2 |
+
|
3 |
+
export default function ProjectsNewPage() {
|
4 |
+
return <AppEditor isNew />;
|
5 |
+
}
|
app/sitemap.ts
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
import { MetadataRoute } from 'next';
|
2 |
-
|
3 |
-
export default function sitemap(): MetadataRoute.Sitemap {
|
4 |
-
const baseUrl = 'https://deepsite.hf.co';
|
5 |
-
|
6 |
-
return [
|
7 |
-
{
|
8 |
-
url: baseUrl,
|
9 |
-
lastModified: new Date(),
|
10 |
-
changeFrequency: 'daily',
|
11 |
-
priority: 1,
|
12 |
-
},
|
13 |
-
{
|
14 |
-
url: `${baseUrl}/new`,
|
15 |
-
lastModified: new Date(),
|
16 |
-
changeFrequency: 'weekly',
|
17 |
-
priority: 0.8,
|
18 |
-
},
|
19 |
-
{
|
20 |
-
url: `${baseUrl}/auth`,
|
21 |
-
lastModified: new Date(),
|
22 |
-
changeFrequency: 'monthly',
|
23 |
-
priority: 0.5,
|
24 |
-
},
|
25 |
-
// Note: Dynamic project routes will be handled by Next.js automatically
|
26 |
-
// but you can add specific high-priority project pages here if needed
|
27 |
-
];
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/deepseek.svg
DELETED
assets/globals.css
CHANGED
@@ -112,10 +112,6 @@
|
|
112 |
--sidebar-ring: oklch(0.556 0 0);
|
113 |
}
|
114 |
|
115 |
-
body {
|
116 |
-
@apply scroll-smooth
|
117 |
-
}
|
118 |
-
|
119 |
@layer base {
|
120 |
* {
|
121 |
@apply border-border outline-ring/50;
|
@@ -148,224 +144,3 @@ body {
|
|
148 |
.matched-line {
|
149 |
@apply bg-sky-500/30;
|
150 |
}
|
151 |
-
|
152 |
-
/* Fast liquid deformation animations */
|
153 |
-
@keyframes liquidBlob1 {
|
154 |
-
0%, 100% {
|
155 |
-
border-radius: 40% 60% 50% 50%;
|
156 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
157 |
-
}
|
158 |
-
12.5% {
|
159 |
-
border-radius: 20% 80% 70% 30%;
|
160 |
-
transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
|
161 |
-
}
|
162 |
-
25% {
|
163 |
-
border-radius: 80% 20% 30% 70%;
|
164 |
-
transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
|
165 |
-
}
|
166 |
-
37.5% {
|
167 |
-
border-radius: 30% 70% 80% 20%;
|
168 |
-
transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
|
169 |
-
}
|
170 |
-
50% {
|
171 |
-
border-radius: 70% 30% 20% 80%;
|
172 |
-
transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
|
173 |
-
}
|
174 |
-
62.5% {
|
175 |
-
border-radius: 25% 75% 60% 40%;
|
176 |
-
transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
|
177 |
-
}
|
178 |
-
75% {
|
179 |
-
border-radius: 75% 25% 40% 60%;
|
180 |
-
transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
|
181 |
-
}
|
182 |
-
87.5% {
|
183 |
-
border-radius: 50% 50% 75% 25%;
|
184 |
-
transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
|
185 |
-
}
|
186 |
-
}
|
187 |
-
|
188 |
-
@keyframes liquidBlob2 {
|
189 |
-
0%, 100% {
|
190 |
-
border-radius: 60% 40% 50% 50%;
|
191 |
-
transform: scaleX(1) scaleY(1) rotate(12deg);
|
192 |
-
}
|
193 |
-
16% {
|
194 |
-
border-radius: 15% 85% 60% 40%;
|
195 |
-
transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
|
196 |
-
}
|
197 |
-
32% {
|
198 |
-
border-radius: 85% 15% 25% 75%;
|
199 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
|
200 |
-
}
|
201 |
-
48% {
|
202 |
-
border-radius: 30% 70% 85% 15%;
|
203 |
-
transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
|
204 |
-
}
|
205 |
-
64% {
|
206 |
-
border-radius: 70% 30% 15% 85%;
|
207 |
-
transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
|
208 |
-
}
|
209 |
-
80% {
|
210 |
-
border-radius: 40% 60% 70% 30%;
|
211 |
-
transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
|
212 |
-
}
|
213 |
-
}
|
214 |
-
|
215 |
-
@keyframes liquidBlob3 {
|
216 |
-
0%, 100% {
|
217 |
-
border-radius: 50% 50% 40% 60%;
|
218 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
219 |
-
}
|
220 |
-
20% {
|
221 |
-
border-radius: 10% 90% 75% 25%;
|
222 |
-
transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
|
223 |
-
}
|
224 |
-
40% {
|
225 |
-
border-radius: 90% 10% 20% 80%;
|
226 |
-
transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
|
227 |
-
}
|
228 |
-
60% {
|
229 |
-
border-radius: 25% 75% 90% 10%;
|
230 |
-
transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
|
231 |
-
}
|
232 |
-
80% {
|
233 |
-
border-radius: 75% 25% 10% 90%;
|
234 |
-
transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
|
235 |
-
}
|
236 |
-
}
|
237 |
-
|
238 |
-
@keyframes liquidBlob4 {
|
239 |
-
0%, 100% {
|
240 |
-
border-radius: 45% 55% 50% 50%;
|
241 |
-
transform: scaleX(1) scaleY(1) rotate(-15deg);
|
242 |
-
}
|
243 |
-
14% {
|
244 |
-
border-radius: 90% 10% 65% 35%;
|
245 |
-
transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
|
246 |
-
}
|
247 |
-
28% {
|
248 |
-
border-radius: 10% 90% 20% 80%;
|
249 |
-
transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
|
250 |
-
}
|
251 |
-
42% {
|
252 |
-
border-radius: 35% 65% 90% 10%;
|
253 |
-
transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
|
254 |
-
}
|
255 |
-
56% {
|
256 |
-
border-radius: 80% 20% 10% 90%;
|
257 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
|
258 |
-
}
|
259 |
-
70% {
|
260 |
-
border-radius: 20% 80% 55% 45%;
|
261 |
-
transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
|
262 |
-
}
|
263 |
-
84% {
|
264 |
-
border-radius: 65% 35% 80% 20%;
|
265 |
-
transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
|
266 |
-
}
|
267 |
-
}
|
268 |
-
|
269 |
-
/* Fast flowing movement animations */
|
270 |
-
@keyframes liquidFlow1 {
|
271 |
-
0%, 100% { transform: translate(0, 0); }
|
272 |
-
16% { transform: translate(60px, -40px); }
|
273 |
-
32% { transform: translate(-45px, -70px); }
|
274 |
-
48% { transform: translate(80px, 25px); }
|
275 |
-
64% { transform: translate(-30px, 60px); }
|
276 |
-
80% { transform: translate(50px, -20px); }
|
277 |
-
}
|
278 |
-
|
279 |
-
@keyframes liquidFlow2 {
|
280 |
-
0%, 100% { transform: translate(0, 0); }
|
281 |
-
20% { transform: translate(-70px, 50px); }
|
282 |
-
40% { transform: translate(90px, -30px); }
|
283 |
-
60% { transform: translate(-40px, -55px); }
|
284 |
-
80% { transform: translate(65px, 35px); }
|
285 |
-
}
|
286 |
-
|
287 |
-
@keyframes liquidFlow3 {
|
288 |
-
0%, 100% { transform: translate(0, 0); }
|
289 |
-
12% { transform: translate(-50px, -60px); }
|
290 |
-
24% { transform: translate(40px, -20px); }
|
291 |
-
36% { transform: translate(-30px, 70px); }
|
292 |
-
48% { transform: translate(70px, 20px); }
|
293 |
-
60% { transform: translate(-60px, -35px); }
|
294 |
-
72% { transform: translate(35px, 55px); }
|
295 |
-
84% { transform: translate(-25px, -45px); }
|
296 |
-
}
|
297 |
-
|
298 |
-
@keyframes liquidFlow4 {
|
299 |
-
0%, 100% { transform: translate(0, 0); }
|
300 |
-
14% { transform: translate(50px, 60px); }
|
301 |
-
28% { transform: translate(-80px, -40px); }
|
302 |
-
42% { transform: translate(30px, -90px); }
|
303 |
-
56% { transform: translate(-55px, 45px); }
|
304 |
-
70% { transform: translate(75px, -25px); }
|
305 |
-
84% { transform: translate(-35px, 65px); }
|
306 |
-
}
|
307 |
-
|
308 |
-
/* Light sweep animation for buttons */
|
309 |
-
@keyframes lightSweep {
|
310 |
-
0% {
|
311 |
-
transform: translateX(-150%);
|
312 |
-
opacity: 0;
|
313 |
-
}
|
314 |
-
8% {
|
315 |
-
opacity: 0.3;
|
316 |
-
}
|
317 |
-
25% {
|
318 |
-
opacity: 0.8;
|
319 |
-
}
|
320 |
-
42% {
|
321 |
-
opacity: 0.3;
|
322 |
-
}
|
323 |
-
50% {
|
324 |
-
transform: translateX(150%);
|
325 |
-
opacity: 0;
|
326 |
-
}
|
327 |
-
58% {
|
328 |
-
opacity: 0.3;
|
329 |
-
}
|
330 |
-
75% {
|
331 |
-
opacity: 0.8;
|
332 |
-
}
|
333 |
-
92% {
|
334 |
-
opacity: 0.3;
|
335 |
-
}
|
336 |
-
100% {
|
337 |
-
transform: translateX(-150%);
|
338 |
-
opacity: 0;
|
339 |
-
}
|
340 |
-
}
|
341 |
-
|
342 |
-
.light-sweep {
|
343 |
-
position: relative;
|
344 |
-
overflow: hidden;
|
345 |
-
}
|
346 |
-
|
347 |
-
.light-sweep::before {
|
348 |
-
content: '';
|
349 |
-
position: absolute;
|
350 |
-
top: 0;
|
351 |
-
left: 0;
|
352 |
-
right: 0;
|
353 |
-
bottom: 0;
|
354 |
-
width: 300%;
|
355 |
-
background: linear-gradient(
|
356 |
-
90deg,
|
357 |
-
transparent 0%,
|
358 |
-
transparent 20%,
|
359 |
-
rgba(56, 189, 248, 0.1) 35%,
|
360 |
-
rgba(56, 189, 248, 0.2) 45%,
|
361 |
-
rgba(255, 255, 255, 0.2) 50%,
|
362 |
-
rgba(168, 85, 247, 0.2) 55%,
|
363 |
-
rgba(168, 85, 247, 0.1) 65%,
|
364 |
-
transparent 80%,
|
365 |
-
transparent 100%
|
366 |
-
);
|
367 |
-
animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
368 |
-
pointer-events: none;
|
369 |
-
z-index: 1;
|
370 |
-
filter: blur(1px);
|
371 |
-
}
|
|
|
112 |
--sidebar-ring: oklch(0.556 0 0);
|
113 |
}
|
114 |
|
|
|
|
|
|
|
|
|
115 |
@layer base {
|
116 |
* {
|
117 |
@apply border-border outline-ring/50;
|
|
|
144 |
.matched-line {
|
145 |
@apply bg-sky-500/30;
|
146 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/kimi.svg
DELETED
assets/qwen.svg
DELETED
assets/zai.svg
DELETED
components.json
CHANGED
@@ -5,7 +5,7 @@
|
|
5 |
"tsx": true,
|
6 |
"tailwind": {
|
7 |
"config": "",
|
8 |
-
"css": "
|
9 |
"baseColor": "neutral",
|
10 |
"cssVariables": true,
|
11 |
"prefix": ""
|
|
|
5 |
"tsx": true,
|
6 |
"tailwind": {
|
7 |
"config": "",
|
8 |
+
"css": "app/globals.css",
|
9 |
"baseColor": "neutral",
|
10 |
"cssVariables": true,
|
11 |
"prefix": ""
|
components/animated-blobs/index.tsx
DELETED
@@ -1,34 +0,0 @@
|
|
1 |
-
export function AnimatedBlobs() {
|
2 |
-
return (
|
3 |
-
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
4 |
-
<div
|
5 |
-
className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
|
6 |
-
style={{
|
7 |
-
animation:
|
8 |
-
"liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
|
9 |
-
}}
|
10 |
-
/>
|
11 |
-
<div
|
12 |
-
className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
|
13 |
-
style={{
|
14 |
-
animation:
|
15 |
-
"liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
|
16 |
-
}}
|
17 |
-
/>
|
18 |
-
<div
|
19 |
-
className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
|
20 |
-
style={{
|
21 |
-
animation:
|
22 |
-
"liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
|
23 |
-
}}
|
24 |
-
/>
|
25 |
-
<div
|
26 |
-
className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
|
27 |
-
style={{
|
28 |
-
animation:
|
29 |
-
"liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
|
30 |
-
}}
|
31 |
-
/>
|
32 |
-
</div>
|
33 |
-
);
|
34 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/animated-text/index.tsx
DELETED
@@ -1,123 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { useState, useEffect } from "react";
|
4 |
-
|
5 |
-
interface AnimatedTextProps {
|
6 |
-
className?: string;
|
7 |
-
}
|
8 |
-
|
9 |
-
export function AnimatedText({ className = "" }: AnimatedTextProps) {
|
10 |
-
const [displayText, setDisplayText] = useState("");
|
11 |
-
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
|
12 |
-
const [isTyping, setIsTyping] = useState(true);
|
13 |
-
const [showCursor, setShowCursor] = useState(true);
|
14 |
-
const [lastTypedIndex, setLastTypedIndex] = useState(-1);
|
15 |
-
const [animationComplete, setAnimationComplete] = useState(false);
|
16 |
-
|
17 |
-
// Randomize suggestions on each component mount
|
18 |
-
const [suggestions] = useState(() => {
|
19 |
-
const baseSuggestions = [
|
20 |
-
"create a stunning portfolio!",
|
21 |
-
"build a tic tac toe game!",
|
22 |
-
"design a website for my restaurant!",
|
23 |
-
"make a sleek landing page!",
|
24 |
-
"build an e-commerce store!",
|
25 |
-
"create a personal blog!",
|
26 |
-
"develop a modern dashboard!",
|
27 |
-
"design a company website!",
|
28 |
-
"build a todo app!",
|
29 |
-
"create an online gallery!",
|
30 |
-
"make a contact form!",
|
31 |
-
"build a weather app!",
|
32 |
-
];
|
33 |
-
|
34 |
-
// Fisher-Yates shuffle algorithm
|
35 |
-
const shuffled = [...baseSuggestions];
|
36 |
-
for (let i = shuffled.length - 1; i > 0; i--) {
|
37 |
-
const j = Math.floor(Math.random() * (i + 1));
|
38 |
-
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
39 |
-
}
|
40 |
-
|
41 |
-
return shuffled;
|
42 |
-
});
|
43 |
-
|
44 |
-
useEffect(() => {
|
45 |
-
if (animationComplete) return;
|
46 |
-
|
47 |
-
let timeout: NodeJS.Timeout;
|
48 |
-
|
49 |
-
const typeText = () => {
|
50 |
-
const currentSuggestion = suggestions[currentSuggestionIndex];
|
51 |
-
|
52 |
-
if (isTyping) {
|
53 |
-
if (displayText.length < currentSuggestion.length) {
|
54 |
-
setDisplayText(currentSuggestion.slice(0, displayText.length + 1));
|
55 |
-
setLastTypedIndex(displayText.length);
|
56 |
-
timeout = setTimeout(typeText, 80);
|
57 |
-
} else {
|
58 |
-
// Finished typing, wait then start erasing
|
59 |
-
setLastTypedIndex(-1);
|
60 |
-
timeout = setTimeout(() => {
|
61 |
-
setIsTyping(false);
|
62 |
-
}, 2000);
|
63 |
-
}
|
64 |
-
}
|
65 |
-
};
|
66 |
-
|
67 |
-
timeout = setTimeout(typeText, 100);
|
68 |
-
return () => clearTimeout(timeout);
|
69 |
-
}, [
|
70 |
-
displayText,
|
71 |
-
currentSuggestionIndex,
|
72 |
-
isTyping,
|
73 |
-
suggestions,
|
74 |
-
animationComplete,
|
75 |
-
]);
|
76 |
-
|
77 |
-
// Cursor blinking effect
|
78 |
-
useEffect(() => {
|
79 |
-
if (animationComplete) {
|
80 |
-
setShowCursor(false);
|
81 |
-
return;
|
82 |
-
}
|
83 |
-
|
84 |
-
const cursorInterval = setInterval(() => {
|
85 |
-
setShowCursor((prev) => !prev);
|
86 |
-
}, 600);
|
87 |
-
|
88 |
-
return () => clearInterval(cursorInterval);
|
89 |
-
}, [animationComplete]);
|
90 |
-
|
91 |
-
useEffect(() => {
|
92 |
-
if (lastTypedIndex >= 0) {
|
93 |
-
const timeout = setTimeout(() => {
|
94 |
-
setLastTypedIndex(-1);
|
95 |
-
}, 400);
|
96 |
-
|
97 |
-
return () => clearTimeout(timeout);
|
98 |
-
}
|
99 |
-
}, [lastTypedIndex]);
|
100 |
-
|
101 |
-
return (
|
102 |
-
<p className={`font-mono ${className}`}>
|
103 |
-
Hey DeepSite,
|
104 |
-
{displayText.split("").map((char, index) => (
|
105 |
-
<span
|
106 |
-
key={`${currentSuggestionIndex}-${index}`}
|
107 |
-
className={`transition-colors duration-300 ${
|
108 |
-
index === lastTypedIndex ? "text-neutral-100" : ""
|
109 |
-
}`}
|
110 |
-
>
|
111 |
-
{char}
|
112 |
-
</span>
|
113 |
-
))}
|
114 |
-
<span
|
115 |
-
className={`${
|
116 |
-
showCursor ? "opacity-100" : "opacity-0"
|
117 |
-
} transition-opacity`}
|
118 |
-
>
|
119 |
-
|
|
120 |
-
</span>
|
121 |
-
</p>
|
122 |
-
);
|
123 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/app-context.tsx
CHANGED
@@ -1,11 +1,12 @@
|
|
1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
"use client";
|
3 |
-
import { useMount } from "react-use";
|
4 |
-
import { toast } from "sonner";
|
5 |
-
import { usePathname, useRouter } from "next/navigation";
|
6 |
|
7 |
import { useUser } from "@/hooks/useUser";
|
8 |
-
import {
|
|
|
|
|
|
|
|
|
9 |
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
10 |
|
11 |
export default function AppContext({
|
@@ -15,7 +16,6 @@ export default function AppContext({
|
|
15 |
children: React.ReactNode;
|
16 |
me?: {
|
17 |
user: User | null;
|
18 |
-
projects: ProjectType[];
|
19 |
errCode: number | null;
|
20 |
};
|
21 |
}) {
|
@@ -49,5 +49,9 @@ export default function AppContext({
|
|
49 |
}
|
50 |
});
|
51 |
|
52 |
-
return
|
|
|
|
|
|
|
|
|
53 |
}
|
|
|
1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
"use client";
|
|
|
|
|
|
|
3 |
|
4 |
import { useUser } from "@/hooks/useUser";
|
5 |
+
import { usePathname, useRouter } from "next/navigation";
|
6 |
+
import { useMount } from "react-use";
|
7 |
+
import { UserContext } from "@/components/contexts/user-context";
|
8 |
+
import { User } from "@/types";
|
9 |
+
import { toast } from "sonner";
|
10 |
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
11 |
|
12 |
export default function AppContext({
|
|
|
16 |
children: React.ReactNode;
|
17 |
me?: {
|
18 |
user: User | null;
|
|
|
19 |
errCode: number | null;
|
20 |
};
|
21 |
}) {
|
|
|
49 |
}
|
50 |
});
|
51 |
|
52 |
+
return (
|
53 |
+
<UserContext value={{ user, loading, logout } as any}>
|
54 |
+
{children}
|
55 |
+
</UserContext>
|
56 |
+
);
|
57 |
}
|
components/contexts/login-context.tsx
DELETED
@@ -1,62 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import React, { createContext, useContext, useState, ReactNode } from "react";
|
4 |
-
import { LoginModal } from "@/components/login-modal";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
interface LoginContextType {
|
8 |
-
isOpen: boolean;
|
9 |
-
openLoginModal: (options?: LoginModalOptions) => void;
|
10 |
-
closeLoginModal: () => void;
|
11 |
-
}
|
12 |
-
|
13 |
-
interface LoginModalOptions {
|
14 |
-
pages?: Page[];
|
15 |
-
title?: string;
|
16 |
-
prompt?: string;
|
17 |
-
description?: string;
|
18 |
-
}
|
19 |
-
|
20 |
-
const LoginContext = createContext<LoginContextType | undefined>(undefined);
|
21 |
-
|
22 |
-
export function LoginProvider({ children }: { children: ReactNode }) {
|
23 |
-
const [isOpen, setIsOpen] = useState(false);
|
24 |
-
const [modalOptions, setModalOptions] = useState<LoginModalOptions>({});
|
25 |
-
|
26 |
-
const openLoginModal = (options: LoginModalOptions = {}) => {
|
27 |
-
setModalOptions(options);
|
28 |
-
setIsOpen(true);
|
29 |
-
};
|
30 |
-
|
31 |
-
const closeLoginModal = () => {
|
32 |
-
setIsOpen(false);
|
33 |
-
setModalOptions({});
|
34 |
-
};
|
35 |
-
|
36 |
-
const value = {
|
37 |
-
isOpen,
|
38 |
-
openLoginModal,
|
39 |
-
closeLoginModal,
|
40 |
-
};
|
41 |
-
|
42 |
-
return (
|
43 |
-
<LoginContext.Provider value={value}>
|
44 |
-
{children}
|
45 |
-
<LoginModal
|
46 |
-
open={isOpen}
|
47 |
-
onClose={setIsOpen}
|
48 |
-
title={modalOptions.title}
|
49 |
-
prompt={modalOptions.prompt}
|
50 |
-
description={modalOptions.description}
|
51 |
-
/>
|
52 |
-
</LoginContext.Provider>
|
53 |
-
);
|
54 |
-
}
|
55 |
-
|
56 |
-
export function useLoginModal() {
|
57 |
-
const context = useContext(LoginContext);
|
58 |
-
if (context === undefined) {
|
59 |
-
throw new Error("useLoginModal must be used within a LoginProvider");
|
60 |
-
}
|
61 |
-
return context;
|
62 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/contexts/pro-context.tsx
DELETED
@@ -1,48 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import React, { createContext, useContext, useState, ReactNode } from "react";
|
4 |
-
import { ProModal } from "@/components/pro-modal";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
import { useEditor } from "@/hooks/useEditor";
|
7 |
-
|
8 |
-
interface ProContextType {
|
9 |
-
isOpen: boolean;
|
10 |
-
openProModal: (pages: Page[]) => void;
|
11 |
-
closeProModal: () => void;
|
12 |
-
}
|
13 |
-
|
14 |
-
const ProContext = createContext<ProContextType | undefined>(undefined);
|
15 |
-
|
16 |
-
export function ProProvider({ children }: { children: ReactNode }) {
|
17 |
-
const [isOpen, setIsOpen] = useState(false);
|
18 |
-
const { pages } = useEditor();
|
19 |
-
|
20 |
-
const openProModal = () => {
|
21 |
-
setIsOpen(true);
|
22 |
-
};
|
23 |
-
|
24 |
-
const closeProModal = () => {
|
25 |
-
setIsOpen(false);
|
26 |
-
};
|
27 |
-
|
28 |
-
const value = {
|
29 |
-
isOpen,
|
30 |
-
openProModal,
|
31 |
-
closeProModal,
|
32 |
-
};
|
33 |
-
|
34 |
-
return (
|
35 |
-
<ProContext.Provider value={value}>
|
36 |
-
{children}
|
37 |
-
<ProModal open={isOpen} onClose={setIsOpen} pages={pages} />
|
38 |
-
</ProContext.Provider>
|
39 |
-
);
|
40 |
-
}
|
41 |
-
|
42 |
-
export function useProModal() {
|
43 |
-
const context = useContext(ProContext);
|
44 |
-
if (context === undefined) {
|
45 |
-
throw new Error("useProModal must be used within a ProProvider");
|
46 |
-
}
|
47 |
-
return context;
|
48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/fake-ask.tsx
DELETED
@@ -1,97 +0,0 @@
|
|
1 |
-
import { useState } from "react";
|
2 |
-
import { useLocalStorage } from "react-use";
|
3 |
-
import { ArrowUp, Dice6 } from "lucide-react";
|
4 |
-
import { useRouter } from "next/navigation";
|
5 |
-
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import { PromptBuilder } from "./prompt-builder";
|
8 |
-
import { EnhancedSettings } from "@/types";
|
9 |
-
import { Settings } from "./settings";
|
10 |
-
import classNames from "classnames";
|
11 |
-
import { PROMPTS_FOR_AI } from "@/lib/prompts";
|
12 |
-
|
13 |
-
export const FakeAskAi = () => {
|
14 |
-
const router = useRouter();
|
15 |
-
const [prompt, setPrompt] = useState("");
|
16 |
-
const [openProvider, setOpenProvider] = useState(false);
|
17 |
-
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
18 |
-
useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
|
19 |
-
isActive: true,
|
20 |
-
primaryColor: undefined,
|
21 |
-
secondaryColor: undefined,
|
22 |
-
theme: undefined,
|
23 |
-
});
|
24 |
-
const [, setPromptStorage] = useLocalStorage("prompt", "");
|
25 |
-
const [randomPromptLoading, setRandomPromptLoading] = useState(false);
|
26 |
-
|
27 |
-
const callAi = async () => {
|
28 |
-
setPromptStorage(prompt);
|
29 |
-
router.push("/new");
|
30 |
-
};
|
31 |
-
|
32 |
-
const randomPrompt = () => {
|
33 |
-
setRandomPromptLoading(true);
|
34 |
-
setTimeout(() => {
|
35 |
-
setPrompt(
|
36 |
-
PROMPTS_FOR_AI[Math.floor(Math.random() * PROMPTS_FOR_AI.length)]
|
37 |
-
);
|
38 |
-
setRandomPromptLoading(false);
|
39 |
-
}, 400);
|
40 |
-
};
|
41 |
-
|
42 |
-
return (
|
43 |
-
<div className="p-3 w-full max-w-xl mx-auto">
|
44 |
-
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-20 w-full group">
|
45 |
-
<div className="w-full relative flex items-start justify-between pr-4 pt-4">
|
46 |
-
<textarea
|
47 |
-
className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 px-4 pb-4 resize-none"
|
48 |
-
placeholder="Ask DeepSite anything..."
|
49 |
-
value={prompt}
|
50 |
-
onChange={(e) => setPrompt(e.target.value)}
|
51 |
-
onKeyDown={(e) => {
|
52 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
53 |
-
callAi();
|
54 |
-
}
|
55 |
-
}}
|
56 |
-
/>
|
57 |
-
<Button
|
58 |
-
size="iconXs"
|
59 |
-
variant="outline"
|
60 |
-
className="!rounded-md"
|
61 |
-
onClick={() => randomPrompt()}
|
62 |
-
>
|
63 |
-
<Dice6
|
64 |
-
className={classNames("size-4", {
|
65 |
-
"animate-spin animation-duration-500": randomPromptLoading,
|
66 |
-
})}
|
67 |
-
/>
|
68 |
-
</Button>
|
69 |
-
</div>
|
70 |
-
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
71 |
-
<div className="flex-1 flex items-center justify-start gap-1.5 flex-wrap">
|
72 |
-
<PromptBuilder
|
73 |
-
enhancedSettings={enhancedSettings!}
|
74 |
-
setEnhancedSettings={setEnhancedSettings}
|
75 |
-
/>
|
76 |
-
<Settings
|
77 |
-
open={openProvider}
|
78 |
-
isFollowUp={false}
|
79 |
-
error=""
|
80 |
-
onClose={setOpenProvider}
|
81 |
-
/>
|
82 |
-
</div>
|
83 |
-
<div className="flex items-center justify-end gap-2">
|
84 |
-
<Button
|
85 |
-
size="iconXs"
|
86 |
-
variant="outline"
|
87 |
-
className="!rounded-md"
|
88 |
-
onClick={() => callAi()}
|
89 |
-
>
|
90 |
-
<ArrowUp className="size-4" />
|
91 |
-
</Button>
|
92 |
-
</div>
|
93 |
-
</div>
|
94 |
-
</div>
|
95 |
-
</div>
|
96 |
-
);
|
97 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/follow-up-tooltip.tsx
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
Popover,
|
3 |
+
PopoverContent,
|
4 |
+
PopoverTrigger,
|
5 |
+
} from "@/components/ui/popover";
|
6 |
+
import { Info } from "lucide-react";
|
7 |
+
|
8 |
+
export const FollowUpTooltip = () => {
|
9 |
+
return (
|
10 |
+
<Popover>
|
11 |
+
<PopoverTrigger asChild>
|
12 |
+
<Info className="size-3 text-neutral-300 cursor-pointer" />
|
13 |
+
</PopoverTrigger>
|
14 |
+
<PopoverContent
|
15 |
+
align="start"
|
16 |
+
className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
|
17 |
+
>
|
18 |
+
<header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
|
19 |
+
<p className="text-base text-neutral-200 font-semibold">
|
20 |
+
⚡ Faster, Smarter Updates
|
21 |
+
</p>
|
22 |
+
</header>
|
23 |
+
<main className="p-4">
|
24 |
+
<p className="text-neutral-300 text-sm">
|
25 |
+
Using the Diff-Patch system, allow DeepSite to intelligently update
|
26 |
+
your project without rewritting the entire codebase.
|
27 |
+
</p>
|
28 |
+
<p className="text-neutral-500 text-sm mt-2">
|
29 |
+
This means faster updates, less data usage, and a more efficient
|
30 |
+
development process.
|
31 |
+
</p>
|
32 |
+
</main>
|
33 |
+
</PopoverContent>
|
34 |
+
</Popover>
|
35 |
+
);
|
36 |
+
};
|
components/editor/ask-ai/index.tsx
CHANGED
@@ -1,94 +1,150 @@
|
|
1 |
-
|
|
|
|
|
2 |
import classNames from "classnames";
|
3 |
-
import { ArrowUp, ChevronDown, CircleStop, Dice6 } from "lucide-react";
|
4 |
-
import { useLocalStorage, useUpdateEffect, useMount } from "react-use";
|
5 |
import { toast } from "sonner";
|
|
|
|
|
|
|
6 |
|
7 |
-
import
|
8 |
-
import { useEditor } from "@/hooks/useEditor";
|
9 |
-
import { EnhancedSettings, Project } from "@/types";
|
10 |
-
import { SelectedFiles } from "@/components/editor/ask-ai/selected-files";
|
11 |
-
import { SelectedHtmlElement } from "@/components/editor/ask-ai/selected-html-element";
|
12 |
-
import { AiLoading } from "@/components/editor/ask-ai/loading";
|
13 |
import { Button } from "@/components/ui/button";
|
14 |
-
import {
|
|
|
|
|
|
|
|
|
15 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
16 |
-
import
|
17 |
-
import {
|
18 |
-
import {
|
19 |
-
import {
|
20 |
-
import {
|
21 |
-
import {
|
22 |
-
import {
|
23 |
-
import {
|
|
|
|
|
24 |
|
25 |
-
export
|
26 |
-
project,
|
27 |
isNew,
|
|
|
|
|
|
|
|
|
28 |
onScrollToBottom,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
}: {
|
30 |
-
project?: Project;
|
31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
isNew?: boolean;
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
audio: hookAudio,
|
50 |
-
cancelRequest,
|
51 |
-
} = useAi(onScrollToBottom);
|
52 |
-
const { openLoginModal } = useLoginModal();
|
53 |
-
const { openProModal } = useProModal();
|
54 |
const [openProvider, setOpenProvider] = useState(false);
|
55 |
const [providerError, setProviderError] = useState("");
|
56 |
-
const
|
57 |
-
|
58 |
-
const [enhancedSettings, setEnhancedSettings, removeEnhancedSettings] =
|
59 |
-
useLocalStorage<EnhancedSettings>("deepsite-enhancedSettings", {
|
60 |
-
isActive: false,
|
61 |
-
primaryColor: undefined,
|
62 |
-
secondaryColor: undefined,
|
63 |
-
theme: undefined,
|
64 |
-
});
|
65 |
-
const [promptStorage, , removePromptStorage] = useLocalStorage("prompt", "");
|
66 |
-
|
67 |
-
const [isFollowUp, setIsFollowUp] = useState(true);
|
68 |
-
const [prompt, setPrompt] = useState(
|
69 |
-
promptStorage && promptStorage.trim() !== "" ? promptStorage : ""
|
70 |
-
);
|
71 |
-
const [think, setThink] = useState("");
|
72 |
const [openThink, setOpenThink] = useState(false);
|
73 |
-
const [
|
|
|
|
|
|
|
|
|
74 |
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
});
|
80 |
|
|
|
|
|
|
|
|
|
81 |
const callAi = async (redesignMarkdown?: string) => {
|
82 |
-
removePromptStorage();
|
83 |
-
if (user && !user.isPro && projects.length >= MAX_FREE_PROJECTS)
|
84 |
-
return openProModal([]);
|
85 |
if (isAiWorking) return;
|
86 |
if (!redesignMarkdown && !prompt.trim()) return;
|
87 |
|
88 |
if (isFollowUp && !redesignMarkdown && !isSameHtml) {
|
89 |
-
|
90 |
-
const
|
|
|
|
|
91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
if (result?.error) {
|
93 |
handleError(result.error, result.message);
|
94 |
return;
|
@@ -100,9 +156,13 @@ export const AskAi = ({
|
|
100 |
} else {
|
101 |
const result = await callAiNewProject(
|
102 |
prompt,
|
103 |
-
|
|
|
104 |
redesignMarkdown,
|
105 |
-
|
|
|
|
|
|
|
106 |
);
|
107 |
|
108 |
if (result?.error) {
|
@@ -112,24 +172,30 @@ export const AskAi = ({
|
|
112 |
|
113 |
if (result?.success) {
|
114 |
setPrompt("");
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
}
|
119 |
}
|
120 |
};
|
121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
const handleError = (error: string, message?: string) => {
|
123 |
switch (error) {
|
124 |
case "login_required":
|
125 |
-
|
126 |
break;
|
127 |
case "provider_required":
|
128 |
setOpenProvider(true);
|
129 |
setProviderError(message || "");
|
130 |
break;
|
131 |
case "pro_required":
|
132 |
-
|
133 |
break;
|
134 |
case "api_error":
|
135 |
toast.error(message || "An error occurred");
|
@@ -148,19 +214,19 @@ export const AskAi = ({
|
|
148 |
}
|
149 |
}, [think]);
|
150 |
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
};
|
160 |
|
161 |
return (
|
162 |
-
<div className="
|
163 |
-
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-
|
164 |
{think && (
|
165 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
166 |
<header
|
@@ -202,7 +268,7 @@ export const AskAi = ({
|
|
202 |
files={selectedFiles}
|
203 |
isAiWorking={isAiWorking}
|
204 |
onDelete={(file) =>
|
205 |
-
setSelectedFiles(
|
206 |
}
|
207 |
/>
|
208 |
{selectedElement && (
|
@@ -215,41 +281,92 @@ export const AskAi = ({
|
|
215 |
</div>
|
216 |
)}
|
217 |
<div className="w-full relative flex items-center justify-between">
|
218 |
-
{(isAiWorking || isUploading
|
219 |
<div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
220 |
-
<
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
231 |
{isAiWorking && (
|
232 |
-
<
|
233 |
-
|
234 |
-
|
235 |
-
className="!rounded-md mr-0.5"
|
236 |
-
onClick={cancelRequest}
|
237 |
>
|
238 |
-
<
|
239 |
-
|
|
|
240 |
)}
|
241 |
</div>
|
242 |
)}
|
243 |
<textarea
|
244 |
-
disabled={
|
245 |
-
isAiWorking || isUploading || isThinking || isLoadingProject
|
246 |
-
}
|
247 |
className={classNames(
|
248 |
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
|
249 |
{
|
250 |
-
"!pt-2.5":
|
251 |
-
selectedElement &&
|
252 |
-
!(isAiWorking || isUploading || isThinking),
|
253 |
}
|
254 |
)}
|
255 |
placeholder={
|
@@ -267,51 +384,112 @@ export const AskAi = ({
|
|
267 |
}
|
268 |
}}
|
269 |
/>
|
270 |
-
{isNew && !isAiWorking && isSameHtml && (
|
271 |
-
<Button
|
272 |
-
size="iconXs"
|
273 |
-
variant="outline"
|
274 |
-
className="!rounded-md -translate-y-2 -translate-x-4"
|
275 |
-
onClick={() => randomPrompt()}
|
276 |
-
>
|
277 |
-
<Dice6
|
278 |
-
className={classNames("size-4", {
|
279 |
-
"animate-spin animation-duration-500": randomPromptLoading,
|
280 |
-
})}
|
281 |
-
/>
|
282 |
-
</Button>
|
283 |
-
)}
|
284 |
</div>
|
285 |
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
286 |
-
<div className="flex-1 flex items-center justify-start gap-1.5
|
287 |
-
<
|
288 |
-
|
289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
290 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
291 |
<Settings
|
|
|
|
|
|
|
|
|
292 |
open={openProvider}
|
293 |
error={providerError}
|
294 |
isFollowUp={!isSameHtml && isFollowUp}
|
295 |
onClose={setOpenProvider}
|
296 |
/>
|
297 |
-
{!isNew && <Uploader project={project} />}
|
298 |
-
{isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
|
299 |
-
{!isNew && !isSameHtml && <Selector />}
|
300 |
-
</div>
|
301 |
-
<div className="flex items-center justify-end gap-2">
|
302 |
<Button
|
303 |
size="iconXs"
|
304 |
-
|
305 |
-
className="!rounded-md"
|
306 |
-
disabled={
|
307 |
-
isAiWorking || isUploading || isThinking || !prompt.trim()
|
308 |
-
}
|
309 |
onClick={() => callAi()}
|
310 |
>
|
311 |
<ArrowUp className="size-4" />
|
312 |
</Button>
|
313 |
</div>
|
314 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
</div>
|
316 |
<audio ref={hookAudio} id="audio" className="hidden">
|
317 |
<source src="/success.mp3" type="audio/mpeg" />
|
@@ -319,4 +497,4 @@ export const AskAi = ({
|
|
319 |
</audio>
|
320 |
</div>
|
321 |
);
|
322 |
-
}
|
|
|
1 |
+
"use client";
|
2 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
3 |
+
import { useState, useMemo, useRef } from "react";
|
4 |
import classNames from "classnames";
|
|
|
|
|
5 |
import { toast } from "sonner";
|
6 |
+
import { useLocalStorage, useUpdateEffect } from "react-use";
|
7 |
+
import { ArrowUp, ChevronDown, Crosshair } from "lucide-react";
|
8 |
+
import { FaStopCircle } from "react-icons/fa";
|
9 |
|
10 |
+
import ProModal from "@/components/pro-modal";
|
|
|
|
|
|
|
|
|
|
|
11 |
import { Button } from "@/components/ui/button";
|
12 |
+
import { MODELS } from "@/lib/providers";
|
13 |
+
import { HtmlHistory, Page, Project } from "@/types";
|
14 |
+
// import { InviteFriends } from "@/components/invite-friends";
|
15 |
+
import { Settings } from "@/components/editor/ask-ai/settings";
|
16 |
+
import { LoginModal } from "@/components/login-modal";
|
17 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
18 |
+
import Loading from "@/components/loading";
|
19 |
+
import { Checkbox } from "@/components/ui/checkbox";
|
20 |
+
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
|
21 |
+
import { TooltipContent } from "@radix-ui/react-tooltip";
|
22 |
+
import { SelectedHtmlElement } from "./selected-html-element";
|
23 |
+
import { FollowUpTooltip } from "./follow-up-tooltip";
|
24 |
+
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
25 |
+
import { useCallAi } from "@/hooks/useCallAi";
|
26 |
+
import { SelectedFiles } from "./selected-files";
|
27 |
+
import { Uploader } from "./uploader";
|
28 |
|
29 |
+
export function AskAI({
|
|
|
30 |
isNew,
|
31 |
+
project,
|
32 |
+
images,
|
33 |
+
currentPage,
|
34 |
+
previousPrompts,
|
35 |
onScrollToBottom,
|
36 |
+
isAiWorking,
|
37 |
+
setisAiWorking,
|
38 |
+
isEditableModeEnabled = false,
|
39 |
+
pages,
|
40 |
+
htmlHistory,
|
41 |
+
selectedElement,
|
42 |
+
setSelectedElement,
|
43 |
+
selectedFiles,
|
44 |
+
setSelectedFiles,
|
45 |
+
setIsEditableModeEnabled,
|
46 |
+
onNewPrompt,
|
47 |
+
onSuccess,
|
48 |
+
setPages,
|
49 |
+
setCurrentPage,
|
50 |
}: {
|
51 |
+
project?: Project | null;
|
52 |
+
currentPage: Page;
|
53 |
+
images?: string[];
|
54 |
+
pages: Page[];
|
55 |
+
onScrollToBottom: () => void;
|
56 |
+
previousPrompts: string[];
|
57 |
+
isAiWorking: boolean;
|
58 |
+
onNewPrompt: (prompt: string) => void;
|
59 |
+
htmlHistory?: HtmlHistory[];
|
60 |
+
setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
|
61 |
isNew?: boolean;
|
62 |
+
onSuccess: (page: Page[], p: string, n?: number[][]) => void;
|
63 |
+
isEditableModeEnabled: boolean;
|
64 |
+
setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
65 |
+
selectedElement?: HTMLElement | null;
|
66 |
+
setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
67 |
+
selectedFiles: string[];
|
68 |
+
setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>;
|
69 |
+
setPages: React.Dispatch<React.SetStateAction<Page[]>>;
|
70 |
+
setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
|
71 |
+
}) {
|
72 |
+
const refThink = useRef<HTMLDivElement | null>(null);
|
73 |
+
|
74 |
+
const [open, setOpen] = useState(false);
|
75 |
+
const [prompt, setPrompt] = useState("");
|
76 |
+
const [provider, setProvider] = useLocalStorage("provider", "auto");
|
77 |
+
const [model, setModel] = useLocalStorage("model", MODELS[0].value);
|
|
|
|
|
|
|
|
|
|
|
78 |
const [openProvider, setOpenProvider] = useState(false);
|
79 |
const [providerError, setProviderError] = useState("");
|
80 |
+
const [openProModal, setOpenProModal] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
const [openThink, setOpenThink] = useState(false);
|
82 |
+
const [isThinking, setIsThinking] = useState(true);
|
83 |
+
const [think, setThink] = useState("");
|
84 |
+
const [isFollowUp, setIsFollowUp] = useState(true);
|
85 |
+
const [isUploading, setIsUploading] = useState(false);
|
86 |
+
const [files, setFiles] = useState<string[]>(images ?? []);
|
87 |
|
88 |
+
const {
|
89 |
+
callAiNewProject,
|
90 |
+
callAiFollowUp,
|
91 |
+
callAiNewPage,
|
92 |
+
stopController,
|
93 |
+
audio: hookAudio,
|
94 |
+
} = useCallAi({
|
95 |
+
onNewPrompt,
|
96 |
+
onSuccess,
|
97 |
+
onScrollToBottom,
|
98 |
+
setPages,
|
99 |
+
setCurrentPage,
|
100 |
+
currentPage,
|
101 |
+
pages,
|
102 |
+
isAiWorking,
|
103 |
+
setisAiWorking,
|
104 |
});
|
105 |
|
106 |
+
const selectedModel = useMemo(() => {
|
107 |
+
return MODELS.find((m: { value: string }) => m.value === model);
|
108 |
+
}, [model]);
|
109 |
+
|
110 |
const callAi = async (redesignMarkdown?: string) => {
|
|
|
|
|
|
|
111 |
if (isAiWorking) return;
|
112 |
if (!redesignMarkdown && !prompt.trim()) return;
|
113 |
|
114 |
if (isFollowUp && !redesignMarkdown && !isSameHtml) {
|
115 |
+
// Use follow-up function for existing projects
|
116 |
+
const selectedElementHtml = selectedElement
|
117 |
+
? selectedElement.outerHTML
|
118 |
+
: "";
|
119 |
|
120 |
+
const result = await callAiFollowUp(
|
121 |
+
prompt,
|
122 |
+
model,
|
123 |
+
provider,
|
124 |
+
previousPrompts,
|
125 |
+
selectedElementHtml,
|
126 |
+
selectedFiles
|
127 |
+
);
|
128 |
+
|
129 |
+
if (result?.error) {
|
130 |
+
handleError(result.error, result.message);
|
131 |
+
return;
|
132 |
+
}
|
133 |
+
|
134 |
+
if (result?.success) {
|
135 |
+
setPrompt("");
|
136 |
+
}
|
137 |
+
} else if (isFollowUp && pages.length > 1 && isSameHtml) {
|
138 |
+
const result = await callAiNewPage(
|
139 |
+
prompt,
|
140 |
+
model,
|
141 |
+
provider,
|
142 |
+
currentPage.path,
|
143 |
+
[
|
144 |
+
...(previousPrompts ?? []),
|
145 |
+
...(htmlHistory?.map((h) => h.prompt) ?? []),
|
146 |
+
]
|
147 |
+
);
|
148 |
if (result?.error) {
|
149 |
handleError(result.error, result.message);
|
150 |
return;
|
|
|
156 |
} else {
|
157 |
const result = await callAiNewProject(
|
158 |
prompt,
|
159 |
+
model,
|
160 |
+
provider,
|
161 |
redesignMarkdown,
|
162 |
+
handleThink,
|
163 |
+
() => {
|
164 |
+
setIsThinking(false);
|
165 |
+
}
|
166 |
);
|
167 |
|
168 |
if (result?.error) {
|
|
|
172 |
|
173 |
if (result?.success) {
|
174 |
setPrompt("");
|
175 |
+
if (selectedModel?.isThinker) {
|
176 |
+
setModel(MODELS[0].value);
|
177 |
+
}
|
178 |
}
|
179 |
}
|
180 |
};
|
181 |
|
182 |
+
const handleThink = (think: string) => {
|
183 |
+
setThink(think);
|
184 |
+
setIsThinking(true);
|
185 |
+
setOpenThink(true);
|
186 |
+
};
|
187 |
+
|
188 |
const handleError = (error: string, message?: string) => {
|
189 |
switch (error) {
|
190 |
case "login_required":
|
191 |
+
setOpen(true);
|
192 |
break;
|
193 |
case "provider_required":
|
194 |
setOpenProvider(true);
|
195 |
setProviderError(message || "");
|
196 |
break;
|
197 |
case "pro_required":
|
198 |
+
setOpenProModal(true);
|
199 |
break;
|
200 |
case "api_error":
|
201 |
toast.error(message || "An error occurred");
|
|
|
214 |
}
|
215 |
}, [think]);
|
216 |
|
217 |
+
useUpdateEffect(() => {
|
218 |
+
if (!isThinking) {
|
219 |
+
setOpenThink(false);
|
220 |
+
}
|
221 |
+
}, [isThinking]);
|
222 |
+
|
223 |
+
const isSameHtml = useMemo(() => {
|
224 |
+
return isTheSameHtml(currentPage.html);
|
225 |
+
}, [currentPage.html]);
|
226 |
|
227 |
return (
|
228 |
+
<div className="px-3">
|
229 |
+
<div className="relative bg-neutral-800 border border-neutral-700 rounded-2xl ring-[4px] focus-within:ring-neutral-500/30 focus-within:border-neutral-600 ring-transparent z-10 w-full group">
|
230 |
{think && (
|
231 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
232 |
<header
|
|
|
268 |
files={selectedFiles}
|
269 |
isAiWorking={isAiWorking}
|
270 |
onDelete={(file) =>
|
271 |
+
setSelectedFiles((prev) => prev.filter((f) => f !== file))
|
272 |
}
|
273 |
/>
|
274 |
{selectedElement && (
|
|
|
281 |
</div>
|
282 |
)}
|
283 |
<div className="w-full relative flex items-center justify-between">
|
284 |
+
{(isAiWorking || isUploading) && (
|
285 |
<div className="absolute bg-neutral-800 top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
286 |
+
<div className="flex items-center justify-start gap-2">
|
287 |
+
<Loading overlay={false} className="!size-4 opacity-50" />
|
288 |
+
<p className="text-neutral-400 text-sm">
|
289 |
+
{isUploading ? (
|
290 |
+
"Uploading images..."
|
291 |
+
) : isAiWorking && !isSameHtml ? (
|
292 |
+
"AI is working..."
|
293 |
+
) : (
|
294 |
+
<span className="inline-flex">
|
295 |
+
{[
|
296 |
+
"D",
|
297 |
+
"e",
|
298 |
+
"e",
|
299 |
+
"p",
|
300 |
+
"S",
|
301 |
+
"i",
|
302 |
+
"t",
|
303 |
+
"e",
|
304 |
+
" ",
|
305 |
+
"i",
|
306 |
+
"s",
|
307 |
+
" ",
|
308 |
+
"T",
|
309 |
+
"h",
|
310 |
+
"i",
|
311 |
+
"n",
|
312 |
+
"k",
|
313 |
+
"i",
|
314 |
+
"n",
|
315 |
+
"g",
|
316 |
+
".",
|
317 |
+
".",
|
318 |
+
".",
|
319 |
+
" ",
|
320 |
+
"W",
|
321 |
+
"a",
|
322 |
+
"i",
|
323 |
+
"t",
|
324 |
+
" ",
|
325 |
+
"a",
|
326 |
+
" ",
|
327 |
+
"m",
|
328 |
+
"o",
|
329 |
+
"m",
|
330 |
+
"e",
|
331 |
+
"n",
|
332 |
+
"t",
|
333 |
+
".",
|
334 |
+
".",
|
335 |
+
".",
|
336 |
+
].map((char, index) => (
|
337 |
+
<span
|
338 |
+
key={index}
|
339 |
+
className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
|
340 |
+
style={{
|
341 |
+
animationDelay: `${index * 0.1}s`,
|
342 |
+
animationDuration: "1.3s",
|
343 |
+
animationIterationCount: "infinite",
|
344 |
+
}}
|
345 |
+
>
|
346 |
+
{char === " " ? "\u00A0" : char}
|
347 |
+
</span>
|
348 |
+
))}
|
349 |
+
</span>
|
350 |
+
)}
|
351 |
+
</p>
|
352 |
+
</div>
|
353 |
{isAiWorking && (
|
354 |
+
<div
|
355 |
+
className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
|
356 |
+
onClick={stopController}
|
|
|
|
|
357 |
>
|
358 |
+
<FaStopCircle />
|
359 |
+
Stop generation
|
360 |
+
</div>
|
361 |
)}
|
362 |
</div>
|
363 |
)}
|
364 |
<textarea
|
365 |
+
disabled={isAiWorking}
|
|
|
|
|
366 |
className={classNames(
|
367 |
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none",
|
368 |
{
|
369 |
+
"!pt-2.5": selectedElement && !isAiWorking,
|
|
|
|
|
370 |
}
|
371 |
)}
|
372 |
placeholder={
|
|
|
384 |
}
|
385 |
}}
|
386 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
387 |
</div>
|
388 |
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
389 |
+
<div className="flex-1 flex items-center justify-start gap-1.5">
|
390 |
+
<Uploader
|
391 |
+
pages={pages}
|
392 |
+
onLoading={setIsUploading}
|
393 |
+
isLoading={isUploading}
|
394 |
+
onFiles={setFiles}
|
395 |
+
onSelectFile={(file) => {
|
396 |
+
if (selectedFiles.includes(file)) {
|
397 |
+
setSelectedFiles((prev) => prev.filter((f) => f !== file));
|
398 |
+
} else {
|
399 |
+
setSelectedFiles((prev) => [...prev, file]);
|
400 |
+
}
|
401 |
+
}}
|
402 |
+
files={files}
|
403 |
+
selectedFiles={selectedFiles}
|
404 |
+
project={project}
|
405 |
/>
|
406 |
+
{isNew && <ReImagine onRedesign={(md) => callAi(md)} />}
|
407 |
+
{!isSameHtml && (
|
408 |
+
<Tooltip>
|
409 |
+
<TooltipTrigger asChild>
|
410 |
+
<Button
|
411 |
+
size="xs"
|
412 |
+
variant={isEditableModeEnabled ? "default" : "outline"}
|
413 |
+
onClick={() => {
|
414 |
+
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
415 |
+
}}
|
416 |
+
className={classNames("h-[28px]", {
|
417 |
+
"!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
|
418 |
+
!isEditableModeEnabled,
|
419 |
+
})}
|
420 |
+
>
|
421 |
+
<Crosshair className="size-4" />
|
422 |
+
Edit
|
423 |
+
</Button>
|
424 |
+
</TooltipTrigger>
|
425 |
+
<TooltipContent
|
426 |
+
align="start"
|
427 |
+
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
428 |
+
>
|
429 |
+
Select an element on the page to ask DeepSite edit it
|
430 |
+
directly.
|
431 |
+
</TooltipContent>
|
432 |
+
</Tooltip>
|
433 |
+
)}
|
434 |
+
{/* <InviteFriends /> */}
|
435 |
+
</div>
|
436 |
+
<div className="flex items-center justify-end gap-2">
|
437 |
<Settings
|
438 |
+
provider={provider as string}
|
439 |
+
model={model as string}
|
440 |
+
onChange={setProvider}
|
441 |
+
onModelChange={setModel}
|
442 |
open={openProvider}
|
443 |
error={providerError}
|
444 |
isFollowUp={!isSameHtml && isFollowUp}
|
445 |
onClose={setOpenProvider}
|
446 |
/>
|
|
|
|
|
|
|
|
|
|
|
447 |
<Button
|
448 |
size="iconXs"
|
449 |
+
disabled={isAiWorking || !prompt.trim()}
|
|
|
|
|
|
|
|
|
450 |
onClick={() => callAi()}
|
451 |
>
|
452 |
<ArrowUp className="size-4" />
|
453 |
</Button>
|
454 |
</div>
|
455 |
</div>
|
456 |
+
<LoginModal open={open} onClose={() => setOpen(false)} pages={pages} />
|
457 |
+
<ProModal
|
458 |
+
pages={pages}
|
459 |
+
open={openProModal}
|
460 |
+
onClose={() => setOpenProModal(false)}
|
461 |
+
/>
|
462 |
+
{pages.length === 1 && (
|
463 |
+
<div className="border border-sky-500/20 bg-sky-500/40 hover:bg-sky-600 transition-all duration-200 text-sky-500 pl-2 pr-4 py-1.5 text-xs rounded-full absolute top-0 -translate-y-[calc(100%+8px)] left-0 max-w-max flex items-center justify-start gap-2">
|
464 |
+
<span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
|
465 |
+
NEW
|
466 |
+
</span>
|
467 |
+
<p className="text-sm text-neutral-100">
|
468 |
+
DeepSite can now create multiple pages at once. Try it!
|
469 |
+
</p>
|
470 |
+
</div>
|
471 |
+
)}
|
472 |
+
{!isSameHtml && (
|
473 |
+
<div className="absolute top-0 right-0 -translate-y-[calc(100%+8px)] select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5">
|
474 |
+
<label
|
475 |
+
htmlFor="diff-patch-checkbox"
|
476 |
+
className="flex items-center gap-1.5 cursor-pointer"
|
477 |
+
>
|
478 |
+
<Checkbox
|
479 |
+
id="diff-patch-checkbox"
|
480 |
+
checked={isFollowUp}
|
481 |
+
onCheckedChange={(e) => {
|
482 |
+
if (e === true && !isSameHtml && selectedModel?.isThinker) {
|
483 |
+
setModel(MODELS[0].value);
|
484 |
+
}
|
485 |
+
setIsFollowUp(e === true);
|
486 |
+
}}
|
487 |
+
/>
|
488 |
+
Diff-Patch Update
|
489 |
+
</label>
|
490 |
+
<FollowUpTooltip />
|
491 |
+
</div>
|
492 |
+
)}
|
493 |
</div>
|
494 |
<audio ref={hookAudio} id="audio" className="hidden">
|
495 |
<source src="/success.mp3" type="audio/mpeg" />
|
|
|
497 |
</audio>
|
498 |
</div>
|
499 |
);
|
500 |
+
}
|
components/editor/ask-ai/loading.tsx
DELETED
@@ -1,68 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
import Loading from "@/components/loading";
|
3 |
-
import { useState, useEffect } from "react";
|
4 |
-
import { useInterval } from "react-use";
|
5 |
-
|
6 |
-
const TEXTS = [
|
7 |
-
"Teaching pixels to dance with style...",
|
8 |
-
"AI is having a creative breakthrough...",
|
9 |
-
"Channeling digital vibes into pure code...",
|
10 |
-
"Summoning the website spirits...",
|
11 |
-
"Brewing some algorithmic magic...",
|
12 |
-
"Composing a symphony of divs and spans...",
|
13 |
-
"Riding the wave of computational creativity...",
|
14 |
-
"Aligning the stars for perfect design...",
|
15 |
-
"Training circus animals to write CSS...",
|
16 |
-
"Launching ideas into the digital stratosphere...",
|
17 |
-
];
|
18 |
-
|
19 |
-
export const AiLoading = ({
|
20 |
-
text,
|
21 |
-
className,
|
22 |
-
}: {
|
23 |
-
text?: string;
|
24 |
-
className?: string;
|
25 |
-
}) => {
|
26 |
-
const [selectedText, setSelectedText] = useState(
|
27 |
-
text ?? TEXTS[0] // Start with first text to avoid hydration issues
|
28 |
-
);
|
29 |
-
|
30 |
-
// Set random text on client-side only to avoid hydration mismatch
|
31 |
-
useEffect(() => {
|
32 |
-
if (!text) {
|
33 |
-
setSelectedText(TEXTS[Math.floor(Math.random() * TEXTS.length)]);
|
34 |
-
}
|
35 |
-
}, [text]);
|
36 |
-
|
37 |
-
useInterval(() => {
|
38 |
-
if (!text) {
|
39 |
-
if (selectedText === TEXTS[TEXTS.length - 1]) {
|
40 |
-
setSelectedText(TEXTS[0]);
|
41 |
-
} else {
|
42 |
-
setSelectedText(TEXTS[TEXTS.indexOf(selectedText) + 1]);
|
43 |
-
}
|
44 |
-
}
|
45 |
-
}, 12000);
|
46 |
-
return (
|
47 |
-
<div className={`flex items-center justify-start gap-2 ${className}`}>
|
48 |
-
<Loading overlay={false} className="!size-5 opacity-50" />
|
49 |
-
<p className="text-neutral-400 text-sm">
|
50 |
-
<span className="inline-flex">
|
51 |
-
{selectedText.split("").map((char, index) => (
|
52 |
-
<span
|
53 |
-
key={index}
|
54 |
-
className="bg-gradient-to-r from-neutral-100 to-neutral-300 bg-clip-text text-transparent animate-pulse"
|
55 |
-
style={{
|
56 |
-
animationDelay: `${index * 0.1}s`,
|
57 |
-
animationDuration: "1.3s",
|
58 |
-
animationIterationCount: "infinite",
|
59 |
-
}}
|
60 |
-
>
|
61 |
-
{char === " " ? "\u00A0" : char}
|
62 |
-
</span>
|
63 |
-
))}
|
64 |
-
</span>
|
65 |
-
</p>
|
66 |
-
</div>
|
67 |
-
);
|
68 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/content-modal.tsx
DELETED
@@ -1,196 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { ChevronRight, RefreshCcw } from "lucide-react";
|
3 |
-
import { useState } from "react";
|
4 |
-
import { TailwindColors } from "./tailwind-colors";
|
5 |
-
import { Switch } from "@/components/ui/switch";
|
6 |
-
import { Button } from "@/components/ui/button";
|
7 |
-
import { Themes } from "./themes";
|
8 |
-
import { EnhancedSettings } from "@/types";
|
9 |
-
|
10 |
-
export const ContentModal = ({
|
11 |
-
enhancedSettings,
|
12 |
-
setEnhancedSettings,
|
13 |
-
}: {
|
14 |
-
enhancedSettings: EnhancedSettings;
|
15 |
-
setEnhancedSettings: (settings: EnhancedSettings) => void;
|
16 |
-
}) => {
|
17 |
-
const [collapsed, setCollapsed] = useState(["colors", "theme"]);
|
18 |
-
return (
|
19 |
-
<main className="overflow-x-hidden max-h-[50dvh] overflow-y-auto">
|
20 |
-
<section className="w-full border-b border-neutral-800/80 px-6 py-3.5 sticky top-0 bg-neutral-900 z-10">
|
21 |
-
<div className="flex items-center justify-between gap-3">
|
22 |
-
<p className="text-base font-semibold text-neutral-200">
|
23 |
-
Allow DeepSite to enhance your prompt
|
24 |
-
</p>
|
25 |
-
<Switch
|
26 |
-
checked={enhancedSettings.isActive}
|
27 |
-
onCheckedChange={() =>
|
28 |
-
setEnhancedSettings({
|
29 |
-
...enhancedSettings,
|
30 |
-
isActive: !enhancedSettings.isActive,
|
31 |
-
})
|
32 |
-
}
|
33 |
-
/>
|
34 |
-
</div>
|
35 |
-
<p className="text-sm text-neutral-500 mt-2">
|
36 |
-
While using DeepSite enhanced prompt, you'll get better results. We'll
|
37 |
-
add more details and features to your request.
|
38 |
-
</p>
|
39 |
-
<div className="text-sm text-sky-500 mt-3 bg-gradient-to-r from-sky-400/15 to-purple-400/15 rounded-md px-3 py-2 border border-white/10">
|
40 |
-
<p className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text">
|
41 |
-
You can also use the custom properties below to set specific
|
42 |
-
information.
|
43 |
-
</p>
|
44 |
-
</div>
|
45 |
-
</section>
|
46 |
-
<section className="py-3.5 border-b border-neutral-800/80">
|
47 |
-
<div
|
48 |
-
className={classNames(
|
49 |
-
"flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
|
50 |
-
{
|
51 |
-
"!text-neutral-200": collapsed.includes("colors"),
|
52 |
-
}
|
53 |
-
)}
|
54 |
-
onClick={() =>
|
55 |
-
setCollapsed((prev) => {
|
56 |
-
if (prev.includes("colors")) {
|
57 |
-
return prev.filter((item) => item !== "colors");
|
58 |
-
}
|
59 |
-
return [...prev, "colors"];
|
60 |
-
})
|
61 |
-
}
|
62 |
-
>
|
63 |
-
<ChevronRight className="size-4" />
|
64 |
-
<p className="text-base font-semibold">Colors</p>
|
65 |
-
</div>
|
66 |
-
{collapsed.includes("colors") && (
|
67 |
-
<div className="mt-4 space-y-4">
|
68 |
-
<article className="w-full">
|
69 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
70 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
71 |
-
Primary Color
|
72 |
-
</p>
|
73 |
-
<Button
|
74 |
-
variant="bordered"
|
75 |
-
size="xss"
|
76 |
-
className={`${
|
77 |
-
enhancedSettings.primaryColor ? "" : "opacity-0"
|
78 |
-
}`}
|
79 |
-
onClick={() =>
|
80 |
-
setEnhancedSettings({
|
81 |
-
...enhancedSettings,
|
82 |
-
primaryColor: undefined,
|
83 |
-
})
|
84 |
-
}
|
85 |
-
>
|
86 |
-
<RefreshCcw className="size-2.5" />
|
87 |
-
Reset
|
88 |
-
</Button>
|
89 |
-
</div>
|
90 |
-
<div className="text-muted-foreground text-sm mt-4">
|
91 |
-
<TailwindColors
|
92 |
-
value={enhancedSettings.primaryColor}
|
93 |
-
onChange={(value) =>
|
94 |
-
setEnhancedSettings({
|
95 |
-
...enhancedSettings,
|
96 |
-
primaryColor: value,
|
97 |
-
})
|
98 |
-
}
|
99 |
-
/>
|
100 |
-
</div>
|
101 |
-
</article>
|
102 |
-
<article className="w-full">
|
103 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
104 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
105 |
-
Secondary Color
|
106 |
-
</p>
|
107 |
-
<Button
|
108 |
-
variant="bordered"
|
109 |
-
size="xss"
|
110 |
-
className={`${
|
111 |
-
enhancedSettings.secondaryColor ? "" : "opacity-0"
|
112 |
-
}`}
|
113 |
-
onClick={() =>
|
114 |
-
setEnhancedSettings({
|
115 |
-
...enhancedSettings,
|
116 |
-
secondaryColor: undefined,
|
117 |
-
})
|
118 |
-
}
|
119 |
-
>
|
120 |
-
<RefreshCcw className="size-2.5" />
|
121 |
-
Reset
|
122 |
-
</Button>
|
123 |
-
</div>
|
124 |
-
<div className="text-muted-foreground text-sm mt-4">
|
125 |
-
<TailwindColors
|
126 |
-
value={enhancedSettings.secondaryColor}
|
127 |
-
onChange={(value) =>
|
128 |
-
setEnhancedSettings({
|
129 |
-
...enhancedSettings,
|
130 |
-
secondaryColor: value,
|
131 |
-
})
|
132 |
-
}
|
133 |
-
/>
|
134 |
-
</div>
|
135 |
-
</article>
|
136 |
-
</div>
|
137 |
-
)}
|
138 |
-
</section>
|
139 |
-
<section className="py-3.5 border-b border-neutral-800/80">
|
140 |
-
<div
|
141 |
-
className={classNames(
|
142 |
-
"flex items-center justify-start gap-3 px-4 cursor-pointer text-neutral-400 hover:text-neutral-200",
|
143 |
-
{
|
144 |
-
"!text-neutral-200": collapsed.includes("theme"),
|
145 |
-
}
|
146 |
-
)}
|
147 |
-
onClick={() =>
|
148 |
-
setCollapsed((prev) => {
|
149 |
-
if (prev.includes("theme")) {
|
150 |
-
return prev.filter((item) => item !== "theme");
|
151 |
-
}
|
152 |
-
return [...prev, "theme"];
|
153 |
-
})
|
154 |
-
}
|
155 |
-
>
|
156 |
-
<ChevronRight className="size-4" />
|
157 |
-
<p className="text-base font-semibold">Theme</p>
|
158 |
-
</div>
|
159 |
-
{collapsed.includes("theme") && (
|
160 |
-
<article className="w-full mt-4">
|
161 |
-
<div className="flex items-center justify-start gap-2 px-5">
|
162 |
-
<p className="text-xs font-medium uppercase text-neutral-400">
|
163 |
-
Theme
|
164 |
-
</p>
|
165 |
-
<Button
|
166 |
-
variant="bordered"
|
167 |
-
size="xss"
|
168 |
-
className={`${enhancedSettings.theme ? "" : "opacity-0"}`}
|
169 |
-
onClick={() =>
|
170 |
-
setEnhancedSettings({
|
171 |
-
...enhancedSettings,
|
172 |
-
theme: undefined,
|
173 |
-
})
|
174 |
-
}
|
175 |
-
>
|
176 |
-
<RefreshCcw className="size-2.5" />
|
177 |
-
Reset
|
178 |
-
</Button>
|
179 |
-
</div>
|
180 |
-
<div className="text-muted-foreground text-sm mt-4">
|
181 |
-
<Themes
|
182 |
-
value={enhancedSettings.theme}
|
183 |
-
onChange={(value) =>
|
184 |
-
setEnhancedSettings({
|
185 |
-
...enhancedSettings,
|
186 |
-
theme: value,
|
187 |
-
})
|
188 |
-
}
|
189 |
-
/>
|
190 |
-
</div>
|
191 |
-
</article>
|
192 |
-
)}
|
193 |
-
</section>
|
194 |
-
</main>
|
195 |
-
);
|
196 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/index.tsx
DELETED
@@ -1,68 +0,0 @@
|
|
1 |
-
import { useState } from "react";
|
2 |
-
import { WandSparkles } from "lucide-react";
|
3 |
-
|
4 |
-
import { Button } from "@/components/ui/button";
|
5 |
-
import { useEditor } from "@/hooks/useEditor";
|
6 |
-
import { useAi } from "@/hooks/useAi";
|
7 |
-
import {
|
8 |
-
Dialog,
|
9 |
-
DialogContent,
|
10 |
-
DialogFooter,
|
11 |
-
DialogTitle,
|
12 |
-
} from "@/components/ui/dialog";
|
13 |
-
import { ContentModal } from "./content-modal";
|
14 |
-
import { EnhancedSettings } from "@/types";
|
15 |
-
|
16 |
-
export const PromptBuilder = ({
|
17 |
-
enhancedSettings,
|
18 |
-
setEnhancedSettings,
|
19 |
-
}: {
|
20 |
-
enhancedSettings: EnhancedSettings;
|
21 |
-
setEnhancedSettings: (settings: EnhancedSettings) => void;
|
22 |
-
}) => {
|
23 |
-
const { globalAiLoading } = useAi();
|
24 |
-
const { globalEditorLoading } = useEditor();
|
25 |
-
|
26 |
-
const [open, setOpen] = useState(false);
|
27 |
-
return (
|
28 |
-
<>
|
29 |
-
<Button
|
30 |
-
size="xs"
|
31 |
-
variant="outline"
|
32 |
-
className="!rounded-md !border-white/10 !bg-gradient-to-r from-sky-400/15 to-purple-400/15 light-sweep hover:brightness-110"
|
33 |
-
disabled={globalAiLoading || globalEditorLoading}
|
34 |
-
onClick={() => {
|
35 |
-
setOpen(true);
|
36 |
-
}}
|
37 |
-
>
|
38 |
-
<WandSparkles className="size-3.5 text-sky-500 relative z-10" />
|
39 |
-
<span className="text-transparent bg-gradient-to-r from-sky-400 to-purple-400 bg-clip-text relative z-10">
|
40 |
-
Enhance
|
41 |
-
</span>
|
42 |
-
</Button>
|
43 |
-
<Dialog open={open} onOpenChange={() => setOpen(false)}>
|
44 |
-
<DialogContent className="sm:max-w-xl !p-0 !rounded-3xl !bg-neutral-900 !border-neutral-800/80 !gap-0">
|
45 |
-
<DialogTitle className="px-6 py-3.5 border-b border-neutral-800">
|
46 |
-
<div className="flex items-center justify-start gap-2 text-neutral-200 text-base font-medium">
|
47 |
-
<WandSparkles className="size-3.5" />
|
48 |
-
<p>Enhance Prompt</p>
|
49 |
-
</div>
|
50 |
-
</DialogTitle>
|
51 |
-
<ContentModal
|
52 |
-
enhancedSettings={enhancedSettings}
|
53 |
-
setEnhancedSettings={setEnhancedSettings}
|
54 |
-
/>
|
55 |
-
<DialogFooter className="px-6 py-3.5 border-t border-neutral-800">
|
56 |
-
<Button
|
57 |
-
variant="bordered"
|
58 |
-
size="default"
|
59 |
-
onClick={() => setOpen(false)}
|
60 |
-
>
|
61 |
-
Close
|
62 |
-
</Button>
|
63 |
-
</DialogFooter>
|
64 |
-
</DialogContent>
|
65 |
-
</Dialog>
|
66 |
-
</>
|
67 |
-
);
|
68 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/tailwind-colors.tsx
DELETED
@@ -1,58 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { useRef } from "react";
|
3 |
-
|
4 |
-
import { TAILWIND_COLORS } from "@/lib/prompt-builder";
|
5 |
-
import { useMount } from "react-use";
|
6 |
-
|
7 |
-
export const TailwindColors = ({
|
8 |
-
value,
|
9 |
-
onChange,
|
10 |
-
}: {
|
11 |
-
value: string | undefined;
|
12 |
-
onChange: (value: string) => void;
|
13 |
-
}) => {
|
14 |
-
const ref = useRef<HTMLDivElement>(null);
|
15 |
-
|
16 |
-
useMount(() => {
|
17 |
-
if (ref.current) {
|
18 |
-
if (value) {
|
19 |
-
const color = ref.current.querySelector(`[data-color="${value}"]`);
|
20 |
-
if (color) {
|
21 |
-
color.scrollIntoView({ inline: "center" });
|
22 |
-
}
|
23 |
-
}
|
24 |
-
}
|
25 |
-
});
|
26 |
-
return (
|
27 |
-
<div
|
28 |
-
ref={ref}
|
29 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
30 |
-
>
|
31 |
-
{TAILWIND_COLORS.map((color) => (
|
32 |
-
<div
|
33 |
-
key={color}
|
34 |
-
className={classNames(
|
35 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-16 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
36 |
-
{
|
37 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
38 |
-
value === color,
|
39 |
-
}
|
40 |
-
)}
|
41 |
-
data-color={color}
|
42 |
-
onClick={() => onChange(color)}
|
43 |
-
>
|
44 |
-
<div
|
45 |
-
className={`w-4 h-4 min-w-4 min-h-4 rounded-xl ${
|
46 |
-
["white", "black"].includes(color)
|
47 |
-
? `bg-${color}`
|
48 |
-
: `bg-${color}-500`
|
49 |
-
}`}
|
50 |
-
/>
|
51 |
-
<p className="text-xs capitalize text-neutral-200 truncate">
|
52 |
-
{color}
|
53 |
-
</p>
|
54 |
-
</div>
|
55 |
-
))}
|
56 |
-
</div>
|
57 |
-
);
|
58 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/prompt-builder/themes.tsx
DELETED
@@ -1,48 +0,0 @@
|
|
1 |
-
import { Theme } from "@/types";
|
2 |
-
import classNames from "classnames";
|
3 |
-
import { Moon, Sun } from "lucide-react";
|
4 |
-
import { useRef } from "react";
|
5 |
-
|
6 |
-
export const Themes = ({
|
7 |
-
value,
|
8 |
-
onChange,
|
9 |
-
}: {
|
10 |
-
value: Theme;
|
11 |
-
onChange: (value: Theme) => void;
|
12 |
-
}) => {
|
13 |
-
const ref = useRef<HTMLDivElement>(null);
|
14 |
-
|
15 |
-
return (
|
16 |
-
<div
|
17 |
-
ref={ref}
|
18 |
-
className="flex items-center justify-start gap-3 overflow-x-auto px-5 scrollbar-hide"
|
19 |
-
>
|
20 |
-
<div
|
21 |
-
className={classNames(
|
22 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
23 |
-
{
|
24 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
25 |
-
value === "light",
|
26 |
-
}
|
27 |
-
)}
|
28 |
-
onClick={() => onChange("light")}
|
29 |
-
>
|
30 |
-
<Sun className="size-4 text-amber-500" />
|
31 |
-
<p className="text-xs capitalize text-neutral-200 truncate">Light</p>
|
32 |
-
</div>
|
33 |
-
<div
|
34 |
-
className={classNames(
|
35 |
-
"flex flex-col items-center justify-center p-3 size-16 min-w-32 gap-2 rounded-lg border border-neutral-800 bg-neutral-800/30 hover:brightness-120 cursor-pointer",
|
36 |
-
{
|
37 |
-
"!border-neutral-700 !bg-neutral-800/80 hover:!brightness-100":
|
38 |
-
value === "dark",
|
39 |
-
}
|
40 |
-
)}
|
41 |
-
onClick={() => onChange("dark")}
|
42 |
-
>
|
43 |
-
<Moon className="size-4 text-indigo-500" />
|
44 |
-
<p className="text-xs capitalize text-neutral-200 truncate">Dark</p>
|
45 |
-
</div>
|
46 |
-
</div>
|
47 |
-
);
|
48 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/re-imagine.tsx
CHANGED
@@ -11,8 +11,6 @@ import {
|
|
11 |
import { Input } from "@/components/ui/input";
|
12 |
import Loading from "@/components/loading";
|
13 |
import { api } from "@/lib/api";
|
14 |
-
import { useAi } from "@/hooks/useAi";
|
15 |
-
import { useEditor } from "@/hooks/useEditor";
|
16 |
|
17 |
export function ReImagine({
|
18 |
onRedesign,
|
@@ -22,8 +20,6 @@ export function ReImagine({
|
|
22 |
const [url, setUrl] = useState<string>("");
|
23 |
const [open, setOpen] = useState(false);
|
24 |
const [isLoading, setIsLoading] = useState(false);
|
25 |
-
const { globalAiLoading } = useAi();
|
26 |
-
const { globalEditorLoading } = useEditor();
|
27 |
|
28 |
const checkIfUrlIsValid = (url: string) => {
|
29 |
const urlPattern = new RegExp(
|
@@ -63,13 +59,11 @@ export function ReImagine({
|
|
63 |
<form>
|
64 |
<PopoverTrigger asChild>
|
65 |
<Button
|
66 |
-
size="
|
67 |
-
variant=
|
68 |
-
className="!
|
69 |
-
disabled={globalAiLoading || globalEditorLoading}
|
70 |
>
|
71 |
-
<Paintbrush className="size-
|
72 |
-
Redesign
|
73 |
</Button>
|
74 |
</PopoverTrigger>
|
75 |
<PopoverContent
|
|
|
11 |
import { Input } from "@/components/ui/input";
|
12 |
import Loading from "@/components/loading";
|
13 |
import { api } from "@/lib/api";
|
|
|
|
|
14 |
|
15 |
export function ReImagine({
|
16 |
onRedesign,
|
|
|
20 |
const [url, setUrl] = useState<string>("");
|
21 |
const [open, setOpen] = useState(false);
|
22 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
23 |
|
24 |
const checkIfUrlIsValid = (url: string) => {
|
25 |
const urlPattern = new RegExp(
|
|
|
59 |
<form>
|
60 |
<PopoverTrigger asChild>
|
61 |
<Button
|
62 |
+
size="iconXs"
|
63 |
+
variant="outline"
|
64 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
|
|
65 |
>
|
66 |
+
<Paintbrush className="size-4" />
|
|
|
67 |
</Button>
|
68 |
</PopoverTrigger>
|
69 |
<PopoverContent
|
components/editor/ask-ai/selector.tsx
DELETED
@@ -1,41 +0,0 @@
|
|
1 |
-
import classNames from "classnames";
|
2 |
-
import { Crosshair } from "lucide-react";
|
3 |
-
|
4 |
-
import { Button } from "@/components/ui/button";
|
5 |
-
import {
|
6 |
-
Tooltip,
|
7 |
-
TooltipContent,
|
8 |
-
TooltipTrigger,
|
9 |
-
} from "@/components/ui/tooltip";
|
10 |
-
import { useAi } from "@/hooks/useAi";
|
11 |
-
import { useEditor } from "@/hooks/useEditor";
|
12 |
-
|
13 |
-
export const Selector = () => {
|
14 |
-
const { globalEditorLoading } = useEditor();
|
15 |
-
const { isEditableModeEnabled, setIsEditableModeEnabled, globalAiLoading } =
|
16 |
-
useAi();
|
17 |
-
return (
|
18 |
-
<Tooltip>
|
19 |
-
<TooltipTrigger asChild>
|
20 |
-
<Button
|
21 |
-
size="xs"
|
22 |
-
variant={isEditableModeEnabled ? "default" : "outline"}
|
23 |
-
onClick={() => {
|
24 |
-
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
25 |
-
}}
|
26 |
-
disabled={globalAiLoading || globalEditorLoading}
|
27 |
-
className="!rounded-md"
|
28 |
-
>
|
29 |
-
<Crosshair className="size-3.5" />
|
30 |
-
Edit
|
31 |
-
</Button>
|
32 |
-
</TooltipTrigger>
|
33 |
-
<TooltipContent
|
34 |
-
align="start"
|
35 |
-
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
36 |
-
>
|
37 |
-
Select an element on the page to ask DeepSite edit it directly.
|
38 |
-
</TooltipContent>
|
39 |
-
</Tooltip>
|
40 |
-
);
|
41 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/editor/ask-ai/settings.tsx
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
-
"use client";
|
2 |
import classNames from "classnames";
|
|
|
|
|
3 |
|
4 |
import {
|
5 |
Popover,
|
@@ -17,273 +18,185 @@ import {
|
|
17 |
SelectTrigger,
|
18 |
SelectValue,
|
19 |
} from "@/components/ui/select";
|
20 |
-
import { useMemo
|
21 |
import { useUpdateEffect } from "react-use";
|
22 |
import Image from "next/image";
|
23 |
-
import { Brain, BrainIcon, CheckCheck, ChevronDown } from "lucide-react";
|
24 |
-
import { useAi } from "@/hooks/useAi";
|
25 |
-
import { getProviders } from "@/lib/get-providers";
|
26 |
-
import Loading from "@/components/loading";
|
27 |
|
28 |
export function Settings({
|
29 |
open,
|
30 |
onClose,
|
|
|
|
|
31 |
error,
|
32 |
isFollowUp = false,
|
|
|
|
|
33 |
}: {
|
34 |
open: boolean;
|
|
|
|
|
35 |
error?: string;
|
36 |
isFollowUp?: boolean;
|
37 |
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
|
38 |
}) {
|
39 |
-
const {
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
const [loadingProviders, setLoadingProviders] = useState(false);
|
49 |
-
|
50 |
-
useEffect(() => {
|
51 |
-
setIsMounted(true);
|
52 |
-
}, []);
|
53 |
-
|
54 |
-
// const modelAvailableProviders = useMemo(() => {
|
55 |
-
// const availableProviders = MODELS.find(
|
56 |
-
// (m: { value: string }) => m.value === model
|
57 |
-
// )?.providers;
|
58 |
-
// if (!availableProviders) return Object.keys(PROVIDERS);
|
59 |
-
// return Object.keys(PROVIDERS).filter((id) =>
|
60 |
-
// availableProviders.includes(id)
|
61 |
-
// );
|
62 |
-
// }, [model]);
|
63 |
|
64 |
useUpdateEffect(() => {
|
65 |
-
if (provider !== "auto" && !
|
66 |
-
|
67 |
}
|
68 |
}, [model, provider]);
|
69 |
|
70 |
-
const formattedModels = useMemo(() => {
|
71 |
-
const lists: ((typeof MODELS)[0] | { isCategory: true; name: string })[] =
|
72 |
-
[];
|
73 |
-
const keys = new Set<string>();
|
74 |
-
MODELS.forEach((model) => {
|
75 |
-
if (!keys.has(model.companyName)) {
|
76 |
-
lists.push({
|
77 |
-
isCategory: true,
|
78 |
-
name: model.companyName,
|
79 |
-
logo: model.logo,
|
80 |
-
});
|
81 |
-
keys.add(model.companyName);
|
82 |
-
}
|
83 |
-
lists.push(model);
|
84 |
-
});
|
85 |
-
return lists;
|
86 |
-
}, [MODELS]);
|
87 |
-
|
88 |
-
const [providers, setProviders] = useState<any[]>([]);
|
89 |
-
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
|
90 |
-
|
91 |
-
useEffect(() => {
|
92 |
-
const loadProviders = async () => {
|
93 |
-
setLoadingProviders(true);
|
94 |
-
if (!model) {
|
95 |
-
setProviders([]);
|
96 |
-
return;
|
97 |
-
}
|
98 |
-
try {
|
99 |
-
const result = await getProviders(model);
|
100 |
-
setProviders(result);
|
101 |
-
} catch (error) {
|
102 |
-
console.error("Failed to load providers:", error);
|
103 |
-
setProviders([]);
|
104 |
-
} finally {
|
105 |
-
setLoadingProviders(false);
|
106 |
-
}
|
107 |
-
};
|
108 |
-
|
109 |
-
loadProviders();
|
110 |
-
}, [model]);
|
111 |
-
|
112 |
-
const handleImageError = (providerId: string) => {
|
113 |
-
setFailedImages((prev) => new Set([...prev, providerId]));
|
114 |
-
};
|
115 |
-
|
116 |
return (
|
117 |
-
<
|
118 |
-
<
|
119 |
-
<
|
120 |
-
variant=
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
124 |
>
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
className=
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
<SelectValue placeholder="Select a model" />
|
161 |
-
</SelectTrigger>
|
162 |
-
<SelectContent>
|
163 |
-
<SelectGroup>
|
164 |
-
{formattedModels.map((item: any) => {
|
165 |
-
if ("isCategory" in item) {
|
166 |
-
return (
|
167 |
-
<SelectLabel
|
168 |
-
key={item.name}
|
169 |
-
className="flex items-center gap-1"
|
170 |
>
|
171 |
-
{
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
{label}
|
189 |
-
{isNew && (
|
190 |
-
<span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
|
191 |
-
New
|
192 |
-
</span>
|
193 |
-
)}
|
194 |
-
</SelectItem>
|
195 |
-
);
|
196 |
-
})}
|
197 |
-
</SelectGroup>
|
198 |
-
</SelectContent>
|
199 |
-
</Select>
|
200 |
-
</label>
|
201 |
-
{/* {isFollowUp && (
|
202 |
-
<div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
|
203 |
-
Note: You can't use a Thinker model for follow-up requests.
|
204 |
-
We automatically switch to the default model for you.
|
205 |
-
</div>
|
206 |
-
)} */}
|
207 |
-
<div className="flex flex-col gap-3">
|
208 |
-
<div className="flex items-center justify-between">
|
209 |
-
<div>
|
210 |
-
<p className="text-neutral-300 text-sm mb-1.5">
|
211 |
-
Use auto-provider
|
212 |
-
</p>
|
213 |
-
<p className="text-xs text-neutral-400/70">
|
214 |
-
We'll automatically select the best provider for you
|
215 |
-
based on your prompt.
|
216 |
-
</p>
|
217 |
</div>
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
setProvider(foundModel.autoProvider);
|
231 |
-
} else {
|
232 |
-
setProvider("auto");
|
233 |
-
}
|
234 |
-
}}
|
235 |
-
>
|
236 |
<div
|
237 |
className={classNames(
|
238 |
-
"w-
|
239 |
{
|
240 |
-
"
|
241 |
}
|
242 |
)}
|
243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
</div>
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
{loadingProviders ? (
|
252 |
-
<Loading overlay={false} />
|
253 |
-
) : (
|
254 |
-
providers.map((id: string) => (
|
255 |
<Button
|
256 |
key={id}
|
257 |
variant={id === provider ? "default" : "secondary"}
|
258 |
size="sm"
|
259 |
onClick={() => {
|
260 |
-
|
261 |
}}
|
262 |
>
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
height={20}
|
272 |
-
onError={() => handleImageError(id)}
|
273 |
-
/>
|
274 |
-
)}
|
275 |
-
{PROVIDERS?.[id as keyof typeof PROVIDERS]?.name || id}
|
276 |
{id === provider && (
|
277 |
-
<
|
278 |
)}
|
279 |
</Button>
|
280 |
-
))
|
281 |
-
|
282 |
-
</
|
283 |
-
</
|
284 |
-
</
|
285 |
-
</
|
286 |
-
</
|
287 |
-
</
|
288 |
);
|
289 |
}
|
|
|
|
|
1 |
import classNames from "classnames";
|
2 |
+
import { PiGearSixFill } from "react-icons/pi";
|
3 |
+
import { RiCheckboxCircleFill } from "react-icons/ri";
|
4 |
|
5 |
import {
|
6 |
Popover,
|
|
|
18 |
SelectTrigger,
|
19 |
SelectValue,
|
20 |
} from "@/components/ui/select";
|
21 |
+
import { useMemo } from "react";
|
22 |
import { useUpdateEffect } from "react-use";
|
23 |
import Image from "next/image";
|
|
|
|
|
|
|
|
|
24 |
|
25 |
export function Settings({
|
26 |
open,
|
27 |
onClose,
|
28 |
+
provider,
|
29 |
+
model,
|
30 |
error,
|
31 |
isFollowUp = false,
|
32 |
+
onChange,
|
33 |
+
onModelChange,
|
34 |
}: {
|
35 |
open: boolean;
|
36 |
+
provider: string;
|
37 |
+
model: string;
|
38 |
error?: string;
|
39 |
isFollowUp?: boolean;
|
40 |
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
41 |
+
onChange: (provider: string) => void;
|
42 |
+
onModelChange: (model: string) => void;
|
43 |
}) {
|
44 |
+
const modelAvailableProviders = useMemo(() => {
|
45 |
+
const availableProviders = MODELS.find(
|
46 |
+
(m: { value: string }) => m.value === model
|
47 |
+
)?.providers;
|
48 |
+
if (!availableProviders) return Object.keys(PROVIDERS);
|
49 |
+
return Object.keys(PROVIDERS).filter((id) =>
|
50 |
+
availableProviders.includes(id)
|
51 |
+
);
|
52 |
+
}, [model]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
|
54 |
useUpdateEffect(() => {
|
55 |
+
if (provider !== "auto" && !modelAvailableProviders.includes(provider)) {
|
56 |
+
onChange("auto");
|
57 |
}
|
58 |
}, [model, provider]);
|
59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
return (
|
61 |
+
<div className="">
|
62 |
+
<Popover open={open} onOpenChange={onClose}>
|
63 |
+
<PopoverTrigger asChild>
|
64 |
+
<Button variant="black" size="sm">
|
65 |
+
<PiGearSixFill className="size-4" />
|
66 |
+
Settings
|
67 |
+
</Button>
|
68 |
+
</PopoverTrigger>
|
69 |
+
<PopoverContent
|
70 |
+
className="!rounded-2xl p-0 !w-96 overflow-hidden !bg-neutral-900"
|
71 |
+
align="center"
|
72 |
>
|
73 |
+
<header className="flex items-center justify-center text-sm px-4 py-3 border-b gap-2 bg-neutral-950 border-neutral-800 font-semibold text-neutral-200">
|
74 |
+
Customize Settings
|
75 |
+
</header>
|
76 |
+
<main className="px-4 pt-5 pb-6 space-y-5">
|
77 |
+
{error !== "" && (
|
78 |
+
<p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
|
79 |
+
{error}
|
80 |
+
</p>
|
81 |
+
)}
|
82 |
+
<label className="block">
|
83 |
+
<p className="text-neutral-300 text-sm mb-2.5">Choose a model</p>
|
84 |
+
<Select defaultValue={model} onValueChange={onModelChange}>
|
85 |
+
<SelectTrigger className="w-full">
|
86 |
+
<SelectValue placeholder="Select a model" />
|
87 |
+
</SelectTrigger>
|
88 |
+
<SelectContent>
|
89 |
+
<SelectGroup>
|
90 |
+
<SelectLabel>Models</SelectLabel>
|
91 |
+
{MODELS.map(
|
92 |
+
({
|
93 |
+
value,
|
94 |
+
label,
|
95 |
+
isNew = false,
|
96 |
+
isThinker = false,
|
97 |
+
}: {
|
98 |
+
value: string;
|
99 |
+
label: string;
|
100 |
+
isNew?: boolean;
|
101 |
+
isThinker?: boolean;
|
102 |
+
}) => (
|
103 |
+
<SelectItem
|
104 |
+
key={value}
|
105 |
+
value={value}
|
106 |
+
className=""
|
107 |
+
disabled={isThinker && isFollowUp}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
>
|
109 |
+
{label}
|
110 |
+
{isNew && (
|
111 |
+
<span className="text-xs bg-gradient-to-br from-sky-400 to-sky-600 text-white rounded-full px-1.5 py-0.5">
|
112 |
+
New
|
113 |
+
</span>
|
114 |
+
)}
|
115 |
+
</SelectItem>
|
116 |
+
)
|
117 |
+
)}
|
118 |
+
</SelectGroup>
|
119 |
+
</SelectContent>
|
120 |
+
</Select>
|
121 |
+
</label>
|
122 |
+
{isFollowUp && (
|
123 |
+
<div className="bg-amber-500/10 border-amber-500/10 p-3 text-xs text-amber-500 border rounded-lg">
|
124 |
+
Note: You can't use a Thinker model for follow-up requests.
|
125 |
+
We automatically switch to the default model for you.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
</div>
|
127 |
+
)}
|
128 |
+
<div className="flex flex-col gap-3">
|
129 |
+
<div className="flex items-center justify-between">
|
130 |
+
<div>
|
131 |
+
<p className="text-neutral-300 text-sm mb-1.5">
|
132 |
+
Use auto-provider
|
133 |
+
</p>
|
134 |
+
<p className="text-xs text-neutral-400/70">
|
135 |
+
We'll automatically select the best provider for you
|
136 |
+
based on your prompt.
|
137 |
+
</p>
|
138 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
<div
|
140 |
className={classNames(
|
141 |
+
"bg-neutral-700 rounded-full min-w-10 w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
|
142 |
{
|
143 |
+
"!bg-sky-500": provider === "auto",
|
144 |
}
|
145 |
)}
|
146 |
+
onClick={() => {
|
147 |
+
const foundModel = MODELS.find(
|
148 |
+
(m: { value: string }) => m.value === model
|
149 |
+
);
|
150 |
+
if (provider === "auto" && foundModel?.autoProvider) {
|
151 |
+
onChange(foundModel.autoProvider);
|
152 |
+
} else {
|
153 |
+
onChange("auto");
|
154 |
+
}
|
155 |
+
}}
|
156 |
+
>
|
157 |
+
<div
|
158 |
+
className={classNames(
|
159 |
+
"w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-neutral-200",
|
160 |
+
{
|
161 |
+
"translate-x-4": provider === "auto",
|
162 |
+
}
|
163 |
+
)}
|
164 |
+
/>
|
165 |
+
</div>
|
166 |
</div>
|
167 |
+
<label className="block">
|
168 |
+
<p className="text-neutral-300 text-sm mb-2">
|
169 |
+
Inference Provider
|
170 |
+
</p>
|
171 |
+
<div className="grid grid-cols-2 gap-1.5">
|
172 |
+
{modelAvailableProviders.map((id: string) => (
|
|
|
|
|
|
|
|
|
173 |
<Button
|
174 |
key={id}
|
175 |
variant={id === provider ? "default" : "secondary"}
|
176 |
size="sm"
|
177 |
onClick={() => {
|
178 |
+
onChange(id);
|
179 |
}}
|
180 |
>
|
181 |
+
<Image
|
182 |
+
src={`/providers/${id}.svg`}
|
183 |
+
alt={PROVIDERS[id as keyof typeof PROVIDERS].name}
|
184 |
+
className="size-5 mr-2"
|
185 |
+
width={20}
|
186 |
+
height={20}
|
187 |
+
/>
|
188 |
+
{PROVIDERS[id as keyof typeof PROVIDERS].name}
|
|
|
|
|
|
|
|
|
|
|
189 |
{id === provider && (
|
190 |
+
<RiCheckboxCircleFill className="ml-2 size-4 text-blue-500" />
|
191 |
)}
|
192 |
</Button>
|
193 |
+
))}
|
194 |
+
</div>
|
195 |
+
</label>
|
196 |
+
</div>
|
197 |
+
</main>
|
198 |
+
</PopoverContent>
|
199 |
+
</Popover>
|
200 |
+
</div>
|
201 |
);
|
202 |
}
|
components/editor/ask-ai/uploader.tsx
CHANGED
@@ -1,12 +1,5 @@
|
|
1 |
import { useRef, useState } from "react";
|
2 |
-
import {
|
3 |
-
CheckCircle,
|
4 |
-
ImageIcon,
|
5 |
-
Images,
|
6 |
-
Link,
|
7 |
-
Paperclip,
|
8 |
-
Upload,
|
9 |
-
} from "lucide-react";
|
10 |
import Image from "next/image";
|
11 |
|
12 |
import {
|
@@ -15,151 +8,196 @@ import {
|
|
15 |
PopoverTrigger,
|
16 |
} from "@/components/ui/popover";
|
17 |
import { Button } from "@/components/ui/button";
|
18 |
-
import { Project } from "@/types";
|
19 |
import Loading from "@/components/loading";
|
|
|
20 |
import { useUser } from "@/hooks/useUser";
|
21 |
-
import {
|
22 |
-
import {
|
23 |
-
import { useLoginModal } from "@/components/contexts/login-context";
|
24 |
|
25 |
-
export const Uploader = ({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
const { user } = useUser();
|
27 |
-
const { openLoginModal } = useLoginModal();
|
28 |
-
const { uploadFiles, isUploading, files, globalEditorLoading } = useEditor();
|
29 |
-
const { selectedFiles, setSelectedFiles, globalAiLoading } = useAi();
|
30 |
|
31 |
const [open, setOpen] = useState(false);
|
32 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
-
|
|
|
49 |
<Popover open={open} onOpenChange={setOpen}>
|
50 |
-
<form
|
51 |
<PopoverTrigger asChild>
|
52 |
<Button
|
53 |
-
size="
|
54 |
-
variant=
|
55 |
-
className="!
|
56 |
-
disabled={globalAiLoading || globalEditorLoading}
|
57 |
>
|
58 |
-
<
|
59 |
-
Attach
|
60 |
</Button>
|
61 |
</PopoverTrigger>
|
62 |
<PopoverContent
|
63 |
align="start"
|
64 |
className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
|
65 |
>
|
66 |
-
|
67 |
-
|
68 |
-
<
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
Add Custom Images
|
80 |
-
</p>
|
81 |
-
<p className="text-sm text-neutral-500 mt-1.5">
|
82 |
-
Upload images to your project and use them with DeepSite!
|
83 |
-
</p>
|
84 |
-
</header>
|
85 |
-
<main className="space-y-4 p-5">
|
86 |
-
<div>
|
87 |
-
<p className="text-xs text-left text-neutral-700 mb-2">
|
88 |
-
Uploaded Images
|
89 |
-
</p>
|
90 |
-
{files?.length > 0 ? (
|
91 |
-
<div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
|
92 |
-
{files.map((file: string) => (
|
93 |
-
<div
|
94 |
-
key={file}
|
95 |
-
className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
|
96 |
-
onClick={() =>
|
97 |
-
setSelectedFiles(
|
98 |
-
selectedFiles.includes(file)
|
99 |
-
? selectedFiles.filter((f) => f !== file)
|
100 |
-
: [...selectedFiles, file]
|
101 |
-
)
|
102 |
-
}
|
103 |
-
>
|
104 |
-
<Image
|
105 |
-
src={file}
|
106 |
-
alt="uploaded image"
|
107 |
-
width={56}
|
108 |
-
height={56}
|
109 |
-
className="object-cover w-full rounded-sm aspect-square"
|
110 |
-
/>
|
111 |
-
{selectedFiles.includes(file) && (
|
112 |
-
<div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
|
113 |
-
<CheckCircle className="size-6 text-neutral-100" />
|
114 |
-
</div>
|
115 |
-
)}
|
116 |
-
</div>
|
117 |
-
))}
|
118 |
</div>
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
|
|
123 |
</p>
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
</PopoverContent>
|
162 |
</form>
|
163 |
</Popover>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
);
|
165 |
};
|
|
|
1 |
import { useRef, useState } from "react";
|
2 |
+
import { Images, Upload } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
import Image from "next/image";
|
4 |
|
5 |
import {
|
|
|
8 |
PopoverTrigger,
|
9 |
} from "@/components/ui/popover";
|
10 |
import { Button } from "@/components/ui/button";
|
11 |
+
import { Page, Project } from "@/types";
|
12 |
import Loading from "@/components/loading";
|
13 |
+
import { RiCheckboxCircleFill } from "react-icons/ri";
|
14 |
import { useUser } from "@/hooks/useUser";
|
15 |
+
import { LoginModal } from "@/components/login-modal";
|
16 |
+
import { DeployButtonContent } from "../deploy-button/content";
|
|
|
17 |
|
18 |
+
export const Uploader = ({
|
19 |
+
pages,
|
20 |
+
onLoading,
|
21 |
+
isLoading,
|
22 |
+
onFiles,
|
23 |
+
onSelectFile,
|
24 |
+
selectedFiles,
|
25 |
+
files,
|
26 |
+
project,
|
27 |
+
}: {
|
28 |
+
pages: Page[];
|
29 |
+
onLoading: (isLoading: boolean) => void;
|
30 |
+
isLoading: boolean;
|
31 |
+
files: string[];
|
32 |
+
onFiles: React.Dispatch<React.SetStateAction<string[]>>;
|
33 |
+
onSelectFile: (file: string) => void;
|
34 |
+
selectedFiles: string[];
|
35 |
+
project?: Project | null;
|
36 |
+
}) => {
|
37 |
const { user } = useUser();
|
|
|
|
|
|
|
38 |
|
39 |
const [open, setOpen] = useState(false);
|
40 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
41 |
|
42 |
+
const uploadFiles = async (files: FileList | null) => {
|
43 |
+
if (!files) return;
|
44 |
+
if (!project) return;
|
45 |
+
|
46 |
+
onLoading(true);
|
47 |
+
|
48 |
+
const images = Array.from(files).filter((file) => {
|
49 |
+
return file.type.startsWith("image/");
|
50 |
+
});
|
51 |
+
|
52 |
+
const data = new FormData();
|
53 |
+
images.forEach((image) => {
|
54 |
+
data.append("images", image);
|
55 |
+
});
|
56 |
+
|
57 |
+
const response = await fetch(
|
58 |
+
`/api/me/projects/${project.space_id}/images`,
|
59 |
+
{
|
60 |
+
method: "POST",
|
61 |
+
body: data,
|
62 |
+
}
|
63 |
);
|
64 |
+
if (response.ok) {
|
65 |
+
const data = await response.json();
|
66 |
+
onFiles((prev) => [...prev, ...data.uploadedFiles]);
|
67 |
+
}
|
68 |
+
onLoading(false);
|
69 |
+
};
|
70 |
|
71 |
+
// TODO FIRST PUBLISH YOUR PROJECT TO UPLOAD IMAGES.
|
72 |
+
return user?.id ? (
|
73 |
<Popover open={open} onOpenChange={setOpen}>
|
74 |
+
<form>
|
75 |
<PopoverTrigger asChild>
|
76 |
<Button
|
77 |
+
size="iconXs"
|
78 |
+
variant="outline"
|
79 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
|
|
80 |
>
|
81 |
+
<Images className="size-4" />
|
|
|
82 |
</Button>
|
83 |
</PopoverTrigger>
|
84 |
<PopoverContent
|
85 |
align="start"
|
86 |
className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
|
87 |
>
|
88 |
+
{project?.space_id ? (
|
89 |
+
<>
|
90 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
91 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
92 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
93 |
+
🎨
|
94 |
+
</div>
|
95 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
96 |
+
🖼️
|
97 |
+
</div>
|
98 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
99 |
+
💻
|
100 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
</div>
|
102 |
+
<p className="text-xl font-semibold text-neutral-950">
|
103 |
+
Add Custom Images
|
104 |
+
</p>
|
105 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
106 |
+
Upload images to your project and use them with DeepSite!
|
107 |
</p>
|
108 |
+
</header>
|
109 |
+
<main className="space-y-4 p-5">
|
110 |
+
<div>
|
111 |
+
<p className="text-xs text-left text-neutral-700 mb-2">
|
112 |
+
Uploaded Images
|
113 |
+
</p>
|
114 |
+
<div className="grid grid-cols-4 gap-1 flex-wrap max-h-40 overflow-y-auto">
|
115 |
+
{files.map((file) => (
|
116 |
+
<div
|
117 |
+
key={file}
|
118 |
+
className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
|
119 |
+
onClick={() => onSelectFile(file)}
|
120 |
+
>
|
121 |
+
<Image
|
122 |
+
src={file}
|
123 |
+
alt="uploaded image"
|
124 |
+
width={56}
|
125 |
+
height={56}
|
126 |
+
className="object-cover w-full rounded-sm aspect-square"
|
127 |
+
/>
|
128 |
+
{selectedFiles.includes(file) && (
|
129 |
+
<div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
|
130 |
+
<RiCheckboxCircleFill className="size-6 text-neutral-100" />
|
131 |
+
</div>
|
132 |
+
)}
|
133 |
+
</div>
|
134 |
+
))}
|
135 |
+
</div>
|
136 |
+
</div>
|
137 |
+
<div>
|
138 |
+
<p className="text-xs text-left text-neutral-700 mb-2">
|
139 |
+
Or import images from your computer
|
140 |
+
</p>
|
141 |
+
<Button
|
142 |
+
variant="black"
|
143 |
+
onClick={() => fileInputRef.current?.click()}
|
144 |
+
className="relative w-full"
|
145 |
+
>
|
146 |
+
{isLoading ? (
|
147 |
+
<>
|
148 |
+
<Loading
|
149 |
+
overlay={false}
|
150 |
+
className="ml-2 size-4 animate-spin"
|
151 |
+
/>
|
152 |
+
Uploading image(s)...
|
153 |
+
</>
|
154 |
+
) : (
|
155 |
+
<>
|
156 |
+
<Upload className="size-4" />
|
157 |
+
Upload Images
|
158 |
+
</>
|
159 |
+
)}
|
160 |
+
</Button>
|
161 |
+
<input
|
162 |
+
ref={fileInputRef}
|
163 |
+
type="file"
|
164 |
+
className="hidden"
|
165 |
+
multiple
|
166 |
+
accept="image/*"
|
167 |
+
onChange={(e) => uploadFiles(e.target.files)}
|
168 |
+
/>
|
169 |
+
</div>
|
170 |
+
</main>
|
171 |
+
</>
|
172 |
+
) : (
|
173 |
+
<DeployButtonContent
|
174 |
+
pages={pages}
|
175 |
+
prompts={[]}
|
176 |
+
options={{
|
177 |
+
description: "Publish your project first to add custom images.",
|
178 |
+
}}
|
179 |
+
/>
|
180 |
+
)}
|
181 |
</PopoverContent>
|
182 |
</form>
|
183 |
</Popover>
|
184 |
+
) : (
|
185 |
+
<>
|
186 |
+
<Button
|
187 |
+
size="iconXs"
|
188 |
+
variant="outline"
|
189 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
190 |
+
onClick={() => setOpen(true)}
|
191 |
+
>
|
192 |
+
<Images className="size-4" />
|
193 |
+
</Button>
|
194 |
+
<LoginModal
|
195 |
+
open={open}
|
196 |
+
onClose={() => setOpen(false)}
|
197 |
+
pages={pages}
|
198 |
+
title="Log In to add Custom Images"
|
199 |
+
description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
|
200 |
+
/>
|
201 |
+
</>
|
202 |
);
|
203 |
};
|
components/editor/deploy-button/content.tsx
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Rocket } from "lucide-react";
|
2 |
+
import Image from "next/image";
|
3 |
+
|
4 |
+
import Loading from "@/components/loading";
|
5 |
+
import { Button } from "@/components/ui/button";
|
6 |
+
import { Input } from "@/components/ui/input";
|
7 |
+
import SpaceIcon from "@/assets/space.svg";
|
8 |
+
import { Page } from "@/types";
|
9 |
+
import { api } from "@/lib/api";
|
10 |
+
import { toast } from "sonner";
|
11 |
+
import { useState } from "react";
|
12 |
+
import { useRouter } from "next/navigation";
|
13 |
+
|
14 |
+
export const DeployButtonContent = ({
|
15 |
+
pages,
|
16 |
+
options,
|
17 |
+
prompts,
|
18 |
+
}: {
|
19 |
+
pages: Page[];
|
20 |
+
options?: {
|
21 |
+
title?: string;
|
22 |
+
description?: string;
|
23 |
+
};
|
24 |
+
prompts: string[];
|
25 |
+
}) => {
|
26 |
+
const router = useRouter();
|
27 |
+
const [loading, setLoading] = useState(false);
|
28 |
+
|
29 |
+
const [config, setConfig] = useState({
|
30 |
+
title: "",
|
31 |
+
});
|
32 |
+
|
33 |
+
const createSpace = async () => {
|
34 |
+
if (!config.title) {
|
35 |
+
toast.error("Please enter a title for your space.");
|
36 |
+
return;
|
37 |
+
}
|
38 |
+
setLoading(true);
|
39 |
+
|
40 |
+
try {
|
41 |
+
const res = await api.post("/me/projects", {
|
42 |
+
title: config.title,
|
43 |
+
pages,
|
44 |
+
prompts,
|
45 |
+
});
|
46 |
+
if (res.data.ok) {
|
47 |
+
router.push(`/projects/${res.data.path}?deploy=true`);
|
48 |
+
} else {
|
49 |
+
toast.error(res?.data?.error || "Failed to create space");
|
50 |
+
}
|
51 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
52 |
+
} catch (err: any) {
|
53 |
+
toast.error(err.response?.data?.error || err.message);
|
54 |
+
} finally {
|
55 |
+
setLoading(false);
|
56 |
+
}
|
57 |
+
};
|
58 |
+
|
59 |
+
return (
|
60 |
+
<>
|
61 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
62 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
63 |
+
<div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
64 |
+
🚀
|
65 |
+
</div>
|
66 |
+
<div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
|
67 |
+
<Image src={SpaceIcon} alt="Space Icon" className="size-7" />
|
68 |
+
</div>
|
69 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
70 |
+
👻
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
<p className="text-xl font-semibold text-neutral-950">
|
74 |
+
Publish as Space!
|
75 |
+
</p>
|
76 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
77 |
+
{options?.description ??
|
78 |
+
"Save and Publish your project to a Space on the Hub. Spaces are a way to share your project with the world."}
|
79 |
+
</p>
|
80 |
+
</header>
|
81 |
+
<main className="space-y-4 p-6">
|
82 |
+
<div>
|
83 |
+
<p className="text-sm text-neutral-700 mb-2">
|
84 |
+
Choose a title for your space:
|
85 |
+
</p>
|
86 |
+
<Input
|
87 |
+
type="text"
|
88 |
+
placeholder="My Awesome Website"
|
89 |
+
value={config.title}
|
90 |
+
onChange={(e) => setConfig({ ...config, title: e.target.value })}
|
91 |
+
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
92 |
+
/>
|
93 |
+
</div>
|
94 |
+
<div>
|
95 |
+
<p className="text-sm text-neutral-700 mb-2">
|
96 |
+
Then, let's publish it!
|
97 |
+
</p>
|
98 |
+
<Button
|
99 |
+
variant="black"
|
100 |
+
onClick={createSpace}
|
101 |
+
className="relative w-full"
|
102 |
+
disabled={loading}
|
103 |
+
>
|
104 |
+
Publish Space <Rocket className="size-4" />
|
105 |
+
{loading && <Loading className="ml-2 size-4 animate-spin" />}
|
106 |
+
</Button>
|
107 |
+
</div>
|
108 |
+
</main>
|
109 |
+
</>
|
110 |
+
);
|
111 |
+
};
|
components/editor/deploy-button/index.tsx
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
+
import { useState } from "react";
|
3 |
+
import { MdSave } from "react-icons/md";
|
4 |
+
|
5 |
+
import { Button } from "@/components/ui/button";
|
6 |
+
import {
|
7 |
+
Popover,
|
8 |
+
PopoverContent,
|
9 |
+
PopoverTrigger,
|
10 |
+
} from "@/components/ui/popover";
|
11 |
+
import { LoginModal } from "@/components/login-modal";
|
12 |
+
import { useUser } from "@/hooks/useUser";
|
13 |
+
import { Page } from "@/types";
|
14 |
+
import { DeployButtonContent } from "./content";
|
15 |
+
|
16 |
+
export function DeployButton({
|
17 |
+
pages,
|
18 |
+
prompts,
|
19 |
+
}: {
|
20 |
+
pages: Page[];
|
21 |
+
prompts: string[];
|
22 |
+
}) {
|
23 |
+
const { user } = useUser();
|
24 |
+
const [open, setOpen] = useState(false);
|
25 |
+
|
26 |
+
return (
|
27 |
+
<div className="flex items-center justify-end gap-5">
|
28 |
+
<div className="relative flex items-center justify-end">
|
29 |
+
{user?.id ? (
|
30 |
+
<Popover>
|
31 |
+
<PopoverTrigger asChild>
|
32 |
+
<div>
|
33 |
+
<Button variant="default" className="max-lg:hidden !px-4">
|
34 |
+
<MdSave className="size-4" />
|
35 |
+
Publish your Project
|
36 |
+
</Button>
|
37 |
+
<Button variant="default" size="sm" className="lg:hidden">
|
38 |
+
Publish
|
39 |
+
</Button>
|
40 |
+
</div>
|
41 |
+
</PopoverTrigger>
|
42 |
+
<PopoverContent
|
43 |
+
className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
|
44 |
+
align="end"
|
45 |
+
>
|
46 |
+
<DeployButtonContent pages={pages} prompts={prompts} />
|
47 |
+
</PopoverContent>
|
48 |
+
</Popover>
|
49 |
+
) : (
|
50 |
+
<>
|
51 |
+
<Button
|
52 |
+
variant="default"
|
53 |
+
className="max-lg:hidden !px-4"
|
54 |
+
onClick={() => setOpen(true)}
|
55 |
+
>
|
56 |
+
<MdSave className="size-4" />
|
57 |
+
Publish your Project
|
58 |
+
</Button>
|
59 |
+
<Button
|
60 |
+
variant="default"
|
61 |
+
size="sm"
|
62 |
+
className="lg:hidden"
|
63 |
+
onClick={() => setOpen(true)}
|
64 |
+
>
|
65 |
+
Publish
|
66 |
+
</Button>
|
67 |
+
</>
|
68 |
+
)}
|
69 |
+
<LoginModal
|
70 |
+
open={open}
|
71 |
+
onClose={() => setOpen(false)}
|
72 |
+
pages={pages}
|
73 |
+
title="Log In to publish your Project"
|
74 |
+
description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
|
75 |
+
/>
|
76 |
+
</div>
|
77 |
+
</div>
|
78 |
+
);
|
79 |
+
}
|
components/editor/footer/index.tsx
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import classNames from "classnames";
|
2 |
+
import { FaMobileAlt } from "react-icons/fa";
|
3 |
+
import { HelpCircle, LogIn, RefreshCcw, SparkleIcon } from "lucide-react";
|
4 |
+
import { FaLaptopCode } from "react-icons/fa6";
|
5 |
+
import { HtmlHistory, Page } from "@/types";
|
6 |
+
import { Button } from "@/components/ui/button";
|
7 |
+
import { MdAdd } from "react-icons/md";
|
8 |
+
import { History } from "@/components/editor/history";
|
9 |
+
import { UserMenu } from "@/components/user-menu";
|
10 |
+
import { useUser } from "@/hooks/useUser";
|
11 |
+
import Link from "next/link";
|
12 |
+
import { useLocalStorage } from "react-use";
|
13 |
+
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
14 |
+
|
15 |
+
const DEVICES = [
|
16 |
+
{
|
17 |
+
name: "desktop",
|
18 |
+
icon: FaLaptopCode,
|
19 |
+
},
|
20 |
+
{
|
21 |
+
name: "mobile",
|
22 |
+
icon: FaMobileAlt,
|
23 |
+
},
|
24 |
+
];
|
25 |
+
|
26 |
+
export function Footer({
|
27 |
+
pages,
|
28 |
+
isNew = false,
|
29 |
+
htmlHistory,
|
30 |
+
setPages,
|
31 |
+
device,
|
32 |
+
setDevice,
|
33 |
+
iframeRef,
|
34 |
+
}: {
|
35 |
+
pages: Page[];
|
36 |
+
isNew?: boolean;
|
37 |
+
htmlHistory?: HtmlHistory[];
|
38 |
+
device: "desktop" | "mobile";
|
39 |
+
setPages: (pages: Page[]) => void;
|
40 |
+
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
41 |
+
setDevice: React.Dispatch<React.SetStateAction<"desktop" | "mobile">>;
|
42 |
+
}) {
|
43 |
+
const { user, openLoginWindow } = useUser();
|
44 |
+
|
45 |
+
const handleRefreshIframe = () => {
|
46 |
+
if (iframeRef?.current) {
|
47 |
+
const iframe = iframeRef.current;
|
48 |
+
const content = iframe.srcdoc;
|
49 |
+
iframe.srcdoc = "";
|
50 |
+
setTimeout(() => {
|
51 |
+
iframe.srcdoc = content;
|
52 |
+
}, 10);
|
53 |
+
}
|
54 |
+
};
|
55 |
+
|
56 |
+
const [, setStorage] = useLocalStorage("pages");
|
57 |
+
const handleClick = async () => {
|
58 |
+
if (pages && !isTheSameHtml(pages[0].html)) {
|
59 |
+
setStorage(pages);
|
60 |
+
}
|
61 |
+
openLoginWindow();
|
62 |
+
};
|
63 |
+
|
64 |
+
return (
|
65 |
+
<footer className="border-t bg-slate-200 border-slate-300 dark:bg-neutral-950 dark:border-neutral-800 px-3 py-2 flex items-center justify-between sticky bottom-0 z-20">
|
66 |
+
<div className="flex items-center gap-2">
|
67 |
+
{user ? (
|
68 |
+
user?.isLocalUse ? (
|
69 |
+
<>
|
70 |
+
<div className="max-w-max bg-amber-500/10 rounded-full px-3 py-1 text-amber-500 border border-amber-500/20 text-sm font-semibold">
|
71 |
+
Local Usage
|
72 |
+
</div>
|
73 |
+
</>
|
74 |
+
) : (
|
75 |
+
<UserMenu className="!p-1 !pr-3 !h-auto" />
|
76 |
+
)
|
77 |
+
) : (
|
78 |
+
<Button size="sm" variant="default" onClick={handleClick}>
|
79 |
+
<LogIn className="text-sm" />
|
80 |
+
Log In
|
81 |
+
</Button>
|
82 |
+
)}
|
83 |
+
{user && !isNew && <p className="text-neutral-700">|</p>}
|
84 |
+
{!isNew && (
|
85 |
+
<Link href="/projects/new">
|
86 |
+
<Button size="sm" variant="secondary">
|
87 |
+
<MdAdd className="text-sm" />
|
88 |
+
New <span className="max-lg:hidden">Project</span>
|
89 |
+
</Button>
|
90 |
+
</Link>
|
91 |
+
)}
|
92 |
+
{htmlHistory && htmlHistory.length > 0 && (
|
93 |
+
<>
|
94 |
+
<p className="text-neutral-700">|</p>
|
95 |
+
<History history={htmlHistory} setPages={setPages} />
|
96 |
+
</>
|
97 |
+
)}
|
98 |
+
</div>
|
99 |
+
<div className="flex justify-end items-center gap-2.5">
|
100 |
+
<a
|
101 |
+
href="https://huggingface.co/spaces/victor/deepsite-gallery"
|
102 |
+
target="_blank"
|
103 |
+
>
|
104 |
+
<Button size="sm" variant="ghost">
|
105 |
+
<SparkleIcon className="size-3.5" />
|
106 |
+
<span className="max-lg:hidden">DeepSite Gallery</span>
|
107 |
+
</Button>
|
108 |
+
</a>
|
109 |
+
<a
|
110 |
+
target="_blank"
|
111 |
+
href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/157"
|
112 |
+
>
|
113 |
+
<Button size="sm" variant="outline">
|
114 |
+
<HelpCircle className="size-3.5" />
|
115 |
+
<span className="max-lg:hidden">Help</span>
|
116 |
+
</Button>
|
117 |
+
</a>
|
118 |
+
<Button size="sm" variant="outline" onClick={handleRefreshIframe}>
|
119 |
+
<RefreshCcw className="size-3.5" />
|
120 |
+
<span className="max-lg:hidden">Refresh Preview</span>
|
121 |
+
</Button>
|
122 |
+
<div className="flex items-center rounded-full p-0.5 bg-neutral-700/70 relative overflow-hidden z-0 max-lg:hidden gap-0.5">
|
123 |
+
<div
|
124 |
+
className={classNames(
|
125 |
+
"absolute left-0.5 top-0.5 rounded-full bg-white size-7 -z-[1] transition-all duration-200",
|
126 |
+
{
|
127 |
+
"translate-x-[calc(100%+2px)]": device === "mobile",
|
128 |
+
}
|
129 |
+
)}
|
130 |
+
/>
|
131 |
+
{DEVICES.map((deviceItem) => (
|
132 |
+
<button
|
133 |
+
key={deviceItem.name}
|
134 |
+
className={classNames(
|
135 |
+
"rounded-full text-neutral-300 size-7 flex items-center justify-center cursor-pointer",
|
136 |
+
{
|
137 |
+
"!text-black": device === deviceItem.name,
|
138 |
+
"hover:bg-neutral-800": device !== deviceItem.name,
|
139 |
+
}
|
140 |
+
)}
|
141 |
+
onClick={() => setDevice(deviceItem.name as "desktop" | "mobile")}
|
142 |
+
>
|
143 |
+
<deviceItem.icon className="text-sm" />
|
144 |
+
</button>
|
145 |
+
))}
|
146 |
+
</div>
|
147 |
+
</div>
|
148 |
+
</footer>
|
149 |
+
);
|
150 |
+
}
|