"use client";
import { useState, useRef, useEffect } 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 } from "./ui-lib";
import { copyToClipboard, downloadAs, isIOS, selectOrCopy } from "../utils";
import Locale from "../locales";
import dynamic from "next/dynamic";
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
);
};
return {
submitKey,
shouldSubmit,
};
}
export function Chat(props: { showSideBar?: () => void }) {
type RenderMessage = Message & { preview?: boolean };
const session = useChatStore((state) => state.currentSession());
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const onUserInput = useChatStore((state) => state.onUserInput);
const onUserSubmit = () => {
if (userInput.length <= 0) return;
setIsLoading(true);
onUserInput(userInput).then(() => setIsLoading(false));
setUserInput("");
};
const onInputKeyDown = (e: KeyboardEvent) => {
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
}
};
const latestMessageRef = useRef(null);
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,
},
]
: []
);
useEffect(() => {
const dom = latestMessageRef.current;
if (dom && !isIOS()) {
dom.scrollIntoView({
behavior: "smooth",
block: "end",
});
}
});
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}
)}
{(message.preview || message.content.length === 0) &&
!isUser ? (
) : (
{
if (selectOrCopy(e.currentTarget, message.content)) {
e.preventDefault();
}
}}
>
)}
{!isUser && !message.preview && (
{message.date.toLocaleString()}
)}
);
})}
-
);
}
function useSwitchTheme() {
const config = useChatStore((state) => state.config);
useEffect(() => {
document.body.classList.remove("light");
document.body.classList.remove("dark");
if (config.theme === "dark") {
document.body.classList.add("dark");
} else if (config.theme === "light") {
document.body.classList.add("light");
}
}, [config.theme]);
}
function exportMessages(messages: Message[], topic: string) {
const mdText =
`# ${topic}\n\n` +
messages
.map((m) => {
return m.role === "user" ? `## ${m.content}` : m.content.trim();
})
.join("\n\n");
const filename = `${topic}.md`;
showModal({
title: Locale.Export.Title,
children: (
),
actions: [
}
bordered
text={Locale.Export.Copy}
onClick={() => copyToClipboard(mdText)}
/>,
}
bordered
text={Locale.Export.Download}
onClick={() => downloadAs(mdText, filename)}
/>,
],
});
}
function showMemoryPrompt(session: ChatSession) {
showModal({
title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
children: (
{session.memoryPrompt || Locale.Memory.EmptyContent}
),
actions: [
}
bordered
text={Locale.Memory.Copy}
onClick={() => copyToClipboard(session.memoryPrompt)}
/>,
],
});
}
export function Home() {
const [createNewSession, currentIndex, removeSession] = useChatStore(
(state) => [
state.newSession,
state.currentSessionIndex,
state.removeSession,
]
);
const loading = !useChatStore?.persist?.hasHydrated();
const [showSideBar, setShowSideBar] = useState(true);
// setting
const [openSettings, setOpenSettings] = useState(false);
const config = useChatStore((state) => state.config);
useSwitchTheme();
if (loading) {
return ;
}
return (
ChatGPT Next
Build your own AI assistant.
{
setOpenSettings(false);
setShowSideBar(false);
}}
>
}
onClick={() => {
if (confirm(Locale.Home.DeleteChat)) {
removeSession(currentIndex);
}
}}
/>
}
onClick={() => {
setOpenSettings(true);
setShowSideBar(false);
}}
/>
}
text={Locale.Home.NewChat}
onClick={createNewSession}
/>
{openSettings ? (
{
setOpenSettings(false);
setShowSideBar(true);
}}
/>
) : (
setShowSideBar(true)} />
)}
);
}