Spaces:
Running
Running
add custom images upload
Browse files- app/api/ask-ai/route.ts +2 -2
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +111 -0
- app/api/me/projects/[namespace]/[repoId]/route.ts +11 -0
- app/projects/[namespace]/[repoId]/page.tsx +3 -1
- components/editor/ask-ai/index.tsx +54 -14
- components/editor/ask-ai/selected-files.tsx +47 -0
- components/editor/ask-ai/uploader.tsx +202 -0
- components/editor/deploy-button/content.tsx +111 -0
- components/editor/deploy-button/index.tsx +2 -95
- components/editor/footer/index.tsx +7 -6
- components/editor/index.tsx +8 -15
- hooks/useCallAi.ts +2 -1
- next.config.ts +3 -0
app/api/ask-ai/route.ts
CHANGED
@@ -197,7 +197,7 @@ export async function PUT(request: NextRequest) {
|
|
197 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
198 |
|
199 |
const body = await request.json();
|
200 |
-
const { prompt, previousPrompt, provider, selectedElementHtml, model, pages } =
|
201 |
body;
|
202 |
|
203 |
if (!prompt || pages.length === 0) {
|
@@ -281,7 +281,7 @@ export async function PUT(request: NextRequest) {
|
|
281 |
selectedElementHtml
|
282 |
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
283 |
: ""
|
284 |
-
}. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}
|
285 |
},
|
286 |
{
|
287 |
role: "user",
|
|
|
197 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
198 |
|
199 |
const body = await request.json();
|
200 |
+
const { prompt, previousPrompt, provider, selectedElementHtml, model, pages, files } =
|
201 |
body;
|
202 |
|
203 |
if (!prompt || pages.length === 0) {
|
|
|
281 |
selectedElementHtml
|
282 |
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
283 |
: ""
|
284 |
+
}. Current pages: ${pages?.map((p: Page) => `- ${p.path} \n${p.html}`).join("\n")}. ${files?.length > 0 ? `Current images: ${files?.map((f: string) => `- ${f}`).join("\n")}.` : ""}`,
|
285 |
},
|
286 |
{
|
287 |
role: "user",
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { RepoDesignation, uploadFiles } from "@huggingface/hub";
|
3 |
+
|
4 |
+
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
|
8 |
+
// No longer need the ImageUpload interface since we're handling FormData with File objects
|
9 |
+
|
10 |
+
export async function POST(
|
11 |
+
req: NextRequest,
|
12 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
13 |
+
) {
|
14 |
+
try {
|
15 |
+
const user = await isAuthenticated();
|
16 |
+
|
17 |
+
if (user instanceof NextResponse || !user) {
|
18 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
19 |
+
}
|
20 |
+
|
21 |
+
await dbConnect();
|
22 |
+
const param = await params;
|
23 |
+
const { namespace, repoId } = param;
|
24 |
+
|
25 |
+
const project = await Project.findOne({
|
26 |
+
user_id: user.id,
|
27 |
+
space_id: `${namespace}/${repoId}`,
|
28 |
+
}).lean();
|
29 |
+
|
30 |
+
if (!project) {
|
31 |
+
return NextResponse.json(
|
32 |
+
{
|
33 |
+
ok: false,
|
34 |
+
error: "Project not found",
|
35 |
+
},
|
36 |
+
{ status: 404 }
|
37 |
+
);
|
38 |
+
}
|
39 |
+
|
40 |
+
// Parse the FormData to get the images
|
41 |
+
const formData = await req.formData();
|
42 |
+
const imageFiles = formData.getAll("images") as File[];
|
43 |
+
|
44 |
+
if (!imageFiles || imageFiles.length === 0) {
|
45 |
+
return NextResponse.json(
|
46 |
+
{
|
47 |
+
ok: false,
|
48 |
+
error: "At least one image file is required under the 'images' key",
|
49 |
+
},
|
50 |
+
{ status: 400 }
|
51 |
+
);
|
52 |
+
}
|
53 |
+
|
54 |
+
const files: File[] = [];
|
55 |
+
for (const file of imageFiles) {
|
56 |
+
if (!(file instanceof File)) {
|
57 |
+
return NextResponse.json(
|
58 |
+
{
|
59 |
+
ok: false,
|
60 |
+
error: "Invalid file format - all items under 'images' key must be files",
|
61 |
+
},
|
62 |
+
{ status: 400 }
|
63 |
+
);
|
64 |
+
}
|
65 |
+
|
66 |
+
if (!file.type.startsWith('image/')) {
|
67 |
+
return NextResponse.json(
|
68 |
+
{
|
69 |
+
ok: false,
|
70 |
+
error: `File ${file.name} is not an image`,
|
71 |
+
},
|
72 |
+
{ status: 400 }
|
73 |
+
);
|
74 |
+
}
|
75 |
+
|
76 |
+
// Create File object with images/ folder prefix
|
77 |
+
const fileName = `images/${file.name}`;
|
78 |
+
const processedFile = new File([file], fileName, { type: file.type });
|
79 |
+
files.push(processedFile);
|
80 |
+
}
|
81 |
+
|
82 |
+
// Upload files to HuggingFace space
|
83 |
+
const repo: RepoDesignation = {
|
84 |
+
type: "space",
|
85 |
+
name: `${namespace}/${repoId}`,
|
86 |
+
};
|
87 |
+
|
88 |
+
await uploadFiles({
|
89 |
+
repo,
|
90 |
+
files,
|
91 |
+
accessToken: user.token as string,
|
92 |
+
commitTitle: `Upload ${files.length} image(s)`,
|
93 |
+
});
|
94 |
+
|
95 |
+
return NextResponse.json({
|
96 |
+
ok: true,
|
97 |
+
message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
|
98 |
+
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
99 |
+
}, { status: 200 });
|
100 |
+
|
101 |
+
} catch (error) {
|
102 |
+
console.error('Error uploading images:', error);
|
103 |
+
return NextResponse.json(
|
104 |
+
{
|
105 |
+
ok: false,
|
106 |
+
error: "Failed to upload images",
|
107 |
+
},
|
108 |
+
{ status: 500 }
|
109 |
+
);
|
110 |
+
}
|
111 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/route.ts
CHANGED
@@ -65,6 +65,9 @@ export async function GET(
|
|
65 |
};
|
66 |
|
67 |
const htmlFiles: Page[] = [];
|
|
|
|
|
|
|
68 |
|
69 |
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
70 |
if (fileInfo.path.endsWith(".html")) {
|
@@ -84,6 +87,13 @@ export async function GET(
|
|
84 |
}
|
85 |
}
|
86 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
}
|
88 |
|
89 |
if (htmlFiles.length === 0) {
|
@@ -101,6 +111,7 @@ export async function GET(
|
|
101 |
project: {
|
102 |
...project,
|
103 |
pages: htmlFiles,
|
|
|
104 |
},
|
105 |
ok: true,
|
106 |
},
|
|
|
65 |
};
|
66 |
|
67 |
const htmlFiles: Page[] = [];
|
68 |
+
const images: string[] = [];
|
69 |
+
|
70 |
+
const allowedImagesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
71 |
|
72 |
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
73 |
if (fileInfo.path.endsWith(".html")) {
|
|
|
87 |
}
|
88 |
}
|
89 |
}
|
90 |
+
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
91 |
+
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
92 |
+
if (allowedImagesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
93 |
+
images.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
94 |
+
}
|
95 |
+
}
|
96 |
+
}
|
97 |
}
|
98 |
|
99 |
if (htmlFiles.length === 0) {
|
|
|
111 |
project: {
|
112 |
...project,
|
113 |
pages: htmlFiles,
|
114 |
+
images,
|
115 |
},
|
116 |
ok: true,
|
117 |
},
|
app/projects/[namespace]/[repoId]/page.tsx
CHANGED
@@ -36,5 +36,7 @@ export default async function ProjectNamespacePage({
|
|
36 |
if (!data?.pages) {
|
37 |
redirect("/projects");
|
38 |
}
|
39 |
-
return
|
|
|
|
|
40 |
}
|
|
|
36 |
if (!data?.pages) {
|
37 |
redirect("/projects");
|
38 |
}
|
39 |
+
return (
|
40 |
+
<AppEditor project={data} pages={data.pages} images={data.images ?? []} />
|
41 |
+
);
|
42 |
}
|
components/editor/ask-ai/index.tsx
CHANGED
@@ -10,8 +10,8 @@ import { FaStopCircle } from "react-icons/fa";
|
|
10 |
import ProModal from "@/components/pro-modal";
|
11 |
import { Button } from "@/components/ui/button";
|
12 |
import { MODELS } from "@/lib/providers";
|
13 |
-
import { HtmlHistory, Page } 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";
|
@@ -23,8 +23,12 @@ import { SelectedHtmlElement } from "./selected-html-element";
|
|
23 |
import { FollowUpTooltip } from "./follow-up-tooltip";
|
24 |
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
25 |
import { useCallAi } from "@/hooks/useCallAi";
|
|
|
|
|
26 |
|
27 |
export function AskAI({
|
|
|
|
|
28 |
currentPage,
|
29 |
previousPrompts,
|
30 |
onScrollToBottom,
|
@@ -35,13 +39,17 @@ export function AskAI({
|
|
35 |
htmlHistory,
|
36 |
selectedElement,
|
37 |
setSelectedElement,
|
|
|
|
|
38 |
setIsEditableModeEnabled,
|
39 |
onNewPrompt,
|
40 |
onSuccess,
|
41 |
setPages,
|
42 |
setCurrentPage,
|
43 |
}: {
|
|
|
44 |
currentPage: Page;
|
|
|
45 |
pages: Page[];
|
46 |
onScrollToBottom: () => void;
|
47 |
previousPrompts: string[];
|
@@ -55,6 +63,8 @@ export function AskAI({
|
|
55 |
setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
56 |
selectedElement?: HTMLElement | null;
|
57 |
setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
|
|
|
|
58 |
setPages: React.Dispatch<React.SetStateAction<Page[]>>;
|
59 |
setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
|
60 |
}) {
|
@@ -72,6 +82,8 @@ export function AskAI({
|
|
72 |
const [isThinking, setIsThinking] = useState(true);
|
73 |
const [think, setThink] = useState("");
|
74 |
const [isFollowUp, setIsFollowUp] = useState(true);
|
|
|
|
|
75 |
|
76 |
const {
|
77 |
callAiNewProject,
|
@@ -110,7 +122,8 @@ export function AskAI({
|
|
110 |
model,
|
111 |
provider,
|
112 |
previousPrompt,
|
113 |
-
selectedElementHtml
|
|
|
114 |
);
|
115 |
|
116 |
if (result?.error) {
|
@@ -254,6 +267,13 @@ export function AskAI({
|
|
254 |
</main>
|
255 |
</div>
|
256 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
{selectedElement && (
|
258 |
<div className="px-4 pt-3">
|
259 |
<SelectedHtmlElement
|
@@ -264,21 +284,25 @@ export function AskAI({
|
|
264 |
</div>
|
265 |
)}
|
266 |
<div className="w-full relative flex items-center justify-between">
|
267 |
-
{isAiWorking && (
|
268 |
<div className="absolute bg-neutral-800 rounded-lg top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
269 |
<div className="flex items-center justify-start gap-2">
|
270 |
<Loading overlay={false} className="!size-4" />
|
271 |
<p className="text-neutral-400 text-sm">
|
272 |
-
|
|
|
|
|
273 |
</p>
|
274 |
</div>
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
|
|
|
|
282 |
</div>
|
283 |
)}
|
284 |
<textarea
|
@@ -307,6 +331,22 @@ export function AskAI({
|
|
307 |
</div>
|
308 |
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
309 |
<div className="flex-1 flex items-center justify-start gap-1.5">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
310 |
<ReImagine onRedesign={(md) => callAi(md)} />
|
311 |
{!isSameHtml && (
|
312 |
<Tooltip>
|
@@ -335,7 +375,7 @@ export function AskAI({
|
|
335 |
</TooltipContent>
|
336 |
</Tooltip>
|
337 |
)}
|
338 |
-
<InviteFriends />
|
339 |
</div>
|
340 |
<div className="flex items-center justify-end gap-2">
|
341 |
<Settings
|
@@ -364,7 +404,7 @@ export function AskAI({
|
|
364 |
onClose={() => setOpenProModal(false)}
|
365 |
/>
|
366 |
{pages.length === 1 && (
|
367 |
-
<div className="border border-sky-500/20 bg-sky-500/
|
368 |
<span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
|
369 |
NEW
|
370 |
</span>
|
|
|
10 |
import ProModal from "@/components/pro-modal";
|
11 |
import { Button } from "@/components/ui/button";
|
12 |
import { MODELS } from "@/lib/providers";
|
13 |
+
import { HtmlHistory, Page, Project } from "@/types";
|
14 |
+
// import { InviteFriends } from "@/components/invite-friends";
|
15 |
import { Settings } from "@/components/editor/ask-ai/settings";
|
16 |
import { LoginModal } from "@/components/login-modal";
|
17 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
|
|
23 |
import { FollowUpTooltip } from "./follow-up-tooltip";
|
24 |
import { isTheSameHtml } from "@/lib/compare-html-diff";
|
25 |
import { useCallAi } from "@/hooks/useCallAi";
|
26 |
+
import { SelectedFiles } from "./selected-files";
|
27 |
+
import { Uploader } from "./uploader";
|
28 |
|
29 |
export function AskAI({
|
30 |
+
project,
|
31 |
+
images,
|
32 |
currentPage,
|
33 |
previousPrompts,
|
34 |
onScrollToBottom,
|
|
|
39 |
htmlHistory,
|
40 |
selectedElement,
|
41 |
setSelectedElement,
|
42 |
+
selectedFiles,
|
43 |
+
setSelectedFiles,
|
44 |
setIsEditableModeEnabled,
|
45 |
onNewPrompt,
|
46 |
onSuccess,
|
47 |
setPages,
|
48 |
setCurrentPage,
|
49 |
}: {
|
50 |
+
project?: Project | null;
|
51 |
currentPage: Page;
|
52 |
+
images?: string[];
|
53 |
pages: Page[];
|
54 |
onScrollToBottom: () => void;
|
55 |
previousPrompts: string[];
|
|
|
63 |
setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
64 |
selectedElement?: HTMLElement | null;
|
65 |
setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
66 |
+
selectedFiles: string[];
|
67 |
+
setSelectedFiles: React.Dispatch<React.SetStateAction<string[]>>;
|
68 |
setPages: React.Dispatch<React.SetStateAction<Page[]>>;
|
69 |
setCurrentPage: React.Dispatch<React.SetStateAction<string>>;
|
70 |
}) {
|
|
|
82 |
const [isThinking, setIsThinking] = useState(true);
|
83 |
const [think, setThink] = useState("");
|
84 |
const [isFollowUp, setIsFollowUp] = useState(true);
|
85 |
+
const [isUploading, setIsUploading] = useState(false);
|
86 |
+
const [files, setFiles] = useState<string[]>(images ?? []);
|
87 |
|
88 |
const {
|
89 |
callAiNewProject,
|
|
|
122 |
model,
|
123 |
provider,
|
124 |
previousPrompt,
|
125 |
+
selectedElementHtml,
|
126 |
+
selectedFiles
|
127 |
);
|
128 |
|
129 |
if (result?.error) {
|
|
|
267 |
</main>
|
268 |
</div>
|
269 |
)}
|
270 |
+
<SelectedFiles
|
271 |
+
files={selectedFiles}
|
272 |
+
isAiWorking={isAiWorking}
|
273 |
+
onDelete={(file) =>
|
274 |
+
setSelectedFiles((prev) => prev.filter((f) => f !== file))
|
275 |
+
}
|
276 |
+
/>
|
277 |
{selectedElement && (
|
278 |
<div className="px-4 pt-3">
|
279 |
<SelectedHtmlElement
|
|
|
284 |
</div>
|
285 |
)}
|
286 |
<div className="w-full relative flex items-center justify-between">
|
287 |
+
{(isAiWorking || isUploading) && (
|
288 |
<div className="absolute bg-neutral-800 rounded-lg top-0 left-4 w-[calc(100%-30px)] h-full z-1 flex items-start pt-3.5 justify-between max-lg:text-sm">
|
289 |
<div className="flex items-center justify-start gap-2">
|
290 |
<Loading overlay={false} className="!size-4" />
|
291 |
<p className="text-neutral-400 text-sm">
|
292 |
+
{isUploading
|
293 |
+
? "Uploading images..."
|
294 |
+
: `AI is ${isThinking ? "thinking" : "coding"}...`}
|
295 |
</p>
|
296 |
</div>
|
297 |
+
{isAiWorking && (
|
298 |
+
<div
|
299 |
+
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"
|
300 |
+
onClick={stopController}
|
301 |
+
>
|
302 |
+
<FaStopCircle />
|
303 |
+
Stop generation
|
304 |
+
</div>
|
305 |
+
)}
|
306 |
</div>
|
307 |
)}
|
308 |
<textarea
|
|
|
331 |
</div>
|
332 |
<div className="flex items-center justify-between gap-2 px-4 pb-3 mt-2">
|
333 |
<div className="flex-1 flex items-center justify-start gap-1.5">
|
334 |
+
<Uploader
|
335 |
+
pages={pages}
|
336 |
+
onLoading={setIsUploading}
|
337 |
+
isLoading={isUploading}
|
338 |
+
onFiles={setFiles}
|
339 |
+
onSelectFile={(file) => {
|
340 |
+
if (selectedFiles.includes(file)) {
|
341 |
+
setSelectedFiles((prev) => prev.filter((f) => f !== file));
|
342 |
+
} else {
|
343 |
+
setSelectedFiles((prev) => [...prev, file]);
|
344 |
+
}
|
345 |
+
}}
|
346 |
+
files={files}
|
347 |
+
selectedFiles={selectedFiles}
|
348 |
+
project={project}
|
349 |
+
/>
|
350 |
<ReImagine onRedesign={(md) => callAi(md)} />
|
351 |
{!isSameHtml && (
|
352 |
<Tooltip>
|
|
|
375 |
</TooltipContent>
|
376 |
</Tooltip>
|
377 |
)}
|
378 |
+
{/* <InviteFriends /> */}
|
379 |
</div>
|
380 |
<div className="flex items-center justify-end gap-2">
|
381 |
<Settings
|
|
|
404 |
onClose={() => setOpenProModal(false)}
|
405 |
/>
|
406 |
{pages.length === 1 && (
|
407 |
+
<div className="border border-sky-500/20 bg-sky-500/40 hover:bg-sky-600 transition-all duration-200 text-sky-500 pl-2 pr-4 py-1.5 text-xs rounded-full absolute top-0 -translate-y-[calc(100%+8px)] left-0 max-w-max flex items-center justify-start gap-2">
|
408 |
<span className="rounded-full text-[10px] font-semibold bg-white text-neutral-900 px-1.5 py-0.5">
|
409 |
NEW
|
410 |
</span>
|
components/editor/ask-ai/selected-files.tsx
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Image from "next/image";
|
2 |
+
|
3 |
+
import { Button } from "@/components/ui/button";
|
4 |
+
import { Minus } from "lucide-react";
|
5 |
+
|
6 |
+
export const SelectedFiles = ({
|
7 |
+
files,
|
8 |
+
isAiWorking,
|
9 |
+
onDelete,
|
10 |
+
}: {
|
11 |
+
files: string[];
|
12 |
+
isAiWorking: boolean;
|
13 |
+
onDelete: (file: string) => void;
|
14 |
+
}) => {
|
15 |
+
if (files.length === 0) return null;
|
16 |
+
return (
|
17 |
+
<div className="px-4 pt-3">
|
18 |
+
<div className="flex items-center justify-start gap-2">
|
19 |
+
{files.map((file) => (
|
20 |
+
<div
|
21 |
+
key={file}
|
22 |
+
className="flex items-center relative justify-start gap-2 p-1 bg-neutral-700 rounded-md"
|
23 |
+
>
|
24 |
+
<Image
|
25 |
+
src={file}
|
26 |
+
alt="uploaded image"
|
27 |
+
className="size-12 rounded-md object-cover"
|
28 |
+
width={40}
|
29 |
+
height={40}
|
30 |
+
/>
|
31 |
+
<Button
|
32 |
+
size="iconXsss"
|
33 |
+
variant="secondary"
|
34 |
+
className={`absolute top-0.5 right-0.5 ${
|
35 |
+
isAiWorking ? "opacity-50 !cursor-not-allowed" : ""
|
36 |
+
}`}
|
37 |
+
disabled={isAiWorking}
|
38 |
+
onClick={() => onDelete(file)}
|
39 |
+
>
|
40 |
+
<Minus className="size-4" />
|
41 |
+
</Button>
|
42 |
+
</div>
|
43 |
+
))}
|
44 |
+
</div>
|
45 |
+
</div>
|
46 |
+
);
|
47 |
+
};
|
components/editor/ask-ai/uploader.tsx
ADDED
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useRef, useState } from "react";
|
2 |
+
import { Images, Upload } from "lucide-react";
|
3 |
+
import Image from "next/image";
|
4 |
+
|
5 |
+
import {
|
6 |
+
Popover,
|
7 |
+
PopoverContent,
|
8 |
+
PopoverTrigger,
|
9 |
+
} from "@/components/ui/popover";
|
10 |
+
import { Button } from "@/components/ui/button";
|
11 |
+
import { Page, Project } from "@/types";
|
12 |
+
import Loading from "@/components/loading";
|
13 |
+
import { RiCheckboxCircleFill } from "react-icons/ri";
|
14 |
+
import { useUser } from "@/hooks/useUser";
|
15 |
+
import { LoginModal } from "@/components/login-modal";
|
16 |
+
import { DeployButtonContent } from "../deploy-button/content";
|
17 |
+
|
18 |
+
export const Uploader = ({
|
19 |
+
pages,
|
20 |
+
onLoading,
|
21 |
+
isLoading,
|
22 |
+
onFiles,
|
23 |
+
onSelectFile,
|
24 |
+
selectedFiles,
|
25 |
+
files,
|
26 |
+
project,
|
27 |
+
}: {
|
28 |
+
pages: Page[];
|
29 |
+
onLoading: (isLoading: boolean) => void;
|
30 |
+
isLoading: boolean;
|
31 |
+
files: string[];
|
32 |
+
onFiles: React.Dispatch<React.SetStateAction<string[]>>;
|
33 |
+
onSelectFile: (file: string) => void;
|
34 |
+
selectedFiles: string[];
|
35 |
+
project?: Project | null;
|
36 |
+
}) => {
|
37 |
+
const { user } = useUser();
|
38 |
+
|
39 |
+
const [open, setOpen] = useState(false);
|
40 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
41 |
+
|
42 |
+
const uploadFiles = async (files: FileList | null) => {
|
43 |
+
if (!files) return;
|
44 |
+
if (!project) return;
|
45 |
+
|
46 |
+
onLoading(true);
|
47 |
+
|
48 |
+
const images = Array.from(files).filter((file) => {
|
49 |
+
return file.type.startsWith("image/");
|
50 |
+
});
|
51 |
+
|
52 |
+
const data = new FormData();
|
53 |
+
images.forEach((image) => {
|
54 |
+
data.append("images", image);
|
55 |
+
});
|
56 |
+
|
57 |
+
const response = await fetch(
|
58 |
+
`/api/me/projects/${project.space_id}/images`,
|
59 |
+
{
|
60 |
+
method: "POST",
|
61 |
+
body: data,
|
62 |
+
}
|
63 |
+
);
|
64 |
+
if (response.ok) {
|
65 |
+
const data = await response.json();
|
66 |
+
onFiles((prev) => [...prev, ...data.uploadedFiles]);
|
67 |
+
}
|
68 |
+
onLoading(false);
|
69 |
+
};
|
70 |
+
|
71 |
+
// TODO FIRST PUBLISH YOUR PROJECT TO UPLOAD IMAGES.
|
72 |
+
return user?.id ? (
|
73 |
+
<Popover open={open} onOpenChange={setOpen}>
|
74 |
+
<form>
|
75 |
+
<PopoverTrigger asChild>
|
76 |
+
<Button
|
77 |
+
size="iconXs"
|
78 |
+
variant="outline"
|
79 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
80 |
+
>
|
81 |
+
<Images className="size-4" />
|
82 |
+
</Button>
|
83 |
+
</PopoverTrigger>
|
84 |
+
<PopoverContent
|
85 |
+
align="start"
|
86 |
+
className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden"
|
87 |
+
>
|
88 |
+
{project?.space_id ? (
|
89 |
+
<>
|
90 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
91 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
92 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
93 |
+
🎨
|
94 |
+
</div>
|
95 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
96 |
+
🖼️
|
97 |
+
</div>
|
98 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
99 |
+
💻
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
<p className="text-xl font-semibold text-neutral-950">
|
103 |
+
Add Custom Images
|
104 |
+
</p>
|
105 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
106 |
+
Upload images to your project and use them with DeepSite!
|
107 |
+
</p>
|
108 |
+
</header>
|
109 |
+
<main className="space-y-4 p-5">
|
110 |
+
<div>
|
111 |
+
<p className="text-xs text-left text-neutral-700 mb-2">
|
112 |
+
Uploaded Images
|
113 |
+
</p>
|
114 |
+
<div className="grid grid-cols-4 gap-1 flex-wrap">
|
115 |
+
{files.map((file) => (
|
116 |
+
<div
|
117 |
+
key={file}
|
118 |
+
className="select-none relative cursor-pointer bg-white rounded-md border-[2px] border-white hover:shadow-2xl transition-all duration-300"
|
119 |
+
onClick={() => onSelectFile(file)}
|
120 |
+
>
|
121 |
+
<Image
|
122 |
+
src={file}
|
123 |
+
alt="uploaded image"
|
124 |
+
width={56}
|
125 |
+
height={56}
|
126 |
+
className="object-cover w-full rounded-sm aspect-square"
|
127 |
+
/>
|
128 |
+
{selectedFiles.includes(file) && (
|
129 |
+
<div className="absolute top-0 right-0 h-full w-full flex items-center justify-center bg-black/50 rounded-md">
|
130 |
+
<RiCheckboxCircleFill className="size-6 text-neutral-100" />
|
131 |
+
</div>
|
132 |
+
)}
|
133 |
+
</div>
|
134 |
+
))}
|
135 |
+
</div>
|
136 |
+
</div>
|
137 |
+
<div>
|
138 |
+
<p className="text-xs text-left text-neutral-700 mb-2">
|
139 |
+
Or import images from your computer
|
140 |
+
</p>
|
141 |
+
<Button
|
142 |
+
variant="black"
|
143 |
+
onClick={() => fileInputRef.current?.click()}
|
144 |
+
className="relative w-full"
|
145 |
+
>
|
146 |
+
{isLoading ? (
|
147 |
+
<>
|
148 |
+
<Loading
|
149 |
+
overlay={false}
|
150 |
+
className="ml-2 size-4 animate-spin"
|
151 |
+
/>
|
152 |
+
Uploading image(s)...
|
153 |
+
</>
|
154 |
+
) : (
|
155 |
+
<>
|
156 |
+
<Upload className="size-4" />
|
157 |
+
Upload Images
|
158 |
+
</>
|
159 |
+
)}
|
160 |
+
</Button>
|
161 |
+
<input
|
162 |
+
ref={fileInputRef}
|
163 |
+
type="file"
|
164 |
+
className="hidden"
|
165 |
+
multiple
|
166 |
+
accept="image/*"
|
167 |
+
onChange={(e) => uploadFiles(e.target.files)}
|
168 |
+
/>
|
169 |
+
</div>
|
170 |
+
</main>
|
171 |
+
</>
|
172 |
+
) : (
|
173 |
+
<DeployButtonContent
|
174 |
+
pages={pages}
|
175 |
+
prompts={[]}
|
176 |
+
options={{
|
177 |
+
description: "Publish your project first to add custom images.",
|
178 |
+
}}
|
179 |
+
/>
|
180 |
+
)}
|
181 |
+
</PopoverContent>
|
182 |
+
</form>
|
183 |
+
</Popover>
|
184 |
+
) : (
|
185 |
+
<>
|
186 |
+
<Button
|
187 |
+
size="iconXs"
|
188 |
+
variant="outline"
|
189 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
190 |
+
>
|
191 |
+
<Images className="size-4" />
|
192 |
+
</Button>
|
193 |
+
<LoginModal
|
194 |
+
open={open}
|
195 |
+
onClose={() => setOpen(false)}
|
196 |
+
pages={pages}
|
197 |
+
title="Log In to add Custom Images"
|
198 |
+
description="Log In through your Hugging Face account to publish your project and increase your monthly free limit."
|
199 |
+
/>
|
200 |
+
</>
|
201 |
+
);
|
202 |
+
};
|
components/editor/deploy-button/content.tsx
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Rocket } from "lucide-react";
|
2 |
+
import Image from "next/image";
|
3 |
+
|
4 |
+
import Loading from "@/components/loading";
|
5 |
+
import { Button } from "@/components/ui/button";
|
6 |
+
import { Input } from "@/components/ui/input";
|
7 |
+
import SpaceIcon from "@/assets/space.svg";
|
8 |
+
import { Page } from "@/types";
|
9 |
+
import { api } from "@/lib/api";
|
10 |
+
import { toast } from "sonner";
|
11 |
+
import { useState } from "react";
|
12 |
+
import { useRouter } from "next/navigation";
|
13 |
+
|
14 |
+
export const DeployButtonContent = ({
|
15 |
+
pages,
|
16 |
+
options,
|
17 |
+
prompts,
|
18 |
+
}: {
|
19 |
+
pages: Page[];
|
20 |
+
options?: {
|
21 |
+
title?: string;
|
22 |
+
description?: string;
|
23 |
+
};
|
24 |
+
prompts: string[];
|
25 |
+
}) => {
|
26 |
+
const router = useRouter();
|
27 |
+
const [loading, setLoading] = useState(false);
|
28 |
+
|
29 |
+
const [config, setConfig] = useState({
|
30 |
+
title: "",
|
31 |
+
});
|
32 |
+
|
33 |
+
const createSpace = async () => {
|
34 |
+
if (!config.title) {
|
35 |
+
toast.error("Please enter a title for your space.");
|
36 |
+
return;
|
37 |
+
}
|
38 |
+
setLoading(true);
|
39 |
+
|
40 |
+
try {
|
41 |
+
const res = await api.post("/me/projects", {
|
42 |
+
title: config.title,
|
43 |
+
pages,
|
44 |
+
prompts,
|
45 |
+
});
|
46 |
+
if (res.data.ok) {
|
47 |
+
router.push(`/projects/${res.data.path}?deploy=true`);
|
48 |
+
} else {
|
49 |
+
toast.error(res?.data?.error || "Failed to create space");
|
50 |
+
}
|
51 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
52 |
+
} catch (err: any) {
|
53 |
+
toast.error(err.response?.data?.error || err.message);
|
54 |
+
} finally {
|
55 |
+
setLoading(false);
|
56 |
+
}
|
57 |
+
};
|
58 |
+
|
59 |
+
return (
|
60 |
+
<>
|
61 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
62 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
63 |
+
<div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
64 |
+
🚀
|
65 |
+
</div>
|
66 |
+
<div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
|
67 |
+
<Image src={SpaceIcon} alt="Space Icon" className="size-7" />
|
68 |
+
</div>
|
69 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
70 |
+
👻
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
<p className="text-xl font-semibold text-neutral-950">
|
74 |
+
Publish as Space!
|
75 |
+
</p>
|
76 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
77 |
+
{options?.description ??
|
78 |
+
"Save and Publish your project to a Space on the Hub. Spaces are a way to share your project with the world."}
|
79 |
+
</p>
|
80 |
+
</header>
|
81 |
+
<main className="space-y-4 p-6">
|
82 |
+
<div>
|
83 |
+
<p className="text-sm text-neutral-700 mb-2">
|
84 |
+
Choose a title for your space:
|
85 |
+
</p>
|
86 |
+
<Input
|
87 |
+
type="text"
|
88 |
+
placeholder="My Awesome Website"
|
89 |
+
value={config.title}
|
90 |
+
onChange={(e) => setConfig({ ...config, title: e.target.value })}
|
91 |
+
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
92 |
+
/>
|
93 |
+
</div>
|
94 |
+
<div>
|
95 |
+
<p className="text-sm text-neutral-700 mb-2">
|
96 |
+
Then, let's publish it!
|
97 |
+
</p>
|
98 |
+
<Button
|
99 |
+
variant="black"
|
100 |
+
onClick={createSpace}
|
101 |
+
className="relative w-full"
|
102 |
+
disabled={loading}
|
103 |
+
>
|
104 |
+
Publish Space <Rocket className="size-4" />
|
105 |
+
{loading && <Loading className="ml-2 size-4 animate-spin" />}
|
106 |
+
</Button>
|
107 |
+
</div>
|
108 |
+
</main>
|
109 |
+
</>
|
110 |
+
);
|
111 |
+
};
|
components/editor/deploy-button/index.tsx
CHANGED
@@ -1,24 +1,17 @@
|
|
1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
import { useState } from "react";
|
3 |
-
import { toast } from "sonner";
|
4 |
-
import Image from "next/image";
|
5 |
-
import { useRouter } from "next/navigation";
|
6 |
import { MdSave } from "react-icons/md";
|
7 |
-
import { Rocket } from "lucide-react";
|
8 |
|
9 |
-
import SpaceIcon from "@/assets/space.svg";
|
10 |
-
import Loading from "@/components/loading";
|
11 |
import { Button } from "@/components/ui/button";
|
12 |
import {
|
13 |
Popover,
|
14 |
PopoverContent,
|
15 |
PopoverTrigger,
|
16 |
} from "@/components/ui/popover";
|
17 |
-
import { Input } from "@/components/ui/input";
|
18 |
-
import { api } from "@/lib/api";
|
19 |
import { LoginModal } from "@/components/login-modal";
|
20 |
import { useUser } from "@/hooks/useUser";
|
21 |
import { Page } from "@/types";
|
|
|
22 |
|
23 |
export function DeployButton({
|
24 |
pages,
|
@@ -27,40 +20,9 @@ export function DeployButton({
|
|
27 |
pages: Page[];
|
28 |
prompts: string[];
|
29 |
}) {
|
30 |
-
const router = useRouter();
|
31 |
const { user } = useUser();
|
32 |
-
const [loading, setLoading] = useState(false);
|
33 |
const [open, setOpen] = useState(false);
|
34 |
|
35 |
-
const [config, setConfig] = useState({
|
36 |
-
title: "",
|
37 |
-
});
|
38 |
-
|
39 |
-
const createSpace = async () => {
|
40 |
-
if (!config.title) {
|
41 |
-
toast.error("Please enter a title for your space.");
|
42 |
-
return;
|
43 |
-
}
|
44 |
-
setLoading(true);
|
45 |
-
|
46 |
-
try {
|
47 |
-
const res = await api.post("/me/projects", {
|
48 |
-
title: config.title,
|
49 |
-
pages,
|
50 |
-
prompts,
|
51 |
-
});
|
52 |
-
if (res.data.ok) {
|
53 |
-
router.push(`/projects/${res.data.path}?deploy=true`);
|
54 |
-
} else {
|
55 |
-
toast.error(res?.data?.error || "Failed to create space");
|
56 |
-
}
|
57 |
-
} catch (err: any) {
|
58 |
-
toast.error(err.response?.data?.error || err.message);
|
59 |
-
} finally {
|
60 |
-
setLoading(false);
|
61 |
-
}
|
62 |
-
};
|
63 |
-
|
64 |
return (
|
65 |
<div className="flex items-center justify-end gap-5">
|
66 |
<div className="relative flex items-center justify-end">
|
@@ -81,62 +43,7 @@ export function DeployButton({
|
|
81 |
className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
|
82 |
align="end"
|
83 |
>
|
84 |
-
<
|
85 |
-
<div className="flex items-center justify-center -space-x-4 mb-3">
|
86 |
-
<div className="size-9 rounded-full bg-amber-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
87 |
-
🚀
|
88 |
-
</div>
|
89 |
-
<div className="size-11 rounded-full bg-red-200 shadow-2xl flex items-center justify-center z-2">
|
90 |
-
<Image
|
91 |
-
src={SpaceIcon}
|
92 |
-
alt="Space Icon"
|
93 |
-
className="size-7"
|
94 |
-
/>
|
95 |
-
</div>
|
96 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
97 |
-
👻
|
98 |
-
</div>
|
99 |
-
</div>
|
100 |
-
<p className="text-xl font-semibold text-neutral-950">
|
101 |
-
Publish as Space!
|
102 |
-
</p>
|
103 |
-
<p className="text-sm text-neutral-500 mt-1.5">
|
104 |
-
Save and Publish your project to a Space on the Hub. Spaces
|
105 |
-
are a way to share your project with the world.
|
106 |
-
</p>
|
107 |
-
</header>
|
108 |
-
<main className="space-y-4 p-6">
|
109 |
-
<div>
|
110 |
-
<p className="text-sm text-neutral-700 mb-2">
|
111 |
-
Choose a title for your space:
|
112 |
-
</p>
|
113 |
-
<Input
|
114 |
-
type="text"
|
115 |
-
placeholder="My Awesome Website"
|
116 |
-
value={config.title}
|
117 |
-
onChange={(e) =>
|
118 |
-
setConfig({ ...config, title: e.target.value })
|
119 |
-
}
|
120 |
-
className="!bg-white !border-neutral-300 !text-neutral-800 !placeholder:text-neutral-400 selection:!bg-blue-100"
|
121 |
-
/>
|
122 |
-
</div>
|
123 |
-
<div>
|
124 |
-
<p className="text-sm text-neutral-700 mb-2">
|
125 |
-
Then, let's publish it!
|
126 |
-
</p>
|
127 |
-
<Button
|
128 |
-
variant="black"
|
129 |
-
onClick={createSpace}
|
130 |
-
className="relative w-full"
|
131 |
-
disabled={loading}
|
132 |
-
>
|
133 |
-
Publish Space <Rocket className="size-4" />
|
134 |
-
{loading && (
|
135 |
-
<Loading className="ml-2 size-4 animate-spin" />
|
136 |
-
)}
|
137 |
-
</Button>
|
138 |
-
</div>
|
139 |
-
</main>
|
140 |
</PopoverContent>
|
141 |
</Popover>
|
142 |
) : (
|
|
|
1 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2 |
import { useState } from "react";
|
|
|
|
|
|
|
3 |
import { MdSave } from "react-icons/md";
|
|
|
4 |
|
|
|
|
|
5 |
import { Button } from "@/components/ui/button";
|
6 |
import {
|
7 |
Popover,
|
8 |
PopoverContent,
|
9 |
PopoverTrigger,
|
10 |
} from "@/components/ui/popover";
|
|
|
|
|
11 |
import { LoginModal } from "@/components/login-modal";
|
12 |
import { useUser } from "@/hooks/useUser";
|
13 |
import { Page } from "@/types";
|
14 |
+
import { DeployButtonContent } from "./content";
|
15 |
|
16 |
export function DeployButton({
|
17 |
pages,
|
|
|
20 |
pages: Page[];
|
21 |
prompts: string[];
|
22 |
}) {
|
|
|
23 |
const { user } = useUser();
|
|
|
24 |
const [open, setOpen] = useState(false);
|
25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
return (
|
27 |
<div className="flex items-center justify-end gap-5">
|
28 |
<div className="relative flex items-center justify-end">
|
|
|
43 |
className="!rounded-2xl !p-0 !bg-white !border-neutral-200 min-w-xs text-center overflow-hidden"
|
44 |
align="end"
|
45 |
>
|
46 |
+
<DeployButtonContent pages={pages} prompts={prompts} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
</PopoverContent>
|
48 |
</Popover>
|
49 |
) : (
|
components/editor/footer/index.tsx
CHANGED
@@ -8,6 +8,7 @@ import { MdAdd } from "react-icons/md";
|
|
8 |
import { History } from "@/components/editor/history";
|
9 |
import { UserMenu } from "@/components/user-menu";
|
10 |
import { useUser } from "@/hooks/useUser";
|
|
|
11 |
|
12 |
const DEVICES = [
|
13 |
{
|
@@ -21,14 +22,12 @@ const DEVICES = [
|
|
21 |
];
|
22 |
|
23 |
export function Footer({
|
24 |
-
onReset,
|
25 |
htmlHistory,
|
26 |
setPages,
|
27 |
device,
|
28 |
setDevice,
|
29 |
iframeRef,
|
30 |
}: {
|
31 |
-
onReset: () => void;
|
32 |
htmlHistory?: HtmlHistory[];
|
33 |
device: "desktop" | "mobile";
|
34 |
setPages: (pages: Page[]) => void;
|
@@ -62,10 +61,12 @@ export function Footer({
|
|
62 |
<UserMenu className="!p-1 !pr-3 !h-auto" />
|
63 |
))}
|
64 |
{user && <p className="text-neutral-700">|</p>}
|
65 |
-
<
|
66 |
-
<
|
67 |
-
|
68 |
-
|
|
|
|
|
69 |
{htmlHistory && htmlHistory.length > 0 && (
|
70 |
<>
|
71 |
<p className="text-neutral-700">|</p>
|
|
|
8 |
import { History } from "@/components/editor/history";
|
9 |
import { UserMenu } from "@/components/user-menu";
|
10 |
import { useUser } from "@/hooks/useUser";
|
11 |
+
import Link from "next/link";
|
12 |
|
13 |
const DEVICES = [
|
14 |
{
|
|
|
22 |
];
|
23 |
|
24 |
export function Footer({
|
|
|
25 |
htmlHistory,
|
26 |
setPages,
|
27 |
device,
|
28 |
setDevice,
|
29 |
iframeRef,
|
30 |
}: {
|
|
|
31 |
htmlHistory?: HtmlHistory[];
|
32 |
device: "desktop" | "mobile";
|
33 |
setPages: (pages: Page[]) => void;
|
|
|
61 |
<UserMenu className="!p-1 !pr-3 !h-auto" />
|
62 |
))}
|
63 |
{user && <p className="text-neutral-700">|</p>}
|
64 |
+
<Link href="/projects/new">
|
65 |
+
<Button size="sm" variant="secondary">
|
66 |
+
<MdAdd className="text-sm" />
|
67 |
+
New <span className="max-lg:hidden">Project</span>
|
68 |
+
</Button>
|
69 |
+
</Link>
|
70 |
{htmlHistory && htmlHistory.length > 0 && (
|
71 |
<>
|
72 |
<p className="text-neutral-700">|</p>
|
components/editor/index.tsx
CHANGED
@@ -31,10 +31,12 @@ import { ListPages } from "./pages";
|
|
31 |
export const AppEditor = ({
|
32 |
project,
|
33 |
pages: initialPages,
|
|
|
34 |
isNew,
|
35 |
}: {
|
36 |
project?: Project | null;
|
37 |
pages?: Page[];
|
|
|
38 |
isNew?: boolean;
|
39 |
}) => {
|
40 |
const [htmlStorage, , removeHtmlStorage] = useLocalStorage("pages");
|
@@ -63,6 +65,7 @@ export const AppEditor = ({
|
|
63 |
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
|
64 |
null
|
65 |
);
|
|
|
66 |
|
67 |
const resetLayout = () => {
|
68 |
if (!editor.current || !preview.current) return;
|
@@ -285,6 +288,8 @@ export const AppEditor = ({
|
|
285 |
onValidate={handleEditorValidation}
|
286 |
/>
|
287 |
<AskAI
|
|
|
|
|
288 |
currentPage={currentPageData}
|
289 |
htmlHistory={htmlHistory}
|
290 |
previousPrompts={project?.prompts ?? []}
|
@@ -297,6 +302,7 @@ export const AppEditor = ({
|
|
297 |
});
|
298 |
setHtmlHistory(currentHistory);
|
299 |
setSelectedElement(null);
|
|
|
300 |
// if xs or sm
|
301 |
if (window.innerWidth <= 1024) {
|
302 |
setCurrentTab("preview");
|
@@ -340,6 +346,8 @@ export const AppEditor = ({
|
|
340 |
setIsEditableModeEnabled={setIsEditableModeEnabled}
|
341 |
selectedElement={selectedElement}
|
342 |
setSelectedElement={setSelectedElement}
|
|
|
|
|
343 |
/>
|
344 |
</div>
|
345 |
<div
|
@@ -367,21 +375,6 @@ export const AppEditor = ({
|
|
367 |
/>
|
368 |
</main>
|
369 |
<Footer
|
370 |
-
onReset={() => {
|
371 |
-
if (isAiWorking) {
|
372 |
-
toast.warning("Please wait for the AI to finish working.");
|
373 |
-
return;
|
374 |
-
}
|
375 |
-
if (
|
376 |
-
window.confirm("You're about to reset the editor. Are you sure?")
|
377 |
-
) {
|
378 |
-
// setHtml(defaultHTML);
|
379 |
-
removeHtmlStorage();
|
380 |
-
editorRef.current?.revealLine(
|
381 |
-
editorRef.current?.getModel()?.getLineCount() ?? 0
|
382 |
-
);
|
383 |
-
}
|
384 |
-
}}
|
385 |
htmlHistory={htmlHistory}
|
386 |
setPages={setPages}
|
387 |
iframeRef={iframeRef}
|
|
|
31 |
export const AppEditor = ({
|
32 |
project,
|
33 |
pages: initialPages,
|
34 |
+
images,
|
35 |
isNew,
|
36 |
}: {
|
37 |
project?: Project | null;
|
38 |
pages?: Page[];
|
39 |
+
images?: string[];
|
40 |
isNew?: boolean;
|
41 |
}) => {
|
42 |
const [htmlStorage, , removeHtmlStorage] = useLocalStorage("pages");
|
|
|
65 |
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
|
66 |
null
|
67 |
);
|
68 |
+
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
69 |
|
70 |
const resetLayout = () => {
|
71 |
if (!editor.current || !preview.current) return;
|
|
|
288 |
onValidate={handleEditorValidation}
|
289 |
/>
|
290 |
<AskAI
|
291 |
+
project={project}
|
292 |
+
images={images}
|
293 |
currentPage={currentPageData}
|
294 |
htmlHistory={htmlHistory}
|
295 |
previousPrompts={project?.prompts ?? []}
|
|
|
302 |
});
|
303 |
setHtmlHistory(currentHistory);
|
304 |
setSelectedElement(null);
|
305 |
+
setSelectedFiles([]);
|
306 |
// if xs or sm
|
307 |
if (window.innerWidth <= 1024) {
|
308 |
setCurrentTab("preview");
|
|
|
346 |
setIsEditableModeEnabled={setIsEditableModeEnabled}
|
347 |
selectedElement={selectedElement}
|
348 |
setSelectedElement={setSelectedElement}
|
349 |
+
setSelectedFiles={setSelectedFiles}
|
350 |
+
selectedFiles={selectedFiles}
|
351 |
/>
|
352 |
</div>
|
353 |
<div
|
|
|
375 |
/>
|
376 |
</main>
|
377 |
<Footer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
htmlHistory={htmlHistory}
|
379 |
setPages={setPages}
|
380 |
iframeRef={iframeRef}
|
hooks/useCallAi.ts
CHANGED
@@ -237,7 +237,7 @@ export const useCallAi = ({
|
|
237 |
}
|
238 |
};
|
239 |
|
240 |
-
const callAiFollowUp = async (prompt: string, model: string | undefined, provider: string | undefined, previousPrompt: string, selectedElementHtml?: string) => {
|
241 |
if (isAiWorking) return;
|
242 |
if (!prompt.trim()) return;
|
243 |
|
@@ -258,6 +258,7 @@ export const useCallAi = ({
|
|
258 |
model,
|
259 |
pages,
|
260 |
selectedElementHtml,
|
|
|
261 |
}),
|
262 |
headers: {
|
263 |
"Content-Type": "application/json",
|
|
|
237 |
}
|
238 |
};
|
239 |
|
240 |
+
const callAiFollowUp = async (prompt: string, model: string | undefined, provider: string | undefined, previousPrompt: string, selectedElementHtml?: string, files?: string[]) => {
|
241 |
if (isAiWorking) return;
|
242 |
if (!prompt.trim()) return;
|
243 |
|
|
|
258 |
model,
|
259 |
pages,
|
260 |
selectedElementHtml,
|
261 |
+
files,
|
262 |
}),
|
263 |
headers: {
|
264 |
"Content-Type": "application/json",
|
next.config.ts
CHANGED
@@ -24,6 +24,9 @@ const nextConfig: NextConfig = {
|
|
24 |
|
25 |
return config;
|
26 |
},
|
|
|
|
|
|
|
27 |
};
|
28 |
|
29 |
export default nextConfig;
|
|
|
24 |
|
25 |
return config;
|
26 |
},
|
27 |
+
images: {
|
28 |
+
remotePatterns: [new URL('https://huggingface.co/**')],
|
29 |
+
},
|
30 |
};
|
31 |
|
32 |
export default nextConfig;
|