arkleinberg commited on
Commit
3c2759c
·
verified ·
1 Parent(s): e4fcfd6

Upload 26 files

Browse files
assets/deepseek-color.svg ADDED
assets/index.css ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ * {
4
+ font-family: "Noto Sans";
5
+ }
6
+
7
+ .font-code {
8
+ font-family: "Source Code Pro";
9
+ }
assets/logo.svg ADDED
assets/space.svg ADDED
assets/success.mp3 ADDED
Binary file (49.3 kB). View file
 
components/App.tsx ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState } from "react";
2
+ import Editor from "@monaco-editor/react";
3
+ import classNames from "classnames";
4
+ import { editor } from "monaco-editor";
5
+ import {
6
+ useMount,
7
+ useUnmount,
8
+ useEvent,
9
+ useLocalStorage,
10
+ useSearchParam,
11
+ } from "react-use";
12
+ import { toast } from "react-toastify";
13
+
14
+ import Header from "./header/header";
15
+ import DeployButton from "./deploy-button/deploy-button";
16
+ import { defaultHTML } from "./../../utils/consts";
17
+ import Tabs from "./tabs/tabs";
18
+ import AskAI from "./ask-ai/ask-ai";
19
+ import { Auth } from "./../../utils/types";
20
+ import Preview from "./preview/preview";
21
+
22
+ function App() {
23
+ const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
24
+ const remix = useSearchParam("remix");
25
+
26
+ const preview = useRef<HTMLDivElement>(null);
27
+ const editor = useRef<HTMLDivElement>(null);
28
+ const resizer = useRef<HTMLDivElement>(null);
29
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
30
+
31
+ const [isResizing, setIsResizing] = useState(false);
32
+ const [error, setError] = useState(false);
33
+ const [html, setHtml] = useState((htmlStorage as string) ?? defaultHTML);
34
+ const [isAiWorking, setisAiWorking] = useState(false);
35
+ const [auth, setAuth] = useState<Auth | undefined>(undefined);
36
+ const [currentView, setCurrentView] = useState<"editor" | "preview">(
37
+ "editor"
38
+ );
39
+ const [prompts, setPrompts] = useState<string[]>([]);
40
+
41
+ const fetchMe = async () => {
42
+ const res = await fetch("/api/@me");
43
+ if (res.ok) {
44
+ const data = await res.json();
45
+ setAuth(data);
46
+ } else {
47
+ setAuth(undefined);
48
+ }
49
+ };
50
+
51
+ const fetchRemix = async () => {
52
+ if (!remix) return;
53
+ const res = await fetch(`/api/remix/${remix}`);
54
+ if (res.ok) {
55
+ const data = await res.json();
56
+ if (data.html) {
57
+ setHtml(data.html);
58
+ toast.success("Remix content loaded successfully.");
59
+ }
60
+ } else {
61
+ toast.error("Failed to load remix content.");
62
+ }
63
+ const url = new URL(window.location.href);
64
+ url.searchParams.delete("remix");
65
+ window.history.replaceState({}, document.title, url.toString());
66
+ };
67
+
68
+ /**
69
+ * Resets the layout based on screen size
70
+ * - For desktop: Sets editor to 1/3 width and preview to 2/3
71
+ * - For mobile: Removes inline styles to let CSS handle it
72
+ */
73
+ const resetLayout = () => {
74
+ if (!editor.current || !preview.current) return;
75
+
76
+ // lg breakpoint is 1024px based on useBreakpoint definition and Tailwind defaults
77
+ if (window.innerWidth >= 1024) {
78
+ // Set initial 1/3 - 2/3 sizes for large screens, accounting for resizer width
79
+ const resizerWidth = resizer.current?.offsetWidth ?? 8; // w-2 = 0.5rem = 8px
80
+ const availableWidth = window.innerWidth - resizerWidth;
81
+ const initialEditorWidth = availableWidth / 3; // Editor takes 1/3 of space
82
+ const initialPreviewWidth = availableWidth - initialEditorWidth; // Preview takes 2/3
83
+ editor.current.style.width = `${initialEditorWidth}px`;
84
+ preview.current.style.width = `${initialPreviewWidth}px`;
85
+ } else {
86
+ // Remove inline styles for smaller screens, let CSS flex-col handle it
87
+ editor.current.style.width = "";
88
+ preview.current.style.width = "";
89
+ }
90
+ };
91
+
92
+ /**
93
+ * Handles resizing when the user drags the resizer
94
+ * Ensures minimum widths are maintained for both panels
95
+ */
96
+ const handleResize = (e: MouseEvent) => {
97
+ if (!editor.current || !preview.current || !resizer.current) return;
98
+
99
+ const resizerWidth = resizer.current.offsetWidth;
100
+ const minWidth = 100; // Minimum width for editor/preview
101
+ const maxWidth = window.innerWidth - resizerWidth - minWidth;
102
+
103
+ const editorWidth = e.clientX;
104
+ const clampedEditorWidth = Math.max(
105
+ minWidth,
106
+ Math.min(editorWidth, maxWidth)
107
+ );
108
+ const calculatedPreviewWidth =
109
+ window.innerWidth - clampedEditorWidth - resizerWidth;
110
+
111
+ editor.current.style.width = `${clampedEditorWidth}px`;
112
+ preview.current.style.width = `${calculatedPreviewWidth}px`;
113
+ };
114
+
115
+ const handleMouseDown = () => {
116
+ setIsResizing(true);
117
+ document.addEventListener("mousemove", handleResize);
118
+ document.addEventListener("mouseup", handleMouseUp);
119
+ };
120
+
121
+ const handleMouseUp = () => {
122
+ setIsResizing(false);
123
+ document.removeEventListener("mousemove", handleResize);
124
+ document.removeEventListener("mouseup", handleMouseUp);
125
+ };
126
+
127
+ // Prevent accidental navigation away when AI is working or content has changed
128
+ useEvent("beforeunload", (e) => {
129
+ if (isAiWorking || html !== defaultHTML) {
130
+ e.preventDefault();
131
+ return "";
132
+ }
133
+ });
134
+
135
+ // Initialize component on mount
136
+ useMount(() => {
137
+ // Fetch user data
138
+ fetchMe();
139
+ fetchRemix();
140
+
141
+ // Restore content from storage if available
142
+ if (htmlStorage) {
143
+ removeHtmlStorage();
144
+ toast.warn("Previous HTML content restored from local storage.");
145
+ }
146
+
147
+ // Set initial layout based on window size
148
+ resetLayout();
149
+
150
+ // Attach event listeners
151
+ if (!resizer.current) return;
152
+ resizer.current.addEventListener("mousedown", handleMouseDown);
153
+ window.addEventListener("resize", resetLayout);
154
+ });
155
+
156
+ // Clean up event listeners on unmount
157
+ useUnmount(() => {
158
+ document.removeEventListener("mousemove", handleResize);
159
+ document.removeEventListener("mouseup", handleMouseUp);
160
+ if (resizer.current) {
161
+ resizer.current.removeEventListener("mousedown", handleMouseDown);
162
+ }
163
+ window.removeEventListener("resize", resetLayout);
164
+ });
165
+
166
+ return (
167
+ <div className="h-screen bg-gray-950 font-sans overflow-hidden">
168
+ <Header
169
+ onReset={() => {
170
+ if (isAiWorking) {
171
+ toast.warn("Please wait for the AI to finish working.");
172
+ return;
173
+ }
174
+ if (
175
+ window.confirm("You're about to reset the editor. Are you sure?")
176
+ ) {
177
+ setHtml(defaultHTML);
178
+ setError(false);
179
+ removeHtmlStorage();
180
+ editorRef.current?.revealLine(
181
+ editorRef.current?.getModel()?.getLineCount() ?? 0
182
+ );
183
+ }
184
+ }}
185
+ >
186
+ <DeployButton
187
+ html={html}
188
+ error={error}
189
+ auth={auth}
190
+ setHtml={setHtml}
191
+ prompts={prompts}
192
+ />
193
+ </Header>
194
+ <main className="max-lg:flex-col flex w-full">
195
+ <div
196
+ ref={editor}
197
+ className={classNames(
198
+ "w-full h-[calc(100dvh-49px)] lg:h-[calc(100dvh-54px)] relative overflow-hidden max-lg:transition-all max-lg:duration-200 select-none",
199
+ {
200
+ "max-lg:h-0": currentView === "preview",
201
+ }
202
+ )}
203
+ >
204
+ <Tabs />
205
+ <div
206
+ onClick={(e) => {
207
+ if (isAiWorking) {
208
+ e.preventDefault();
209
+ e.stopPropagation();
210
+ toast.warn("Please wait for the AI to finish working.");
211
+ }
212
+ }}
213
+ >
214
+ <Editor
215
+ language="html"
216
+ theme="vs-dark"
217
+ className={classNames(
218
+ "h-[calc(100dvh-90px)] lg:h-[calc(100dvh-96px)]",
219
+ {
220
+ "pointer-events-none": isAiWorking,
221
+ }
222
+ )}
223
+ value={html}
224
+ onValidate={(markers) => {
225
+ if (markers?.length > 0) {
226
+ setError(true);
227
+ }
228
+ }}
229
+ onChange={(value) => {
230
+ const newValue = value ?? "";
231
+ setHtml(newValue);
232
+ setError(false);
233
+ }}
234
+ onMount={(editor) => (editorRef.current = editor)}
235
+ />
236
+ </div>
237
+ <AskAI
238
+ html={html}
239
+ setHtml={setHtml}
240
+ isAiWorking={isAiWorking}
241
+ setisAiWorking={setisAiWorking}
242
+ setView={setCurrentView}
243
+ onNewPrompt={(prompt) => {
244
+ setPrompts((prev) => [...prev, prompt]);
245
+ }}
246
+ onScrollToBottom={() => {
247
+ editorRef.current?.revealLine(
248
+ editorRef.current?.getModel()?.getLineCount() ?? 0
249
+ );
250
+ }}
251
+ />
252
+ </div>
253
+ <div
254
+ ref={resizer}
255
+ className="bg-gray-700 hover:bg-blue-500 w-2 cursor-col-resize h-[calc(100dvh-53px)] max-lg:hidden"
256
+ />
257
+ <Preview
258
+ html={html}
259
+ isResizing={isResizing}
260
+ isAiWorking={isAiWorking}
261
+ ref={preview}
262
+ setView={setCurrentView}
263
+ />
264
+ </main>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ export default App;
components/ask-ai/ask-ai.tsx ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState } from "react";
3
+ import { RiSparkling2Fill } from "react-icons/ri";
4
+ import { GrSend } from "react-icons/gr";
5
+ import classNames from "classnames";
6
+ import { toast } from "react-toastify";
7
+ import { useLocalStorage } from "react-use";
8
+ import { MdPreview } from "react-icons/md";
9
+
10
+ import Login from "../login/login";
11
+ import { defaultHTML } from "./../../../utils/consts";
12
+ import SuccessSound from "./../../assets/success.mp3";
13
+ import Settings from "../settings/settings";
14
+ import ProModal from "../pro-modal/pro-modal";
15
+ // import SpeechPrompt from "../speech-prompt/speech-prompt";
16
+
17
+ function AskAI({
18
+ html,
19
+ setHtml,
20
+ onScrollToBottom,
21
+ isAiWorking,
22
+ setisAiWorking,
23
+ setView,
24
+ onNewPrompt,
25
+ }: {
26
+ html: string;
27
+ setHtml: (html: string) => void;
28
+ onScrollToBottom: () => void;
29
+ isAiWorking: boolean;
30
+ onNewPrompt: (prompt: string) => void;
31
+ setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
32
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
33
+ }) {
34
+ const [open, setOpen] = useState(false);
35
+ const [prompt, setPrompt] = useState("");
36
+ const [hasAsked, setHasAsked] = useState(false);
37
+ const [previousPrompt, setPreviousPrompt] = useState("");
38
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
39
+ const [openProvider, setOpenProvider] = useState(false);
40
+ const [providerError, setProviderError] = useState("");
41
+ const [openProModal, setOpenProModal] = useState(false);
42
+
43
+ const audio = new Audio(SuccessSound);
44
+ audio.volume = 0.5;
45
+
46
+ const callAi = async () => {
47
+ if (isAiWorking || !prompt.trim()) return;
48
+ setisAiWorking(true);
49
+ setProviderError("");
50
+
51
+ let contentResponse = "";
52
+ let lastRenderTime = 0;
53
+ try {
54
+ onNewPrompt(prompt);
55
+ const request = await fetch("/api/ask-ai", {
56
+ method: "POST",
57
+ body: JSON.stringify({
58
+ prompt,
59
+ provider,
60
+ ...(html === defaultHTML ? {} : { html }),
61
+ ...(previousPrompt ? { previousPrompt } : {}),
62
+ }),
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ },
66
+ });
67
+ if (request && request.body) {
68
+ if (!request.ok) {
69
+ const res = await request.json();
70
+ if (res.openLogin) {
71
+ setOpen(true);
72
+ } else if (res.openSelectProvider) {
73
+ setOpenProvider(true);
74
+ setProviderError(res.message);
75
+ } else if (res.openProModal) {
76
+ setOpenProModal(true);
77
+ } else {
78
+ toast.error(res.message);
79
+ }
80
+ setisAiWorking(false);
81
+ return;
82
+ }
83
+ const reader = request.body.getReader();
84
+ const decoder = new TextDecoder("utf-8");
85
+
86
+ const read = async () => {
87
+ const { done, value } = await reader.read();
88
+ if (done) {
89
+ toast.success("AI responded successfully");
90
+ setPrompt("");
91
+ setPreviousPrompt(prompt);
92
+ setisAiWorking(false);
93
+ setHasAsked(true);
94
+ audio.play();
95
+ setView("preview");
96
+
97
+ // Now we have the complete HTML including </html>, so set it to be sure
98
+ const finalDoc = contentResponse.match(
99
+ /<!DOCTYPE html>[\s\S]*<\/html>/
100
+ )?.[0];
101
+ if (finalDoc) {
102
+ setHtml(finalDoc);
103
+ }
104
+
105
+ return;
106
+ }
107
+
108
+ const chunk = decoder.decode(value, { stream: true });
109
+ contentResponse += chunk;
110
+ const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
111
+ if (newHtml) {
112
+ // Force-close the HTML tag so the iframe doesn't render half-finished markup
113
+ let partialDoc = newHtml;
114
+ if (!partialDoc.includes("</html>")) {
115
+ partialDoc += "\n</html>";
116
+ }
117
+
118
+ // Throttle the re-renders to avoid flashing/flicker
119
+ const now = Date.now();
120
+ if (now - lastRenderTime > 300) {
121
+ setHtml(partialDoc);
122
+ lastRenderTime = now;
123
+ }
124
+
125
+ if (partialDoc.length > 200) {
126
+ onScrollToBottom();
127
+ }
128
+ }
129
+ read();
130
+ };
131
+
132
+ read();
133
+ }
134
+
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ } catch (error: any) {
137
+ setisAiWorking(false);
138
+ toast.error(error.message);
139
+ if (error.openLogin) {
140
+ setOpen(true);
141
+ }
142
+ }
143
+ };
144
+
145
+ return (
146
+ <div
147
+ className={`bg-gray-950 rounded-xl py-2 lg:py-2.5 pl-3.5 lg:pl-4 pr-2 lg:pr-2.5 absolute lg:sticky bottom-3 left-3 lg:bottom-4 lg:left-4 w-[calc(100%-1.5rem)] lg:w-[calc(100%-2rem)] z-10 group ${
148
+ isAiWorking ? "animate-pulse" : ""
149
+ }`}
150
+ >
151
+ {defaultHTML !== html && (
152
+ <button
153
+ className="bg-white lg:hidden -translate-y-[calc(100%+8px)] absolute left-0 top-0 shadow-md text-gray-950 text-xs font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-100 hover:brightness-150 transition-all duration-100 cursor-pointer"
154
+ onClick={() => setView("preview")}
155
+ >
156
+ <MdPreview className="text-sm" />
157
+ View Preview
158
+ </button>
159
+ )}
160
+ <div className="w-full relative flex items-center justify-between">
161
+ <RiSparkling2Fill className="text-lg lg:text-xl text-gray-500 group-focus-within:text-pink-500" />
162
+ <input
163
+ type="text"
164
+ disabled={isAiWorking}
165
+ className="w-full bg-transparent max-lg:text-sm outline-none px-3 text-white placeholder:text-gray-500 font-code"
166
+ placeholder={
167
+ hasAsked ? "What do you want to ask AI next?" : "Ask AI anything..."
168
+ }
169
+ value={prompt}
170
+ onChange={(e) => setPrompt(e.target.value)}
171
+ onKeyDown={(e) => {
172
+ if (e.key === "Enter") {
173
+ callAi();
174
+ }
175
+ }}
176
+ />
177
+ <div className="flex items-center justify-end gap-2">
178
+ {/* <SpeechPrompt setPrompt={setPrompt} /> */}
179
+ <Settings
180
+ provider={provider as string}
181
+ onChange={setProvider}
182
+ open={openProvider}
183
+ error={providerError}
184
+ onClose={setOpenProvider}
185
+ />
186
+ <button
187
+ disabled={isAiWorking}
188
+ className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-pink-500 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
189
+ onClick={callAi}
190
+ >
191
+ <GrSend className="-translate-x-[1px]" />
192
+ </button>
193
+ </div>
194
+ </div>
195
+ <div
196
+ className={classNames(
197
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
198
+ {
199
+ "opacity-0 pointer-events-none": !open,
200
+ }
201
+ )}
202
+ onClick={() => setOpen(false)}
203
+ ></div>
204
+ <div
205
+ className={classNames(
206
+ "absolute top-0 -translate-y-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
207
+ {
208
+ "opacity-0 pointer-events-none": !open,
209
+ }
210
+ )}
211
+ >
212
+ <Login html={html}>
213
+ <p className="text-gray-500 text-sm mb-3">
214
+ You reached the limit of free AI usage. Please login to continue.
215
+ </p>
216
+ </Login>
217
+ </div>
218
+ <ProModal
219
+ html={html}
220
+ open={openProModal}
221
+ onClose={() => setOpenProModal(false)}
222
+ />
223
+ </div>
224
+ );
225
+ }
226
+
227
+ export default AskAI;
components/deploy-button/deploy-button.tsx ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState } from "react";
3
+ import classNames from "classnames";
4
+ import { toast } from "react-toastify";
5
+ import { FaPowerOff } from "react-icons/fa6";
6
+
7
+ import SpaceIcon from "@/assets/space.svg";
8
+ import Loading from "../loading/loading";
9
+ import Login from "../login/login";
10
+ import { Auth } from "./../../../utils/types";
11
+ import LoadButton from "../load-button/load-button";
12
+
13
+ const MsgToast = ({ url }: { url: string }) => (
14
+ <div className="w-full flex items-center justify-center gap-3">
15
+ Your space is live!
16
+ <button
17
+ className="bg-black text-sm block text-white rounded-md px-3 py-1.5 hover:bg-gray-900 cursor-pointer"
18
+ onClick={() => {
19
+ window.open(url, "_blank");
20
+ }}
21
+ >
22
+ See Space
23
+ </button>
24
+ </div>
25
+ );
26
+
27
+ function DeployButton({
28
+ html,
29
+ error = false,
30
+ auth,
31
+ setHtml,
32
+ prompts,
33
+ }: {
34
+ html: string;
35
+ error: boolean;
36
+ auth?: Auth;
37
+ setHtml: (html: string) => void;
38
+ prompts: string[];
39
+ }) {
40
+ const [open, setOpen] = useState(false);
41
+ const [loading, setLoading] = useState(false);
42
+ const [path, setPath] = useState<string | undefined>(undefined);
43
+
44
+ const [config, setConfig] = useState({
45
+ title: "",
46
+ });
47
+
48
+ const createSpace = async () => {
49
+ setLoading(true);
50
+
51
+ try {
52
+ const request = await fetch("/api/deploy", {
53
+ method: "POST",
54
+ body: JSON.stringify({
55
+ title: config.title,
56
+ path,
57
+ html,
58
+ prompts,
59
+ }),
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ },
63
+ });
64
+ const response = await request.json();
65
+ if (response.ok) {
66
+ toast.success(
67
+ <MsgToast
68
+ url={`https://huggingface.co/spaces/${response.path ?? path}`}
69
+ />,
70
+ {
71
+ autoClose: 10000,
72
+ }
73
+ );
74
+ setPath(response.path);
75
+ } else {
76
+ toast.error(response.message);
77
+ }
78
+ } catch (err: any) {
79
+ toast.error(err.message);
80
+ } finally {
81
+ setLoading(false);
82
+ setOpen(false);
83
+ }
84
+ };
85
+
86
+ return (
87
+ <div className="flex items-center justify-end gap-5">
88
+ <LoadButton auth={auth} setHtml={setHtml} setPath={setPath} />
89
+ <div className="relative flex items-center justify-end">
90
+ {auth &&
91
+ (auth.isLocalUse ? (
92
+ <>
93
+ <div className="bg-amber-500/10 border border-amber-10 text-amber-500 font-semibold leading-5 lg:leading-6 py-1 px-5 text-xs lg:text-sm rounded-md mr-4 select-none">
94
+ Local Usage
95
+ </div>
96
+ </>
97
+ ) : (
98
+ <>
99
+ <button
100
+ className="mr-2 cursor-pointer"
101
+ onClick={() => {
102
+ if (confirm("Are you sure you want to log out?")) {
103
+ // go to /auth/logout page
104
+ window.location.href = "/auth/logout";
105
+ }
106
+ }}
107
+ >
108
+ <FaPowerOff className="text-lg text-red-500" />
109
+ </button>
110
+ <p className="mr-3 text-xs lg:text-sm text-gray-300">
111
+ <span className="max-lg:hidden">Connected as </span>
112
+ <a
113
+ href={`https://huggingface.co/${auth.preferred_username}`}
114
+ target="_blank"
115
+ className="underline hover:text-white"
116
+ >
117
+ {auth.preferred_username}
118
+ </a>
119
+ </p>
120
+ </>
121
+ ))}
122
+ <button
123
+ className={classNames(
124
+ "relative cursor-pointer flex-none flex items-center justify-center rounded-md text-xs lg:text-sm font-semibold leading-5 lg:leading-6 py-1.5 px-5 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20",
125
+ {
126
+ "bg-pink-400": open,
127
+ "bg-pink-500": !open,
128
+ }
129
+ )}
130
+ onClick={() => setOpen(!open)}
131
+ >
132
+ {path ? "Update Space" : "Deploy to Space"}
133
+ </button>
134
+ <div
135
+ className={classNames(
136
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
137
+ {
138
+ "opacity-0 pointer-events-none": !open,
139
+ }
140
+ )}
141
+ onClick={() => setOpen(false)}
142
+ ></div>
143
+ <div
144
+ className={classNames(
145
+ "absolute top-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
146
+ {
147
+ "opacity-0 pointer-events-none": !open,
148
+ }
149
+ )}
150
+ >
151
+ {!auth ? (
152
+ <Login html={html}>
153
+ <p className="text-gray-500 text-sm mb-3">
154
+ Host this project for free and share it with your friends.
155
+ </p>
156
+ </Login>
157
+ ) : (
158
+ <>
159
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
160
+ <span className="text-xs bg-pink-500/10 text-pink-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
161
+ <img src={SpaceIcon} alt="Space Icon" className="size-4" />
162
+ Space
163
+ </span>
164
+ Configure Deployment
165
+ </header>
166
+ <main className="px-4 pt-3 pb-4 space-y-3">
167
+ <p className="text-xs text-amber-600 bg-amber-500/10 rounded-md p-2">
168
+ {path ? (
169
+ <span>
170
+ Your space is live at{" "}
171
+ <a
172
+ href={`https://huggingface.co/spaces/${path}`}
173
+ target="_blank"
174
+ className="underline hover:text-amber-700"
175
+ >
176
+ huggingface.co/{path}
177
+ </a>
178
+ . You can update it by deploying again.
179
+ </span>
180
+ ) : (
181
+ "Deploy your project to a space on the Hub. Spaces are a way to share your project with the world."
182
+ )}
183
+ </p>
184
+ {!path && (
185
+ <label className="block">
186
+ <p className="text-gray-600 text-sm font-medium mb-1.5">
187
+ Space Title
188
+ </p>
189
+ <input
190
+ type="text"
191
+ value={config.title}
192
+ className="mr-2 border rounded-md px-3 py-1.5 border-gray-300 w-full text-sm"
193
+ placeholder="My Awesome Space"
194
+ onChange={(e) =>
195
+ setConfig({ ...config, title: e.target.value })
196
+ }
197
+ />
198
+ </label>
199
+ )}
200
+ {error && (
201
+ <p className="text-red-500 text-xs bg-red-500/10 rounded-md p-2">
202
+ Your code has errors. Fix them before deploying.
203
+ </p>
204
+ )}
205
+ <div className="pt-2 text-right">
206
+ <button
207
+ disabled={error || loading || (!path && !config.title)}
208
+ className="relative rounded-full bg-black px-5 py-2 text-white font-semibold text-xs hover:bg-black/90 transition-all duration-100 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
209
+ onClick={createSpace}
210
+ >
211
+ {path ? "Update Space" : "Create Space"}
212
+ {loading && <Loading />}
213
+ </button>
214
+ </div>
215
+ </main>
216
+ </>
217
+ )}
218
+ </div>
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ export default DeployButton;
components/header/header.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ReactNode } from "react";
2
+ import { MdAdd } from "react-icons/md";
3
+
4
+ import Logo from "@/assets/logo.svg";
5
+
6
+ function Header({
7
+ onReset,
8
+ children,
9
+ }: {
10
+ onReset: () => void;
11
+ children?: ReactNode;
12
+ }) {
13
+ return (
14
+ <header className="border-b border-gray-900 bg-gray-950 px-3 lg:px-6 py-2 flex justify-between items-center sticky top-0 z-20">
15
+ <div className="flex items-center justify-start gap-3">
16
+ <h1 className="text-white text-lg lg:text-xl font-bold flex items-center justify-start">
17
+ <img
18
+ src={Logo}
19
+ alt="DeepSite Logo"
20
+ className="size-6 lg:size-8 mr-2"
21
+ />
22
+ DeepSite
23
+ </h1>
24
+ <p className="text-gray-700 max-md:hidden">|</p>
25
+ <button
26
+ className="max-md:hidden relative cursor-pointer flex-none flex items-center justify-center rounded-md text-xs font-semibold leading-4 py-1.5 px-3 hover:bg-gray-700 text-gray-100 shadow-sm dark:shadow-highlight/20 bg-gray-800"
27
+ onClick={onReset}
28
+ >
29
+ <MdAdd className="mr-1 text-base" />
30
+ New
31
+ </button>
32
+ <p className="text-gray-500 text-sm max-md:hidden">
33
+ Imagine and Share in 1-Click
34
+ </p>
35
+ </div>
36
+ {children}
37
+ </header>
38
+ );
39
+ }
40
+
41
+ export default Header;
components/load-button/load-button.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useState } from "react";
3
+ import { toast } from "react-toastify";
4
+
5
+ import SpaceIcon from "@/assets/space.svg";
6
+ import Loading from "../loading/loading";
7
+ import { Auth } from "../../../utils/types";
8
+
9
+ function LoadButton({
10
+ auth,
11
+ setHtml,
12
+ setPath,
13
+ }: {
14
+ auth?: Auth;
15
+ setHtml: (html: string) => void;
16
+ setPath: (path?: string) => void;
17
+ }) {
18
+ const [open, setOpen] = useState(false);
19
+ const [loading, setLoading] = useState(false);
20
+ const [error, setError] = useState(false);
21
+ const [url, setUrl] = useState<string | undefined>(undefined);
22
+
23
+ const loadSpace = async () => {
24
+ setLoading(true);
25
+ try {
26
+ const res = await fetch(`/api/remix/${url}`);
27
+ const data = await res.json();
28
+ if (res.ok) {
29
+ if (data.html) {
30
+ setHtml(data.html);
31
+ toast.success("Space loaded successfully.");
32
+ }
33
+ if (data.isOwner) {
34
+ setPath(data.path);
35
+ } else {
36
+ setPath(undefined);
37
+ }
38
+ setOpen(false);
39
+ } else {
40
+ toast.error(data.message);
41
+ setError(data.message);
42
+ }
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ } catch (error: any) {
45
+ toast.error(error.message);
46
+ setError(error.message);
47
+ }
48
+ setLoading(false);
49
+ };
50
+
51
+ return (
52
+ <div
53
+ className={classNames("max-md:hidden", {
54
+ "border-r border-gray-700 pr-5": auth,
55
+ })}
56
+ >
57
+ <p
58
+ className="underline hover:text-white cursor-pointer text-xs lg:text-sm text-gray-300"
59
+ onClick={() => setOpen(!open)}
60
+ >
61
+ Load Space
62
+ </p>
63
+ <div
64
+ className={classNames(
65
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
66
+ {
67
+ "opacity-0 pointer-events-none": !open,
68
+ }
69
+ )}
70
+ onClick={() => setOpen(false)}
71
+ ></div>
72
+ <div
73
+ className={classNames(
74
+ "absolute top-[calc(100%+8px)] right-2 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
75
+ {
76
+ "opacity-0 pointer-events-none": !open,
77
+ }
78
+ )}
79
+ >
80
+ <>
81
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
82
+ <span className="text-xs bg-pink-500/10 text-pink-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
83
+ <img src={SpaceIcon} alt="Space Icon" className="size-4" />
84
+ Space
85
+ </span>
86
+ Load Project
87
+ </header>
88
+ <main className="px-4 pt-3 pb-4 space-y-3">
89
+ <p className="text-sm text-pink-600 bg-pink-100 rounded-md px-3 py-2">
90
+ Load an existing DeepSite Space to continue working on it.
91
+ </p>
92
+ <label className="block">
93
+ <p className="text-gray-600 text-sm font-medium mb-1.5">
94
+ Space URL
95
+ </p>
96
+ <input
97
+ type="text"
98
+ value={url}
99
+ className="mr-2 border rounded-md px-3 py-1.5 border-gray-300 w-full text-sm"
100
+ placeholder="https://huggingface.co/spaces/username/space-name"
101
+ onChange={(e) => setUrl(e.target.value)}
102
+ onFocus={() => setError(false)}
103
+ onBlur={(e) => {
104
+ const pathParts = e.target.value.split("/");
105
+ setUrl(
106
+ `${pathParts[pathParts.length - 2]}/${
107
+ pathParts[pathParts.length - 1]
108
+ }`
109
+ );
110
+ setError(false);
111
+ }}
112
+ />
113
+ </label>
114
+ {error && (
115
+ <p className="text-red-500 text-xs bg-red-500/10 rounded-md p-2 break-all">
116
+ {error}
117
+ </p>
118
+ )}
119
+ <div className="pt-2 text-right">
120
+ <button
121
+ disabled={error || loading || !url}
122
+ className="relative rounded-full bg-black px-5 py-2 text-white font-semibold text-xs hover:bg-black/90 transition-all duration-100 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
123
+ onClick={loadSpace}
124
+ >
125
+ Load Space
126
+ {loading && <Loading />}
127
+ </button>
128
+ </div>
129
+ </main>
130
+ </>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+ export default LoadButton;
components/loading/loading.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Loading() {
2
+ return (
3
+ <div className="absolute left-0 top-0 h-full w-full flex items-center justify-center bg-white/30 z-20">
4
+ <svg
5
+ className="size-5 animate-spin text-white"
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ fill="none"
8
+ viewBox="0 0 24 24"
9
+ >
10
+ <circle
11
+ className="opacity-25"
12
+ cx="12"
13
+ cy="12"
14
+ r="10"
15
+ stroke="currentColor"
16
+ strokeWidth="4"
17
+ ></circle>
18
+ <path
19
+ className="opacity-75"
20
+ fill="currentColor"
21
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
22
+ ></path>
23
+ </svg>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export default Loading;
components/login/login.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useLocalStorage } from "react-use";
2
+ import { defaultHTML } from "./../../../utils/consts";
3
+
4
+ function Login({
5
+ html,
6
+ children,
7
+ }: {
8
+ html?: string;
9
+ children?: React.ReactNode;
10
+ }) {
11
+ const [, setStorage] = useLocalStorage("html_content");
12
+
13
+ const handleClick = async () => {
14
+ if (html !== defaultHTML) {
15
+ setStorage(html);
16
+ }
17
+ const request = await fetch("/api/login");
18
+ const res = await request.json();
19
+ if (res?.redirectUrl) {
20
+ window.open(res.redirectUrl, "_blank");
21
+ }
22
+ };
23
+
24
+ return (
25
+ <>
26
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
27
+ <span className="text-xs bg-red-500/10 text-red-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
28
+ REQUIRED
29
+ </span>
30
+ Login with Hugging Face
31
+ </header>
32
+ <main className="px-4 py-4 space-y-3">
33
+ {children}
34
+ <button onClick={handleClick}>
35
+ <img
36
+ src="https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-lg-dark.svg"
37
+ alt="Sign in with Hugging Face"
38
+ className="mx-auto"
39
+ />
40
+ </button>
41
+ </main>
42
+ </>
43
+ );
44
+ }
45
+
46
+ export default Login;
components/preview/preview.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useRef } from "react";
3
+ import { TbReload } from "react-icons/tb";
4
+ import { toast } from "react-toastify";
5
+ import { FaLaptopCode } from "react-icons/fa6";
6
+ import { defaultHTML } from "../../../utils/consts";
7
+
8
+ function Preview({
9
+ html,
10
+ isResizing,
11
+ isAiWorking,
12
+ setView,
13
+ ref,
14
+ }: {
15
+ html: string;
16
+ isResizing: boolean;
17
+ isAiWorking: boolean;
18
+ setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
19
+ ref: React.RefObject<HTMLDivElement | null>;
20
+ }) {
21
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
22
+
23
+ const handleRefreshIframe = () => {
24
+ if (iframeRef.current) {
25
+ const iframe = iframeRef.current;
26
+ const content = iframe.srcdoc;
27
+ iframe.srcdoc = "";
28
+ setTimeout(() => {
29
+ iframe.srcdoc = content;
30
+ }, 10);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div
36
+ ref={ref}
37
+ className="w-full border-l border-gray-900 bg-white h-[calc(100dvh-49px)] lg:h-[calc(100dvh-53px)] relative"
38
+ onClick={(e) => {
39
+ if (isAiWorking) {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ toast.warn("Please wait for the AI to finish working.");
43
+ }
44
+ }}
45
+ >
46
+ <iframe
47
+ ref={iframeRef}
48
+ title="output"
49
+ className={classNames("w-full h-full select-none", {
50
+ "pointer-events-none": isResizing || isAiWorking,
51
+ })}
52
+ srcDoc={html}
53
+ />
54
+ <div className="flex items-center justify-start gap-3 absolute bottom-3 lg:bottom-5 max-lg:left-3 lg:right-5">
55
+ <button
56
+ className="lg:hidden bg-gray-950 shadow-md text-white text-xs lg:text-sm font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-900 hover:brightness-150 transition-all duration-100 cursor-pointer"
57
+ onClick={() => setView("editor")}
58
+ >
59
+ <FaLaptopCode className="text-sm" />
60
+ Hide preview
61
+ </button>
62
+ {html === defaultHTML && (
63
+ <a
64
+ href="https://huggingface.co/spaces/victor/deepsite-gallery"
65
+ target="_blank"
66
+ className="bg-gray-200 text-gray-950 text-xs lg:text-sm font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-200 hover:bg-gray-300 transition-all duration-100 cursor-pointer"
67
+ >
68
+ 🖼️ <span>DeepSite Gallery</span>
69
+ </a>
70
+ )}
71
+ {!isAiWorking && (
72
+ <button
73
+ className="bg-white lg:bg-gray-950 shadow-md text-gray-950 lg:text-white text-xs lg:text-sm font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-100 lg:border-gray-900 hover:brightness-150 transition-all duration-100 cursor-pointer"
74
+ onClick={handleRefreshIframe}
75
+ >
76
+ <TbReload className="text-sm" />
77
+ Refresh Preview
78
+ </button>
79
+ )}
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ export default Preview;
components/pro-modal/pro-modal.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useLocalStorage } from "react-use";
3
+ import { defaultHTML } from "../../../utils/consts";
4
+
5
+ function ProModal({
6
+ open,
7
+ html,
8
+ onClose,
9
+ }: {
10
+ open: boolean;
11
+ html: string;
12
+ onClose: React.Dispatch<React.SetStateAction<boolean>>;
13
+ }) {
14
+ const [, setStorage] = useLocalStorage("html_content");
15
+
16
+ const handleProClick = () => {
17
+ if (html !== defaultHTML) {
18
+ setStorage(html);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <>
24
+ <div
25
+ className={classNames(
26
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-40",
27
+ {
28
+ "opacity-0 pointer-events-none": !open,
29
+ }
30
+ )}
31
+ onClick={() => onClose(false)}
32
+ ></div>
33
+ <div
34
+ className={classNames(
35
+ "absolute top-0 -translate-y-[calc(100%+16px)] right-0 z-40 w-96 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
36
+ {
37
+ "opacity-0 pointer-events-none": !open,
38
+ }
39
+ )}
40
+ >
41
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
42
+ <span className="bg-linear-to-br shadow-green-500/10 dark:shadow-green-500/20 inline-block -skew-x-12 border border-gray-200 from-pink-300 via-green-200 to-yellow-200 text-xs font-bold text-black shadow-lg dark:from-pink-500 dark:via-green-500 dark:to-yellow-500 dark:text-black rounded-lg px-2.5 py-0.5 ">
43
+ PRO
44
+ </span>
45
+ Your free inference quota is exhausted
46
+ </header>
47
+ <main className="px-4 pt-3 pb-4">
48
+ <p className="text-gray-950 text-sm font-semibold flex items-center justify-between">
49
+ Upgrade to a PRO account to activate Inference Providers and
50
+ continue using DeepSite.
51
+ </p>
52
+ <p className="text-sm text-pretty text-gray-500 mt-2">
53
+ It also unlocks thousands of Space apps powered by ZeroGPU for 3d,
54
+ audio, music, video and more!
55
+ </p>
56
+ <a
57
+ href="https://huggingface.co/subscribe/pro"
58
+ target="_blank"
59
+ className="mt-4 bg-black text-white cursor-pointer rounded-full py-2 px-3 text-sm font-medium w-full block text-center hover:bg-gray-800 transition duration-200 ease-in-out"
60
+ onClick={handleProClick}
61
+ >
62
+ Subscribe to PRO ($9/month)
63
+ </a>
64
+ </main>
65
+ </div>
66
+ </>
67
+ );
68
+ }
69
+
70
+ export default ProModal;
components/settings/settings.tsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import classNames from "classnames";
3
+
4
+ import { PiGearSixFill } from "react-icons/pi";
5
+ // @ts-expect-error not needed
6
+ import { PROVIDERS } from "./../../../utils/providers";
7
+
8
+ function Settings({
9
+ open,
10
+ onClose,
11
+ provider,
12
+ error,
13
+ onChange,
14
+ }: {
15
+ open: boolean;
16
+ provider: string;
17
+ error?: string;
18
+ onClose: React.Dispatch<React.SetStateAction<boolean>>;
19
+ onChange: (provider: string) => void;
20
+ }) {
21
+ return (
22
+ <div className="">
23
+ <button
24
+ className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-base font-semibold size-8 text-center bg-gray-800 hover:bg-gray-700 text-gray-100 shadow-sm dark:shadow-highlight/20"
25
+ onClick={() => {
26
+ onClose((prev) => !prev);
27
+ }}
28
+ >
29
+ <PiGearSixFill />
30
+ </button>
31
+ <div
32
+ className={classNames(
33
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-40",
34
+ {
35
+ "opacity-0 pointer-events-none": !open,
36
+ }
37
+ )}
38
+ onClick={() => onClose(false)}
39
+ ></div>
40
+ <div
41
+ className={classNames(
42
+ "absolute top-0 -translate-y-[calc(100%+16px)] right-0 z-40 w-96 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
43
+ {
44
+ "opacity-0 pointer-events-none": !open,
45
+ }
46
+ )}
47
+ >
48
+ <header className="flex items-center text-sm px-4 py-2 border-b border-gray-200 gap-2 bg-gray-100 font-semibold text-gray-700">
49
+ <span className="text-xs bg-blue-500/10 text-blue-500 rounded-full pl-1.5 pr-2.5 py-0.5 flex items-center justify-start gap-1.5">
50
+ Provider
51
+ </span>
52
+ Customize Settings
53
+ </header>
54
+ <main className="px-4 pt-3 pb-4 space-y-4">
55
+ {/* toggle using tailwind css */}
56
+ <div>
57
+ <a
58
+ href="https://huggingface.co/spaces/enzostvs/deepsite/discussions/74"
59
+ target="_blank"
60
+ className="w-full flex items-center justify-between text-gray-600 bg-gray-50 border border-gray-100 px-2 py-2 rounded-lg mb-3 text-sm font-medium hover:brightness-95"
61
+ >
62
+ How to use it locally?
63
+ <button className="bg-black text-white rounded-md px-3 py-1.5 text-xs font-semibold cursor-pointer">
64
+ See the guide
65
+ </button>
66
+ </a>
67
+ <div className="flex items-center justify-between">
68
+ <p className="text-gray-800 text-sm font-medium flex items-center justify-between">
69
+ Use auto-provider
70
+ </p>
71
+ <div
72
+ className={classNames(
73
+ "bg-gray-200 rounded-full w-10 h-6 flex items-center justify-between p-1 cursor-pointer transition-all duration-200",
74
+ {
75
+ "!bg-blue-500": provider === "auto",
76
+ }
77
+ )}
78
+ onClick={() => {
79
+ onChange(provider === "auto" ? "fireworks-ai" : "auto");
80
+ }}
81
+ >
82
+ <div
83
+ className={classNames(
84
+ "w-4 h-4 rounded-full shadow-md transition-all duration-200 bg-white",
85
+ {
86
+ "translate-x-4": provider === "auto",
87
+ }
88
+ )}
89
+ />
90
+ </div>
91
+ </div>
92
+ <p className="text-xs text-gray-500 mt-2">
93
+ We'll automatically select the best provider for you based on your
94
+ prompt.
95
+ </p>
96
+ </div>
97
+ {error !== "" && (
98
+ <p className="text-red-500 text-sm font-medium mb-2 flex items-center justify-between bg-red-500/10 p-2 rounded-md">
99
+ {error}
100
+ </p>
101
+ )}
102
+ <label className="block">
103
+ <p className="text-gray-800 text-sm font-medium mb-2 flex items-center justify-between">
104
+ Inference Provider
105
+ </p>
106
+ <div className="grid grid-cols-2 gap-1.5">
107
+ {Object.keys(PROVIDERS).map((id: string) => (
108
+ <div
109
+ key={id}
110
+ className={classNames(
111
+ "text-gray-600 text-sm font-medium cursor-pointer border p-2 rounded-md flex items-center justify-start gap-2",
112
+ {
113
+ "bg-blue-500/10 border-blue-500/15 text-blue-500":
114
+ id === provider,
115
+ "hover:bg-gray-100 border-gray-100": id !== provider,
116
+ }
117
+ )}
118
+ onClick={() => {
119
+ onChange(id);
120
+ }}
121
+ >
122
+ <img
123
+ src={`/providers/${id}.svg`}
124
+ alt={PROVIDERS[id].name}
125
+ className="size-5"
126
+ />
127
+ {PROVIDERS[id].name}
128
+ </div>
129
+ ))}
130
+ </div>
131
+ </label>
132
+ </main>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+ export default Settings;
components/speech-prompt/speech-prompt.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { FaMicrophone } from "react-icons/fa";
3
+ import SpeechRecognition, {
4
+ useSpeechRecognition,
5
+ } from "react-speech-recognition";
6
+ import { useUpdateEffect } from "react-use";
7
+
8
+ function SpeechPrompt({
9
+ setPrompt,
10
+ }: {
11
+ setPrompt: React.Dispatch<React.SetStateAction<string>>;
12
+ }) {
13
+ const {
14
+ transcript,
15
+ listening,
16
+ browserSupportsSpeechRecognition,
17
+ resetTranscript,
18
+ } = useSpeechRecognition();
19
+
20
+ const startListening = () =>
21
+ SpeechRecognition.startListening({ continuous: true });
22
+
23
+ useUpdateEffect(() => {
24
+ if (transcript) setPrompt(transcript);
25
+ }, [transcript]);
26
+
27
+ useUpdateEffect(() => {
28
+ if (!listening) resetTranscript();
29
+ }, [listening]);
30
+
31
+ if (!browserSupportsSpeechRecognition) {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <button
37
+ className={classNames(
38
+ "flex cursor-pointer flex-none items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-gray-800 hover:bg-gray-700 text-white shadow-sm dark:shadow-highlight/20 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300",
39
+ {
40
+ "animate-pulse !bg-orange-500": listening,
41
+ }
42
+ )}
43
+ onTouchStart={startListening}
44
+ onMouseDown={startListening}
45
+ onTouchEnd={SpeechRecognition.stopListening}
46
+ onMouseUp={SpeechRecognition.stopListening}
47
+ >
48
+ <FaMicrophone className="text-base" />
49
+ </button>
50
+ );
51
+ }
52
+
53
+ export default SpeechPrompt;
components/tabs/tabs.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Deepseek from "./../../assets/deepseek-color.svg";
2
+
3
+ function Tabs({ children }: { children?: React.ReactNode }) {
4
+ return (
5
+ <div className="border-b border-gray-800 pl-4 lg:pl-7 pr-3 flex items-center justify-between">
6
+ <div
7
+ className="
8
+ space-x-6"
9
+ >
10
+ <button className="rounded-md text-sm cursor-pointer transition-all duration-100 font-medium relative py-2.5 text-white">
11
+ index.html
12
+ <span className="absolute bottom-0 left-0 h-0.5 w-full transition-all duration-100 bg-white" />
13
+ </button>
14
+ </div>
15
+ <div className="flex items-center justify-end gap-3">
16
+ <a
17
+ href="https://huggingface.co/deepseek-ai/DeepSeek-V3-0324"
18
+ target="_blank"
19
+ className="text-[12px] text-gray-300 hover:brightness-120 flex items-center gap-1 font-code"
20
+ >
21
+ Powered by <img src={Deepseek} className="size-5" /> Deepseek
22
+ </a>
23
+ {children}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default Tabs;
main.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { ToastContainer } from "react-toastify";
4
+ import "./assets/index.css";
5
+ import App from "./components/App.tsx";
6
+
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <App />
10
+ <ToastContainer className="pt-11 max-md:p-4" />
11
+ </StrictMode>
12
+ );
public/arrow.svg ADDED
public/logo.svg ADDED
public/providers/fireworks-ai.svg ADDED
public/providers/hyperbolic.svg ADDED
public/providers/nebius.svg ADDED
public/providers/novita.svg ADDED
public/providers/sambanova.svg ADDED
vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />