browser adapter: fix gap in flexbox (chronium <84)

This commit is contained in:
Zhang Minghan 2023-10-22 07:58:07 +08:00
parent 663fcf809b
commit cb5829b85a
6 changed files with 512 additions and 524 deletions

View File

@ -148,7 +148,6 @@
display: flex;
flex-direction: row;
width: 80%;
gap: 8px;
margin: 0 auto;
max-width: 680px;
@ -160,6 +159,7 @@
border-radius: var(--radius);
border: 1px solid hsl(var(--border-hover));
letter-spacing: 1px;
margin-right: 8px;
}
.action {

View File

@ -274,12 +274,12 @@
align-items: center;
flex-wrap: nowrap;
width: 100%;
gap: 4px;
height: min-content;
.chat-box {
position: relative;
flex-grow: 1;
margin: 0 4px;
}
.input-box {

View File

@ -0,0 +1,77 @@
import {useEffect, useRef, useState} from "react";
import {Message} from "../../conversation/types.ts";
import {useSelector} from "react-redux";
import {selectCurrent, selectMessages} from "../../store/chat.ts";
import {Button} from "../ui/button.tsx";
import {ChevronDown} from "lucide-react";
import MessageSegment from "../Message.tsx";
import {connectionEvent} from "../../events/connection.ts";
function ChatInterface() {
const ref = useRef(null);
const [scroll, setScroll] = useState(false);
const messages: Message[] = useSelector(selectMessages);
const current: number = useSelector(selectCurrent);
function listenScrolling() {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
const offset = el.scrollHeight - el.scrollTop - el.clientHeight;
setScroll(offset > 100);
}
useEffect(
function () {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.scrollTop = el.scrollHeight;
listenScrolling();
},
[messages],
);
useEffect(() => {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.addEventListener("scroll", listenScrolling);
}, [ref]);
return (
<>
<div className={`chat-content`} ref={ref}>
<div className={`scroll-action ${scroll ? "active" : ""}`}>
<Button
variant={`outline`}
size={`icon`}
onClick={() => {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.scrollTo({
top: el.scrollHeight,
behavior: "smooth",
});
}}
>
<ChevronDown className={`h-4 w-4`} />
</Button>
</div>
{messages.map((message, i) => (
<MessageSegment
message={message}
end={i === messages.length - 1}
onEvent={(e: string) => {
connectionEvent.emit({
id: current,
event: e,
});
}}
key={i}
/>
))}
</div>
</>
);
}
export default ChatInterface;

View File

@ -0,0 +1,172 @@
import {useTranslation} from "react-i18next";
import React, {useEffect, useRef, useState} from "react";
import FileProvider, {FileObject} from "../FileProvider.tsx";
import {useDispatch, useSelector} from "react-redux";
import {selectAuthenticated, selectInit} from "../../store/auth.ts";
import {selectMessages, selectModel, selectWeb, setWeb} from "../../store/chat.ts";
import {manager} from "../../conversation/manager.ts";
import {formatMessage} from "../../utils.ts";
import ChatInterface from "./ChatInterface.tsx";
import {Button} from "../ui/button.tsx";
import router from "../../router.ts";
import {ChevronRight, FolderKanban, Globe} from "lucide-react";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "../ui/tooltip.tsx";
import {Toggle} from "../ui/toggle.tsx";
import {Input} from "../ui/input.tsx";
import EditorProvider from "../EditorProvider.tsx";
import ModelSelector from "./ModelSelector.tsx";
function ChatWrapper() {
const { t } = useTranslation();
const [file, setFile] = useState<FileObject>({
name: "",
content: "",
});
const [clearEvent, setClearEvent] = useState<() => void>(() => {});
const [input, setInput] = useState("");
const dispatch = useDispatch();
const init = useSelector(selectInit);
const auth = useSelector(selectAuthenticated);
const model = useSelector(selectModel);
const web = useSelector(selectWeb);
const messages = useSelector(selectMessages);
const target = useRef(null);
manager.setDispatch(dispatch);
function clearFile() {
clearEvent?.();
}
async function processSend(
data: string,
auth: boolean,
model: string,
web: boolean,
): Promise<boolean> {
const message: string = formatMessage(file, data);
if (message.length > 0 && data.trim().length > 0) {
if (await manager.send(t, auth, { message, web, model, type: "chat" })) {
clearFile();
return true;
}
}
return false;
}
async function handleSend(auth: boolean, model: string, web: boolean) {
// because of the function wrapper, we need to update the selector state using props.
if (await processSend(input, auth, model, web)) {
setInput("");
}
}
window.addEventListener("load", () => {
const el = document.getElementById("input");
if (el) el.focus();
});
useEffect(() => {
if (!init) return;
const search = new URLSearchParams(window.location.search);
const query = (search.get("q") || "").trim();
if (query.length > 0) processSend(query, auth, model, web).then();
window.history.replaceState({}, "", "/");
}, [init]);
return (
<div className={`chat-container`}>
<div className={`chat-wrapper`}>
{messages.length > 0 ? (
<ChatInterface />
) : (
<div className={`chat-product`}>
<Button
variant={`outline`}
onClick={() => router.navigate("/generate")}
>
<FolderKanban className={`h-4 w-4 mr-1.5`} />
{t("generate.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
</div>
)}
<div className={`chat-input`}>
<div className={`input-wrapper`}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Toggle
aria-label={t("chat.web-aria")}
defaultPressed={true}
onPressedChange={(state: boolean) =>
dispatch(setWeb(state))
}
variant={`outline`}
>
<Globe className={`h-4 w-4 web ${web ? "enable" : ""}`} />
</Toggle>
</TooltipTrigger>
<TooltipContent>
<p className={`tooltip`}>{t("chat.web")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className={`chat-box`}>
{auth && (
<FileProvider
id={`file`}
className={`file`}
onChange={setFile}
maxLength={4000 * 1.25}
setClearEvent={setClearEvent}
/>
)}
<Input
id={`input`}
className={`input-box`}
ref={target}
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setInput(e.target.value)
}
placeholder={t("chat.placeholder")}
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") await handleSend(auth, model, web);
}}
/>
<EditorProvider
value={input}
onChange={setInput}
className={`editor`}
id={`editor`}
placeholder={t("chat.placeholder")}
maxLength={8000}
/>
</div>
<Button
size={`icon`}
variant="outline"
className={`send-button`}
onClick={() => handleSend(auth, model, web)}
>
<svg
className="h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="m21.426 11.095-17-8A1 1 0 0 0 3.03 4.242l1.212 4.849L12 12l-7.758 2.909-1.212 4.849a.998.998 0 0 0 1.396 1.147l17-8a1 1 0 0 0 0-1.81z"></path>
</svg>
</Button>
</div>
<div className={`input-options`}>
<ModelSelector />
</div>
</div>
</div>
</div>
);
}
export default ChatWrapper;

View File

@ -0,0 +1,259 @@
import {useTranslation} from "react-i18next";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "../../store";
import {selectAuthenticated} from "../../store/auth.ts";
import {selectCurrent, selectHistory} from "../../store/chat.ts";
import {useRef, useState} from "react";
import {ConversationInstance} from "../../conversation/types.ts";
import {useToast} from "../ui/use-toast.ts";
import {copyClipboard, extractMessage, filterMessage, mobile, useAnimation, useEffectAsync} from "../../utils.ts";
import {deleteConversation, toggleConversation, updateConversationList} from "../../conversation/history.ts";
import {Button} from "../ui/button.tsx";
import {setMenu} from "../../store/menu.ts";
import {Copy, LogIn, Plus, RotateCw} from "lucide-react";
import ConversationSegment from "./ConversationSegment.tsx";
import {
AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription, AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle
} from "../ui/alert-dialog.tsx";
import {shareConversation} from "../../conversation/sharing.ts";
import {Input} from "../ui/input.tsx";
import {login} from "../../conf.ts";
function SideBar() {
const { t } = useTranslation();
const dispatch = useDispatch();
const open = useSelector((state: RootState) => state.menu.open);
const auth = useSelector(selectAuthenticated);
const current = useSelector(selectCurrent);
const [operateConversation, setOperateConversation] = useState<{
target: ConversationInstance | null;
type: string;
}>({ target: null, type: "" });
const { toast } = useToast();
const history: ConversationInstance[] = useSelector(selectHistory);
const refresh = useRef(null);
const [shared, setShared] = useState<string>("");
useEffectAsync(async () => {
await updateConversationList(dispatch);
}, []);
// @ts-ignore
return (
<div className={`sidebar ${open ? "open" : ""}`}>
{auth ? (
<div className={`sidebar-content`}>
<div className={`sidebar-action`}>
<Button
variant={`ghost`}
size={`icon`}
onClick={async () => {
await toggleConversation(dispatch, -1);
if (mobile) dispatch(setMenu(false));
}}
>
<Plus className={`h-4 w-4`} />
</Button>
<div className={`grow`} />
<Button
className={`refresh-action`}
variant={`ghost`}
size={`icon`}
id={`refresh`}
ref={refresh}
onClick={() => {
const hook = useAnimation(refresh, "active", 500);
updateConversationList(dispatch)
.catch(() =>
toast({
title: t("conversation.refresh-failed"),
description: t("conversation.refresh-failed-prompt"),
}),
)
.finally(hook);
}}
>
<RotateCw className={`h-4 w-4`} />
</Button>
</div>
<div className={`conversation-list`}>
{history.length ? (
history.map((conversation, i) => (
<ConversationSegment
operate={setOperateConversation}
conversation={conversation}
current={current}
key={i}
/>
))
) : (
<div className={`empty`}>{t("conversation.empty")}</div>
)}
</div>
<AlertDialog
open={
operateConversation.type === "delete" &&
!!operateConversation.target
}
onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" });
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("conversation.remove-title")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("conversation.remove-description")}
<strong className={`conversation-name`}>
{extractMessage(
filterMessage(operateConversation?.target?.name || ""),
)}
</strong>
{t("end")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("conversation.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
if (
await deleteConversation(
dispatch,
operateConversation?.target?.id || -1,
)
)
toast({
title: t("conversation.delete-success"),
description: t("conversation.delete-success-prompt"),
});
else
toast({
title: t("conversation.delete-failed"),
description: t("conversation.delete-failed-prompt"),
});
setOperateConversation({ target: null, type: "" });
}}
>
{t("conversation.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={
operateConversation.type === "share" &&
!!operateConversation.target
}
onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" });
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("share.title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("share.description")}
<strong className={`conversation-name`}>
{extractMessage(
filterMessage(operateConversation?.target?.name || ""),
)}
</strong>
{t("end")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("conversation.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
const resp = await shareConversation(
operateConversation?.target?.id || -1,
);
if (resp.status)
setShared(`${location.origin}/share/${resp.data}`);
else
toast({
title: t("share.failed"),
description: resp.message,
});
setOperateConversation({ target: null, type: "" });
}}
>
{t("share.title")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={shared.length > 0}
onOpenChange={(open) => {
if (!open) {
setShared("");
setOperateConversation({ target: null, type: "" });
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("share.success")}</AlertDialogTitle>
<AlertDialogDescription>
<div className={`share-wrapper mt-4 mb-2`}>
<Input value={shared} />
<Button
variant={`default`}
size={`icon`}
onClick={async () => {
await copyClipboard(shared);
toast({
title: t("share.copied"),
description: t("share.copied-description"),
});
}}
>
<Copy className={`h-4 w-4`} />
</Button>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("close")}</AlertDialogCancel>
<AlertDialogAction
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
window.open(shared, "_blank");
}}
>
{t("share.view")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
) : (
<Button className={`login-action`} variant={`default`} onClick={login}>
<LogIn className={`h-3 w-3 mr-2`} /> {t("login")}
</Button>
)}
</div>
);
}
export default SideBar;

View File

@ -1,527 +1,7 @@
import "../assets/home.less";
import "../assets/chat.less";
import { Input } from "../components/ui/input.tsx";
import { Toggle } from "../components/ui/toggle.tsx";
import {
ChevronDown,
ChevronRight,
Copy,
FolderKanban,
Globe,
LogIn,
Plus,
RotateCw,
} from "lucide-react";
import { Button } from "../components/ui/button.tsx";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../components/ui/tooltip.tsx";
import { useDispatch, useSelector } from "react-redux";
import type { RootState } from "../store";
import { selectAuthenticated, selectInit } from "../store/auth.ts";
import { login } from "../conf.ts";
import {
deleteConversation,
toggleConversation,
updateConversationList,
} from "../conversation/history.ts";
import { shareConversation } from "../conversation/sharing.ts";
import React, { useEffect, useRef, useState } from "react";
import {
filterMessage,
extractMessage,
formatMessage,
mobile,
useAnimation,
useEffectAsync,
copyClipboard,
} from "../utils.ts";
import { useToast } from "../components/ui/use-toast.ts";
import { ConversationInstance, Message } from "../conversation/types.ts";
import {
selectCurrent,
selectModel,
selectHistory,
selectMessages,
selectWeb,
setWeb,
} from "../store/chat.ts";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../components/ui/alert-dialog.tsx";
import { manager } from "../conversation/manager.ts";
import { useTranslation } from "react-i18next";
import MessageSegment from "../components/Message.tsx";
import { setMenu } from "../store/menu.ts";
import FileProvider, { FileObject } from "../components/FileProvider.tsx";
import router from "../router.ts";
import EditorProvider from "../components/EditorProvider.tsx";
import ConversationSegment from "../components/home/ConversationSegment.tsx";
import { connectionEvent } from "../events/connection.ts";
import ModelSelector from "../components/home/ModelSelector.tsx";
function SideBar() {
const { t } = useTranslation();
const dispatch = useDispatch();
const open = useSelector((state: RootState) => state.menu.open);
const auth = useSelector(selectAuthenticated);
const current = useSelector(selectCurrent);
const [operateConversation, setOperateConversation] = useState<{
target: ConversationInstance | null;
type: string;
}>({ target: null, type: "" });
const { toast } = useToast();
const history: ConversationInstance[] = useSelector(selectHistory);
const refresh = useRef(null);
const [shared, setShared] = useState<string>("");
useEffectAsync(async () => {
await updateConversationList(dispatch);
}, []);
// @ts-ignore
return (
<div className={`sidebar ${open ? "open" : ""}`}>
{auth ? (
<div className={`sidebar-content`}>
<div className={`sidebar-action`}>
<Button
variant={`ghost`}
size={`icon`}
onClick={async () => {
await toggleConversation(dispatch, -1);
if (mobile) dispatch(setMenu(false));
}}
>
<Plus className={`h-4 w-4`} />
</Button>
<div className={`grow`} />
<Button
className={`refresh-action`}
variant={`ghost`}
size={`icon`}
id={`refresh`}
ref={refresh}
onClick={() => {
const hook = useAnimation(refresh, "active", 500);
updateConversationList(dispatch)
.catch(() =>
toast({
title: t("conversation.refresh-failed"),
description: t("conversation.refresh-failed-prompt"),
}),
)
.finally(hook);
}}
>
<RotateCw className={`h-4 w-4`} />
</Button>
</div>
<div className={`conversation-list`}>
{history.length ? (
history.map((conversation, i) => (
<ConversationSegment
operate={setOperateConversation}
conversation={conversation}
current={current}
key={i}
/>
))
) : (
<div className={`empty`}>{t("conversation.empty")}</div>
)}
</div>
<AlertDialog
open={
operateConversation.type === "delete" &&
!!operateConversation.target
}
onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" });
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("conversation.remove-title")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("conversation.remove-description")}
<strong className={`conversation-name`}>
{extractMessage(
filterMessage(operateConversation?.target?.name || ""),
)}
</strong>
{t("end")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("conversation.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
if (
await deleteConversation(
dispatch,
operateConversation?.target?.id || -1,
)
)
toast({
title: t("conversation.delete-success"),
description: t("conversation.delete-success-prompt"),
});
else
toast({
title: t("conversation.delete-failed"),
description: t("conversation.delete-failed-prompt"),
});
setOperateConversation({ target: null, type: "" });
}}
>
{t("conversation.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={
operateConversation.type === "share" &&
!!operateConversation.target
}
onOpenChange={(open) => {
if (!open) setOperateConversation({ target: null, type: "" });
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("share.title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("share.description")}
<strong className={`conversation-name`}>
{extractMessage(
filterMessage(operateConversation?.target?.name || ""),
)}
</strong>
{t("end")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("conversation.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
const resp = await shareConversation(
operateConversation?.target?.id || -1,
);
if (resp.status)
setShared(`${location.origin}/share/${resp.data}`);
else
toast({
title: t("share.failed"),
description: resp.message,
});
setOperateConversation({ target: null, type: "" });
}}
>
{t("share.title")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog
open={shared.length > 0}
onOpenChange={(open) => {
if (!open) {
setShared("");
setOperateConversation({ target: null, type: "" });
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("share.success")}</AlertDialogTitle>
<AlertDialogDescription>
<div className={`share-wrapper mt-4 mb-2`}>
<Input value={shared} />
<Button
variant={`default`}
size={`icon`}
onClick={async () => {
await copyClipboard(shared);
toast({
title: t("share.copied"),
description: t("share.copied-description"),
});
}}
>
<Copy className={`h-4 w-4`} />
</Button>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("close")}</AlertDialogCancel>
<AlertDialogAction
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
window.open(shared, "_blank");
}}
>
{t("share.view")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
) : (
<Button className={`login-action`} variant={`default`} onClick={login}>
<LogIn className={`h-3 w-3 mr-2`} /> {t("login")}
</Button>
)}
</div>
);
}
function ChatInterface() {
const ref = useRef(null);
const [scroll, setScroll] = useState(false);
const messages: Message[] = useSelector(selectMessages);
const current: number = useSelector(selectCurrent);
function listenScrolling() {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
const offset = el.scrollHeight - el.scrollTop - el.clientHeight;
setScroll(offset > 100);
}
useEffect(
function () {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.scrollTop = el.scrollHeight;
listenScrolling();
},
[messages],
);
useEffect(() => {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.addEventListener("scroll", listenScrolling);
}, [ref]);
return (
<>
<div className={`chat-content`} ref={ref}>
<div className={`scroll-action ${scroll ? "active" : ""}`}>
<Button
variant={`outline`}
size={`icon`}
onClick={() => {
if (!ref.current) return;
const el = ref.current as HTMLDivElement;
el.scrollTo({
top: el.scrollHeight,
behavior: "smooth",
});
}}
>
<ChevronDown className={`h-4 w-4`} />
</Button>
</div>
{messages.map((message, i) => (
<MessageSegment
message={message}
end={i === messages.length - 1}
onEvent={(e: string) => {
connectionEvent.emit({
id: current,
event: e,
});
}}
key={i}
/>
))}
</div>
</>
);
}
function ChatWrapper() {
const { t } = useTranslation();
const [file, setFile] = useState<FileObject>({
name: "",
content: "",
});
const [clearEvent, setClearEvent] = useState<() => void>(() => {});
const [input, setInput] = useState("");
const dispatch = useDispatch();
const init = useSelector(selectInit);
const auth = useSelector(selectAuthenticated);
const model = useSelector(selectModel);
const web = useSelector(selectWeb);
const messages = useSelector(selectMessages);
const target = useRef(null);
manager.setDispatch(dispatch);
function clearFile() {
clearEvent?.();
}
async function processSend(
data: string,
auth: boolean,
model: string,
web: boolean,
): Promise<boolean> {
const message: string = formatMessage(file, data);
if (message.length > 0 && data.trim().length > 0) {
if (await manager.send(t, auth, { message, web, model, type: "chat" })) {
clearFile();
return true;
}
}
return false;
}
async function handleSend(auth: boolean, model: string, web: boolean) {
// because of the function wrapper, we need to update the selector state using props.
if (await processSend(input, auth, model, web)) {
setInput("");
}
}
window.addEventListener("load", () => {
const el = document.getElementById("input");
if (el) el.focus();
});
useEffect(() => {
if (!init) return;
const search = new URLSearchParams(window.location.search);
const query = (search.get("q") || "").trim();
if (query.length > 0) processSend(query, auth, model, web).then();
window.history.replaceState({}, "", "/");
}, [init]);
return (
<div className={`chat-container`}>
<div className={`chat-wrapper`}>
{messages.length > 0 ? (
<ChatInterface />
) : (
<div className={`chat-product`}>
<Button
variant={`outline`}
onClick={() => router.navigate("/generate")}
>
<FolderKanban className={`h-4 w-4 mr-1.5`} />
{t("generate.title")}
<ChevronRight className={`h-4 w-4 ml-2`} />
</Button>
</div>
)}
<div className={`chat-input`}>
<div className={`input-wrapper`}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Toggle
aria-label={t("chat.web-aria")}
defaultPressed={true}
onPressedChange={(state: boolean) =>
dispatch(setWeb(state))
}
variant={`outline`}
>
<Globe className={`h-4 w-4 web ${web ? "enable" : ""}`} />
</Toggle>
</TooltipTrigger>
<TooltipContent>
<p className={`tooltip`}>{t("chat.web")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className={`chat-box`}>
{auth && (
<FileProvider
id={`file`}
className={`file`}
onChange={setFile}
maxLength={4000 * 1.25}
setClearEvent={setClearEvent}
/>
)}
<Input
id={`input`}
className={`input-box`}
ref={target}
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setInput(e.target.value)
}
placeholder={t("chat.placeholder")}
onKeyDown={async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") await handleSend(auth, model, web);
}}
/>
<EditorProvider
value={input}
onChange={setInput}
className={`editor`}
id={`editor`}
placeholder={t("chat.placeholder")}
maxLength={8000}
/>
</div>
<Button
size={`icon`}
variant="outline"
className={`send-button`}
onClick={() => handleSend(auth, model, web)}
>
<svg
className="h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="m21.426 11.095-17-8A1 1 0 0 0 3.03 4.242l1.212 4.849L12 12l-7.758 2.909-1.212 4.849a.998.998 0 0 0 1.396 1.147l17-8a1 1 0 0 0 0-1.81z"></path>
</svg>
</Button>
</div>
<div className={`input-options`}>
<ModelSelector />
</div>
</div>
</div>
</div>
);
}
import ChatWrapper from "../components/home/ChatWrapper.tsx";
import SideBar from "../components/home/SideBar.tsx";
function Home() {
return (