Spaces:
Running
Running
Upload 35 files
#197
by
archjayte
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .astro/content-assets.mjs +1 -0
- .astro/content-modules.mjs +1 -0
- .astro/data-store.json +1 -0
- .astro/settings.json +5 -0
- .astro/types.d.ts +1 -0
- .bolt/config.json +3 -0
- .codesandbox/Dockerfile +1 -0
- .gitignore +15 -32
- .vscode/extensions.json +4 -0
- .vscode/launch.json +11 -0
- README.md +48 -29
- 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/api/{ask → ask-ai}/route.ts +135 -350
- 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 +0 -113
- app/api/me/projects/[namespace]/[repoId]/route.ts +162 -112
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
- app/api/me/projects/route.ts +92 -73
- app/api/me/route.ts +1 -22
- app/auth/callback/page.tsx +42 -67
- app/layout.tsx +30 -50
- app/new/page.tsx +0 -14
- app/projects/[namespace]/[repoId]/page.tsx +40 -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
- astro.config.mjs +30 -0
- 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 +342 -199
- 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
.astro/content-assets.mjs
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export default new Map();
|
.astro/content-modules.mjs
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export default new Map();
|
.astro/data-store.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.10.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://blueprintrak.com\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"i18n\":{\"defaultLocale\":\"ar\",\"locales\":[\"ar\",\"en\"],\"routing\":{\"prefixDefaultLocale\":false,\"redirectToDefaultLocale\":true,\"fallbackType\":\"redirect\"}},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false},\"legacy\":{\"collections\":false}}"]
|
.astro/settings.json
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"_variables": {
|
3 |
+
"lastUpdateCheck": 1751112608444
|
4 |
+
}
|
5 |
+
}
|
.astro/types.d.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
/// <reference types="astro/client" />
|
.bolt/config.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"template": "astro"
|
3 |
+
}
|
.codesandbox/Dockerfile
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
FROM node:18-bullseye
|
.gitignore
CHANGED
@@ -1,41 +1,24 @@
|
|
1 |
-
#
|
|
|
2 |
|
3 |
-
#
|
4 |
-
/
|
5 |
-
/.pnp
|
6 |
-
.pnp.*
|
7 |
-
.yarn/*
|
8 |
-
!.yarn/patches
|
9 |
-
!.yarn/plugins
|
10 |
-
!.yarn/releases
|
11 |
-
!.yarn/versions
|
12 |
-
|
13 |
-
# testing
|
14 |
-
/coverage
|
15 |
-
|
16 |
-
# next.js
|
17 |
-
/.next/
|
18 |
-
/out/
|
19 |
-
|
20 |
-
# production
|
21 |
-
/build
|
22 |
|
23 |
-
#
|
24 |
-
|
25 |
-
*.pem
|
26 |
|
27 |
-
#
|
28 |
npm-debug.log*
|
29 |
yarn-debug.log*
|
30 |
yarn-error.log*
|
31 |
-
|
32 |
|
33 |
-
#
|
34 |
-
.env
|
|
|
35 |
|
36 |
-
#
|
37 |
-
.
|
38 |
|
39 |
-
#
|
40 |
-
|
41 |
-
next-env.d.ts
|
|
|
1 |
+
# build output
|
2 |
+
dist/
|
3 |
|
4 |
+
# generated types
|
5 |
+
.astro/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
+
# dependencies
|
8 |
+
node_modules/
|
|
|
9 |
|
10 |
+
# logs
|
11 |
npm-debug.log*
|
12 |
yarn-debug.log*
|
13 |
yarn-error.log*
|
14 |
+
pnpm-debug.log*
|
15 |
|
16 |
+
# environment variables
|
17 |
+
.env
|
18 |
+
.env.production
|
19 |
|
20 |
+
# macOS-specific files
|
21 |
+
.DS_Store
|
22 |
|
23 |
+
# jetbrains setting folder
|
24 |
+
.idea/
|
|
.vscode/extensions.json
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"recommendations": ["astro-build.astro-vscode"],
|
3 |
+
"unwantedRecommendations": []
|
4 |
+
}
|
.vscode/launch.json
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"version": "0.2.0",
|
3 |
+
"configurations": [
|
4 |
+
{
|
5 |
+
"command": "./node_modules/.bin/astro dev",
|
6 |
+
"name": "Development server",
|
7 |
+
"request": "launch",
|
8 |
+
"type": "node-terminal"
|
9 |
+
}
|
10 |
+
]
|
11 |
+
}
|
README.md
CHANGED
@@ -1,29 +1,48 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Astro Starter Kit: Basics
|
2 |
+
|
3 |
+
```sh
|
4 |
+
npm create astro@latest -- --template basics
|
5 |
+
```
|
6 |
+
|
7 |
+
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
8 |
+
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
9 |
+
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
10 |
+
|
11 |
+
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
12 |
+
|
13 |
+

|
14 |
+
|
15 |
+
## 🚀 Project Structure
|
16 |
+
|
17 |
+
Inside of your Astro project, you'll see the following folders and files:
|
18 |
+
|
19 |
+
```text
|
20 |
+
/
|
21 |
+
├── public/
|
22 |
+
│ └── favicon.svg
|
23 |
+
├── src/
|
24 |
+
│ ├── layouts/
|
25 |
+
│ │ └── Layout.astro
|
26 |
+
│ └── pages/
|
27 |
+
│ └── index.astro
|
28 |
+
└── package.json
|
29 |
+
```
|
30 |
+
|
31 |
+
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
|
32 |
+
|
33 |
+
## 🧞 Commands
|
34 |
+
|
35 |
+
All commands are run from the root of the project, from a terminal:
|
36 |
+
|
37 |
+
| Command | Action |
|
38 |
+
| :------------------------ | :----------------------------------------------- |
|
39 |
+
| `npm install` | Installs dependencies |
|
40 |
+
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
41 |
+
| `npm run build` | Build your production site to `./dist/` |
|
42 |
+
| `npm run preview` | Preview your build locally, before deploying |
|
43 |
+
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
44 |
+
| `npm run astro -- --help` | Get help using the Astro CLI |
|
45 |
+
|
46 |
+
## 👀 Want to learn more?
|
47 |
+
|
48 |
+
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
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/api/{ask → ask-ai}/route.ts
RENAMED
@@ -4,29 +4,16 @@ 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,
|
11 |
INITIAL_SYSTEM_PROMPT,
|
12 |
MAX_REQUESTS_PER_IP,
|
13 |
-
NEW_PAGE_END,
|
14 |
-
NEW_PAGE_START,
|
15 |
REPLACE_END,
|
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 +22,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(
|
@@ -47,7 +34,6 @@ export async function POST(request: NextRequest) {
|
|
47 |
const selectedModel = MODELS.find(
|
48 |
(m) => m.value === model || m.label === model
|
49 |
);
|
50 |
-
|
51 |
if (!selectedModel) {
|
52 |
return NextResponse.json(
|
53 |
{ ok: false, error: "Invalid model selected" },
|
@@ -55,8 +41,18 @@ export async function POST(request: NextRequest) {
|
|
55 |
);
|
56 |
}
|
57 |
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
let billTo: string | null = null;
|
61 |
|
62 |
/**
|
@@ -89,19 +85,19 @@ export async function POST(request: NextRequest) {
|
|
89 |
billTo = "huggingface";
|
90 |
}
|
91 |
|
92 |
-
const
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
// rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
|
98 |
-
}
|
99 |
|
100 |
try {
|
|
|
101 |
const encoder = new TextEncoder();
|
102 |
const stream = new TransformStream();
|
103 |
const writer = stream.writable.getWriter();
|
104 |
|
|
|
105 |
const response = new NextResponse(stream.readable, {
|
106 |
headers: {
|
107 |
"Content-Type": "text/plain; charset=utf-8",
|
@@ -111,52 +107,75 @@ export async function POST(request: NextRequest) {
|
|
111 |
});
|
112 |
|
113 |
(async () => {
|
114 |
-
|
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 |
}
|
151 |
|
152 |
const chunk = value.choices[0]?.delta?.content;
|
153 |
if (chunk) {
|
154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 +187,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 +200,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,38 +219,22 @@ export async function POST(request: NextRequest) {
|
|
216 |
}
|
217 |
|
218 |
export async function PUT(request: NextRequest) {
|
219 |
-
console.log("PUT request received");
|
220 |
-
const user = await isAuthenticated();
|
221 |
-
if (user instanceof NextResponse || !user) {
|
222 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
223 |
-
}
|
224 |
-
|
225 |
const authHeaders = await headers();
|
|
|
226 |
|
227 |
const body = await request.json();
|
228 |
-
const { prompt,
|
229 |
-
body;
|
230 |
-
|
231 |
-
let repoId = repoIdFromBody;
|
232 |
|
233 |
-
if (!prompt ||
|
234 |
return NextResponse.json(
|
235 |
{ ok: false, error: "Missing required fields" },
|
236 |
{ status: 400 }
|
237 |
);
|
238 |
}
|
239 |
|
240 |
-
const selectedModel = MODELS
|
241 |
-
(m) => m.value === model || m.label === model
|
242 |
-
);
|
243 |
-
if (!selectedModel) {
|
244 |
-
return NextResponse.json(
|
245 |
-
{ ok: false, error: "Invalid model selected" },
|
246 |
-
{ status: 400 }
|
247 |
-
);
|
248 |
-
}
|
249 |
|
250 |
-
let token =
|
251 |
let billTo: string | null = null;
|
252 |
|
253 |
/**
|
@@ -282,80 +269,52 @@ export async function PUT(request: NextRequest) {
|
|
282 |
|
283 |
const client = new InferenceClient(token);
|
284 |
|
285 |
-
const
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
let searchRegex = escapeRegExp(searchBlock)
|
291 |
-
.replace(/\s+/g, '\\s*')
|
292 |
-
.replace(/>\s*</g, '>\\s*<')
|
293 |
-
.replace(/\s*>/g, '\\s*>');
|
294 |
-
|
295 |
-
return new RegExp(searchRegex, 'g');
|
296 |
-
};
|
297 |
-
|
298 |
-
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
299 |
|
300 |
try {
|
301 |
-
const
|
302 |
-
const userContext = "You are modifying the HTML file based on the user's request.";
|
303 |
-
|
304 |
-
// Send all pages without filtering
|
305 |
-
const allPages = pages || [];
|
306 |
-
const pagesContext = allPages
|
307 |
-
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
308 |
-
.join("\n\n");
|
309 |
-
|
310 |
-
const assistantContext = `${
|
311 |
-
selectedElementHtml
|
312 |
-
? `\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.`
|
313 |
-
: ""
|
314 |
-
}. Current pages (${allPages.length} total): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f).join(', ')}.` : ""}`;
|
315 |
-
|
316 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
317 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
318 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
319 |
-
|
320 |
-
const chatCompletion = client.chatCompletionStream(
|
321 |
{
|
322 |
model: selectedModel.value,
|
323 |
-
provider: selectedProvider.
|
324 |
messages: [
|
325 |
{
|
326 |
role: "system",
|
327 |
-
content:
|
328 |
},
|
329 |
{
|
330 |
role: "user",
|
331 |
-
content:
|
|
|
|
|
332 |
},
|
333 |
{
|
334 |
role: "assistant",
|
335 |
-
|
|
|
|
|
|
|
|
|
|
|
336 |
},
|
337 |
{
|
338 |
role: "user",
|
339 |
content: prompt,
|
340 |
},
|
341 |
],
|
342 |
-
...
|
|
|
|
|
|
|
|
|
343 |
},
|
344 |
billTo ? { billTo } : {}
|
345 |
);
|
346 |
|
347 |
-
|
348 |
-
while (true) {
|
349 |
-
const { done, value } = await chatCompletion.next();
|
350 |
-
if (done) {
|
351 |
-
break;
|
352 |
-
}
|
353 |
-
|
354 |
-
const deltaContent = value.choices[0]?.delta?.content;
|
355 |
-
if (deltaContent) {
|
356 |
-
chunk += deltaContent;
|
357 |
-
}
|
358 |
-
}
|
359 |
if (!chunk) {
|
360 |
return NextResponse.json(
|
361 |
{ ok: false, message: "No content returned from the model" },
|
@@ -365,234 +324,61 @@ export async function PUT(request: NextRequest) {
|
|
365 |
|
366 |
if (chunk) {
|
367 |
const updatedLines: number[][] = [];
|
368 |
-
let newHtml =
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
378 |
-
if (pageIndex !== -1) {
|
379 |
-
let pageHtml = updatedPages[pageIndex].html;
|
380 |
-
|
381 |
-
let processedContent = pageContent;
|
382 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
383 |
-
if (htmlMatch) {
|
384 |
-
processedContent = htmlMatch[1];
|
385 |
-
}
|
386 |
-
let position = 0;
|
387 |
-
let moreBlocks = true;
|
388 |
-
|
389 |
-
while (moreBlocks) {
|
390 |
-
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
391 |
-
if (searchStartIndex === -1) {
|
392 |
-
moreBlocks = false;
|
393 |
-
continue;
|
394 |
-
}
|
395 |
-
|
396 |
-
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
397 |
-
if (dividerIndex === -1) {
|
398 |
-
moreBlocks = false;
|
399 |
-
continue;
|
400 |
-
}
|
401 |
-
|
402 |
-
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
403 |
-
if (replaceEndIndex === -1) {
|
404 |
-
moreBlocks = false;
|
405 |
-
continue;
|
406 |
-
}
|
407 |
-
|
408 |
-
const searchBlock = processedContent.substring(
|
409 |
-
searchStartIndex + SEARCH_START.length,
|
410 |
-
dividerIndex
|
411 |
-
);
|
412 |
-
const replaceBlock = processedContent.substring(
|
413 |
-
dividerIndex + DIVIDER.length,
|
414 |
-
replaceEndIndex
|
415 |
-
);
|
416 |
-
|
417 |
-
if (searchBlock.trim() === "") {
|
418 |
-
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
419 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
420 |
-
} else {
|
421 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
422 |
-
const match = regex.exec(pageHtml);
|
423 |
-
|
424 |
-
if (match) {
|
425 |
-
const matchedText = match[0];
|
426 |
-
const beforeText = pageHtml.substring(0, match.index);
|
427 |
-
const startLineNumber = beforeText.split("\n").length;
|
428 |
-
const replaceLines = replaceBlock.split("\n").length;
|
429 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
430 |
-
|
431 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
432 |
-
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
433 |
-
}
|
434 |
-
}
|
435 |
-
|
436 |
-
position = replaceEndIndex + REPLACE_END.length;
|
437 |
-
}
|
438 |
-
|
439 |
-
updatedPages[pageIndex].html = pageHtml;
|
440 |
-
|
441 |
-
if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
|
442 |
-
newHtml = pageHtml;
|
443 |
-
}
|
444 |
}
|
445 |
-
}
|
446 |
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
const [, pagePath, pageContent] = newPageMatch;
|
452 |
-
|
453 |
-
let pageHtml = pageContent;
|
454 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
455 |
-
if (htmlMatch) {
|
456 |
-
pageHtml = htmlMatch[1];
|
457 |
}
|
458 |
-
|
459 |
-
const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
460 |
-
|
461 |
-
if (existingPageIndex !== -1) {
|
462 |
-
updatedPages[existingPageIndex] = {
|
463 |
-
path: pagePath,
|
464 |
-
html: pageHtml.trim()
|
465 |
-
};
|
466 |
-
} else {
|
467 |
-
updatedPages.push({
|
468 |
-
path: pagePath,
|
469 |
-
html: pageHtml.trim()
|
470 |
-
});
|
471 |
-
}
|
472 |
-
}
|
473 |
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
480 |
-
if (searchStartIndex === -1) {
|
481 |
-
moreBlocks = false;
|
482 |
-
continue;
|
483 |
-
}
|
484 |
-
|
485 |
-
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
486 |
-
if (dividerIndex === -1) {
|
487 |
-
moreBlocks = false;
|
488 |
-
continue;
|
489 |
-
}
|
490 |
-
|
491 |
-
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
492 |
-
if (replaceEndIndex === -1) {
|
493 |
-
moreBlocks = false;
|
494 |
-
continue;
|
495 |
-
}
|
496 |
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
521 |
-
newHtml = newHtml.replace(matchedText, replaceBlock);
|
522 |
-
}
|
523 |
}
|
524 |
-
|
525 |
-
position = replaceEndIndex + REPLACE_END.length;
|
526 |
-
}
|
527 |
-
|
528 |
-
// Update the main HTML if it's the index page
|
529 |
-
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
530 |
-
if (mainPageIndex !== -1) {
|
531 |
-
updatedPages[mainPageIndex].html = newHtml;
|
532 |
}
|
533 |
-
}
|
534 |
-
|
535 |
-
const files: File[] = [];
|
536 |
-
updatedPages.forEach((page: Page) => {
|
537 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
538 |
-
files.push(file);
|
539 |
-
});
|
540 |
|
541 |
-
|
542 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
543 |
-
const formattedTitle = projectName?.toLowerCase()
|
544 |
-
.replace(/[^a-z0-9]+/g, "-")
|
545 |
-
.split("-")
|
546 |
-
.filter(Boolean)
|
547 |
-
.join("-")
|
548 |
-
.slice(0, 96);
|
549 |
-
const repo: RepoDesignation = {
|
550 |
-
type: "space",
|
551 |
-
name: `${user.name}/${formattedTitle}`,
|
552 |
-
};
|
553 |
-
const { repoUrl} = await createRepo({
|
554 |
-
repo,
|
555 |
-
accessToken: user.token as string,
|
556 |
-
});
|
557 |
-
repoId = repoUrl.split("/").slice(-2).join("/");
|
558 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
559 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
560 |
-
const README = `---
|
561 |
-
title: ${projectName}
|
562 |
-
colorFrom: ${colorFrom}
|
563 |
-
colorTo: ${colorTo}
|
564 |
-
emoji: 🐳
|
565 |
-
sdk: static
|
566 |
-
pinned: false
|
567 |
-
tags:
|
568 |
-
- deepsite-v3
|
569 |
-
---
|
570 |
-
|
571 |
-
# Welcome to your new DeepSite project!
|
572 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
573 |
-
`;
|
574 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
575 |
}
|
576 |
|
577 |
-
const response = await uploadFiles({
|
578 |
-
repo: {
|
579 |
-
type: "space",
|
580 |
-
name: repoId,
|
581 |
-
},
|
582 |
-
files,
|
583 |
-
commitTitle: prompt,
|
584 |
-
accessToken: user.token as string,
|
585 |
-
});
|
586 |
-
|
587 |
return NextResponse.json({
|
588 |
ok: true,
|
|
|
589 |
updatedLines,
|
590 |
-
pages: updatedPages,
|
591 |
-
repoId,
|
592 |
-
commit: {
|
593 |
-
...response.commit,
|
594 |
-
title: prompt,
|
595 |
-
}
|
596 |
});
|
597 |
} else {
|
598 |
return NextResponse.json(
|
@@ -622,4 +408,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
|
|
622 |
);
|
623 |
}
|
624 |
}
|
625 |
-
|
|
|
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,
|
11 |
INITIAL_SYSTEM_PROMPT,
|
12 |
MAX_REQUESTS_PER_IP,
|
|
|
|
|
13 |
REPLACE_END,
|
14 |
SEARCH_START,
|
|
|
|
|
|
|
15 |
} from "@/lib/prompts";
|
|
|
16 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
const ipAddresses = new Map();
|
19 |
|
|
|
22 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
23 |
|
24 |
const body = await request.json();
|
25 |
+
const { prompt, provider, model, redesignMarkdown, html } = body;
|
26 |
|
27 |
if (!model || (!prompt && !redesignMarkdown)) {
|
28 |
return NextResponse.json(
|
|
|
34 |
const selectedModel = MODELS.find(
|
35 |
(m) => m.value === model || m.label === model
|
36 |
);
|
|
|
37 |
if (!selectedModel) {
|
38 |
return NextResponse.json(
|
39 |
{ ok: false, error: "Invalid model selected" },
|
|
|
41 |
);
|
42 |
}
|
43 |
|
44 |
+
if (!selectedModel.providers.includes(provider) && provider !== "auto") {
|
45 |
+
return NextResponse.json(
|
46 |
+
{
|
47 |
+
ok: false,
|
48 |
+
error: `The selected model does not support the ${provider} provider.`,
|
49 |
+
openSelectProvider: true,
|
50 |
+
},
|
51 |
+
{ status: 400 }
|
52 |
+
);
|
53 |
+
}
|
54 |
+
|
55 |
+
let token = userToken;
|
56 |
let billTo: string | null = null;
|
57 |
|
58 |
/**
|
|
|
85 |
billTo = "huggingface";
|
86 |
}
|
87 |
|
88 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
89 |
+
const selectedProvider =
|
90 |
+
provider === "auto"
|
91 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
92 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
93 |
|
94 |
try {
|
95 |
+
// Create a stream response
|
96 |
const encoder = new TextEncoder();
|
97 |
const stream = new TransformStream();
|
98 |
const writer = stream.writable.getWriter();
|
99 |
|
100 |
+
// Start the response
|
101 |
const response = new NextResponse(stream.readable, {
|
102 |
headers: {
|
103 |
"Content-Type": "text/plain; charset=utf-8",
|
|
|
107 |
});
|
108 |
|
109 |
(async () => {
|
110 |
+
let completeResponse = "";
|
111 |
try {
|
112 |
const client = new InferenceClient(token);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
const chatCompletion = client.chatCompletionStream(
|
114 |
{
|
115 |
model: selectedModel.value,
|
116 |
+
provider: selectedProvider.id as any,
|
117 |
messages: [
|
118 |
{
|
119 |
role: "system",
|
120 |
+
content: INITIAL_SYSTEM_PROMPT,
|
121 |
},
|
122 |
{
|
123 |
role: "user",
|
124 |
+
content: redesignMarkdown
|
125 |
+
? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
|
126 |
+
: html
|
127 |
+
? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
|
128 |
+
: prompt,
|
129 |
},
|
130 |
],
|
131 |
+
max_tokens: selectedProvider.max_tokens,
|
132 |
},
|
133 |
billTo ? { billTo } : {}
|
134 |
);
|
135 |
|
136 |
while (true) {
|
137 |
+
const { done, value } = await chatCompletion.next();
|
138 |
if (done) {
|
139 |
break;
|
140 |
}
|
141 |
|
142 |
const chunk = value.choices[0]?.delta?.content;
|
143 |
if (chunk) {
|
144 |
+
let newChunk = chunk;
|
145 |
+
if (!selectedModel?.isThinker) {
|
146 |
+
if (provider !== "sambanova") {
|
147 |
+
await writer.write(encoder.encode(chunk));
|
148 |
+
completeResponse += chunk;
|
149 |
+
|
150 |
+
if (completeResponse.includes("</html>")) {
|
151 |
+
break;
|
152 |
+
}
|
153 |
+
} else {
|
154 |
+
if (chunk.includes("</html>")) {
|
155 |
+
newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
|
156 |
+
}
|
157 |
+
completeResponse += newChunk;
|
158 |
+
await writer.write(encoder.encode(newChunk));
|
159 |
+
if (newChunk.includes("</html>")) {
|
160 |
+
break;
|
161 |
+
}
|
162 |
+
}
|
163 |
+
} else {
|
164 |
+
const lastThinkTagIndex =
|
165 |
+
completeResponse.lastIndexOf("</think>");
|
166 |
+
completeResponse += newChunk;
|
167 |
+
await writer.write(encoder.encode(newChunk));
|
168 |
+
if (lastThinkTagIndex !== -1) {
|
169 |
+
const afterLastThinkTag = completeResponse.slice(
|
170 |
+
lastThinkTagIndex + "</think>".length
|
171 |
+
);
|
172 |
+
if (afterLastThinkTag.includes("</html>")) {
|
173 |
+
break;
|
174 |
+
}
|
175 |
+
}
|
176 |
+
}
|
177 |
}
|
178 |
}
|
|
|
|
|
|
|
179 |
} catch (error: any) {
|
180 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
181 |
await writer.write(
|
|
|
187 |
})
|
188 |
)
|
189 |
);
|
190 |
+
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
await writer.write(
|
192 |
encoder.encode(
|
193 |
JSON.stringify({
|
|
|
200 |
);
|
201 |
}
|
202 |
} finally {
|
203 |
+
await writer?.close();
|
|
|
|
|
|
|
|
|
|
|
204 |
}
|
205 |
})();
|
206 |
|
|
|
219 |
}
|
220 |
|
221 |
export async function PUT(request: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
|
|
222 |
const authHeaders = await headers();
|
223 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
224 |
|
225 |
const body = await request.json();
|
226 |
+
const { prompt, html, previousPrompt, provider, selectedElementHtml } = body;
|
|
|
|
|
|
|
227 |
|
228 |
+
if (!prompt || !html) {
|
229 |
return NextResponse.json(
|
230 |
{ ok: false, error: "Missing required fields" },
|
231 |
{ status: 400 }
|
232 |
);
|
233 |
}
|
234 |
|
235 |
+
const selectedModel = MODELS[0];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
|
237 |
+
let token = userToken;
|
238 |
let billTo: string | null = null;
|
239 |
|
240 |
/**
|
|
|
269 |
|
270 |
const client = new InferenceClient(token);
|
271 |
|
272 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
273 |
+
const selectedProvider =
|
274 |
+
provider === "auto"
|
275 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
276 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
277 |
|
278 |
try {
|
279 |
+
const response = await client.chatCompletion(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
280 |
{
|
281 |
model: selectedModel.value,
|
282 |
+
provider: selectedProvider.id as any,
|
283 |
messages: [
|
284 |
{
|
285 |
role: "system",
|
286 |
+
content: FOLLOW_UP_SYSTEM_PROMPT,
|
287 |
},
|
288 |
{
|
289 |
role: "user",
|
290 |
+
content: previousPrompt
|
291 |
+
? previousPrompt
|
292 |
+
: "You are modifying the HTML file based on the user's request.",
|
293 |
},
|
294 |
{
|
295 |
role: "assistant",
|
296 |
+
|
297 |
+
content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
|
298 |
+
selectedElementHtml
|
299 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
300 |
+
: ""
|
301 |
+
}`,
|
302 |
},
|
303 |
{
|
304 |
role: "user",
|
305 |
content: prompt,
|
306 |
},
|
307 |
],
|
308 |
+
...(selectedProvider.id !== "sambanova"
|
309 |
+
? {
|
310 |
+
max_tokens: selectedProvider.max_tokens,
|
311 |
+
}
|
312 |
+
: {}),
|
313 |
},
|
314 |
billTo ? { billTo } : {}
|
315 |
);
|
316 |
|
317 |
+
const chunk = response.choices[0]?.message?.content;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
318 |
if (!chunk) {
|
319 |
return NextResponse.json(
|
320 |
{ ok: false, message: "No content returned from the model" },
|
|
|
324 |
|
325 |
if (chunk) {
|
326 |
const updatedLines: number[][] = [];
|
327 |
+
let newHtml = html;
|
328 |
+
let position = 0;
|
329 |
+
let moreBlocks = true;
|
330 |
+
|
331 |
+
while (moreBlocks) {
|
332 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
333 |
+
if (searchStartIndex === -1) {
|
334 |
+
moreBlocks = false;
|
335 |
+
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
336 |
}
|
|
|
337 |
|
338 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
339 |
+
if (dividerIndex === -1) {
|
340 |
+
moreBlocks = false;
|
341 |
+
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
342 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
|
344 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
345 |
+
if (replaceEndIndex === -1) {
|
346 |
+
moreBlocks = false;
|
347 |
+
continue;
|
348 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
349 |
|
350 |
+
const searchBlock = chunk.substring(
|
351 |
+
searchStartIndex + SEARCH_START.length,
|
352 |
+
dividerIndex
|
353 |
+
);
|
354 |
+
const replaceBlock = chunk.substring(
|
355 |
+
dividerIndex + DIVIDER.length,
|
356 |
+
replaceEndIndex
|
357 |
+
);
|
358 |
|
359 |
+
if (searchBlock.trim() === "") {
|
360 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
361 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
362 |
+
} else {
|
363 |
+
const blockPosition = newHtml.indexOf(searchBlock);
|
364 |
+
if (blockPosition !== -1) {
|
365 |
+
const beforeText = newHtml.substring(0, blockPosition);
|
366 |
+
const startLineNumber = beforeText.split("\n").length;
|
367 |
+
const replaceLines = replaceBlock.split("\n").length;
|
368 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
369 |
+
|
370 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
371 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
|
|
|
|
|
|
|
|
372 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
|
375 |
+
position = replaceEndIndex + REPLACE_END.length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
376 |
}
|
377 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
return NextResponse.json({
|
379 |
ok: true,
|
380 |
+
html: newHtml,
|
381 |
updatedLines,
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
});
|
383 |
} else {
|
384 |
return NextResponse.json(
|
|
|
408 |
);
|
409 |
}
|
410 |
}
|
|
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
DELETED
@@ -1,113 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, spaceInfo, 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 |
-
export async function POST(
|
9 |
-
req: NextRequest,
|
10 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
11 |
-
) {
|
12 |
-
try {
|
13 |
-
const user = await isAuthenticated();
|
14 |
-
|
15 |
-
if (user instanceof NextResponse || !user) {
|
16 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
-
}
|
18 |
-
|
19 |
-
const param = await params;
|
20 |
-
const { namespace, repoId } = param;
|
21 |
-
|
22 |
-
const space = await spaceInfo({
|
23 |
-
name: `${namespace}/${repoId}`,
|
24 |
-
accessToken: user.token as string,
|
25 |
-
additionalFields: ["author"],
|
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 (space.author !== user.name) {
|
36 |
-
return NextResponse.json(
|
37 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
38 |
-
{ status: 403 }
|
39 |
-
);
|
40 |
-
}
|
41 |
-
|
42 |
-
// Parse the FormData to get the images
|
43 |
-
const formData = await req.formData();
|
44 |
-
const imageFiles = formData.getAll("images") as File[];
|
45 |
-
|
46 |
-
if (!imageFiles || imageFiles.length === 0) {
|
47 |
-
return NextResponse.json(
|
48 |
-
{
|
49 |
-
ok: false,
|
50 |
-
error: "At least one image file is required under the 'images' key",
|
51 |
-
},
|
52 |
-
{ status: 400 }
|
53 |
-
);
|
54 |
-
}
|
55 |
-
|
56 |
-
const files: File[] = [];
|
57 |
-
for (const file of imageFiles) {
|
58 |
-
if (!(file instanceof File)) {
|
59 |
-
return NextResponse.json(
|
60 |
-
{
|
61 |
-
ok: false,
|
62 |
-
error: "Invalid file format - all items under 'images' key must be files",
|
63 |
-
},
|
64 |
-
{ status: 400 }
|
65 |
-
);
|
66 |
-
}
|
67 |
-
|
68 |
-
if (!file.type.startsWith('image/')) {
|
69 |
-
return NextResponse.json(
|
70 |
-
{
|
71 |
-
ok: false,
|
72 |
-
error: `File ${file.name} is not an image`,
|
73 |
-
},
|
74 |
-
{ status: 400 }
|
75 |
-
);
|
76 |
-
}
|
77 |
-
|
78 |
-
// Create File object with images/ folder prefix
|
79 |
-
const fileName = `images/${file.name}`;
|
80 |
-
const processedFile = new File([file], fileName, { type: file.type });
|
81 |
-
files.push(processedFile);
|
82 |
-
}
|
83 |
-
|
84 |
-
// Upload files to HuggingFace space
|
85 |
-
const repo: RepoDesignation = {
|
86 |
-
type: "space",
|
87 |
-
name: `${namespace}/${repoId}`,
|
88 |
-
};
|
89 |
-
|
90 |
-
await uploadFiles({
|
91 |
-
repo,
|
92 |
-
files,
|
93 |
-
accessToken: user.token as string,
|
94 |
-
commitTitle: `Upload ${files.length} image(s)`,
|
95 |
-
});
|
96 |
-
|
97 |
-
return NextResponse.json({
|
98 |
-
ok: true,
|
99 |
-
message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
|
100 |
-
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
101 |
-
}, { status: 200 });
|
102 |
-
|
103 |
-
} catch (error) {
|
104 |
-
console.error('Error uploading images:', error);
|
105 |
-
return NextResponse.json(
|
106 |
-
{
|
107 |
-
ok: false,
|
108 |
-
error: "Failed to upload images",
|
109 |
-
},
|
110 |
-
{ status: 500 }
|
111 |
-
);
|
112 |
-
}
|
113 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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,24 @@ 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,
|
@@ -97,75 +60,26 @@ export async function GET(
|
|
97 |
);
|
98 |
}
|
99 |
|
100 |
-
const
|
101 |
-
|
102 |
-
name: `${namespace}/${repoId}`,
|
103 |
-
};
|
104 |
-
|
105 |
-
const htmlFiles: Page[] = [];
|
106 |
-
const files: string[] = [];
|
107 |
-
|
108 |
-
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
109 |
-
|
110 |
-
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
111 |
-
if (fileInfo.path.endsWith(".html")) {
|
112 |
-
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
113 |
-
const html = await blob?.text();
|
114 |
-
if (!html) {
|
115 |
-
continue;
|
116 |
-
}
|
117 |
-
if (fileInfo.path === "index.html") {
|
118 |
-
htmlFiles.unshift({
|
119 |
-
path: fileInfo.path,
|
120 |
-
html,
|
121 |
-
});
|
122 |
-
} else {
|
123 |
-
htmlFiles.push({
|
124 |
-
path: fileInfo.path,
|
125 |
-
html,
|
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 (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
132 |
-
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
133 |
-
}
|
134 |
-
}
|
135 |
-
}
|
136 |
-
}
|
137 |
-
const commits: Commit[] = [];
|
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 |
{
|
152 |
ok: false,
|
153 |
-
error: "
|
154 |
},
|
155 |
{ status: 404 }
|
156 |
);
|
157 |
}
|
|
|
|
|
|
|
|
|
158 |
return NextResponse.json(
|
159 |
{
|
160 |
project: {
|
161 |
-
|
162 |
-
|
163 |
-
private: space.private,
|
164 |
-
_updatedAt: space.updatedAt,
|
165 |
},
|
166 |
-
pages: htmlFiles,
|
167 |
-
files,
|
168 |
-
commits,
|
169 |
ok: true,
|
170 |
},
|
171 |
{ status: 200 }
|
@@ -174,6 +88,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 +103,135 @@ export async function GET(
|
|
185 |
);
|
186 |
}
|
187 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
import { getPTag } from "@/lib/utils";
|
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 |
+
const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
try {
|
38 |
const space = await spaceInfo({
|
39 |
name: namespace + "/" + repoId,
|
|
|
60 |
);
|
61 |
}
|
62 |
|
63 |
+
const response = await fetch(space_url);
|
64 |
+
if (!response.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
return NextResponse.json(
|
66 |
{
|
67 |
ok: false,
|
68 |
+
error: "Failed to fetch space HTML",
|
69 |
},
|
70 |
{ status: 404 }
|
71 |
);
|
72 |
}
|
73 |
+
let html = await response.text();
|
74 |
+
// remove the last p tag including this url https://enzostvs-deepsite.hf.space
|
75 |
+
html = html.replace(getPTag(namespace + "/" + repoId), "");
|
76 |
+
|
77 |
return NextResponse.json(
|
78 |
{
|
79 |
project: {
|
80 |
+
...project,
|
81 |
+
html,
|
|
|
|
|
82 |
},
|
|
|
|
|
|
|
83 |
ok: true,
|
84 |
},
|
85 |
{ status: 200 }
|
|
|
88 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
89 |
} catch (error: any) {
|
90 |
if (error.statusCode === 404) {
|
91 |
+
await Project.deleteOne({
|
92 |
+
user_id: user.id,
|
93 |
+
space_id: `${namespace}/${repoId}`,
|
94 |
+
});
|
95 |
return NextResponse.json(
|
96 |
{ error: "Space not found", ok: false },
|
97 |
{ status: 404 }
|
|
|
103 |
);
|
104 |
}
|
105 |
}
|
106 |
+
|
107 |
+
export async function PUT(
|
108 |
+
req: NextRequest,
|
109 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
110 |
+
) {
|
111 |
+
const user = await isAuthenticated();
|
112 |
+
|
113 |
+
if (user instanceof NextResponse || !user) {
|
114 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
115 |
+
}
|
116 |
+
|
117 |
+
await dbConnect();
|
118 |
+
const param = await params;
|
119 |
+
const { namespace, repoId } = param;
|
120 |
+
const { html, prompts } = await req.json();
|
121 |
+
|
122 |
+
const project = await Project.findOne({
|
123 |
+
user_id: user.id,
|
124 |
+
space_id: `${namespace}/${repoId}`,
|
125 |
+
}).lean();
|
126 |
+
if (!project) {
|
127 |
+
return NextResponse.json(
|
128 |
+
{
|
129 |
+
ok: false,
|
130 |
+
error: "Project not found",
|
131 |
+
},
|
132 |
+
{ status: 404 }
|
133 |
+
);
|
134 |
+
}
|
135 |
+
|
136 |
+
const repo: RepoDesignation = {
|
137 |
+
type: "space",
|
138 |
+
name: `${namespace}/${repoId}`,
|
139 |
+
};
|
140 |
+
|
141 |
+
const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
142 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
143 |
+
await uploadFile({
|
144 |
+
repo,
|
145 |
+
file,
|
146 |
+
accessToken: user.token as string,
|
147 |
+
commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
|
148 |
+
});
|
149 |
+
|
150 |
+
await Project.updateOne(
|
151 |
+
{ user_id: user.id, space_id: `${namespace}/${repoId}` },
|
152 |
+
{
|
153 |
+
$set: {
|
154 |
+
prompts: [
|
155 |
+
...(project && "prompts" in project ? project.prompts : []),
|
156 |
+
...prompts,
|
157 |
+
],
|
158 |
+
},
|
159 |
+
}
|
160 |
+
);
|
161 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
162 |
+
}
|
163 |
+
|
164 |
+
export async function POST(
|
165 |
+
req: NextRequest,
|
166 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
167 |
+
) {
|
168 |
+
const user = await isAuthenticated();
|
169 |
+
|
170 |
+
if (user instanceof NextResponse || !user) {
|
171 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
172 |
+
}
|
173 |
+
|
174 |
+
await dbConnect();
|
175 |
+
const param = await params;
|
176 |
+
const { namespace, repoId } = param;
|
177 |
+
|
178 |
+
const space = await spaceInfo({
|
179 |
+
name: namespace + "/" + repoId,
|
180 |
+
accessToken: user.token as string,
|
181 |
+
additionalFields: ["author"],
|
182 |
+
});
|
183 |
+
|
184 |
+
if (!space || space.sdk !== "static") {
|
185 |
+
return NextResponse.json(
|
186 |
+
{
|
187 |
+
ok: false,
|
188 |
+
error: "Space is not a static space",
|
189 |
+
},
|
190 |
+
{ status: 404 }
|
191 |
+
);
|
192 |
+
}
|
193 |
+
if (space.author !== user.name) {
|
194 |
+
return NextResponse.json(
|
195 |
+
{
|
196 |
+
ok: false,
|
197 |
+
error: "Space does not belong to the authenticated user",
|
198 |
+
},
|
199 |
+
{ status: 403 }
|
200 |
+
);
|
201 |
+
}
|
202 |
+
|
203 |
+
const project = await Project.findOne({
|
204 |
+
user_id: user.id,
|
205 |
+
space_id: `${namespace}/${repoId}`,
|
206 |
+
}).lean();
|
207 |
+
if (project) {
|
208 |
+
// redirect to the project page if it already exists
|
209 |
+
return NextResponse.json(
|
210 |
+
{
|
211 |
+
ok: false,
|
212 |
+
error: "Project already exists",
|
213 |
+
redirect: `/projects/${namespace}/${repoId}`,
|
214 |
+
},
|
215 |
+
{ status: 400 }
|
216 |
+
);
|
217 |
+
}
|
218 |
+
|
219 |
+
const newProject = new Project({
|
220 |
+
user_id: user.id,
|
221 |
+
space_id: `${namespace}/${repoId}`,
|
222 |
+
prompts: [],
|
223 |
+
});
|
224 |
+
|
225 |
+
await newProject.save();
|
226 |
+
return NextResponse.json(
|
227 |
+
{
|
228 |
+
ok: true,
|
229 |
+
project: {
|
230 |
+
id: newProject._id,
|
231 |
+
space_id: newProject.space_id,
|
232 |
+
prompts: newProject.prompts,
|
233 |
+
},
|
234 |
+
},
|
235 |
+
{ status: 201 }
|
236 |
+
);
|
237 |
+
}
|
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,126 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import {
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
6 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
const user = await isAuthenticated();
|
|
|
12 |
if (user instanceof NextResponse || !user) {
|
13 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
}
|
15 |
|
16 |
-
const { title
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
|
|
|
|
|
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 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
54 |
-
files.push(file);
|
55 |
-
});
|
56 |
-
|
57 |
-
try {
|
58 |
-
const { repoUrl} = await createRepo({
|
59 |
-
repo,
|
60 |
-
accessToken: user.token as string,
|
61 |
});
|
62 |
-
const
|
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, getPTag } from "@/lib/utils";
|
8 |
+
// import type user
|
9 |
+
export async function GET() {
|
10 |
+
const user = await isAuthenticated();
|
11 |
+
|
12 |
+
if (user instanceof NextResponse || !user) {
|
13 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
+
}
|
15 |
|
16 |
+
await dbConnect();
|
17 |
+
|
18 |
+
const projects = await Project.find({
|
19 |
+
user_id: user?.id,
|
20 |
+
})
|
21 |
+
.sort({ _createdAt: -1 })
|
22 |
+
.limit(100)
|
23 |
+
.lean();
|
24 |
+
if (!projects) {
|
25 |
+
return NextResponse.json(
|
26 |
+
{
|
27 |
+
ok: false,
|
28 |
+
projects: [],
|
29 |
+
},
|
30 |
+
{ status: 404 }
|
31 |
+
);
|
32 |
+
}
|
33 |
+
return NextResponse.json(
|
34 |
+
{
|
35 |
+
ok: true,
|
36 |
+
projects,
|
37 |
+
},
|
38 |
+
{ status: 200 }
|
39 |
+
);
|
40 |
+
}
|
41 |
+
|
42 |
+
/**
|
43 |
+
* This API route creates a new project in Hugging Face Spaces.
|
44 |
+
* It requires an Authorization header with a valid token and a JSON body with the project details.
|
45 |
+
*/
|
46 |
+
export async function POST(request: NextRequest) {
|
47 |
const user = await isAuthenticated();
|
48 |
+
|
49 |
if (user instanceof NextResponse || !user) {
|
50 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
51 |
}
|
52 |
|
53 |
+
const { title, html, prompts } = await request.json();
|
54 |
+
|
55 |
+
if (!title || !html) {
|
56 |
+
return NextResponse.json(
|
57 |
+
{ message: "Title and HTML content are required.", ok: false },
|
58 |
+
{ status: 400 }
|
59 |
+
);
|
60 |
+
}
|
61 |
+
|
62 |
+
await dbConnect();
|
63 |
|
64 |
+
try {
|
65 |
+
let readme = "";
|
66 |
+
let newHtml = html;
|
67 |
|
68 |
+
const newTitle = title
|
69 |
+
.toLowerCase()
|
70 |
+
.replace(/[^a-z0-9]+/g, "-")
|
71 |
+
.split("-")
|
72 |
+
.filter(Boolean)
|
73 |
+
.join("-")
|
74 |
+
.slice(0, 96);
|
75 |
|
76 |
+
const repo: RepoDesignation = {
|
77 |
+
type: "space",
|
78 |
+
name: `${user.name}/${newTitle}`,
|
79 |
+
};
|
80 |
+
|
81 |
+
const { repoUrl } = await createRepo({
|
82 |
+
repo,
|
83 |
+
accessToken: user.token as string,
|
84 |
+
});
|
85 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
86 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
87 |
+
readme = `---
|
88 |
+
title: ${newTitle}
|
89 |
+
emoji: 🐳
|
90 |
colorFrom: ${colorFrom}
|
91 |
colorTo: ${colorTo}
|
|
|
92 |
sdk: static
|
93 |
pinned: false
|
94 |
tags:
|
95 |
+
- deepsite
|
96 |
---
|
97 |
|
98 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
|
|
|
|
|
99 |
|
100 |
+
newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
101 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
102 |
+
const readmeFile = new File([readme], "README.md", {
|
103 |
+
type: "text/markdown",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
});
|
105 |
+
const files = [file, readmeFile];
|
106 |
await uploadFiles({
|
107 |
repo,
|
108 |
files,
|
109 |
accessToken: user.token as string,
|
110 |
+
commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
|
111 |
});
|
|
|
112 |
const path = repoUrl.split("/").slice(-2).join("/");
|
113 |
+
const project = await Project.create({
|
114 |
+
user_id: user.id,
|
115 |
+
space_id: path,
|
116 |
+
prompts,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
});
|
118 |
+
return NextResponse.json({ project, path, ok: true }, { status: 201 });
|
119 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
} catch (err: any) {
|
121 |
return NextResponse.json(
|
122 |
{ error: err.message, ok: false },
|
123 |
{ status: 500 }
|
124 |
);
|
125 |
}
|
126 |
+
}
|
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,14 @@
|
|
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 { LoginProvider } from "@/components/contexts/login-context";
|
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 +23,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 +58,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,16 +69,16 @@ 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 |
|
@@ -76,35 +88,8 @@ export default async function RootLayout({
|
|
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"
|
@@ -113,15 +98,10 @@ export default async function RootLayout({
|
|
113 |
<body
|
114 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
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 |
|
14 |
const inter = Inter({
|
15 |
variable: "--font-inter-sans",
|
|
|
23 |
});
|
24 |
|
25 |
export const metadata: Metadata = {
|
26 |
+
title: "DeepSite | Build with AI ✨",
|
27 |
+
description:
|
28 |
+
"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.",
|
29 |
+
openGraph: {
|
30 |
+
title: "DeepSite | Build with AI ✨",
|
31 |
+
description:
|
32 |
+
"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.",
|
33 |
+
url: "https://deepsite.hf.co",
|
34 |
+
siteName: "DeepSite",
|
35 |
+
images: [
|
36 |
+
{
|
37 |
+
url: "https://deepsite.hf.co/banner.png",
|
38 |
+
width: 1200,
|
39 |
+
height: 630,
|
40 |
+
alt: "DeepSite Open Graph Image",
|
41 |
+
},
|
42 |
+
],
|
43 |
+
},
|
44 |
+
twitter: {
|
45 |
+
card: "summary_large_image",
|
46 |
title: "DeepSite | Build with AI ✨",
|
47 |
description:
|
48 |
"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.",
|
49 |
+
images: ["https://deepsite.hf.co/banner.png"],
|
50 |
+
},
|
51 |
appleWebApp: {
|
52 |
capable: true,
|
53 |
title: "DeepSite",
|
|
|
58 |
shortcut: "/logo.svg",
|
59 |
apple: "/logo.svg",
|
60 |
},
|
|
|
|
|
|
|
61 |
};
|
62 |
|
63 |
export const viewport: Viewport = {
|
|
|
69 |
async function getMe() {
|
70 |
const cookieStore = await cookies();
|
71 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
72 |
+
if (!token) return { user: null, errCode: null };
|
73 |
try {
|
74 |
const res = await apiServer.get("/me", {
|
75 |
headers: {
|
76 |
Authorization: `Bearer ${token}`,
|
77 |
},
|
78 |
});
|
79 |
+
return { user: res.data.user, errCode: null };
|
80 |
} catch (err: any) {
|
81 |
+
return { user: null, errCode: err.status };
|
82 |
}
|
83 |
}
|
84 |
|
|
|
88 |
children: React.ReactNode;
|
89 |
}>) {
|
90 |
const data = await getMe();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
return (
|
92 |
<html lang="en">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
<Script
|
94 |
defer
|
95 |
data-domain="deepsite.hf.co"
|
|
|
98 |
<body
|
99 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
100 |
>
|
|
|
101 |
<Toaster richColors position="bottom-center" />
|
102 |
+
<TanstackProvider>
|
103 |
+
<AppContext me={data}>{children}</AppContext>
|
104 |
+
</TanstackProvider>
|
|
|
|
|
|
|
|
|
105 |
</body>
|
106 |
</html>
|
107 |
);
|
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,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 project = await getProject(namespace, repoId);
|
36 |
+
if (!project?.html) {
|
37 |
+
redirect("/projects");
|
38 |
+
}
|
39 |
+
return <AppEditor project={project} />;
|
40 |
+
}
|
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 />;
|
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
astro.config.mjs
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
import { defineConfig } from 'astro/config';
|
3 |
+
import tailwind from '@astrojs/tailwind';
|
4 |
+
import sitemap from '@astrojs/sitemap';
|
5 |
+
|
6 |
+
// https://astro.build/config
|
7 |
+
export default defineConfig({
|
8 |
+
site: 'https://blueprintrak.com',
|
9 |
+
integrations: [
|
10 |
+
tailwind({
|
11 |
+
applyBaseStyles: false,
|
12 |
+
}),
|
13 |
+
sitemap({
|
14 |
+
i18n: {
|
15 |
+
defaultLocale: 'ar',
|
16 |
+
locales: {
|
17 |
+
ar: 'ar-AE',
|
18 |
+
en: 'en-US'
|
19 |
+
}
|
20 |
+
}
|
21 |
+
})
|
22 |
+
],
|
23 |
+
i18n: {
|
24 |
+
defaultLocale: 'ar',
|
25 |
+
locales: ['ar', 'en'],
|
26 |
+
routing: {
|
27 |
+
prefixDefaultLocale: false
|
28 |
+
}
|
29 |
+
}
|
30 |
+
});
|
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,144 +1,270 @@
|
|
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 { PROMPTS_FOR_AI } from "@/lib/prompts";
|
24 |
|
25 |
-
export
|
26 |
-
|
27 |
-
|
28 |
onScrollToBottom,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
}: {
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
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 |
-
if (promptStorage && promptStorage.trim() !== "") {
|
77 |
-
callAi();
|
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 |
-
|
89 |
-
|
90 |
-
|
91 |
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
redesignMarkdown,
|
105 |
-
!!user
|
106 |
-
);
|
107 |
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
|
|
|
|
|
|
118 |
}
|
119 |
}
|
120 |
};
|
121 |
|
122 |
-
const
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
break;
|
131 |
-
case "pro_required":
|
132 |
-
openProModal([]);
|
133 |
-
break;
|
134 |
-
case "api_error":
|
135 |
-
toast.error(message || "An error occurred");
|
136 |
-
break;
|
137 |
-
case "network_error":
|
138 |
-
toast.error(message || "Network error occurred");
|
139 |
-
break;
|
140 |
-
default:
|
141 |
-
toast.error("An unexpected error occurred");
|
142 |
}
|
143 |
};
|
144 |
|
@@ -148,19 +274,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
|
@@ -198,13 +324,6 @@ export const AskAi = ({
|
|
198 |
</main>
|
199 |
</div>
|
200 |
)}
|
201 |
-
<SelectedFiles
|
202 |
-
files={selectedFiles}
|
203 |
-
isAiWorking={isAiWorking}
|
204 |
-
onDelete={(file) =>
|
205 |
-
setSelectedFiles(selectedFiles.filter((f) => f !== file))
|
206 |
-
}
|
207 |
-
/>
|
208 |
{selectedElement && (
|
209 |
<div className="px-4 pt-3">
|
210 |
<SelectedHtmlElement
|
@@ -215,47 +334,36 @@ export const AskAi = ({
|
|
215 |
</div>
|
216 |
)}
|
217 |
<div className="w-full relative flex items-center justify-between">
|
218 |
-
{
|
219 |
-
<div className="absolute bg-neutral-800
|
220 |
-
<
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
size="iconXs"
|
234 |
-
variant="outline"
|
235 |
-
className="!rounded-md mr-0.5"
|
236 |
-
onClick={cancelRequest}
|
237 |
-
>
|
238 |
-
<CircleStop className="size-4" />
|
239 |
-
</Button>
|
240 |
-
)}
|
241 |
</div>
|
242 |
)}
|
243 |
-
<
|
244 |
-
|
245 |
-
|
246 |
-
}
|
247 |
className={classNames(
|
248 |
-
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4
|
249 |
{
|
250 |
-
"!pt-2.5":
|
251 |
-
selectedElement &&
|
252 |
-
!(isAiWorking || isUploading || isThinking),
|
253 |
}
|
254 |
)}
|
255 |
placeholder={
|
256 |
selectedElement
|
257 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
258 |
-
:
|
259 |
? "Ask DeepSite for edits"
|
260 |
: "Ask DeepSite anything..."
|
261 |
}
|
@@ -267,56 +375,91 @@ 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
|
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={
|
317 |
<source src="/success.mp3" type="audio/mpeg" />
|
318 |
Your browser does not support the audio element.
|
319 |
</audio>
|
320 |
</div>
|
321 |
);
|
322 |
-
}
|
|
|
1 |
+
"use client";
|
2 |
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
3 |
+
import { useState, useRef, useMemo } 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 } 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 |
|
26 |
+
export function AskAI({
|
27 |
+
html,
|
28 |
+
setHtml,
|
29 |
onScrollToBottom,
|
30 |
+
isAiWorking,
|
31 |
+
setisAiWorking,
|
32 |
+
isEditableModeEnabled = false,
|
33 |
+
selectedElement,
|
34 |
+
setSelectedElement,
|
35 |
+
setIsEditableModeEnabled,
|
36 |
+
onNewPrompt,
|
37 |
+
onSuccess,
|
38 |
}: {
|
39 |
+
html: string;
|
40 |
+
setHtml: (html: string) => void;
|
41 |
+
onScrollToBottom: () => void;
|
42 |
+
isAiWorking: boolean;
|
43 |
+
onNewPrompt: (prompt: string) => void;
|
44 |
+
htmlHistory?: HtmlHistory[];
|
45 |
+
setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
|
46 |
+
onSuccess: (h: string, p: string, n?: number[][]) => void;
|
47 |
+
isEditableModeEnabled: boolean;
|
48 |
+
setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
49 |
+
selectedElement?: HTMLElement | null;
|
50 |
+
setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
51 |
+
}) {
|
52 |
+
const refThink = useRef<HTMLDivElement | null>(null);
|
53 |
+
const audio = useRef<HTMLAudioElement | null>(null);
|
54 |
+
|
55 |
+
const [open, setOpen] = useState(false);
|
56 |
+
const [prompt, setPrompt] = useState("");
|
57 |
+
const [hasAsked, setHasAsked] = useState(false);
|
58 |
+
const [previousPrompt, setPreviousPrompt] = useState("");
|
59 |
+
const [provider, setProvider] = useLocalStorage("provider", "auto");
|
60 |
+
const [model, setModel] = useLocalStorage("model", MODELS[0].value);
|
|
|
|
|
61 |
const [openProvider, setOpenProvider] = useState(false);
|
62 |
const [providerError, setProviderError] = useState("");
|
63 |
+
const [openProModal, setOpenProModal] = useState(false);
|
64 |
+
const [think, setThink] = useState<string | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
const [openThink, setOpenThink] = useState(false);
|
66 |
+
const [isThinking, setIsThinking] = useState(true);
|
67 |
+
const [controller, setController] = useState<AbortController | null>(null);
|
68 |
+
const [isFollowUp, setIsFollowUp] = useState(true);
|
|
|
|
|
|
|
|
|
69 |
|
70 |
const callAi = async (redesignMarkdown?: string) => {
|
|
|
|
|
|
|
71 |
if (isAiWorking) return;
|
72 |
if (!redesignMarkdown && !prompt.trim()) return;
|
73 |
+
setisAiWorking(true);
|
74 |
+
setProviderError("");
|
75 |
+
setThink("");
|
76 |
+
setOpenThink(false);
|
77 |
+
setIsThinking(true);
|
78 |
|
79 |
+
let contentResponse = "";
|
80 |
+
let thinkResponse = "";
|
81 |
+
let lastRenderTime = 0;
|
82 |
|
83 |
+
const abortController = new AbortController();
|
84 |
+
setController(abortController);
|
85 |
+
try {
|
86 |
+
onNewPrompt(prompt);
|
87 |
+
if (isFollowUp && !redesignMarkdown && !isSameHtml) {
|
88 |
+
const selectedElementHtml = selectedElement
|
89 |
+
? selectedElement.outerHTML
|
90 |
+
: "";
|
91 |
+
const request = await fetch("/api/ask-ai", {
|
92 |
+
method: "PUT",
|
93 |
+
body: JSON.stringify({
|
94 |
+
prompt,
|
95 |
+
provider,
|
96 |
+
previousPrompt,
|
97 |
+
model,
|
98 |
+
html,
|
99 |
+
selectedElementHtml,
|
100 |
+
}),
|
101 |
+
headers: {
|
102 |
+
"Content-Type": "application/json",
|
103 |
+
"x-forwarded-for": window.location.hostname,
|
104 |
+
},
|
105 |
+
signal: abortController.signal,
|
106 |
+
});
|
107 |
+
if (request && request.body) {
|
108 |
+
const res = await request.json();
|
109 |
+
if (!request.ok) {
|
110 |
+
if (res.openLogin) {
|
111 |
+
setOpen(true);
|
112 |
+
} else if (res.openSelectProvider) {
|
113 |
+
setOpenProvider(true);
|
114 |
+
setProviderError(res.message);
|
115 |
+
} else if (res.openProModal) {
|
116 |
+
setOpenProModal(true);
|
117 |
+
} else {
|
118 |
+
toast.error(res.message);
|
119 |
+
}
|
120 |
+
setisAiWorking(false);
|
121 |
+
return;
|
122 |
+
}
|
123 |
+
setHtml(res.html);
|
124 |
+
toast.success("AI responded successfully");
|
125 |
+
setPreviousPrompt(prompt);
|
126 |
+
setPrompt("");
|
127 |
+
setisAiWorking(false);
|
128 |
+
onSuccess(res.html, prompt, res.updatedLines);
|
129 |
+
if (audio.current) audio.current.play();
|
130 |
+
}
|
131 |
+
} else {
|
132 |
+
const request = await fetch("/api/ask-ai", {
|
133 |
+
method: "POST",
|
134 |
+
body: JSON.stringify({
|
135 |
+
prompt,
|
136 |
+
provider,
|
137 |
+
model,
|
138 |
+
html: isSameHtml ? "" : html,
|
139 |
+
redesignMarkdown,
|
140 |
+
}),
|
141 |
+
headers: {
|
142 |
+
"Content-Type": "application/json",
|
143 |
+
"x-forwarded-for": window.location.hostname,
|
144 |
+
},
|
145 |
+
signal: abortController.signal,
|
146 |
+
});
|
147 |
+
if (request && request.body) {
|
148 |
+
const reader = request.body.getReader();
|
149 |
+
const decoder = new TextDecoder("utf-8");
|
150 |
+
const selectedModel = MODELS.find(
|
151 |
+
(m: { value: string }) => m.value === model
|
152 |
+
);
|
153 |
+
let contentThink: string | undefined = undefined;
|
154 |
+
const read = async () => {
|
155 |
+
const { done, value } = await reader.read();
|
156 |
+
if (done) {
|
157 |
+
const isJson =
|
158 |
+
contentResponse.trim().startsWith("{") &&
|
159 |
+
contentResponse.trim().endsWith("}");
|
160 |
+
const jsonResponse = isJson ? JSON.parse(contentResponse) : null;
|
161 |
+
if (jsonResponse && !jsonResponse.ok) {
|
162 |
+
if (jsonResponse.openLogin) {
|
163 |
+
setOpen(true);
|
164 |
+
} else if (jsonResponse.openSelectProvider) {
|
165 |
+
setOpenProvider(true);
|
166 |
+
setProviderError(jsonResponse.message);
|
167 |
+
} else if (jsonResponse.openProModal) {
|
168 |
+
setOpenProModal(true);
|
169 |
+
} else {
|
170 |
+
toast.error(jsonResponse.message);
|
171 |
+
}
|
172 |
+
setisAiWorking(false);
|
173 |
+
return;
|
174 |
+
}
|
175 |
|
176 |
+
toast.success("AI responded successfully");
|
177 |
+
setPreviousPrompt(prompt);
|
178 |
+
setPrompt("");
|
179 |
+
setisAiWorking(false);
|
180 |
+
setHasAsked(true);
|
181 |
+
setModel(MODELS[0].value);
|
182 |
+
if (audio.current) audio.current.play();
|
|
|
|
|
|
|
183 |
|
184 |
+
// Now we have the complete HTML including </html>, so set it to be sure
|
185 |
+
const finalDoc = contentResponse.match(
|
186 |
+
/<!DOCTYPE html>[\s\S]*<\/html>/
|
187 |
+
)?.[0];
|
188 |
+
if (finalDoc) {
|
189 |
+
setHtml(finalDoc);
|
190 |
+
}
|
191 |
+
onSuccess(finalDoc ?? contentResponse, prompt);
|
192 |
+
|
193 |
+
return;
|
194 |
+
}
|
195 |
+
|
196 |
+
const chunk = decoder.decode(value, { stream: true });
|
197 |
+
thinkResponse += chunk;
|
198 |
+
if (selectedModel?.isThinker) {
|
199 |
+
const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
|
200 |
+
if (thinkMatch && !thinkResponse?.includes("</think>")) {
|
201 |
+
if ((contentThink?.length ?? 0) < 3) {
|
202 |
+
setOpenThink(true);
|
203 |
+
}
|
204 |
+
setThink(thinkMatch.replace("<think>", "").trim());
|
205 |
+
contentThink += chunk;
|
206 |
+
return read();
|
207 |
+
}
|
208 |
+
}
|
209 |
+
|
210 |
+
contentResponse += chunk;
|
211 |
+
|
212 |
+
const newHtml = contentResponse.match(
|
213 |
+
/<!DOCTYPE html>[\s\S]*/
|
214 |
+
)?.[0];
|
215 |
+
if (newHtml) {
|
216 |
+
setIsThinking(false);
|
217 |
+
let partialDoc = newHtml;
|
218 |
+
if (
|
219 |
+
partialDoc.includes("<head>") &&
|
220 |
+
!partialDoc.includes("</head>")
|
221 |
+
) {
|
222 |
+
partialDoc += "\n</head>";
|
223 |
+
}
|
224 |
+
if (
|
225 |
+
partialDoc.includes("<body") &&
|
226 |
+
!partialDoc.includes("</body>")
|
227 |
+
) {
|
228 |
+
partialDoc += "\n</body>";
|
229 |
+
}
|
230 |
+
if (!partialDoc.includes("</html>")) {
|
231 |
+
partialDoc += "\n</html>";
|
232 |
+
}
|
233 |
+
|
234 |
+
// Throttle the re-renders to avoid flashing/flicker
|
235 |
+
const now = Date.now();
|
236 |
+
if (now - lastRenderTime > 300) {
|
237 |
+
setHtml(partialDoc);
|
238 |
+
lastRenderTime = now;
|
239 |
+
}
|
240 |
+
|
241 |
+
if (partialDoc.length > 200) {
|
242 |
+
onScrollToBottom();
|
243 |
+
}
|
244 |
+
}
|
245 |
+
read();
|
246 |
+
};
|
247 |
|
248 |
+
read();
|
249 |
+
}
|
250 |
+
}
|
251 |
+
} catch (error: any) {
|
252 |
+
setisAiWorking(false);
|
253 |
+
toast.error(error.message);
|
254 |
+
if (error.openLogin) {
|
255 |
+
setOpen(true);
|
256 |
}
|
257 |
}
|
258 |
};
|
259 |
|
260 |
+
const stopController = () => {
|
261 |
+
if (controller) {
|
262 |
+
controller.abort();
|
263 |
+
setController(null);
|
264 |
+
setisAiWorking(false);
|
265 |
+
setThink("");
|
266 |
+
setOpenThink(false);
|
267 |
+
setIsThinking(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
268 |
}
|
269 |
};
|
270 |
|
|
|
274 |
}
|
275 |
}, [think]);
|
276 |
|
277 |
+
useUpdateEffect(() => {
|
278 |
+
if (!isThinking) {
|
279 |
+
setOpenThink(false);
|
280 |
+
}
|
281 |
+
}, [isThinking]);
|
282 |
+
|
283 |
+
const isSameHtml = useMemo(() => {
|
284 |
+
return isTheSameHtml(html);
|
285 |
+
}, [html]);
|
286 |
|
287 |
return (
|
288 |
+
<div className="px-3">
|
289 |
+
<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">
|
290 |
{think && (
|
291 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
292 |
<header
|
|
|
324 |
</main>
|
325 |
</div>
|
326 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
{selectedElement && (
|
328 |
<div className="px-4 pt-3">
|
329 |
<SelectedHtmlElement
|
|
|
334 |
</div>
|
335 |
)}
|
336 |
<div className="w-full relative flex items-center justify-between">
|
337 |
+
{isAiWorking && (
|
338 |
+
<div className="absolute bg-neutral-800 rounded-lg bottom-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-center justify-between max-lg:text-sm">
|
339 |
+
<div className="flex items-center justify-start gap-2">
|
340 |
+
<Loading overlay={false} className="!size-4" />
|
341 |
+
<p className="text-neutral-400 text-sm">
|
342 |
+
AI is {isThinking ? "thinking" : "coding"}...{" "}
|
343 |
+
</p>
|
344 |
+
</div>
|
345 |
+
<div
|
346 |
+
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"
|
347 |
+
onClick={stopController}
|
348 |
+
>
|
349 |
+
<FaStopCircle />
|
350 |
+
Stop generation
|
351 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
</div>
|
353 |
)}
|
354 |
+
<input
|
355 |
+
type="text"
|
356 |
+
disabled={isAiWorking}
|
|
|
357 |
className={classNames(
|
358 |
+
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
|
359 |
{
|
360 |
+
"!pt-2.5": selectedElement && !isAiWorking,
|
|
|
|
|
361 |
}
|
362 |
)}
|
363 |
placeholder={
|
364 |
selectedElement
|
365 |
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
366 |
+
: hasAsked
|
367 |
? "Ask DeepSite for edits"
|
368 |
: "Ask DeepSite anything..."
|
369 |
}
|
|
|
375 |
}
|
376 |
}}
|
377 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
</div>
|
379 |
+
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
380 |
+
<div className="flex-1 flex items-center justify-start gap-1.5">
|
381 |
+
<ReImagine onRedesign={(md) => callAi(md)} />
|
382 |
+
{!isSameHtml && (
|
383 |
+
<Tooltip>
|
384 |
+
<TooltipTrigger asChild>
|
385 |
+
<Button
|
386 |
+
size="xs"
|
387 |
+
variant={isEditableModeEnabled ? "default" : "outline"}
|
388 |
+
onClick={() => {
|
389 |
+
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
390 |
+
}}
|
391 |
+
className={classNames("h-[28px]", {
|
392 |
+
"!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
|
393 |
+
!isEditableModeEnabled,
|
394 |
+
})}
|
395 |
+
>
|
396 |
+
<Crosshair className="size-4" />
|
397 |
+
Edit
|
398 |
+
</Button>
|
399 |
+
</TooltipTrigger>
|
400 |
+
<TooltipContent
|
401 |
+
align="start"
|
402 |
+
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
403 |
+
>
|
404 |
+
Select an element on the page to ask DeepSite edit it
|
405 |
+
directly.
|
406 |
+
</TooltipContent>
|
407 |
+
</Tooltip>
|
408 |
+
)}
|
409 |
+
<InviteFriends />
|
410 |
+
</div>
|
411 |
+
<div className="flex items-center justify-end gap-2">
|
412 |
<Settings
|
413 |
+
provider={provider as string}
|
414 |
+
model={model as string}
|
415 |
+
onChange={setProvider}
|
416 |
+
onModelChange={setModel}
|
417 |
open={openProvider}
|
418 |
error={providerError}
|
419 |
isFollowUp={!isSameHtml && isFollowUp}
|
420 |
onClose={setOpenProvider}
|
421 |
/>
|
|
|
|
|
|
|
|
|
|
|
422 |
<Button
|
423 |
size="iconXs"
|
424 |
+
disabled={isAiWorking || !prompt.trim()}
|
|
|
|
|
|
|
|
|
425 |
onClick={() => callAi()}
|
426 |
>
|
427 |
<ArrowUp className="size-4" />
|
428 |
</Button>
|
429 |
</div>
|
430 |
</div>
|
431 |
+
<LoginModal open={open} onClose={() => setOpen(false)} html={html} />
|
432 |
+
<ProModal
|
433 |
+
html={html}
|
434 |
+
open={openProModal}
|
435 |
+
onClose={() => setOpenProModal(false)}
|
436 |
+
/>
|
437 |
+
{!isSameHtml && (
|
438 |
+
<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">
|
439 |
+
<label
|
440 |
+
htmlFor="diff-patch-checkbox"
|
441 |
+
className="flex items-center gap-1.5 cursor-pointer"
|
442 |
+
>
|
443 |
+
<Checkbox
|
444 |
+
id="diff-patch-checkbox"
|
445 |
+
checked={isFollowUp}
|
446 |
+
onCheckedChange={(e) => {
|
447 |
+
if (e === true && !isSameHtml) {
|
448 |
+
setModel(MODELS[0].value);
|
449 |
+
}
|
450 |
+
setIsFollowUp(e === true);
|
451 |
+
}}
|
452 |
+
/>
|
453 |
+
Diff-Patch Update
|
454 |
+
</label>
|
455 |
+
<FollowUpTooltip />
|
456 |
+
</div>
|
457 |
+
)}
|
458 |
</div>
|
459 |
+
<audio ref={audio} id="audio" className="hidden">
|
460 |
<source src="/success.mp3" type="audio/mpeg" />
|
461 |
Your browser does not support the audio element.
|
462 |
</audio>
|
463 |
</div>
|
464 |
);
|
465 |
+
}
|
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 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|