enzostvs's picture
enzostvs HF Staff
update providers
4c01d00
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useRef } from "react";
import { RiSparkling2Fill } from "react-icons/ri";
import { GrSend } from "react-icons/gr";
import classNames from "classnames";
import { toast } from "sonner";
import { useLocalStorage, useUpdateEffect } from "react-use";
import { ChevronDown } from "lucide-react";
import Login from "../login/login";
import { defaultHTML } from "../../../utils/consts";
import SuccessSound from "./../../assets/success.mp3";
import Settings from "../settings/settings";
import ProModal from "../pro-modal/pro-modal";
import { Button } from "../ui/button";
// @ts-expect-error not needed
import { MODELS } from "./../../../utils/providers";
function AskAI({
html,
setHtml,
onScrollToBottom,
isAiWorking,
setisAiWorking,
onNewPrompt,
onSuccess,
}: {
html: string;
setHtml: (html: string) => void;
onScrollToBottom: () => void;
isAiWorking: boolean;
onNewPrompt: (prompt: string) => void;
setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
onSuccess: (h: string, p: string) => void;
}) {
const refThink = useRef<HTMLDivElement | null>(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<string | undefined>(undefined);
const [openThink, setOpenThink] = useState(false);
const [isThinking, setIsThinking] = useState(true);
const audio = new Audio(SuccessSound);
audio.volume = 0.5;
const callAi = async () => {
if (isAiWorking || !prompt.trim()) return;
setisAiWorking(true);
setProviderError("");
setThink("");
setOpenThink(false);
setIsThinking(true);
let contentResponse = "";
let lastRenderTime = 0;
try {
onNewPrompt(prompt);
const request = await fetch("/api/ask-ai", {
method: "POST",
body: JSON.stringify({
prompt,
provider,
model,
...(html === defaultHTML ? {} : { html }),
...(previousPrompt ? { previousPrompt } : {}),
}),
headers: {
"Content-Type": "application/json",
},
});
if (request && request.body) {
if (!request.ok) {
const res = await request.json();
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;
}
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) {
toast.success("AI responded successfully");
setPreviousPrompt(prompt);
setPrompt("");
setisAiWorking(false);
setHasAsked(true);
audio.play();
// Now we have the complete HTML including </html>, so set it to be sure
const finalDoc = contentResponse.match(
/<!DOCTYPE html>[\s\S]*<\/html>/
)?.[0];
if (finalDoc) {
setHtml(finalDoc);
}
onSuccess(finalDoc ?? contentResponse, prompt);
return;
}
const chunk = decoder.decode(value, { stream: true });
contentResponse += chunk;
if (selectedModel?.isThinker) {
const thinkMatch = contentResponse.match(/<think>[\s\S]*/)?.[0];
if (thinkMatch && !contentResponse?.includes("</think>")) {
if ((contentThink?.length ?? 0) < 3) {
setOpenThink(true);
}
setThink(thinkMatch.replace("<think>", "").trim());
contentThink += chunk;
}
}
const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
if (newHtml) {
setIsThinking(false);
let partialDoc = newHtml;
if (!partialDoc.includes("</html>")) {
partialDoc += "\n</html>";
}
// 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);
}
}
};
useUpdateEffect(() => {
if (refThink.current) {
refThink.current.scrollTop = refThink.current.scrollHeight;
}
}, [think]);
useUpdateEffect(() => {
if (!isThinking) {
setOpenThink(false);
}
}, [isThinking]);
// TODO: auto scroll is not working properly, fix it
return (
<div className="bg-neutral-800 border border-neutral-700 rounded-lg ring-[5px] focus-within:ring-sky-500/50 ring-transparent z-10 absolute bottom-3 left-3 w-[calc(100%-20px)] group">
{think && (
<div className="w-full border-b border-neutral-700 relative overflow-hidden">
<header
className="flex items-center justify-between px-5 py-2.5 group hover:bg-neutral-600/20 transition-colors duration-200 cursor-pointer"
onClick={() => {
setOpenThink(!openThink);
}}
>
<p className="text-sm font-medium text-neutral-300 group-hover:text-neutral-200 transition-colors duration-200">
{isThinking ? "AI is thinking..." : "AI's plan"}
</p>
<ChevronDown
className={classNames(
"size-4 text-neutral-400 group-hover:text-neutral-300 transition-all duration-200",
{
"rotate-180": openThink,
}
)}
/>
</header>
<main
ref={refThink}
className={classNames(
"overflow-y-auto transition-all duration-200 ease-in-out",
{
"max-h-[0px]": !openThink,
"min-h-[250px] max-h-[250px] border-t border-neutral-700":
openThink,
}
)}
>
<p className="text-[13px] text-neutral-400 whitespace-pre-line px-5 pb-4 pt-3">
{think}
</p>
</main>
</div>
)}
<div
className={classNames(
"w-full relative flex items-center justify-between pl-3.5 p-2 lg:p-2 lg:pl-4",
{
"animate-pulse": isAiWorking,
}
)}
>
{isAiWorking && (
<div className="absolute bg-neutral-800 rounded-lg bottom-0 left-10 w-[calc(100%-92px)] h-full z-1 flex items-center justify-start max-lg:text-sm">
<p className="text-neutral-400 font-code">
AI is {isThinking ? "thinking" : "coding"}...
</p>
</div>
)}
<RiSparkling2Fill className="size-5 text-neutral-400 group-focus-within:text-sky-500" />
<input
type="text"
disabled={isAiWorking}
className="w-full bg-transparent max-lg:text-sm outline-none px-3 text-white placeholder:text-neutral-400 font-code"
placeholder={
hasAsked ? "What do you want to ask AI next?" : "Ask AI anything..."
}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
callAi();
}
}}
/>
<div className="flex items-center justify-end gap-2">
{/* <SpeechPrompt setPrompt={setPrompt} /> */}
<Settings
provider={provider as string}
model={model as string}
onChange={setProvider}
onModelChange={setModel}
open={openProvider}
error={providerError}
onClose={setOpenProvider}
/>
<Button
variant="pink"
size="icon"
disabled={isAiWorking || !prompt.trim()}
onClick={callAi}
>
<GrSend className="-translate-x-[1px]" />
</Button>
</div>
</div>
<div
className={classNames(
"h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
{
"opacity-0 pointer-events-none": !open,
}
)}
onClick={() => setOpen(false)}
></div>
<div
className={classNames(
"absolute top-0 -translate-y-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
{
"opacity-0 pointer-events-none": !open,
}
)}
>
<Login html={html}>
<p className="text-gray-500 text-sm mb-3">
You reached the limit of free AI usage. Please login to continue.
</p>
</Login>
</div>
<ProModal
html={html}
open={openProModal}
onClose={() => setOpenProModal(false)}
/>
</div>
);
}
export default AskAI;