enzostvs HF Staff commited on
Commit
980d166
·
1 Parent(s): 92b720b

Add a way to stop the generation and get back to the previous version

Browse files
package-lock.json CHANGED
@@ -20,6 +20,7 @@
20
  "@radix-ui/react-tabs": "^1.1.12",
21
  "@radix-ui/react-toggle": "^1.1.9",
22
  "@radix-ui/react-toggle-group": "^1.1.10",
 
23
  "@tailwindcss/vite": "^4.0.15",
24
  "@xenova/transformers": "^2.17.2",
25
  "body-parser": "^1.20.3",
@@ -1791,6 +1792,40 @@
1791
  }
1792
  }
1793
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1794
  "node_modules/@radix-ui/react-use-callback-ref": {
1795
  "version": "1.1.1",
1796
  "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
 
20
  "@radix-ui/react-tabs": "^1.1.12",
21
  "@radix-ui/react-toggle": "^1.1.9",
22
  "@radix-ui/react-toggle-group": "^1.1.10",
23
+ "@radix-ui/react-tooltip": "^1.2.7",
24
  "@tailwindcss/vite": "^4.0.15",
25
  "@xenova/transformers": "^2.17.2",
26
  "body-parser": "^1.20.3",
 
1792
  }
1793
  }
1794
  },
