"use client"; import { useState, useRef, useEffect, useLayoutEffect } from "react"; import { IconButton } from "./button"; import styles from "./home.module.scss"; import SettingsIcon from "../icons/settings.svg"; import GithubIcon from "../icons/github.svg"; import ChatGptIcon from "../icons/chatgpt.svg"; import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; import ExportIcon from "../icons/export.svg"; import BotIcon from "../icons/bot.svg"; import AddIcon from "../icons/add.svg"; import DeleteIcon from "../icons/delete.svg"; import LoadingIcon from "../icons/three-dots.svg"; import MenuIcon from "../icons/menu.svg"; import CloseIcon from "../icons/close.svg"; import CopyIcon from "../icons/copy.svg"; import DownloadIcon from "../icons/download.svg"; import { Message, SubmitKey, useChatStore, ChatSession } from "../store"; import { showModal, showToast } from "./ui-lib"; import { copyToClipboard, downloadAs, isIOS, selectOrCopy } from "../utils"; import Locale from "../locales"; import dynamic from "next/dynamic"; import { REPO_URL } from "../constant"; import { ControllerPool } from "../requests"; export function Loading(props: { noLogo?: boolean }) { return (
{!props.noLogo && }
); } const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, { loading: () => , }); export function Avatar(props: { role: Message["role"] }) { const config = useChatStore((state) => state.config); if (props.role === "assistant") { return ; } return (
); } export function ChatItem(props: { onClick?: () => void; onDelete?: () => void; title: string; count: number; time: string; selected: boolean; }) { return (
{props.title}
{Locale.ChatItem.ChatItemCount(props.count)}
{props.time}
); } export function ChatList() { const [sessions, selectedIndex, selectSession, removeSession] = useChatStore( (state) => [ state.sessions, state.currentSessionIndex, state.selectSession, state.removeSession, ] ); return (
{sessions.map((item, i) => ( selectSession(i)} onDelete={() => removeSession(i)} /> ))}
); } function useSubmitHandler() { const config = useChatStore((state) => state.config); const submitKey = config.submitKey; const shouldSubmit = (e: KeyboardEvent) => { if (e.key !== "Enter") return false; return ( (config.submitKey === SubmitKey.AltEnter && e.altKey) || (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) || (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) || (config.submitKey === SubmitKey.Enter && !e.altKey && !e.ctrlKey && !e.shiftKey) ); }; return { submitKey, shouldSubmit, }; } export function Chat(props: { showSideBar?: () => void }) { type RenderMessage = Message & { preview?: boolean }; const [session, sessionIndex] = useChatStore((state) => [ state.currentSession(), state.currentSessionIndex, ]); const [userInput, setUserInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); const onUserInput = useChatStore((state) => state.onUserInput); // submit user input const onUserSubmit = () => { if (userInput.length <= 0) return; setIsLoading(true); onUserInput(userInput).then(() => setIsLoading(false)); setUserInput(""); }; // stop response const onUserStop = (messageIndex: number) => { console.log(ControllerPool, sessionIndex, messageIndex); ControllerPool.stop(sessionIndex, messageIndex); }; // check if should send message const onInputKeyDown = (e: KeyboardEvent) => { if (shouldSubmit(e)) { onUserSubmit(); e.preventDefault(); } }; const onRightClick = (e: any, message: Message) => { // auto fill user input if (message.role === "user") { setUserInput(message.content); } // copy to clipboard if (selectOrCopy(e.currentTarget, message.content)) { e.preventDefault(); } }; const onResend = (botIndex: number) => { // find last user input message and resend for (let i = botIndex; i >= 0; i -= 1) { if (messages[i].role === "user") { setIsLoading(true); onUserInput(messages[i].content).then(() => setIsLoading(false)); return; } } }; // for auto-scroll const latestMessageRef = useRef(null); // wont scroll while hovering messages const [autoScroll, setAutoScroll] = useState(false); // preview messages const messages = (session.messages as RenderMessage[]) .concat( isLoading ? [ { role: "assistant", content: "……", date: new Date().toLocaleString(), preview: true, }, ] : [] ) .concat( userInput.length > 0 ? [ { role: "user", content: userInput, date: new Date().toLocaleString(), preview: true, }, ] : [] ); // auto scroll useLayoutEffect(() => { setTimeout(() => { const dom = latestMessageRef.current; if (dom && !isIOS() && autoScroll) { dom.scrollIntoView({ behavior: "smooth", block: "end", }); } }, 500); }); return (
{session.topic}
{Locale.Chat.SubTitle(session.messages.length)}
} bordered title={Locale.Chat.Actions.ChatList} onClick={props?.showSideBar} />
} bordered title={Locale.Chat.Actions.CompressedHistory} onClick={() => { showMemoryPrompt(session); }} />
} bordered title={Locale.Chat.Actions.Export} onClick={() => { exportMessages(session.messages, session.topic); }} />
{messages.map((message, i) => { const isUser = message.role === "user"; return (
{(message.preview || message.streaming) && (
{Locale.Chat.Typing}
)}
{!isUser && (
{message.streaming ? (
onUserStop(i)} > {Locale.Chat.Actions.Stop}
) : (
onResend(i)} > {Locale.Chat.Actions.Retry}
)}
copyToClipboard(message.content)} > {Locale.Chat.Actions.Copy}
)} {(message.preview || message.content.length === 0) && !isUser ? ( ) : (
onRightClick(e, message)} >
)}
{!isUser && !message.preview && (
{message.date.toLocaleString()}
)}
); })}
-