"use client"; /* eslint-disable @typescript-eslint/no-explicit-any */ import { useState, useRef, useMemo } from "react"; import classNames from "classnames"; import { toast } from "sonner"; import { useLocalStorage, useUpdateEffect } from "react-use"; import { ArrowUp, ChevronDown, Crosshair } from "lucide-react"; import { FaStopCircle } from "react-icons/fa"; import ProModal from "@/components/pro-modal"; import { Button } from "@/components/ui/button"; import { MODELS } from "@/lib/providers"; import { HtmlHistory } from "@/types"; import { InviteFriends } from "@/components/invite-friends"; import { Settings } from "@/components/editor/ask-ai/settings"; import { LoginModal } from "@/components/login-modal"; import { ReImagine } from "@/components/editor/ask-ai/re-imagine"; import Loading from "@/components/loading"; import { Checkbox } from "@/components/ui/checkbox"; import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip"; import { TooltipContent } from "@radix-ui/react-tooltip"; import { SelectedHtmlElement } from "./selected-html-element"; import { FollowUpTooltip } from "./follow-up-tooltip"; import { isTheSameHtml } from "@/lib/compare-html-diff"; import { areErrorsFixable, deduplicateErrors, createErrorFixPrompt, } from "@/lib/error-formatter"; import { AlertCircle } from "lucide-react"; import type { PreviewError } from "@/types/preview-error"; import { MAX_AUTO_FIX_ATTEMPTS, AUTO_FIX_DELAY_MS } from "@/lib/constants"; // Reset function for cleaning up state export function resetAutoFixState( setAutoFixEnabled: (value: boolean) => void, setFixAttempts: (value: number) => void, ) { setAutoFixEnabled(false); setFixAttempts(0); } export function AskAI({ html, setHtml, onScrollToBottom, isAiWorking, setisAiWorking, isEditableModeEnabled = false, selectedElement, setSelectedElement, setIsEditableModeEnabled, onNewPrompt, onSuccess, previewErrors = [], }: { html: string; setHtml: (html: string) => void; onScrollToBottom: () => void; isAiWorking: boolean; onNewPrompt: (prompt: string) => void; htmlHistory?: HtmlHistory[]; setisAiWorking: React.Dispatch>; onSuccess: (h: string, p: string, n?: number[][]) => void; isEditableModeEnabled: boolean; setIsEditableModeEnabled: React.Dispatch>; selectedElement?: HTMLElement | null; setSelectedElement: React.Dispatch>; previewErrors?: PreviewError[]; }) { const refThink = useRef(null); const audio = useRef(null); const [open, setOpen] = useState(false); const [prompt, setPrompt] = useState(""); const [hasAsked, setHasAsked] = useState(false); const [previousPrompt, setPreviousPrompt] = useState(""); const [provider, setProvider] = useLocalStorage("provider", "auto"); const [model, setModel] = useLocalStorage("model", MODELS[0].value); const [openProvider, setOpenProvider] = useState(false); const [providerError, setProviderError] = useState(""); const [openProModal, setOpenProModal] = useState(false); const [think, setThink] = useState(undefined); const [openThink, setOpenThink] = useState(false); const [isThinking, setIsThinking] = useState(true); const [controller, setController] = useState(null); const [isFollowUp, setIsFollowUp] = useState(true); const [autoFixEnabled, setAutoFixEnabled] = useLocalStorage( "autoFixEnabled", false, ); const [fixAttempts, setFixAttempts] = useState(0); const [isFixingErrors, setIsFixingErrors] = useState(false); const selectedModel = useMemo(() => { return MODELS.find((m: { value: string }) => m.value === model); }, [model]); const callAi = async (redesignMarkdown?: string, errors?: PreviewError[]) => { if (isAiWorking) return; if (!redesignMarkdown && !prompt.trim() && (!errors || errors.length === 0)) return; setisAiWorking(true); setProviderError(""); setThink(""); setOpenThink(false); setIsThinking(true); let contentResponse = ""; let thinkResponse = ""; let lastRenderTime = 0; const abortController = new AbortController(); setController(abortController); try { onNewPrompt(prompt); if (isFollowUp && !redesignMarkdown && !isSameHtml) { const selectedElementHtml = selectedElement ? selectedElement.outerHTML : ""; const request = await fetch("/api/ask-ai", { method: "PUT", body: JSON.stringify({ prompt: errors && errors.length > 0 ? createErrorFixPrompt(errors, html) : prompt, provider, previousPrompt, model, html, selectedElementHtml, errors, }), headers: { "Content-Type": "application/json", "x-forwarded-for": window.location.hostname, }, signal: abortController.signal, }); if (request && request.body) { const res = await request.json(); if (!request.ok) { if (res.openLogin) { setOpen(true); } else if (res.openSelectProvider) { setOpenProvider(true); setProviderError(res.message); } else if (res.openProModal) { setOpenProModal(true); } else { toast.error(res.message); } setisAiWorking(false); return; } setHtml(res.html); toast.success("AI responded successfully"); setPreviousPrompt(prompt); setPrompt(""); setisAiWorking(false); setIsFixingErrors(false); onSuccess( res.html, errors ? "Fixed errors automatically" : prompt, res.updatedLines, ); if (audio.current) audio.current.play(); } } else { const request = await fetch("/api/ask-ai", { method: "POST", body: JSON.stringify({ prompt, provider, model, html: isSameHtml ? "" : html, redesignMarkdown, }), headers: { "Content-Type": "application/json", "x-forwarded-for": window.location.hostname, }, signal: abortController.signal, }); if (request && request.body) { const reader = request.body.getReader(); const decoder = new TextDecoder("utf-8"); const selectedModel = MODELS.find( (m: { value: string }) => m.value === model, ); let contentThink: string | undefined = undefined; const read = async () => { const { done, value } = await reader.read(); if (done) { const isJson = contentResponse.trim().startsWith("{") && contentResponse.trim().endsWith("}"); const jsonResponse = isJson ? JSON.parse(contentResponse) : null; if (jsonResponse && !jsonResponse.ok) { if (jsonResponse.openLogin) { setOpen(true); } else if (jsonResponse.openSelectProvider) { setOpenProvider(true); setProviderError(jsonResponse.message); } else if (jsonResponse.openProModal) { setOpenProModal(true); } else { toast.error(jsonResponse.message); } setisAiWorking(false); return; } toast.success("AI responded successfully"); setPreviousPrompt(prompt); setPrompt(""); setisAiWorking(false); setIsFixingErrors(false); setHasAsked(true); if (selectedModel?.isThinker) { setModel(MODELS[0].value); } if (audio.current) audio.current.play(); // Now we have the complete HTML including , so set it to be sure const finalDoc = contentResponse.match( /[\s\S]*<\/html>/, )?.[0]; if (finalDoc) { setHtml(finalDoc); } onSuccess(finalDoc ?? contentResponse, prompt); return; } const chunk = decoder.decode(value, { stream: true }); thinkResponse += chunk; if (selectedModel?.isThinker) { const thinkMatch = thinkResponse.match(/[\s\S]*/)?.[0]; if (thinkMatch && !thinkResponse?.includes("")) { if ((contentThink?.length ?? 0) < 3) { setOpenThink(true); } setThink(thinkMatch.replace("", "").trim()); contentThink += chunk; return read(); } } contentResponse += chunk; const newHtml = contentResponse.match( /[\s\S]*/, )?.[0]; if (newHtml) { setIsThinking(false); let partialDoc = newHtml; if ( partialDoc.includes("") && !partialDoc.includes("") ) { partialDoc += "\n"; } if ( partialDoc.includes("") ) { partialDoc += "\n"; } if (!partialDoc.includes("")) { partialDoc += "\n"; } // Throttle the re-renders to avoid flashing/flicker const now = Date.now(); if (now - lastRenderTime > 300) { setHtml(partialDoc); lastRenderTime = now; } if (partialDoc.length > 200) { onScrollToBottom(); } } read(); }; read(); } } } catch (error: any) { setisAiWorking(false); toast.error(error.message); if (error.openLogin) { setOpen(true); } } }; const stopController = () => { if (controller) { controller.abort(); setController(null); setisAiWorking(false); setThink(""); setOpenThink(false); setIsThinking(false); } }; useUpdateEffect(() => { if (refThink.current) { refThink.current.scrollTop = refThink.current.scrollHeight; } }, [think]); useUpdateEffect(() => { if (!isThinking) { setOpenThink(false); } }, [isThinking]); const isSameHtml = useMemo(() => { return isTheSameHtml(html); }, [html]); // Process and deduplicate errors const fixableErrors = useMemo(() => { if (!previewErrors || previewErrors.length === 0) return []; const dedupedErrors = deduplicateErrors(previewErrors); return dedupedErrors.filter((error) => areErrorsFixable([error])); }, [previewErrors]); // Auto-fix errors when detected useUpdateEffect(() => { if ( autoFixEnabled && fixableErrors.length > 0 && !isAiWorking && !isFixingErrors && fixAttempts < MAX_AUTO_FIX_ATTEMPTS ) { // Add a small delay to avoid immediate re-triggering const timer = setTimeout(() => { setIsFixingErrors(true); setFixAttempts((prev) => prev + 1); toast.info( `Auto-fixing ${fixableErrors.length} error${fixableErrors.length > 1 ? "s" : ""}...`, ); callAi(undefined, fixableErrors); }, AUTO_FIX_DELAY_MS); return () => clearTimeout(timer); } else if ( autoFixEnabled && fixableErrors.length > 0 && fixAttempts >= MAX_AUTO_FIX_ATTEMPTS && !isAiWorking ) { // Max attempts reached, notify user toast.error( `Failed to auto-fix after ${MAX_AUTO_FIX_ATTEMPTS} attempts. Please fix manually.`, ); setAutoFixEnabled(false); } }, [fixableErrors, autoFixEnabled, isAiWorking, fixAttempts]); // Reset fix attempts when errors are cleared useUpdateEffect(() => { if (fixableErrors.length === 0 && fixAttempts > 0) { setFixAttempts(0); setIsFixingErrors(false); if (autoFixEnabled) { toast.success("All errors fixed!"); } } }, [fixableErrors]); return (
{think && (
{ setOpenThink(!openThink); }} >

{isThinking ? "DeepSite is thinking..." : "DeepSite's plan"}

{think}

)} {selectedElement && (
setSelectedElement(null)} />
)}
{isAiWorking && (

AI is {isThinking ? "thinking" : "coding"}...{" "}

Stop generation
)} setPrompt(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { callAi(); } }} />
callAi(md)} /> {!isSameHtml && ( Select an element on the page to ask DeepSite edit it directly. )} {fixableErrors.length > 0 && ( {autoFixEnabled ? `Auto-fix is ON. Click to disable. ${fixAttempts > 0 ? `Attempted ${fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS} fixes.` : ""}` : "Click to enable auto-fix for detected errors"} )}
setOpen(false)} html={html} /> setOpenProModal(false)} /> {!isSameHtml && (
)}
); }