diff --git a/app/components/button.module.scss b/app/components/button.module.scss
index b882a0c1..88da9748 100644
--- a/app/components/button.module.scss
+++ b/app/components/button.module.scss
@@ -6,19 +6,21 @@
justify-content: center;
padding: 10px;
- box-shadow: var(--card-shadow);
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
user-select: none;
}
+.shadow {
+ box-shadow: var(--card-shadow);
+}
+
.border {
border: var(--border-in-light);
}
.icon-button:hover {
- filter: brightness(0.9);
border-color: var(--primary);
}
@@ -36,25 +38,7 @@
}
}
-@mixin dark-button {
- div:not(:global(.no-dark))>.icon-button-icon {
- filter: invert(0.5);
- }
-
- .icon-button:hover {
- filter: brightness(1.2);
- }
-}
-
-:global(.dark) {
- @include dark-button;
-}
-
-@media (prefers-color-scheme: dark) {
- @include dark-button;
-}
-
.icon-button-text {
margin-left: 5px;
font-size: 12px;
-}
\ No newline at end of file
+}
diff --git a/app/components/button.tsx b/app/components/button.tsx
index 43b699b6..f40a4e8f 100644
--- a/app/components/button.tsx
+++ b/app/components/button.tsx
@@ -7,6 +7,7 @@ export function IconButton(props: {
icon: JSX.Element;
text?: string;
bordered?: boolean;
+ shadow?: boolean;
className?: string;
title?: string;
}) {
@@ -14,10 +15,13 @@ export function IconButton(props: {
{props.icon}
{props.text && (
diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx
new file mode 100644
index 00000000..8ad2b7dc
--- /dev/null
+++ b/app/components/chat-list.tsx
@@ -0,0 +1,73 @@
+import { useState, useRef, useEffect, useLayoutEffect } from "react";
+import DeleteIcon from "../icons/delete.svg";
+import styles from "./home.module.scss";
+
+import {
+ Message,
+ SubmitKey,
+ useChatStore,
+ ChatSession,
+ BOT_HELLO,
+} from "../store";
+
+import Locale from "../locales";
+import { isMobileScreen } from "../utils";
+
+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={() =>
+ (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
+ removeSession(i)
+ }
+ />
+ ))}
+
+ );
+}
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
new file mode 100644
index 00000000..b52baa12
--- /dev/null
+++ b/app/components/chat.module.scss
@@ -0,0 +1,71 @@
+.prompt-toast {
+ position: absolute;
+ bottom: -50px;
+ z-index: 999;
+ display: flex;
+ justify-content: center;
+ width: calc(100% - 40px);
+
+ .prompt-toast-inner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 12px;
+ background-color: var(--white);
+ color: var(--black);
+
+ border: var(--border-in-light);
+ box-shadow: var(--card-shadow);
+ padding: 10px 20px;
+ border-radius: 100px;
+
+ .prompt-toast-content {
+ margin-left: 10px;
+ }
+ }
+}
+
+.context-prompt {
+ .context-prompt-row {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ margin-bottom: 10px;
+
+ .context-role {
+ margin-right: 10px;
+ }
+
+ .context-content {
+ flex: 1;
+ max-width: 100%;
+ text-align: left;
+ }
+
+ .context-delete-button {
+ margin-left: 10px;
+ }
+ }
+
+ .context-prompt-button {
+ flex: 1;
+ }
+}
+
+.memory-prompt {
+ margin-top: 20px;
+
+ .memory-prompt-title {
+ font-size: 12px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ }
+
+ .memory-prompt-content {
+ background-color: var(--gray);
+ border-radius: 6px;
+ padding: 10px;
+ font-size: 12px;
+ user-select: text;
+ }
+}
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
new file mode 100644
index 00000000..4a80fe14
--- /dev/null
+++ b/app/components/chat.tsx
@@ -0,0 +1,622 @@
+import { useDebouncedCallback } from "use-debounce";
+import { useState, useRef, useEffect, useLayoutEffect } from "react";
+
+import SendWhiteIcon from "../icons/send-white.svg";
+import BrainIcon from "../icons/brain.svg";
+import ExportIcon from "../icons/export.svg";
+import MenuIcon from "../icons/menu.svg";
+import CopyIcon from "../icons/copy.svg";
+import DownloadIcon from "../icons/download.svg";
+import LoadingIcon from "../icons/three-dots.svg";
+import BotIcon from "../icons/bot.svg";
+import AddIcon from "../icons/add.svg";
+import DeleteIcon from "../icons/delete.svg";
+
+import {
+ Message,
+ SubmitKey,
+ useChatStore,
+ ChatSession,
+ BOT_HELLO,
+ ROLES,
+} from "../store";
+
+import {
+ copyToClipboard,
+ downloadAs,
+ isMobileScreen,
+ selectOrCopy,
+} from "../utils";
+
+import dynamic from "next/dynamic";
+
+import { ControllerPool } from "../requests";
+import { Prompt, usePromptStore } from "../store/prompt";
+import Locale from "../locales";
+
+import { IconButton } from "./button";
+import styles from "./home.module.scss";
+import chatStyle from "./chat.module.scss";
+
+import { Modal, showModal, showToast } from "./ui-lib";
+
+const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
+ 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 (
+
+
+
+ );
+}
+
+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 PromptToast(props: {
+ showModal: boolean;
+ setShowModal: (_: boolean) => void;
+}) {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const context = session.context;
+
+ const addContextPrompt = (prompt: Message) => {
+ chatStore.updateCurrentSession((session) => {
+ session.context.push(prompt);
+ });
+ };
+
+ const removeContextPrompt = (i: number) => {
+ chatStore.updateCurrentSession((session) => {
+ session.context.splice(i, 1);
+ });
+ };
+
+ const updateContextPrompt = (i: number, prompt: Message) => {
+ chatStore.updateCurrentSession((session) => {
+ session.context[i] = prompt;
+ });
+ };
+
+ return (
+
+
props.setShowModal(true)}
+ >
+
+
+ {Locale.Context.Toast(context.length)}
+
+
+ {props.showModal && (
+
+
props.setShowModal(false)}
+ actions={[
+ }
+ bordered
+ text={Locale.Memory.Copy}
+ onClick={() => copyToClipboard(session.memoryPrompt)}
+ />,
+ ]}
+ >
+ <>
+ {" "}
+
+ {context.map((c, i) => (
+
+
+
+ updateContextPrompt(i, {
+ ...c,
+ content: e.target.value as any,
+ })
+ }
+ >
+ }
+ className={chatStyle["context-delete-button"]}
+ onClick={() => removeContextPrompt(i)}
+ />
+
+ ))}
+
+
+ }
+ text={Locale.Context.Add}
+ bordered
+ className={chatStyle["context-prompt-button"]}
+ onClick={() =>
+ addContextPrompt({
+ role: "system",
+ content: "",
+ date: "",
+ })
+ }
+ />
+
+
+
+
+ {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
+ {session.messages.length})
+
+
+ {session.memoryPrompt || Locale.Memory.EmptyContent}
+
+
+ >
+
+
+ )}
+
+ );
+}
+
+function useSubmitHandler() {
+ const config = useChatStore((state) => state.config);
+ const submitKey = config.submitKey;
+
+ const shouldSubmit = (e: React.KeyboardEvent
) => {
+ if (e.key !== "Enter") return false;
+ if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
+ return (
+ (config.submitKey === SubmitKey.AltEnter && e.altKey) ||
+ (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
+ (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
+ (config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
+ (config.submitKey === SubmitKey.Enter &&
+ !e.altKey &&
+ !e.ctrlKey &&
+ !e.shiftKey &&
+ !e.metaKey)
+ );
+ };
+
+ return {
+ submitKey,
+ shouldSubmit,
+ };
+}
+
+export function PromptHints(props: {
+ prompts: Prompt[];
+ onPromptSelect: (prompt: Prompt) => void;
+}) {
+ if (props.prompts.length === 0) return null;
+
+ return (
+
+ {props.prompts.map((prompt, i) => (
+
props.onPromptSelect(prompt)}
+ >
+
{prompt.title}
+
{prompt.content}
+
+ ))}
+
+ );
+}
+
+function useScrollToBottom() {
+ // for auto-scroll
+ const scrollRef = useRef(null);
+ const [autoScroll, setAutoScroll] = useState(true);
+
+ // auto scroll
+ useLayoutEffect(() => {
+ const dom = scrollRef.current;
+ if (dom && autoScroll) {
+ setTimeout(() => (dom.scrollTop = dom.scrollHeight), 500);
+ }
+ });
+
+ return {
+ scrollRef,
+ autoScroll,
+ setAutoScroll,
+ };
+}
+
+export function Chat(props: {
+ showSideBar?: () => void;
+ sideBarShowing?: boolean;
+}) {
+ type RenderMessage = Message & { preview?: boolean };
+
+ const chatStore = useChatStore();
+ const [session, sessionIndex] = useChatStore((state) => [
+ state.currentSession(),
+ state.currentSessionIndex,
+ ]);
+ const fontSize = useChatStore((state) => state.config.fontSize);
+
+ const inputRef = useRef(null);
+ const [userInput, setUserInput] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const { submitKey, shouldSubmit } = useSubmitHandler();
+ const { scrollRef, setAutoScroll } = useScrollToBottom();
+
+ // prompt hints
+ const promptStore = usePromptStore();
+ const [promptHints, setPromptHints] = useState([]);
+ const onSearch = useDebouncedCallback(
+ (text: string) => {
+ setPromptHints(promptStore.search(text));
+ },
+ 100,
+ { leading: true, trailing: true },
+ );
+
+ const onPromptSelect = (prompt: Prompt) => {
+ setUserInput(prompt.content);
+ setPromptHints([]);
+ inputRef.current?.focus();
+ };
+
+ const scrollInput = () => {
+ const dom = inputRef.current;
+ if (!dom) return;
+ const paddingBottomNum: number = parseInt(
+ window.getComputedStyle(dom).paddingBottom,
+ 10,
+ );
+ dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
+ };
+
+ // only search prompts when user input is short
+ const SEARCH_TEXT_LIMIT = 30;
+ const onInput = (text: string) => {
+ scrollInput();
+ setUserInput(text);
+ const n = text.trim().length;
+
+ // clear search results
+ if (n === 0) {
+ setPromptHints([]);
+ } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
+ // check if need to trigger auto completion
+ if (text.startsWith("/")) {
+ let searchText = text.slice(1);
+ if (searchText.length === 0) {
+ searchText = " ";
+ }
+ onSearch(searchText);
+ }
+ }
+ };
+
+ // submit user input
+ const onUserSubmit = () => {
+ if (userInput.length <= 0) return;
+ setIsLoading(true);
+ chatStore.onUserInput(userInput).then(() => setIsLoading(false));
+ setUserInput("");
+ setPromptHints([]);
+ inputRef.current?.focus();
+ };
+
+ // stop response
+ const onUserStop = (messageIndex: number) => {
+ ControllerPool.stop(sessionIndex, messageIndex);
+ };
+
+ // check if should send message
+ const onInputKeyDown = (e: React.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);
+ chatStore
+ .onUserInput(messages[i].content)
+ .then(() => setIsLoading(false));
+ inputRef.current?.focus();
+ return;
+ }
+ }
+ };
+
+ const config = useChatStore((state) => state.config);
+
+ const context: RenderMessage[] = session.context.slice();
+
+ if (
+ context.length === 0 &&
+ session.messages.at(0)?.content !== BOT_HELLO.content
+ ) {
+ context.push(BOT_HELLO);
+ }
+
+ // preview messages
+ const messages = context
+ .concat(session.messages as RenderMessage[])
+ .concat(
+ isLoading
+ ? [
+ {
+ role: "assistant",
+ content: "……",
+ date: new Date().toLocaleString(),
+ preview: true,
+ },
+ ]
+ : [],
+ )
+ .concat(
+ userInput.length > 0 && config.sendPreviewBubble
+ ? [
+ {
+ role: "user",
+ content: userInput,
+ date: new Date().toLocaleString(),
+ preview: false,
+ },
+ ]
+ : [],
+ );
+
+ const [showPromptModal, setShowPromptModal] = useState(false);
+
+ return (
+
+
+
+
{
+ const newTopic = prompt(Locale.Chat.Rename, session.topic);
+ if (newTopic && newTopic !== session.topic) {
+ chatStore.updateCurrentSession(
+ (session) => (session.topic = newTopic!),
+ );
+ }
+ }}
+ >
+ {session.topic}
+
+
+ {Locale.Chat.SubTitle(session.messages.length)}
+
+
+
+
+ }
+ bordered
+ title={Locale.Chat.Actions.ChatList}
+ onClick={props?.showSideBar}
+ />
+
+
+ }
+ bordered
+ title={Locale.Chat.Actions.CompressedHistory}
+ onClick={() => {
+ setShowPromptModal(true);
+ }}
+ />
+
+
+ }
+ 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}
+
+ )}
+
inputRef.current?.blur()}
+ >
+ {!isUser &&
+ !(message.preview || message.content.length === 0) && (
+
+ {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)}
+ onDoubleClickCapture={() => {
+ if (!isMobileScreen()) return;
+ setUserInput(message.content);
+ }}
+ >
+
+
+ )}
+
+ {!isUser && !message.preview && (
+
+
+ {message.date.toLocaleString()}
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/app/components/home.module.scss b/app/components/home.module.scss
index 764805d8..24b1f1bf 100644
--- a/app/components/home.module.scss
+++ b/app/components/home.module.scss
@@ -218,6 +218,7 @@
flex: 1;
overflow: auto;
padding: 20px;
+ position: relative;
}
.chat-body-title {
diff --git a/app/components/home.tsx b/app/components/home.tsx
index 7ed35dfb..13db93e2 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -1,7 +1,6 @@
"use client";
import { useState, useRef, useEffect, useLayoutEffect } from "react";
-import { useDebouncedCallback } from "use-debounce";
import { IconButton } from "./button";
import styles from "./home.module.scss";
@@ -9,33 +8,31 @@ 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 {
+ Message,
+ SubmitKey,
+ useChatStore,
+ ChatSession,
+ BOT_HELLO,
+} from "../store";
import {
copyToClipboard,
downloadAs,
- isIOS,
isMobileScreen,
selectOrCopy,
} from "../utils";
import Locale from "../locales";
+import { ChatList } from "./chat-list";
+import { Chat } from "./chat";
import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
-import { ControllerPool } from "../requests";
-import { Prompt, usePromptStore } from "../store/prompt";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -46,469 +43,10 @@ export function Loading(props: { noLogo?: boolean }) {
);
}
-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={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
- />
- ))}
-
- );
-}
-
-function useSubmitHandler() {
- const config = useChatStore((state) => state.config);
- const submitKey = config.submitKey;
-
- const shouldSubmit = (e: React.KeyboardEvent) => {
- if (e.key !== "Enter") return false;
- if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
- return (
- (config.submitKey === SubmitKey.AltEnter && e.altKey) ||
- (config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
- (config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
- (config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
- (config.submitKey === SubmitKey.Enter &&
- !e.altKey &&
- !e.ctrlKey &&
- !e.shiftKey &&
- !e.metaKey)
- );
- };
-
- return {
- submitKey,
- shouldSubmit,
- };
-}
-
-export function PromptHints(props: {
- prompts: Prompt[];
- onPromptSelect: (prompt: Prompt) => void;
-}) {
- if (props.prompts.length === 0) return null;
-
- return (
-
- {props.prompts.map((prompt, i) => (
-
props.onPromptSelect(prompt)}
- >
-
{prompt.title}
-
{prompt.content}
-
- ))}
-
- );
-}
-
-export function Chat(props: {
- showSideBar?: () => void;
- sideBarShowing?: boolean;
-}) {
- type RenderMessage = Message & { preview?: boolean };
-
- const chatStore = useChatStore();
- const [session, sessionIndex] = useChatStore((state) => [
- state.currentSession(),
- state.currentSessionIndex,
- ]);
- const fontSize = useChatStore((state) => state.config.fontSize);
-
- const inputRef = useRef(null);
- const [userInput, setUserInput] = useState("");
- const [isLoading, setIsLoading] = useState(false);
- const { submitKey, shouldSubmit } = useSubmitHandler();
-
- // prompt hints
- const promptStore = usePromptStore();
- const [promptHints, setPromptHints] = useState([]);
- const onSearch = useDebouncedCallback(
- (text: string) => {
- setPromptHints(promptStore.search(text));
- },
- 100,
- { leading: true, trailing: true },
- );
-
- const onPromptSelect = (prompt: Prompt) => {
- setUserInput(prompt.content);
- setPromptHints([]);
- inputRef.current?.focus();
- };
-
- const scrollInput = () => {
- const dom = inputRef.current;
- if (!dom) return;
- const paddingBottomNum: number = parseInt(
- window.getComputedStyle(dom).paddingBottom,
- 10,
- );
- dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
- };
-
- // only search prompts when user input is short
- const SEARCH_TEXT_LIMIT = 30;
- const onInput = (text: string) => {
- scrollInput();
- setUserInput(text);
- const n = text.trim().length;
-
- // clear search results
- if (n === 0) {
- setPromptHints([]);
- } else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
- // check if need to trigger auto completion
- if (text.startsWith("/") && text.length > 1) {
- onSearch(text.slice(1));
- }
- }
- };
-
- // submit user input
- const onUserSubmit = () => {
- if (userInput.length <= 0) return;
- setIsLoading(true);
- chatStore.onUserInput(userInput).then(() => setIsLoading(false));
- setUserInput("");
- setPromptHints([]);
- inputRef.current?.focus();
- };
-
- // stop response
- const onUserStop = (messageIndex: number) => {
- console.log(ControllerPool, sessionIndex, messageIndex);
- ControllerPool.stop(sessionIndex, messageIndex);
- };
-
- // check if should send message
- const onInputKeyDown = (e: React.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);
- chatStore
- .onUserInput(messages[i].content)
- .then(() => setIsLoading(false));
- inputRef.current?.focus();
- return;
- }
- }
- };
-
- // for auto-scroll
- const latestMessageRef = useRef(null);
- const [autoScroll, setAutoScroll] = useState(true);
-
- const config = useChatStore((state) => state.config);
-
- // preview messages
- const messages = (session.messages as RenderMessage[])
- .concat(
- isLoading
- ? [
- {
- role: "assistant",
- content: "……",
- date: new Date().toLocaleString(),
- preview: true,
- },
- ]
- : [],
- ).concat(
- userInput.length > 0 && config.sendPreviewBubble
- ? [
- {
- role: "user",
- content: userInput,
- date: new Date().toLocaleString(),
- preview: false,
- },
- ]
- : [],
- );
-
- // auto scroll
- useLayoutEffect(() => {
- setTimeout(() => {
- const dom = latestMessageRef.current;
- const inputDom = inputRef.current;
-
- // only scroll when input overlaped message body
- let shouldScroll = true;
- if (dom && inputDom) {
- const domRect = dom.getBoundingClientRect();
- const inputRect = inputDom.getBoundingClientRect();
- shouldScroll = domRect.top > inputRect.top;
- }
-
- if (dom && autoScroll && shouldScroll) {
- dom.scrollIntoView({
- block: "end",
- });
- }
- }, 500);
- });
-
- return (
-
-
-
-
{
- const newTopic = prompt(Locale.Chat.Rename, session.topic);
- if (newTopic && newTopic !== session.topic) {
- chatStore.updateCurrentSession(
- (session) => (session.topic = newTopic!),
- );
- }
- }}
- >
- {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.preview || message.content.length === 0) && (
-
- {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)}
- onDoubleClickCapture={() => {
- if (!isMobileScreen()) return;
- setUserInput(message.content);
- }}
- >
-
-
- )}
-
- {!isUser && !message.preview && (
-
-
- {message.date.toLocaleString()}
-
-
- )}
-
-
- );
- })}
-
- -
-
-
-
-
-
- );
-}
-
function useSwitchTheme() {
const config = useChatStore((state) => state.config);
@@ -530,64 +68,6 @@ function useSwitchTheme() {
}, [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)}
- />,
- ],
- });
-}
-
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState(false);
@@ -669,11 +149,12 @@ export function Home() {
setOpenSettings(true);
setShowSideBar(false);
}}
+ shadow
/>