1795
+ "node_modules/@radix-ui/react-tooltip": {
1796
+ "version": "1.2.7",
1797
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
1798
+ "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
1799
+ "license": "MIT",
1800
+ "dependencies": {
1801
+ "@radix-ui/primitive": "1.1.2",
1802
+ "@radix-ui/react-compose-refs": "1.1.2",
1803
+ "@radix-ui/react-context": "1.1.2",
1804
+ "@radix-ui/react-dismissable-layer": "1.1.10",
1805
+ "@radix-ui/react-id": "1.1.1",
1806
+ "@radix-ui/react-popper": "1.2.7",
1807
+ "@radix-ui/react-portal": "1.1.9",
1808
+ "@radix-ui/react-presence": "1.1.4",
1809
+ "@radix-ui/react-primitive": "2.1.3",
1810
+ "@radix-ui/react-slot": "1.2.3",
1811
+ "@radix-ui/react-use-controllable-state": "1.2.2",
1812
+ "@radix-ui/react-visually-hidden": "1.2.3"
1813
+ },
1814
+ "peerDependencies": {
1815
+ "@types/react": "*",
1816
+ "@types/react-dom": "*",
1817
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1818
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1819
+ },
1820
+ "peerDependenciesMeta": {
1821
+ "@types/react": {
1822
+ "optional": true
1823
+ },
1824
+ "@types/react-dom": {
1825
+ "optional": true
1826
+ }
1827
+ }
1828
+ },
1829
  "node_modules/@radix-ui/react-use-callback-ref": {
1830
  "version": "1.1.1",
1831
  "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
package.json CHANGED
@@ -23,6 +23,7 @@
23
  "@radix-ui/react-tabs": "^1.1.12",
24
  "@radix-ui/react-toggle": "^1.1.9",
25
  "@radix-ui/react-toggle-group": "^1.1.10",
 
26
  "@tailwindcss/vite": "^4.0.15",
27
  "@xenova/transformers": "^2.17.2",
28
  "body-parser": "^1.20.3",
 
23
  "@radix-ui/react-tabs": "^1.1.12",
24
  "@radix-ui/react-toggle": "^1.1.9",
25
  "@radix-ui/react-toggle-group": "^1.1.10",
26
+ "@radix-ui/react-tooltip": "^1.2.7",
27
  "@tailwindcss/vite": "^4.0.15",
28
  "@xenova/transformers": "^2.17.2",
29
  "body-parser": "^1.20.3",
src/components/ask-ai/ask-ai-new.tsx ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState, useRef } from "react";
3
+ import classNames from "classnames";
4
+ import { toast } from "sonner";
5
+ import { useLocalStorage, useUpdateEffect } from "react-use";
6
+ import { ArrowUp, ChevronDown, ImagePlus, X } from "lucide-react";
7
+ import { FaStopCircle } from "react-icons/fa";
8
+
9
+ import Login from "../login/login";
10
+ import { defaultHTML } from "../../../utils/consts";
11
+ import SuccessSound from "./../../assets/success.mp3";
12
+ import Settings from "../settings/settings";
13
+ import ProModal from "../pro-modal/pro-modal";
14
+ import { Button } from "../ui/button";
15
+ // @ts-expect-error not needed
16
+ import { MODELS } from "../../../utils/providers";
17
+ import Loading from "../loading/loading";
18
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
19
+ import { HtmlHistory } from "../../../utils/types";
20
+
21
+ function AskAI({
22
+ html,
23
+ setHtml,
24
+ onScrollToBottom,
25
+ isAiWorking,
26
+ setisAiWorking,
27
+ htmlHistory,
28
+ onNewPrompt,
29
+ onSuccess,
30
+ }: {
31
+ html: string;
32
+ setHtml: (html: string) => void;
33
+ onScrollToBottom: () => void;
34
+ isAiWorking: boolean;
35
+ onNewPrompt: (prompt: string) => void;
36
+ htmlHistory?: HtmlHistory[];
37
+ setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
38
+ onSuccess: (h: string, p: string, n?: number[][]) => void;
39
+ }) {
40
+ const refThink = useRef<HTMLDivElement | null>(null);
41
+ const uploadInputRef = useRef<HTMLInputElement | null>(null);
42
+
43
+ const [open, setOpen] = useState(false);
44
+ const [prompt, setPrompt] = useState("");
45
+ const [hasAsked, setHasAsked] = useState(false);
46
+ const [previousPrompt, setPreviousPrompt] = useState("");
47
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
48
+ const [model, setModel] = useLocalStorage("model", MODELS[0].value);
49
+ const [openProvider, setOpenProvider] = useState(false);
50
+ const [providerError, setProviderError] = useState("");
51
+ const [openProModal, setOpenProModal] = useState(false);
52
+ const [think, setThink] = useState<string | undefined>(undefined);
53
+ const [openThink, setOpenThink] = useState(false);
54
+ const [isThinking, setIsThinking] = useState(true);
55
+ const [files, setFiles] = useState<File[]>([]);
56
+ const [controller, setController] = useState<AbortController | null>(null);
57
+
58
+ const audio = new Audio(SuccessSound);
59
+ audio.volume = 0.5;
60
+
61
+ const callAi = async () => {
62
+ if (isAiWorking || !prompt.trim()) return;
63
+ setisAiWorking(true);
64
+ setProviderError("");
65
+ setThink("");
66
+ setOpenThink(false);
67
+ setIsThinking(true);
68
+
69
+ let contentResponse = "";
70
+ let thinkResponse = "";
71
+ let lastRenderTime = 0;
72
+
73
+ const isFollowUp = html !== defaultHTML;
74
+ const abortController = new AbortController();
75
+ setController(abortController);
76
+ try {
77
+ onNewPrompt(prompt);
78
+ if (isFollowUp) {
79
+ const request = await fetch("/api/ask-ai", {
80
+ method: "PUT",
81
+ body: JSON.stringify({
82
+ prompt,
83
+ provider,
84
+ previousPrompt,
85
+ html,
86
+ }),
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ },
90
+ signal: abortController.signal,
91
+ });
92
+ if (request && request.body) {
93
+ const res = await request.json();
94
+ if (!request.ok) {
95
+ if (res.openLogin) {
96
+ setOpen(true);
97
+ } else if (res.openSelectProvider) {
98
+ setOpenProvider(true);
99
+ setProviderError(res.message);
100
+ } else if (res.openProModal) {
101
+ setOpenProModal(true);
102
+ } else {
103
+ toast.error(res.message);
104
+ }
105
+ setisAiWorking(false);
106
+ return;
107
+ }
108
+ setHtml(res.html);
109
+ toast.success("AI responded successfully");
110
+ setPreviousPrompt(prompt);
111
+ setPrompt("");
112
+ setisAiWorking(false);
113
+ onSuccess(res.html, prompt, res.updatedLines);
114
+ audio.play();
115
+ }
116
+ } else {
117
+ const request = await fetch("/api/ask-ai", {
118
+ method: "POST",
119
+ body: JSON.stringify({
120
+ prompt,
121
+ provider,
122
+ model,
123
+ }),
124
+ headers: {
125
+ "Content-Type": "application/json",
126
+ },
127
+ signal: abortController.signal,
128
+ });
129
+ if (request && request.body) {
130
+ if (!request.ok) {
131
+ const res = await request.json();
132
+ if (res.openLogin) {
133
+ setOpen(true);
134
+ } else if (res.openSelectProvider) {
135
+ setOpenProvider(true);
136
+ setProviderError(res.message);
137
+ } else if (res.openProModal) {
138
+ setOpenProModal(true);
139
+ } else {
140
+ toast.error(res.message);
141
+ }
142
+ setisAiWorking(false);
143
+ return;
144
+ }
145
+ const reader = request.body.getReader();
146
+ const decoder = new TextDecoder("utf-8");
147
+ const selectedModel = MODELS.find(
148
+ (m: { value: string }) => m.value === model
149
+ );
150
+ let contentThink: string | undefined = undefined;
151
+ const read = async () => {
152
+ const { done, value } = await reader.read();
153
+ if (done) {
154
+ toast.success("AI responded successfully");
155
+ setPreviousPrompt(prompt);
156
+ setPrompt("");
157
+ setisAiWorking(false);
158
+ setHasAsked(true);
159
+ audio.play();
160
+
161
+ // Now we have the complete HTML including </html>, so set it to be sure
162
+ const finalDoc = contentResponse.match(
163
+ /<!DOCTYPE html>[\s\S]*<\/html>/
164
+ )?.[0];
165
+ if (finalDoc) {
166
+ setHtml(finalDoc);
167
+ }
168
+ onSuccess(finalDoc ?? contentResponse, prompt);
169
+
170
+ return;
171
+ }
172
+
173
+ const chunk = decoder.decode(value, { stream: true });
174
+ thinkResponse += chunk;
175
+ if (selectedModel?.isThinker) {
176
+ const thinkMatch = thinkResponse.match(/<think>[\s\S]*/)?.[0];
177
+ if (thinkMatch && !thinkResponse?.includes("</think>")) {
178
+ if ((contentThink?.length ?? 0) < 3) {
179
+ setOpenThink(true);
180
+ }
181
+ setThink(thinkMatch.replace("<think>", "").trim());
182
+ contentThink += chunk;
183
+ return read();
184
+ }
185
+ }
186
+
187
+ contentResponse += chunk;
188
+
189
+ const newHtml = contentResponse.match(
190
+ /<!DOCTYPE html>[\s\S]*/
191
+ )?.[0];
192
+ if (newHtml) {
193
+ setIsThinking(false);
194
+ let partialDoc = newHtml;
195
+ if (
196
+ partialDoc.includes("<head>") &&
197
+ !partialDoc.includes("</head>")
198
+ ) {
199
+ partialDoc += "\n</head>";
200
+ }
201
+ if (
202
+ partialDoc.includes("<body") &&
203
+ !partialDoc.includes("</body>")
204
+ ) {
205
+ partialDoc += "\n</body>";
206
+ }
207
+ if (!partialDoc.includes("</html>")) {
208
+ partialDoc += "\n</html>";
209
+ }
210
+
211
+ // Throttle the re-renders to avoid flashing/flicker
212
+ const now = Date.now();
213
+ if (now - lastRenderTime > 300) {
214
+ setHtml(partialDoc);
215
+ lastRenderTime = now;
216
+ }
217
+
218
+ if (partialDoc.length > 200) {
219
+ onScrollToBottom();
220
+ }
221
+ }
222
+ read();
223
+ };
224
+
225
+ read();
226
+ }
227
+ }
228
+ } catch (error: any) {
229
+ setisAiWorking(false);
230
+ toast.error(error.message);
231
+ if (error.openLogin) {
232
+ setOpen(true);
233
+ }
234
+ }
235
+ };
236
+
237
+ const stopController = () => {
238
+ if (controller) {
239
+ controller.abort();
240
+ setController(null);
241
+ setisAiWorking(false);
242
+ setThink("");
243
+ setOpenThink(false);
244
+ setIsThinking(false);
245
+ if (htmlHistory && htmlHistory?.length > 0) {
246
+ const lastHtml = htmlHistory[htmlHistory.length - 1].html;
247
+ setHtml(lastHtml);
248
+ toast.info("AI generation stopped, reverted to last HTML");
249
+ } else {
250
+ setHtml(defaultHTML);
251
+ toast.info("AI generation stopped");
252
+ }
253
+ }
254
+ };
255
+
256
+ const handleUploadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
257
+ const filesList = event.target.files;
258
+ if (filesList && filesList.length > 0) {
259
+ // add files to the state to show them in the UI
260
+ const newFiles = Array.from(filesList);
261
+ setFiles((prevFiles) => [...prevFiles, ...newFiles]);
262
+ // clear the input value to allow re-uploading the same file
263
+ event.target.value = "";
264
+ }
265
+ };
266
+
267
+ useUpdateEffect(() => {
268
+ if (refThink.current) {
269
+ refThink.current.scrollTop = refThink.current.scrollHeight;
270
+ }
271
+ }, [think]);
272
+
273
+ useUpdateEffect(() => {
274
+ if (!isThinking) {
275
+ setOpenThink(false);
276
+ }
277
+ }, [isThinking]);
278
+
279
+ return (
280
+ <div className="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 absolute bottom-3 left-3 w-[calc(100%-20px)] group">
281
+ {files.length > 0 && (
282
+ <div className="w-full absolute top-0 left-0 -translate-y-full pb-4 flex items-center justify-start gap-4">
283
+ {files.map((file, index) => (
284
+ <div
285
+ key={index}
286
+ className="flex items-center justify-between bg-neutral-700/50 rounded-lg w-20 aspect-square relative"
287
+ style={{
288
+ backgroundImage: `url(${URL.createObjectURL(file)})`,
289
+ backgroundSize: "cover",
290
+ backgroundPosition: "center",
291
+ }}
292
+ >
293
+ <Button
294
+ size="iconXss"
295
+ className="absolute -top-2 -right-2 ring-[3px] ring-neutral-900"
296
+ onClick={() => {
297
+ setFiles((prevFiles) =>
298
+ prevFiles.filter((f) => f.name !== file.name)
299
+ );
300
+ }}
301
+ >
302
+ <X className="size-4" />
303
+ </Button>
304
+ </div>
305
+ ))}
306
+ </div>
307
+ )}
308
+ {think && (
309
+ <div className="w-full border-b border-neutral-700 relative overflow-hidden">
310
+ <header
311
+ className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
312
+ onClick={() => {
313
+ setOpenThink(!openThink);
314
+ }}
315
+ >
316
+ <p className="text-sm font-medium text-neutral-300 group-hover:text-neutral-200 transition-colors duration-200">
317
+ {isThinking ? "AI is thinking..." : "AI's plan"}
318
+ </p>
319
+ <ChevronDown
320
+ className={classNames(
321
+ "size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
322
+ {
323
+ "rotate-180": openThink,
324
+ }
325
+ )}
326
+ />
327
+ </header>
328
+ <main
329
+ ref={refThink}
330
+ className={classNames(
331
+ "overflow-y-auto transition-all duration-200 ease-in-out",
332
+ {
333
+ "max-h-[0px]": !openThink,
334
+ "min-h-[250px] max-h-[250px] border-t border-neutral-700":
335
+ openThink,
336
+ }
337
+ )}
338
+ >
339
+ <p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
340
+ {think}
341
+ </p>
342
+ </main>
343
+ </div>
344
+ )}
345
+ <div className="w-full relative flex items-center justify-between">
346
+ {isAiWorking && (
347
+ <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">
348
+ <div className="flex items-center justify-start gap-2">
349
+ <Loading overlay={false} className="!size-4" />
350
+ <p className="text-neutral-400 text-sm">
351
+ AI is {isThinking ? "thinking" : "coding"}...{" "}
352
+ </p>
353
+ </div>
354
+ <div
355
+ className="text-xs text-neutral-400 px-1 py-0.5 rounded-md border border-neutral-600 flex items-center justify-center gap-1.5 bg-neutral-800 hover:brightness-110 transition-all duration-200 cursor-pointer"
356
+ onClick={stopController}
357
+ >
358
+ <FaStopCircle />
359
+ Stop generation
360
+ </div>
361
+ </div>
362
+ )}
363
+ <input
364
+ type="text"
365
+ disabled={isAiWorking}
366
+ className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4"
367
+ placeholder={
368
+ hasAsked ? "Ask DeepSite for edits" : "Ask DeepSite anything..."
369
+ }
370
+ value={prompt}
371
+ onChange={(e) => setPrompt(e.target.value)}
372
+ onKeyDown={(e) => {
373
+ if (e.key === "Enter" && !e.shiftKey) {
374
+ callAi();
375
+ }
376
+ }}
377
+ />
378
+ </div>
379
+ <div className="flex items-center justify-between gap-2 px-4 pb-3">
380
+ <div className="flex-1">
381
+ <Tooltip>
382
+ <TooltipTrigger asChild>
383
+ <Button
384
+ size="iconXs"
385
+ variant="outline"
386
+ className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
387
+ // onClick={() => {
388
+ // if (uploadInputRef.current) {
389
+ // uploadInputRef.current.click();
390
+ // }
391
+ // }}
392
+ >
393
+ <ImagePlus className="size-4" />
394
+ </Button>
395
+ </TooltipTrigger>
396
+ <TooltipContent>
397
+ <p>
398
+ Attach files <span className="italic">(coming soon)</span>
399
+ </p>
400
+ </TooltipContent>
401
+ </Tooltip>
402
+ <input
403
+ ref={uploadInputRef}
404
+ type="file"
405
+ accept="image/*"
406
+ multiple
407
+ onChange={handleUploadFile}
408
+ className="hidden"
409
+ id="file-upload"
410
+ />
411
+ </div>
412
+ <div className="flex items-center justify-end gap-2">
413
+ <Settings
414
+ provider={provider as string}
415
+ model={model as string}
416
+ onChange={setProvider}
417
+ onModelChange={setModel}
418
+ open={openProvider}
419
+ error={providerError}
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
+ <div
432
+ className={classNames(
433
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
434
+ {
435
+ "opacity-0 pointer-events-none": !open,
436
+ }
437
+ )}
438
+ onClick={() => setOpen(false)}
439
+ ></div>
440
+ <div
441
+ className={classNames(
442
+ "absolute top-0 -translate-y-[calc(100%+8px)] right-0 z-10 w-80 border border-neutral-800 !bg-neutral-900 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
443
+ {
444
+ "opacity-0 pointer-events-none": !open,
445
+ }
446
+ )}
447
+ >
448
+ <Login html={html}>
449
+ <p className="text-gray-500 text-sm mb-3">
450
+ You reached the limit of free AI usage. Please login to continue.
451
+ </p>
452
+ </Login>
453
+ </div>
454
+ <ProModal
455
+ html={html}
456
+ open={openProModal}
457
+ onClose={() => setOpenProModal(false)}
458
+ />
459
+ </div>
460
+ );
461
+ }
462
+
463
+ export default AskAI;
src/components/loading/loading.tsx CHANGED
@@ -1,8 +1,21 @@
1
- function Loading() {
 
 
 
 
 
 
 
 
2
  return (
3
- <div className="absolute left-0 top-0 h-full w-full flex items-center justify-center z-20 bg-black/50 rounded-full">
 
 
 
 
 
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"
 
1
+ import classNames from "classnames";
2
+
3
+ function Loading({
4
+ overlay = true,
5
+ className,
6
+ }: {
7
+ overlay?: boolean;
8
+ className?: string;
9
+ }) {
10
  return (
11
+ <div
12
+ className={classNames("", {
13
+ "absolute left-0 top-0 h-full w-full flex items-center justify-center z-20 bg-black/50 rounded-full":
14
+ overlay,
15
+ })}
16
+ >
17
  <svg
18
+ className={`size-5 animate-spin text-white ${className}`}
19
  xmlns="http://www.w3.org/2000/svg"
20
  fill="none"
21
  viewBox="0 0 24 24"
src/components/settings/settings.tsx CHANGED
@@ -56,8 +56,9 @@ function Settings({
56
  <div className="">
57
  <Popover open={open} onOpenChange={onClose}>
58
  <PopoverTrigger asChild>
59
- <Button variant="gray" size="icon">
60
- <PiGearSixFill className="size-5" />
 
61
  </Button>
62
  </PopoverTrigger>
63
  <PopoverContent
 
56
  <div className="">
57
  <Popover open={open} onOpenChange={onClose}>
58
  <PopoverTrigger asChild>
59
+ <Button variant="gray" size="sm">
60
+ <PiGearSixFill className="size-4" />
61
+ Settings
62
  </Button>
63
  </PopoverTrigger>
64
  <PopoverContent
src/components/ui/button.tsx CHANGED
@@ -11,8 +11,7 @@ const buttonVariants = cva(
11
  variant: {
12
  default:
13
  "bg-neutral-900 text-neutral-50 shadow-xs hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
14
- destructive:
15
- "bg-red-500 text-white shadow-xs hover:bg-red-500/90 focus-visible:ring-red-500/20 dark:focus-visible:ring-red-500/40 dark:bg-red-500/60 dark:bg-red-900 dark:hover:bg-red-900/90 dark:focus-visible:ring-red-900/20 dark:dark:focus-visible:ring-red-900/40 dark:dark:bg-red-900/60",
16
  outline:
17
  "border bg-white shadow-xs hover:bg-neutral-100 hover:text-neutral-900 dark:bg-neutral-200/30 dark:border-neutral-200 dark:hover:bg-neutral-200/50 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50 dark:dark:bg-neutral-800/30 dark:dark:border-neutral-800 dark:dark:hover:bg-neutral-800/50",
18
  secondary:
@@ -29,6 +28,7 @@ const buttonVariants = cva(
29
  lg: "h-10 rounded-full px-6 has-[>svg]:px-4",
30
  icon: "size-9",
31
  iconXs: "size-7",
 
32
  xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
33
  },
34
  },
 
11
  variant: {
12
  default:
13
  "bg-neutral-900 text-neutral-50 shadow-xs hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
14
+ destructive: "bg-red-600 text-white shadow-xs hover:bg-red-500",
 
15
  outline:
16
  "border bg-white shadow-xs hover:bg-neutral-100 hover:text-neutral-900 dark:bg-neutral-200/30 dark:border-neutral-200 dark:hover:bg-neutral-200/50 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50 dark:dark:bg-neutral-800/30 dark:dark:border-neutral-800 dark:dark:hover:bg-neutral-800/50",
17
  secondary:
 
28
  lg: "h-10 rounded-full px-6 has-[>svg]:px-4",
29
  icon: "size-9",
30
  iconXs: "size-7",
31
+ iconXss: "size-6",
32
  xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
33
  },
34
  },
src/components/ui/tooltip.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3
+
4
+ import { cn } from "./../../lib/utils";
5
+
6
+ function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delayDuration={delayDuration}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function Tooltip({
20
+ ...props
21
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
22
+ return (
23
+ <TooltipProvider>
24
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
25
+ </TooltipProvider>
26
+ );
27
+ }
28
+
29
+ function TooltipTrigger({
30
+ ...props
31
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
32
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
33
+ }
34
+
35
+ function TooltipContent({
36
+ className,
37
+ sideOffset = 0,
38
+ children,
39
+ ...props
40
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
41
+ return (
42
+ <TooltipPrimitive.Portal>
43
+ <TooltipPrimitive.Content
44
+ data-slot="tooltip-content"
45
+ sideOffset={sideOffset}
46
+ className={cn(
47
+ "bg-black text-neutral-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
48
+ className
49
+ )}
50
+ {...props}
51
+ >
52
+ {children}
53
+ <TooltipPrimitive.Arrow className="bg-black fill-black z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
54
+ </TooltipPrimitive.Content>
55
+ </TooltipPrimitive.Portal>
56
+ );
57
+ }
58
+
59
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
src/views/App.tsx CHANGED
@@ -21,7 +21,7 @@ import { defaultHTML } from "../../utils/consts";
21
  import DeployButton from "../components/deploy-button/deploy-button";
22
  import Preview from "../components/preview/preview";
23
  import Footer from "../components/footer/footer";
24
- import AskAI from "../components/ask-ai/ask-ai";
25
 
26
  export default function App() {
27
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
@@ -218,6 +218,9 @@ export default function App() {
218
  fontLigatures: true,
219
  theme: "vs-dark",
220
  minimap: { enabled: false },
 
 
 
221
  }}
222
  value={html}
223
  onChange={(value) => {
@@ -234,6 +237,7 @@ export default function App() {
234
  setHtml={(newHtml: string) => {
235
  setHtml(newHtml);
236
  }}
 
237
  onSuccess={(
238
  finalHtml: string,
239
  p: string,
 
21
  import DeployButton from "../components/deploy-button/deploy-button";
22
  import Preview from "../components/preview/preview";
23
  import Footer from "../components/footer/footer";
24
+ import AskAI from "../components/ask-ai/ask-ai-new";
25
 
26
  export default function App() {
27
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
 
218
  fontLigatures: true,
219
  theme: "vs-dark",
220
  minimap: { enabled: false },
221
+ scrollbar: {
222
+ horizontal: "hidden",
223
+ },
224
  }}
225
  value={html}
226
  onChange={(value) => {
 
237
  setHtml={(newHtml: string) => {
238
  setHtml(newHtml);
239
  }}
240
+ htmlHistory={htmlHistory}
241
  onSuccess={(
242
  finalHtml: string,
243
  p: string,