Spaces:
Running
Running
wip target element + no follow up option
Browse files- app/api/ask-ai/route.ts +4 -2
- components/editor/ask-ai/index.tsx +108 -8
- components/editor/ask-ai/re-imagine.tsx +32 -19
- components/editor/ask-ai/selected-html-element.tsx +42 -0
- components/editor/index.tsx +15 -0
- components/editor/preview/index.tsx +99 -0
- components/public/navigation/index.tsx +2 -2
- components/space/ask-ai/index.tsx +32 -27
- components/ui/checkbox.tsx +32 -0
- components/ui/collapsible.tsx +33 -0
- components/ui/switch.tsx +31 -0
- lib/html-tag-to-text.ts +96 -0
- package-lock.json +92 -0
- package.json +3 -0
app/api/ask-ai/route.ts
CHANGED
@@ -223,7 +223,7 @@ export async function PUT(request: NextRequest) {
|
|
223 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
224 |
|
225 |
const body = await request.json();
|
226 |
-
const { prompt, html, previousPrompt, provider } = body;
|
227 |
|
228 |
if (!prompt || !html) {
|
229 |
return NextResponse.json(
|
@@ -294,7 +294,9 @@ export async function PUT(request: NextRequest) {
|
|
294 |
},
|
295 |
{
|
296 |
role: "assistant",
|
297 |
-
content:
|
|
|
|
|
298 |
},
|
299 |
{
|
300 |
role: "user",
|
|
|
223 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
224 |
|
225 |
const body = await request.json();
|
226 |
+
const { prompt, html, previousPrompt, provider, selectedElementHtml } = body;
|
227 |
|
228 |
if (!prompt || !html) {
|
229 |
return NextResponse.json(
|
|
|
294 |
},
|
295 |
{
|
296 |
role: "assistant",
|
297 |
+
content: selectedElementHtml
|
298 |
+
? `Here is the selected HTML element:\n\n${selectedElementHtml}`
|
299 |
+
: `The current code is: \n\`\`\`html\n${html}\n\`\`\``,
|
300 |
},
|
301 |
{
|
302 |
role: "user",
|
components/editor/ask-ai/index.tsx
CHANGED
@@ -4,7 +4,7 @@ import { useState, useRef } from "react";
|
|
4 |
import classNames from "classnames";
|
5 |
import { toast } from "sonner";
|
6 |
import { useLocalStorage, useUpdateEffect } from "react-use";
|
7 |
-
import { ArrowUp, ChevronDown } from "lucide-react";
|
8 |
import { FaStopCircle } from "react-icons/fa";
|
9 |
|
10 |
import { defaultHTML } from "@/lib/consts";
|
@@ -12,11 +12,20 @@ import ProModal from "@/components/pro-modal";
|
|
12 |
import { Button } from "@/components/ui/button";
|
13 |
import { MODELS } from "@/lib/providers";
|
14 |
import { HtmlHistory } from "@/types";
|
15 |
-
import { InviteFriends } from "@/components/invite-friends";
|
16 |
import { Settings } from "@/components/editor/ask-ai/settings";
|
17 |
import { LoginModal } from "@/components/login-modal";
|
18 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
19 |
import Loading from "@/components/loading";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
export function AskAI({
|
22 |
html,
|
@@ -24,6 +33,10 @@ export function AskAI({
|
|
24 |
onScrollToBottom,
|
25 |
isAiWorking,
|
26 |
setisAiWorking,
|
|
|
|
|
|
|
|
|
27 |
onNewPrompt,
|
28 |
onSuccess,
|
29 |
}: {
|
@@ -35,6 +48,10 @@ export function AskAI({
|
|
35 |
htmlHistory?: HtmlHistory[];
|
36 |
setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
|
37 |
onSuccess: (h: string, p: string, n?: number[][]) => void;
|
|
|
|
|
|
|
|
|
38 |
}) {
|
39 |
const refThink = useRef<HTMLDivElement | null>(null);
|
40 |
const audio = useRef<HTMLAudioElement | null>(null);
|
@@ -52,6 +69,7 @@ export function AskAI({
|
|
52 |
const [openThink, setOpenThink] = useState(false);
|
53 |
const [isThinking, setIsThinking] = useState(true);
|
54 |
const [controller, setController] = useState<AbortController | null>(null);
|
|
|
55 |
|
56 |
const callAi = async (redesignMarkdown?: string) => {
|
57 |
if (isAiWorking) return;
|
@@ -66,13 +84,14 @@ export function AskAI({
|
|
66 |
let thinkResponse = "";
|
67 |
let lastRenderTime = 0;
|
68 |
|
69 |
-
const isFollowUp = html !== defaultHTML;
|
70 |
const abortController = new AbortController();
|
71 |
setController(abortController);
|
72 |
try {
|
73 |
onNewPrompt(prompt);
|
74 |
-
if (isFollowUp && !redesignMarkdown) {
|
75 |
-
|
|
|
|
|
76 |
const request = await fetch("/api/ask-ai", {
|
77 |
method: "PUT",
|
78 |
body: JSON.stringify({
|
@@ -81,6 +100,7 @@ export function AskAI({
|
|
81 |
previousPrompt,
|
82 |
model,
|
83 |
html,
|
|
|
84 |
}),
|
85 |
headers: {
|
86 |
"Content-Type": "application/json",
|
@@ -263,6 +283,43 @@ export function AskAI({
|
|
263 |
|
264 |
return (
|
265 |
<>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
266 |
<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 w-full group">
|
267 |
{think && (
|
268 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
@@ -301,6 +358,14 @@ export function AskAI({
|
|
301 |
</main>
|
302 |
</div>
|
303 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
<div className="w-full relative flex items-center justify-between">
|
305 |
{isAiWorking && (
|
306 |
<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">
|
@@ -322,9 +387,18 @@ export function AskAI({
|
|
322 |
<input
|
323 |
type="text"
|
324 |
disabled={isAiWorking}
|
325 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
326 |
placeholder={
|
327 |
-
|
|
|
|
|
|
|
|
|
328 |
}
|
329 |
value={prompt}
|
330 |
onChange={(e) => setPrompt(e.target.value)}
|
@@ -338,7 +412,33 @@ export function AskAI({
|
|
338 |
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
339 |
<div className="flex-1 flex items-center justify-start gap-1.5">
|
340 |
<ReImagine onRedesign={(md) => callAi(md)} />
|
341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
342 |
</div>
|
343 |
<div className="flex items-center justify-end gap-2">
|
344 |
<Settings
|
|
|
4 |
import classNames from "classnames";
|
5 |
import { toast } from "sonner";
|
6 |
import { useLocalStorage, useUpdateEffect } from "react-use";
|
7 |
+
import { ArrowUp, ChevronDown, Info, Crosshair } from "lucide-react";
|
8 |
import { FaStopCircle } from "react-icons/fa";
|
9 |
|
10 |
import { defaultHTML } from "@/lib/consts";
|
|
|
12 |
import { Button } from "@/components/ui/button";
|
13 |
import { MODELS } from "@/lib/providers";
|
14 |
import { HtmlHistory } from "@/types";
|
15 |
+
// import { InviteFriends } from "@/components/invite-friends";
|
16 |
import { Settings } from "@/components/editor/ask-ai/settings";
|
17 |
import { LoginModal } from "@/components/login-modal";
|
18 |
import { ReImagine } from "@/components/editor/ask-ai/re-imagine";
|
19 |
import Loading from "@/components/loading";
|
20 |
+
import {
|
21 |
+
Popover,
|
22 |
+
PopoverContent,
|
23 |
+
PopoverTrigger,
|
24 |
+
} from "@/components/ui/popover";
|
25 |
+
import { Checkbox } from "@/components/ui/checkbox";
|
26 |
+
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
|
27 |
+
import { TooltipContent } from "@radix-ui/react-tooltip";
|
28 |
+
import { SelectedHtmlElement } from "./selected-html-element";
|
29 |
|
30 |
export function AskAI({
|
31 |
html,
|
|
|
33 |
onScrollToBottom,
|
34 |
isAiWorking,
|
35 |
setisAiWorking,
|
36 |
+
isEditableModeEnabled = false,
|
37 |
+
selectedElement,
|
38 |
+
setSelectedElement,
|
39 |
+
setIsEditableModeEnabled,
|
40 |
onNewPrompt,
|
41 |
onSuccess,
|
42 |
}: {
|
|
|
48 |
htmlHistory?: HtmlHistory[];
|
49 |
setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
|
50 |
onSuccess: (h: string, p: string, n?: number[][]) => void;
|
51 |
+
isEditableModeEnabled: boolean;
|
52 |
+
setIsEditableModeEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
53 |
+
selectedElement?: HTMLElement | null;
|
54 |
+
setSelectedElement: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
55 |
}) {
|
56 |
const refThink = useRef<HTMLDivElement | null>(null);
|
57 |
const audio = useRef<HTMLAudioElement | null>(null);
|
|
|
69 |
const [openThink, setOpenThink] = useState(false);
|
70 |
const [isThinking, setIsThinking] = useState(true);
|
71 |
const [controller, setController] = useState<AbortController | null>(null);
|
72 |
+
const [isFollowUp, setIsFollowUp] = useState(true);
|
73 |
|
74 |
const callAi = async (redesignMarkdown?: string) => {
|
75 |
if (isAiWorking) return;
|
|
|
84 |
let thinkResponse = "";
|
85 |
let lastRenderTime = 0;
|
86 |
|
|
|
87 |
const abortController = new AbortController();
|
88 |
setController(abortController);
|
89 |
try {
|
90 |
onNewPrompt(prompt);
|
91 |
+
if (isFollowUp && !redesignMarkdown && html !== defaultHTML) {
|
92 |
+
const selectedElementHtml = selectedElement
|
93 |
+
? selectedElement.outerHTML
|
94 |
+
: "";
|
95 |
const request = await fetch("/api/ask-ai", {
|
96 |
method: "PUT",
|
97 |
body: JSON.stringify({
|
|
|
100 |
previousPrompt,
|
101 |
model,
|
102 |
html,
|
103 |
+
selectedElementHtml,
|
104 |
}),
|
105 |
headers: {
|
106 |
"Content-Type": "application/json",
|
|
|
283 |
|
284 |
return (
|
285 |
<>
|
286 |
+
<div className="ml-auto select-none text-xs text-neutral-400 flex items-center justify-center gap-2 bg-neutral-800 border border-neutral-700 rounded-md p-1 pr-2.5 max-w-max">
|
287 |
+
<label
|
288 |
+
htmlFor="follow-up-checkbox"
|
289 |
+
className="flex items-center gap-1.5 cursor-pointer"
|
290 |
+
>
|
291 |
+
<Checkbox
|
292 |
+
id="follow-up-checkbox"
|
293 |
+
checked={isFollowUp}
|
294 |
+
onCheckedChange={(e) => {
|
295 |
+
setIsFollowUp(e === true);
|
296 |
+
}}
|
297 |
+
/>
|
298 |
+
Follow-Up
|
299 |
+
</label>
|
300 |
+
<Popover>
|
301 |
+
<PopoverTrigger asChild>
|
302 |
+
<Info className="size-3 text-neutral-300 cursor-pointer" />
|
303 |
+
</PopoverTrigger>
|
304 |
+
<PopoverContent
|
305 |
+
align="start"
|
306 |
+
className="!rounded-2xl !p-0 min-w-xs text-center overflow-hidden"
|
307 |
+
>
|
308 |
+
<header className="bg-neutral-950 px-4 py-3 border-b border-neutral-700/70">
|
309 |
+
<p className="text-base text-neutral-200 font-semibold">
|
310 |
+
What is a Follow-Up?
|
311 |
+
</p>
|
312 |
+
</header>
|
313 |
+
<main className="p-4">
|
314 |
+
<p className="text-sm text-neutral-400">
|
315 |
+
A Follow-Up is a request to DeepSite to edit the current HTML
|
316 |
+
instead of starting from scratch. This is useful when you want
|
317 |
+
to make small changes or improvements to the existing design.
|
318 |
+
</p>
|
319 |
+
</main>
|
320 |
+
</PopoverContent>
|
321 |
+
</Popover>
|
322 |
+
</div>
|
323 |
<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 w-full group">
|
324 |
{think && (
|
325 |
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
|
|
|
358 |
</main>
|
359 |
</div>
|
360 |
)}
|
361 |
+
{!isAiWorking && selectedElement && (
|
362 |
+
<div className="px-4 pt-3">
|
363 |
+
<SelectedHtmlElement
|
364 |
+
element={selectedElement}
|
365 |
+
onDelete={() => setSelectedElement(null)}
|
366 |
+
/>
|
367 |
+
</div>
|
368 |
+
)}
|
369 |
<div className="w-full relative flex items-center justify-between">
|
370 |
{isAiWorking && (
|
371 |
<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">
|
|
|
387 |
<input
|
388 |
type="text"
|
389 |
disabled={isAiWorking}
|
390 |
+
className={classNames(
|
391 |
+
"w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4",
|
392 |
+
{
|
393 |
+
"!pt-2.5": selectedElement && !isAiWorking,
|
394 |
+
}
|
395 |
+
)}
|
396 |
placeholder={
|
397 |
+
selectedElement
|
398 |
+
? `Ask DeepSite about ${selectedElement.tagName.toLowerCase()}...`
|
399 |
+
: hasAsked
|
400 |
+
? "Ask DeepSite for edits"
|
401 |
+
: "Ask DeepSite anything..."
|
402 |
}
|
403 |
value={prompt}
|
404 |
onChange={(e) => setPrompt(e.target.value)}
|
|
|
412 |
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
413 |
<div className="flex-1 flex items-center justify-start gap-1.5">
|
414 |
<ReImagine onRedesign={(md) => callAi(md)} />
|
415 |
+
{html !== defaultHTML && (
|
416 |
+
<Tooltip>
|
417 |
+
<TooltipTrigger asChild>
|
418 |
+
<Button
|
419 |
+
size="xs"
|
420 |
+
variant={isEditableModeEnabled ? "default" : "outline"}
|
421 |
+
onClick={() => {
|
422 |
+
setIsEditableModeEnabled?.(!isEditableModeEnabled);
|
423 |
+
}}
|
424 |
+
className={classNames("h-[28px]", {
|
425 |
+
"!text-neutral-400 hover:!text-neutral-200 !border-neutral-600 !hover:!border-neutral-500":
|
426 |
+
!isEditableModeEnabled,
|
427 |
+
})}
|
428 |
+
>
|
429 |
+
<Crosshair className="size-4" />
|
430 |
+
Edit
|
431 |
+
</Button>
|
432 |
+
</TooltipTrigger>
|
433 |
+
<TooltipContent
|
434 |
+
align="start"
|
435 |
+
className="bg-neutral-950 text-xs text-neutral-200 py-1 px-2 rounded-md -translate-y-0.5"
|
436 |
+
>
|
437 |
+
Select an element on the page to ask DeepSite edit it
|
438 |
+
directly.
|
439 |
+
</TooltipContent>
|
440 |
+
</Tooltip>
|
441 |
+
)}
|
442 |
</div>
|
443 |
<div className="flex items-center justify-end gap-2">
|
444 |
<Settings
|
components/editor/ask-ai/re-imagine.tsx
CHANGED
@@ -10,6 +10,7 @@ import {
|
|
10 |
} from "@/components/ui/popover";
|
11 |
import { Input } from "@/components/ui/input";
|
12 |
import Loading from "@/components/loading";
|
|
|
13 |
|
14 |
export function ReImagine({
|
15 |
onRedesign,
|
@@ -29,6 +30,7 @@ export function ReImagine({
|
|
29 |
};
|
30 |
|
31 |
const handleClick = async () => {
|
|
|
32 |
if (!url) {
|
33 |
toast.error("Please enter a URL.");
|
34 |
return;
|
@@ -37,24 +39,25 @@ export function ReImagine({
|
|
37 |
toast.error("Please enter a valid URL.");
|
38 |
return;
|
39 |
}
|
|
|
40 |
// Here you would typically handle the re-design logic
|
41 |
setIsLoading(true);
|
42 |
-
const request = await
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
});
|
49 |
-
const response = await request.json();
|
50 |
-
if (response.ok) {
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
} else {
|
56 |
-
|
57 |
-
}
|
58 |
setIsLoading(false);
|
59 |
};
|
60 |
|
@@ -126,10 +129,20 @@ export function ReImagine({
|
|
126 |
variant="black"
|
127 |
onClick={handleClick}
|
128 |
className="relative w-full"
|
129 |
-
disabled={isLoading}
|
130 |
>
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
</Button>
|
134 |
</div>
|
135 |
</main>
|
|
|
10 |
} from "@/components/ui/popover";
|
11 |
import { Input } from "@/components/ui/input";
|
12 |
import Loading from "@/components/loading";
|
13 |
+
import { api } from "@/lib/api";
|
14 |
|
15 |
export function ReImagine({
|
16 |
onRedesign,
|
|
|
30 |
};
|
31 |
|
32 |
const handleClick = async () => {
|
33 |
+
if (isLoading) return; // Prevent multiple clicks while loading
|
34 |
if (!url) {
|
35 |
toast.error("Please enter a URL.");
|
36 |
return;
|
|
|
39 |
toast.error("Please enter a valid URL.");
|
40 |
return;
|
41 |
}
|
42 |
+
// TODO implement the API call to redesign the site
|
43 |
// Here you would typically handle the re-design logic
|
44 |
setIsLoading(true);
|
45 |
+
// const request = await api.post("/api/re-design", {
|
46 |
+
// method: "POST",
|
47 |
+
// body: JSON.stringify({ url }),
|
48 |
+
// headers: {
|
49 |
+
// "Content-Type": "application/json",
|
50 |
+
// },
|
51 |
+
// });
|
52 |
+
// const response = await request.json();
|
53 |
+
// if (response.ok) {
|
54 |
+
// setOpen(false);
|
55 |
+
// setUrl("");
|
56 |
+
// onRedesign(response.markdown);
|
57 |
+
// toast.success("DeepSite is redesigning your site! Let him cook... 🔥");
|
58 |
+
// } else {
|
59 |
+
// toast.error(response.message || "Failed to redesign the site.");
|
60 |
+
// }
|
61 |
setIsLoading(false);
|
62 |
};
|
63 |
|
|
|
129 |
variant="black"
|
130 |
onClick={handleClick}
|
131 |
className="relative w-full"
|
|
|
132 |
>
|
133 |
+
{isLoading ? (
|
134 |
+
<>
|
135 |
+
<Loading
|
136 |
+
overlay={false}
|
137 |
+
className="ml-2 size-4 animate-spin"
|
138 |
+
/>
|
139 |
+
Fetching your site...
|
140 |
+
</>
|
141 |
+
) : (
|
142 |
+
<>
|
143 |
+
Redesign <Paintbrush className="size-4" />
|
144 |
+
</>
|
145 |
+
)}
|
146 |
</Button>
|
147 |
</div>
|
148 |
</main>
|
components/editor/ask-ai/selected-html-element.tsx
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible";
|
2 |
+
import { htmlTagToText } from "@/lib/html-tag-to-text";
|
3 |
+
import { Code, XCircle } from "lucide-react";
|
4 |
+
|
5 |
+
export const SelectedHtmlElement = ({
|
6 |
+
element,
|
7 |
+
onDelete,
|
8 |
+
}: {
|
9 |
+
element: HTMLElement | null;
|
10 |
+
onDelete?: () => void;
|
11 |
+
}) => {
|
12 |
+
if (!element) return null;
|
13 |
+
|
14 |
+
const tagName = element.tagName.toLowerCase();
|
15 |
+
return (
|
16 |
+
<Collapsible
|
17 |
+
className="border border-neutral-700 rounded-xl p-1.5 pr-3 max-w-max hover:brightness-110 transition-all duration-200 ease-in-out !cursor-pointer"
|
18 |
+
onClick={onDelete}
|
19 |
+
>
|
20 |
+
<CollapsibleTrigger className="flex items-center justify-start gap-2 cursor-pointer">
|
21 |
+
<div className="rounded-lg bg-neutral-700 size-6 flex items-center justify-center">
|
22 |
+
<Code className="text-neutral-300 size-3.5" />
|
23 |
+
</div>
|
24 |
+
<p className="text-sm font-semibold text-neutral-300">
|
25 |
+
{htmlTagToText(tagName)}
|
26 |
+
</p>
|
27 |
+
<XCircle className="text-neutral-300 size-4" />
|
28 |
+
</CollapsibleTrigger>
|
29 |
+
{/* <CollapsibleContent className="border-t border-neutral-700 pt-2 mt-2">
|
30 |
+
<div className="text-xs text-neutral-400">
|
31 |
+
<p>
|
32 |
+
<span className="font-semibold">ID:</span> {element.id || "No ID"}
|
33 |
+
</p>
|
34 |
+
<p>
|
35 |
+
<span className="font-semibold">Classes:</span>{" "}
|
36 |
+
{element.className || "No classes"}
|
37 |
+
</p>
|
38 |
+
</div>
|
39 |
+
</CollapsibleContent> */}
|
40 |
+
</Collapsible>
|
41 |
+
);
|
42 |
+
};
|
components/editor/index.tsx
CHANGED
@@ -47,6 +47,10 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
|
|
47 |
const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
|
48 |
const [isResizing, setIsResizing] = useState(false);
|
49 |
const [isAiWorking, setIsAiWorking] = useState(false);
|
|
|
|
|
|
|
|
|
50 |
|
51 |
/**
|
52 |
* Resets the layout based on screen size
|
@@ -158,6 +162,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
|
|
158 |
resizer.current.addEventListener("mousedown", handleMouseDown);
|
159 |
}
|
160 |
} else {
|
|
|
161 |
if (preview.current) {
|
162 |
// Reset preview width when switching to preview tab
|
163 |
preview.current.style.width = "100%";
|
@@ -234,6 +239,7 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
|
|
234 |
prompt: p,
|
235 |
});
|
236 |
setHtmlHistory(currentHistory);
|
|
|
237 |
// if xs or sm
|
238 |
if (window.innerWidth <= 1024) {
|
239 |
setCurrentTab("preview");
|
@@ -269,6 +275,10 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
|
|
269 |
editorRef.current?.getModel()?.getLineCount() ?? 0
|
270 |
);
|
271 |
}}
|
|
|
|
|
|
|
|
|
272 |
/>
|
273 |
</div>
|
274 |
<div
|
@@ -284,7 +294,12 @@ export const AppEditor = ({ project }: { project?: Project | null }) => {
|
|
284 |
ref={preview}
|
285 |
device={device}
|
286 |
currentTab={currentTab}
|
|
|
287 |
iframeRef={iframeRef}
|
|
|
|
|
|
|
|
|
288 |
/>
|
289 |
</main>
|
290 |
<Footer
|
|
|
47 |
const [device, setDevice] = useState<"desktop" | "mobile">("desktop");
|
48 |
const [isResizing, setIsResizing] = useState(false);
|
49 |
const [isAiWorking, setIsAiWorking] = useState(false);
|
50 |
+
const [isEditableModeEnabled, setIsEditableModeEnabled] = useState(false);
|
51 |
+
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
|
52 |
+
null
|
53 |
+
);
|
54 |
|
55 |
/**
|
56 |
* Resets the layout based on screen size
|
|
|
162 |
resizer.current.addEventListener("mousedown", handleMouseDown);
|
163 |
}
|
164 |
} else {
|
165 |
+
setIsEditableModeEnabled(false);
|
166 |
if (preview.current) {
|
167 |
// Reset preview width when switching to preview tab
|
168 |
preview.current.style.width = "100%";
|
|
|
239 |
prompt: p,
|
240 |
});
|
241 |
setHtmlHistory(currentHistory);
|
242 |
+
setSelectedElement(null);
|
243 |
// if xs or sm
|
244 |
if (window.innerWidth <= 1024) {
|
245 |
setCurrentTab("preview");
|
|
|
275 |
editorRef.current?.getModel()?.getLineCount() ?? 0
|
276 |
);
|
277 |
}}
|
278 |
+
isEditableModeEnabled={isEditableModeEnabled}
|
279 |
+
setIsEditableModeEnabled={setIsEditableModeEnabled}
|
280 |
+
selectedElement={selectedElement}
|
281 |
+
setSelectedElement={setSelectedElement}
|
282 |
/>
|
283 |
</div>
|
284 |
<div
|
|
|
294 |
ref={preview}
|
295 |
device={device}
|
296 |
currentTab={currentTab}
|
297 |
+
isEditableModeEnabled={isEditableModeEnabled}
|
298 |
iframeRef={iframeRef}
|
299 |
+
onClickElement={(element) => {
|
300 |
+
setIsEditableModeEnabled(false);
|
301 |
+
setSelectedElement(element);
|
302 |
+
}}
|
303 |
/>
|
304 |
</main>
|
305 |
<Footer
|
components/editor/preview/index.tsx
CHANGED
@@ -1,9 +1,12 @@
|
|
1 |
"use client";
|
|
|
|
|
2 |
import classNames from "classnames";
|
3 |
import { toast } from "sonner";
|
4 |
|
5 |
import { cn } from "@/lib/utils";
|
6 |
import { GridPattern } from "@/components/magic-ui/grid-pattern";
|
|
|
7 |
|
8 |
export const Preview = ({
|
9 |
html,
|
@@ -13,6 +16,8 @@ export const Preview = ({
|
|
13 |
device,
|
14 |
currentTab,
|
15 |
iframeRef,
|
|
|
|
|
16 |
}: {
|
17 |
html: string;
|
18 |
isResizing: boolean;
|
@@ -21,7 +26,80 @@ export const Preview = ({
|
|
21 |
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
22 |
device: "desktop" | "mobile";
|
23 |
currentTab: string;
|
|
|
|
|
24 |
}) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
return (
|
26 |
<div
|
27 |
ref={ref}
|
@@ -49,6 +127,27 @@ export const Preview = ({
|
|
49 |
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]"
|
50 |
)}
|
51 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
<iframe
|
53 |
id="preview-iframe"
|
54 |
ref={iframeRef}
|
|
|
1 |
"use client";
|
2 |
+
import { useUpdateEffect } from "react-use";
|
3 |
+
import { useMemo, useState } from "react";
|
4 |
import classNames from "classnames";
|
5 |
import { toast } from "sonner";
|
6 |
|
7 |
import { cn } from "@/lib/utils";
|
8 |
import { GridPattern } from "@/components/magic-ui/grid-pattern";
|
9 |
+
import { htmlTagToText } from "@/lib/html-tag-to-text";
|
10 |
|
11 |
export const Preview = ({
|
12 |
html,
|
|
|
16 |
device,
|
17 |
currentTab,
|
18 |
iframeRef,
|
19 |
+
isEditableModeEnabled,
|
20 |
+
onClickElement,
|
21 |
}: {
|
22 |
html: string;
|
23 |
isResizing: boolean;
|
|
|
26 |
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
27 |
device: "desktop" | "mobile";
|
28 |
currentTab: string;
|
29 |
+
isEditableModeEnabled?: boolean;
|
30 |
+
onClickElement?: (element: HTMLElement) => void;
|
31 |
}) => {
|
32 |
+
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(
|
33 |
+
null
|
34 |
+
);
|
35 |
+
|
36 |
+
// add event listener to the iframe to track hovered elements
|
37 |
+
const handleMouseOver = (event: MouseEvent) => {
|
38 |
+
if (iframeRef?.current) {
|
39 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
40 |
+
if (iframeDocument) {
|
41 |
+
const targetElement = event.target as HTMLElement;
|
42 |
+
if (
|
43 |
+
hoveredElement !== targetElement &&
|
44 |
+
targetElement !== iframeDocument.body
|
45 |
+
) {
|
46 |
+
setHoveredElement(targetElement);
|
47 |
+
targetElement.classList.add("hovered-element");
|
48 |
+
} else {
|
49 |
+
return setHoveredElement(null);
|
50 |
+
}
|
51 |
+
}
|
52 |
+
}
|
53 |
+
};
|
54 |
+
const handleMouseOut = () => {
|
55 |
+
setHoveredElement(null);
|
56 |
+
};
|
57 |
+
const handleClick = (event: MouseEvent) => {
|
58 |
+
if (iframeRef?.current) {
|
59 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
60 |
+
if (iframeDocument) {
|
61 |
+
const targetElement = event.target as HTMLElement;
|
62 |
+
if (targetElement !== iframeDocument.body) {
|
63 |
+
onClickElement?.(targetElement);
|
64 |
+
}
|
65 |
+
}
|
66 |
+
}
|
67 |
+
};
|
68 |
+
|
69 |
+
useUpdateEffect(() => {
|
70 |
+
const cleanupListeners = () => {
|
71 |
+
if (iframeRef?.current?.contentDocument) {
|
72 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
73 |
+
iframeDocument.removeEventListener("mouseover", handleMouseOver);
|
74 |
+
iframeDocument.removeEventListener("mouseout", handleMouseOut);
|
75 |
+
iframeDocument.removeEventListener("click", handleClick);
|
76 |
+
}
|
77 |
+
};
|
78 |
+
|
79 |
+
if (iframeRef?.current) {
|
80 |
+
const iframeDocument = iframeRef.current.contentDocument;
|
81 |
+
if (iframeDocument) {
|
82 |
+
// Clean up existing listeners first
|
83 |
+
cleanupListeners();
|
84 |
+
|
85 |
+
if (isEditableModeEnabled) {
|
86 |
+
iframeDocument.addEventListener("mouseover", handleMouseOver);
|
87 |
+
iframeDocument.addEventListener("mouseout", handleMouseOut);
|
88 |
+
iframeDocument.addEventListener("click", handleClick);
|
89 |
+
}
|
90 |
+
}
|
91 |
+
}
|
92 |
+
|
93 |
+
// Clean up when component unmounts or dependencies change
|
94 |
+
return cleanupListeners;
|
95 |
+
}, [iframeRef, isEditableModeEnabled]);
|
96 |
+
|
97 |
+
const selectedElement = useMemo(() => {
|
98 |
+
if (!isEditableModeEnabled) return null;
|
99 |
+
if (!hoveredElement) return null;
|
100 |
+
return hoveredElement;
|
101 |
+
}, [hoveredElement, isEditableModeEnabled]);
|
102 |
+
|
103 |
return (
|
104 |
<div
|
105 |
ref={ref}
|
|
|
127 |
"[mask-image:radial-gradient(900px_circle_at_center,white,transparent)]"
|
128 |
)}
|
129 |
/>
|
130 |
+
{!isAiWorking && hoveredElement && selectedElement && (
|
131 |
+
<div
|
132 |
+
className="cursor-pointer absolute bg-sky-500/10 border-[2px] border-dashed border-sky-500 rounded-r-lg rounded-b-lg p-3 z-10 pointer-events-none"
|
133 |
+
style={{
|
134 |
+
top:
|
135 |
+
selectedElement.getBoundingClientRect().top +
|
136 |
+
(iframeRef?.current?.contentWindow?.scrollY || 0) +
|
137 |
+
24,
|
138 |
+
left:
|
139 |
+
selectedElement.getBoundingClientRect().left +
|
140 |
+
(iframeRef?.current?.contentWindow?.scrollX || 0) +
|
141 |
+
24,
|
142 |
+
width: selectedElement.getBoundingClientRect().width,
|
143 |
+
height: selectedElement.getBoundingClientRect().height,
|
144 |
+
}}
|
145 |
+
>
|
146 |
+
<span className="bg-sky-500 rounded-t-md text-sm text-neutral-100 px-2 py-0.5 -translate-y-7 absolute top-0 left-0">
|
147 |
+
{htmlTagToText(selectedElement.tagName.toLowerCase())}
|
148 |
+
</span>
|
149 |
+
</div>
|
150 |
+
)}
|
151 |
<iframe
|
152 |
id="preview-iframe"
|
153 |
ref={iframeRef}
|
components/public/navigation/index.tsx
CHANGED
@@ -85,7 +85,7 @@ export default function Navigation() {
|
|
85 |
}
|
86 |
)}
|
87 |
>
|
88 |
-
<nav className="grid grid-cols-
|
89 |
<Link href="/" className="flex items-center gap-1">
|
90 |
<Image
|
91 |
src={Logo}
|
@@ -96,7 +96,7 @@ export default function Navigation() {
|
|
96 |
/>
|
97 |
<p className="font-sans text-white text-xl font-bold">DeepSite</p>
|
98 |
</Link>
|
99 |
-
<ul className="
|
100 |
{navigationLinks.map((link) => (
|
101 |
<li
|
102 |
key={link.name}
|
|
|
85 |
}
|
86 |
)}
|
87 |
>
|
88 |
+
<nav className="grid grid-cols-2 p-4 container mx-auto">
|
89 |
<Link href="/" className="flex items-center gap-1">
|
90 |
<Image
|
91 |
src={Logo}
|
|
|
96 |
/>
|
97 |
<p className="font-sans text-white text-xl font-bold">DeepSite</p>
|
98 |
</Link>
|
99 |
+
<ul className="items-center justify-center gap-6 hidden">
|
100 |
{navigationLinks.map((link) => (
|
101 |
<li
|
102 |
key={link.name}
|
components/space/ask-ai/index.tsx
CHANGED
@@ -8,34 +8,39 @@ import { Button } from "@/components/ui/button";
|
|
8 |
|
9 |
export const AskAi = () => {
|
10 |
return (
|
11 |
-
|
12 |
-
<
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
37 |
</div>
|
38 |
</div>
|
39 |
-
|
40 |
);
|
41 |
};
|
|
|
8 |
|
9 |
export const AskAi = () => {
|
10 |
return (
|
11 |
+
<>
|
12 |
+
<div className="bg-red-500 flex items-center justify-end">
|
13 |
+
No Follow-up mode
|
14 |
+
</div>
|
15 |
+
<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 group">
|
16 |
+
<textarea
|
17 |
+
rows={3}
|
18 |
+
className="w-full bg-transparent text-sm outline-none text-white placeholder:text-neutral-400 p-4 resize-none mb-1"
|
19 |
+
placeholder="Ask DeepSite anything..."
|
20 |
+
onChange={() => {}}
|
21 |
+
onKeyDown={() => {}}
|
22 |
+
/>
|
23 |
+
<div className="flex items-center justify-between gap-2 px-4 pb-3">
|
24 |
+
<div className="flex-1 flex justify-start">
|
25 |
+
<Button
|
26 |
+
size="iconXs"
|
27 |
+
variant="outline"
|
28 |
+
className="!border-neutral-600 !text-neutral-400 !hover:!border-neutral-500 hover:!text-neutral-300"
|
29 |
+
>
|
30 |
+
<TiUserAdd className="size-4" />
|
31 |
+
</Button>
|
32 |
+
</div>
|
33 |
+
<div className="flex items-center justify-end gap-2">
|
34 |
+
<Button variant="black" size="sm">
|
35 |
+
<PiGearSixFill className="size-4" />
|
36 |
+
Settings
|
37 |
+
</Button>
|
38 |
+
<Button size="iconXs">
|
39 |
+
<ArrowUp className="size-4" />
|
40 |
+
</Button>
|
41 |
+
</div>
|
42 |
</div>
|
43 |
</div>
|
44 |
+
</>
|
45 |
);
|
46 |
};
|
components/ui/checkbox.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
import * as React from "react";
|
4 |
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
5 |
+
import { CheckIcon } from "lucide-react";
|
6 |
+
|
7 |
+
import { cn } from "@/lib/utils";
|
8 |
+
|
9 |
+
function Checkbox({
|
10 |
+
className,
|
11 |
+
...props
|
12 |
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
13 |
+
return (
|
14 |
+
<CheckboxPrimitive.Root
|
15 |
+
data-slot="checkbox"
|
16 |
+
className={cn(
|
17 |
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-3.5 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
18 |
+
className
|
19 |
+
)}
|
20 |
+
{...props}
|
21 |
+
>
|
22 |
+
<CheckboxPrimitive.Indicator
|
23 |
+
data-slot="checkbox-indicator"
|
24 |
+
className="flex items-center justify-center text-current transition-none"
|
25 |
+
>
|
26 |
+
<CheckIcon className="size-3" />
|
27 |
+
</CheckboxPrimitive.Indicator>
|
28 |
+
</CheckboxPrimitive.Root>
|
29 |
+
);
|
30 |
+
}
|
31 |
+
|
32 |
+
export { Checkbox };
|
components/ui/collapsible.tsx
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
4 |
+
|
5 |
+
function Collapsible({
|
6 |
+
...props
|
7 |
+
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
8 |
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
9 |
+
}
|
10 |
+
|
11 |
+
function CollapsibleTrigger({
|
12 |
+
...props
|
13 |
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
14 |
+
return (
|
15 |
+
<CollapsiblePrimitive.CollapsibleTrigger
|
16 |
+
data-slot="collapsible-trigger"
|
17 |
+
{...props}
|
18 |
+
/>
|
19 |
+
)
|
20 |
+
}
|
21 |
+
|
22 |
+
function CollapsibleContent({
|
23 |
+
...props
|
24 |
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
25 |
+
return (
|
26 |
+
<CollapsiblePrimitive.CollapsibleContent
|
27 |
+
data-slot="collapsible-content"
|
28 |
+
{...props}
|
29 |
+
/>
|
30 |
+
)
|
31 |
+
}
|
32 |
+
|
33 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
components/ui/switch.tsx
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import * as React from "react"
|
4 |
+
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
5 |
+
|
6 |
+
import { cn } from "@/lib/utils"
|
7 |
+
|
8 |
+
function Switch({
|
9 |
+
className,
|
10 |
+
...props
|
11 |
+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
12 |
+
return (
|
13 |
+
<SwitchPrimitive.Root
|
14 |
+
data-slot="switch"
|
15 |
+
className={cn(
|
16 |
+
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
17 |
+
className
|
18 |
+
)}
|
19 |
+
{...props}
|
20 |
+
>
|
21 |
+
<SwitchPrimitive.Thumb
|
22 |
+
data-slot="switch-thumb"
|
23 |
+
className={cn(
|
24 |
+
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
25 |
+
)}
|
26 |
+
/>
|
27 |
+
</SwitchPrimitive.Root>
|
28 |
+
)
|
29 |
+
}
|
30 |
+
|
31 |
+
export { Switch }
|
lib/html-tag-to-text.ts
ADDED
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const htmlTagToText = (tagName: string): string => {
|
2 |
+
switch (tagName.toLowerCase()) {
|
3 |
+
case "h1":
|
4 |
+
return "Heading 1";
|
5 |
+
case "h2":
|
6 |
+
return "Heading 2";
|
7 |
+
case "h3":
|
8 |
+
return "Heading 3";
|
9 |
+
case "h4":
|
10 |
+
return "Heading 4";
|
11 |
+
case "h5":
|
12 |
+
return "Heading 5";
|
13 |
+
case "h6":
|
14 |
+
return "Heading 6";
|
15 |
+
case "p":
|
16 |
+
return "Text Paragraph";
|
17 |
+
case "span":
|
18 |
+
return "Inline Text";
|
19 |
+
case "button":
|
20 |
+
return "Button";
|
21 |
+
case "input":
|
22 |
+
return "Input Field";
|
23 |
+
case "select":
|
24 |
+
return "Select Dropdown";
|
25 |
+
case "textarea":
|
26 |
+
return "Text Area";
|
27 |
+
case "form":
|
28 |
+
return "Form";
|
29 |
+
case "table":
|
30 |
+
return "Table";
|
31 |
+
case "thead":
|
32 |
+
return "Table Header";
|
33 |
+
case "tbody":
|
34 |
+
return "Table Body";
|
35 |
+
case "tr":
|
36 |
+
return "Table Row";
|
37 |
+
case "th":
|
38 |
+
return "Table Header Cell";
|
39 |
+
case "td":
|
40 |
+
return "Table Data Cell";
|
41 |
+
case "nav":
|
42 |
+
return "Navigation";
|
43 |
+
case "header":
|
44 |
+
return "Header";
|
45 |
+
case "footer":
|
46 |
+
return "Footer";
|
47 |
+
case "section":
|
48 |
+
return "Section";
|
49 |
+
case "article":
|
50 |
+
return "Article";
|
51 |
+
case "aside":
|
52 |
+
return "Aside";
|
53 |
+
case "div":
|
54 |
+
return "Division";
|
55 |
+
case "main":
|
56 |
+
return "Main Content";
|
57 |
+
case "details":
|
58 |
+
return "Details";
|
59 |
+
case "summary":
|
60 |
+
return "Summary";
|
61 |
+
case "code":
|
62 |
+
return "Code Snippet";
|
63 |
+
case "pre":
|
64 |
+
return "Preformatted Text";
|
65 |
+
case "kbd":
|
66 |
+
return "Keyboard Input";
|
67 |
+
case "label":
|
68 |
+
return "Label";
|
69 |
+
case "canvas":
|
70 |
+
return "Canvas";
|
71 |
+
case "svg":
|
72 |
+
return "SVG Graphic";
|
73 |
+
case "video":
|
74 |
+
return "Video Player";
|
75 |
+
case "audio":
|
76 |
+
return "Audio Player";
|
77 |
+
case "iframe":
|
78 |
+
return "Embedded Frame";
|
79 |
+
case "link":
|
80 |
+
return "Link";
|
81 |
+
case "a":
|
82 |
+
return "Link";
|
83 |
+
case "img":
|
84 |
+
return "Image";
|
85 |
+
case "ul":
|
86 |
+
return "Unordered List";
|
87 |
+
case "ol":
|
88 |
+
return "Ordered List";
|
89 |
+
case "li":
|
90 |
+
return "List Item";
|
91 |
+
case "blockquote":
|
92 |
+
return "Blockquote";
|
93 |
+
default:
|
94 |
+
return tagName.charAt(0).toUpperCase() + tagName.slice(1);
|
95 |
+
}
|
96 |
+
};
|
package-lock.json
CHANGED
@@ -12,11 +12,14 @@
|
|
12 |
"@huggingface/inference": "^4.0.3",
|
13 |
"@monaco-editor/react": "^4.7.0",
|
14 |
"@radix-ui/react-avatar": "^1.1.10",
|
|
|
|
|
15 |
"@radix-ui/react-dialog": "^1.1.14",
|
16 |
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
17 |
"@radix-ui/react-popover": "^1.1.14",
|
18 |
"@radix-ui/react-select": "^2.2.5",
|
19 |
"@radix-ui/react-slot": "^1.2.3",
|
|
|
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",
|
@@ -1169,6 +1172,66 @@
|
|
1169 |
}
|
1170 |
}
|
1171 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1172 |
"node_modules/@radix-ui/react-collection": {
|
1173 |
"version": "1.1.7",
|
1174 |
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
@@ -1662,6 +1725,35 @@
|
|
1662 |
}
|
1663 |
}
|
1664 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1665 |
"node_modules/@radix-ui/react-tabs": {
|
1666 |
"version": "1.1.12",
|
1667 |
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
|
|
|
12 |
"@huggingface/inference": "^4.0.3",
|
13 |
"@monaco-editor/react": "^4.7.0",
|
14 |
"@radix-ui/react-avatar": "^1.1.10",
|
15 |
+
"@radix-ui/react-checkbox": "^1.3.2",
|
16 |
+
"@radix-ui/react-collapsible": "^1.1.11",
|
17 |
"@radix-ui/react-dialog": "^1.1.14",
|
18 |
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
19 |
"@radix-ui/react-popover": "^1.1.14",
|
20 |
"@radix-ui/react-select": "^2.2.5",
|
21 |
"@radix-ui/react-slot": "^1.2.3",
|
22 |
+
"@radix-ui/react-switch": "^1.2.5",
|
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",
|
|
|
1172 |
}
|
1173 |
}
|
1174 |
},
|
1175 |
+
"node_modules/@radix-ui/react-checkbox": {
|
1176 |
+
"version": "1.3.2",
|
1177 |
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
|
1178 |
+
"integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
|
1179 |
+
"license": "MIT",
|
1180 |
+
"dependencies": {
|
1181 |
+
"@radix-ui/primitive": "1.1.2",
|
1182 |
+
"@radix-ui/react-compose-refs": "1.1.2",
|
1183 |
+
"@radix-ui/react-context": "1.1.2",
|
1184 |
+
"@radix-ui/react-presence": "1.1.4",
|
1185 |
+
"@radix-ui/react-primitive": "2.1.3",
|
1186 |
+
"@radix-ui/react-use-controllable-state": "1.2.2",
|
1187 |
+
"@radix-ui/react-use-previous": "1.1.1",
|
1188 |
+
"@radix-ui/react-use-size": "1.1.1"
|
1189 |
+
},
|
1190 |
+
"peerDependencies": {
|
1191 |
+
"@types/react": "*",
|
1192 |
+
"@types/react-dom": "*",
|
1193 |
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
1194 |
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
1195 |
+
},
|
1196 |
+
"peerDependenciesMeta": {
|
1197 |
+
"@types/react": {
|
1198 |
+
"optional": true
|
1199 |
+
},
|
1200 |
+
"@types/react-dom": {
|
1201 |
+
"optional": true
|
1202 |
+
}
|
1203 |
+
}
|
1204 |
+
},
|
1205 |
+
"node_modules/@radix-ui/react-collapsible": {
|
1206 |
+
"version": "1.1.11",
|
1207 |
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz",
|
1208 |
+
"integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==",
|
1209 |
+
"license": "MIT",
|
1210 |
+
"dependencies": {
|
1211 |
+
"@radix-ui/primitive": "1.1.2",
|
1212 |
+
"@radix-ui/react-compose-refs": "1.1.2",
|
1213 |
+
"@radix-ui/react-context": "1.1.2",
|
1214 |
+
"@radix-ui/react-id": "1.1.1",
|
1215 |
+
"@radix-ui/react-presence": "1.1.4",
|
1216 |
+
"@radix-ui/react-primitive": "2.1.3",
|
1217 |
+
"@radix-ui/react-use-controllable-state": "1.2.2",
|
1218 |
+
"@radix-ui/react-use-layout-effect": "1.1.1"
|
1219 |
+
},
|
1220 |
+
"peerDependencies": {
|
1221 |
+
"@types/react": "*",
|
1222 |
+
"@types/react-dom": "*",
|
1223 |
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
1224 |
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
1225 |
+
},
|
1226 |
+
"peerDependenciesMeta": {
|
1227 |
+
"@types/react": {
|
1228 |
+
"optional": true
|
1229 |
+
},
|
1230 |
+
"@types/react-dom": {
|
1231 |
+
"optional": true
|
1232 |
+
}
|
1233 |
+
}
|
1234 |
+
},
|
1235 |
"node_modules/@radix-ui/react-collection": {
|
1236 |
"version": "1.1.7",
|
1237 |
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
|
|
1725 |
}
|
1726 |
}
|
1727 |
},
|
1728 |
+
"node_modules/@radix-ui/react-switch": {
|
1729 |
+
"version": "1.2.5",
|
1730 |
+
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
|
1731 |
+
"integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==",
|
1732 |
+
"license": "MIT",
|
1733 |
+
"dependencies": {
|
1734 |
+
"@radix-ui/primitive": "1.1.2",
|
1735 |
+
"@radix-ui/react-compose-refs": "1.1.2",
|
1736 |
+
"@radix-ui/react-context": "1.1.2",
|
1737 |
+
"@radix-ui/react-primitive": "2.1.3",
|
1738 |
+
"@radix-ui/react-use-controllable-state": "1.2.2",
|
1739 |
+
"@radix-ui/react-use-previous": "1.1.1",
|
1740 |
+
"@radix-ui/react-use-size": "1.1.1"
|
1741 |
+
},
|
1742 |
+
"peerDependencies": {
|
1743 |
+
"@types/react": "*",
|
1744 |
+
"@types/react-dom": "*",
|
1745 |
+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
1746 |
+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
1747 |
+
},
|
1748 |
+
"peerDependenciesMeta": {
|
1749 |
+
"@types/react": {
|
1750 |
+
"optional": true
|
1751 |
+
},
|
1752 |
+
"@types/react-dom": {
|
1753 |
+
"optional": true
|
1754 |
+
}
|
1755 |
+
}
|
1756 |
+
},
|
1757 |
"node_modules/@radix-ui/react-tabs": {
|
1758 |
"version": "1.1.12",
|
1759 |
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
|
package.json
CHANGED
@@ -13,11 +13,14 @@
|
|
13 |
"@huggingface/inference": "^4.0.3",
|
14 |
"@monaco-editor/react": "^4.7.0",
|
15 |
"@radix-ui/react-avatar": "^1.1.10",
|
|
|
|
|
16 |
"@radix-ui/react-dialog": "^1.1.14",
|
17 |
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
18 |
"@radix-ui/react-popover": "^1.1.14",
|
19 |
"@radix-ui/react-select": "^2.2.5",
|
20 |
"@radix-ui/react-slot": "^1.2.3",
|
|
|
21 |
"@radix-ui/react-tabs": "^1.1.12",
|
22 |
"@radix-ui/react-toggle": "^1.1.9",
|
23 |
"@radix-ui/react-toggle-group": "^1.1.10",
|
|
|
13 |
"@huggingface/inference": "^4.0.3",
|
14 |
"@monaco-editor/react": "^4.7.0",
|
15 |
"@radix-ui/react-avatar": "^1.1.10",
|
16 |
+
"@radix-ui/react-checkbox": "^1.3.2",
|
17 |
+
"@radix-ui/react-collapsible": "^1.1.11",
|
18 |
"@radix-ui/react-dialog": "^1.1.14",
|
19 |
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
20 |
"@radix-ui/react-popover": "^1.1.14",
|
21 |
"@radix-ui/react-select": "^2.2.5",
|
22 |
"@radix-ui/react-slot": "^1.2.3",
|
23 |
+
"@radix-ui/react-switch": "^1.2.5",
|
24 |
"@radix-ui/react-tabs": "^1.1.12",
|
25 |
"@radix-ui/react-toggle": "^1.1.9",
|
26 |
"@radix-ui/react-toggle-group": "^1.1.10",
|