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",
|