|
@@ -1,5 +1,11 @@
|
|
|
import { useDebouncedCallback } from "use-debounce";
|
|
|
-import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
|
|
+import React, {
|
|
|
+ useState,
|
|
|
+ useRef,
|
|
|
+ useEffect,
|
|
|
+ useLayoutEffect,
|
|
|
+ useMemo,
|
|
|
+} from "react";
|
|
|
|
|
|
import SendWhiteIcon from "../icons/send-white.svg";
|
|
|
import BrainIcon from "../icons/brain.svg";
|
|
@@ -7,13 +13,14 @@ import RenameIcon from "../icons/rename.svg";
|
|
|
import ExportIcon from "../icons/share.svg";
|
|
|
import ReturnIcon from "../icons/return.svg";
|
|
|
import CopyIcon from "../icons/copy.svg";
|
|
|
-import DownloadIcon from "../icons/download.svg";
|
|
|
import LoadingIcon from "../icons/three-dots.svg";
|
|
|
import PromptIcon from "../icons/prompt.svg";
|
|
|
import MaskIcon from "../icons/mask.svg";
|
|
|
import MaxIcon from "../icons/max.svg";
|
|
|
import MinIcon from "../icons/min.svg";
|
|
|
import ResetIcon from "../icons/reload.svg";
|
|
|
+import BreakIcon from "../icons/break.svg";
|
|
|
+import SettingsIcon from "../icons/chat-settings.svg";
|
|
|
|
|
|
import LightIcon from "../icons/light.svg";
|
|
|
import DarkIcon from "../icons/dark.svg";
|
|
@@ -22,7 +29,7 @@ import BottomIcon from "../icons/bottom.svg";
|
|
|
import StopIcon from "../icons/pause.svg";
|
|
|
|
|
|
import {
|
|
|
- Message,
|
|
|
+ ChatMessage,
|
|
|
SubmitKey,
|
|
|
useChatStore,
|
|
|
BOT_HELLO,
|
|
@@ -43,7 +50,7 @@ import {
|
|
|
|
|
|
import dynamic from "next/dynamic";
|
|
|
|
|
|
-import { ControllerPool } from "../requests";
|
|
|
+import { ChatControllerPool } from "../client/controller";
|
|
|
import { Prompt, usePromptStore } from "../store/prompt";
|
|
|
import Locale from "../locales";
|
|
|
|
|
@@ -51,56 +58,21 @@ import { IconButton } from "./button";
|
|
|
import styles from "./home.module.scss";
|
|
|
import chatStyle from "./chat.module.scss";
|
|
|
|
|
|
-import { ListItem, Modal, showModal } from "./ui-lib";
|
|
|
+import { ListItem, Modal } from "./ui-lib";
|
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
|
-import { LAST_INPUT_KEY, Path } from "../constant";
|
|
|
+import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
|
|
|
import { Avatar } from "./emoji";
|
|
|
import { MaskAvatar, MaskConfig } from "./mask";
|
|
|
import { useMaskStore } from "../store/mask";
|
|
|
import { useCommand } from "../command";
|
|
|
+import { prettyObject } from "../utils/format";
|
|
|
+import { ExportMessageModal } from "./exporter";
|
|
|
+import { getClientConfig } from "../config/client";
|
|
|
|
|
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|
|
loading: () => <LoadingIcon />,
|
|
|
});
|
|
|
|
|
|
-function exportMessages(messages: Message[], topic: string) {
|
|
|
- const mdText =
|
|
|
- `# ${topic}\n\n` +
|
|
|
- messages
|
|
|
- .map((m) => {
|
|
|
- return m.role === "user"
|
|
|
- ? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
|
|
|
- : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
|
|
|
- })
|
|
|
- .join("\n\n");
|
|
|
- const filename = `${topic}.md`;
|
|
|
-
|
|
|
- showModal({
|
|
|
- title: Locale.Export.Title,
|
|
|
- children: (
|
|
|
- <div className="markdown-body">
|
|
|
- <pre className={styles["export-content"]}>{mdText}</pre>
|
|
|
- </div>
|
|
|
- ),
|
|
|
- actions: [
|
|
|
- <IconButton
|
|
|
- key="copy"
|
|
|
- icon={<CopyIcon />}
|
|
|
- bordered
|
|
|
- text={Locale.Export.Copy}
|
|
|
- onClick={() => copyToClipboard(mdText)}
|
|
|
- />,
|
|
|
- <IconButton
|
|
|
- key="download"
|
|
|
- icon={<DownloadIcon />}
|
|
|
- bordered
|
|
|
- text={Locale.Export.Download}
|
|
|
- onClick={() => downloadAs(mdText, filename)}
|
|
|
- />,
|
|
|
- ],
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
export function SessionConfigModel(props: { onClose: () => void }) {
|
|
|
const chatStore = useChatStore();
|
|
|
const session = chatStore.currentSession();
|
|
@@ -118,9 +90,13 @@ export function SessionConfigModel(props: { onClose: () => void }) {
|
|
|
icon={<ResetIcon />}
|
|
|
bordered
|
|
|
text={Locale.Chat.Config.Reset}
|
|
|
- onClick={() =>
|
|
|
- confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
|
|
|
- }
|
|
|
+ onClick={() => {
|
|
|
+ if (confirm(Locale.Memory.ResetConfirm)) {
|
|
|
+ chatStore.updateCurrentSession(
|
|
|
+ (session) => (session.memoryPrompt = ""),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }}
|
|
|
/>,
|
|
|
<IconButton
|
|
|
key="copy"
|
|
@@ -143,6 +119,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
|
|
|
updater(mask);
|
|
|
chatStore.updateCurrentSession((session) => (session.mask = mask));
|
|
|
}}
|
|
|
+ shouldSyncFromGlobal
|
|
|
extraListItems={
|
|
|
session.mask.modelConfig.sendMemory ? (
|
|
|
<ListItem
|
|
@@ -230,7 +207,9 @@ export function PromptHints(props: {
|
|
|
useEffect(() => {
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
|
if (noPrompts) return;
|
|
|
-
|
|
|
+ if (e.metaKey || e.altKey || e.ctrlKey) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
// arrow up / down to select prompt
|
|
|
const changeIndex = (delta: number) => {
|
|
|
e.stopPropagation();
|
|
@@ -261,7 +240,7 @@ export function PromptHints(props: {
|
|
|
|
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
- }, [noPrompts, selectIndex]);
|
|
|
+ }, [props.prompts.length, selectIndex]);
|
|
|
|
|
|
if (noPrompts) return null;
|
|
|
return (
|
|
@@ -285,6 +264,79 @@ export function PromptHints(props: {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+function ClearContextDivider() {
|
|
|
+ const chatStore = useChatStore();
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className={chatStyle["clear-context"]}
|
|
|
+ onClick={() =>
|
|
|
+ chatStore.updateCurrentSession(
|
|
|
+ (session) => (session.clearContextIndex = undefined),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div className={chatStyle["clear-context-tips"]}>
|
|
|
+ {Locale.Context.Clear}
|
|
|
+ </div>
|
|
|
+ <div className={chatStyle["clear-context-revert-btn"]}>
|
|
|
+ {Locale.Context.Revert}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function ChatAction(props: {
|
|
|
+ text: string;
|
|
|
+ icon: JSX.Element;
|
|
|
+ onClick: () => void;
|
|
|
+}) {
|
|
|
+ const iconRef = useRef<HTMLDivElement>(null);
|
|
|
+ const textRef = useRef<HTMLDivElement>(null);
|
|
|
+ const [width, setWidth] = useState({
|
|
|
+ full: 20,
|
|
|
+ icon: 20,
|
|
|
+ });
|
|
|
+
|
|
|
+ function updateWidth() {
|
|
|
+ if (!iconRef.current || !textRef.current) return;
|
|
|
+ const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
|
|
|
+ const textWidth = getWidth(textRef.current);
|
|
|
+ const iconWidth = getWidth(iconRef.current);
|
|
|
+ setWidth({
|
|
|
+ full: textWidth + iconWidth,
|
|
|
+ icon: iconWidth,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ updateWidth();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ onClick={() => {
|
|
|
+ props.onClick();
|
|
|
+ setTimeout(updateWidth, 1);
|
|
|
+ }}
|
|
|
+ style={
|
|
|
+ {
|
|
|
+ "--icon-width": `${width.icon}px`,
|
|
|
+ "--full-width": `${width.full}px`,
|
|
|
+ } as React.CSSProperties
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div ref={iconRef} className={chatStyle["icon"]}>
|
|
|
+ {props.icon}
|
|
|
+ </div>
|
|
|
+ <div className={chatStyle["text"]} ref={textRef}>
|
|
|
+ {props.text}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function useScrollToBottom() {
|
|
|
// for auto-scroll
|
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -317,6 +369,7 @@ export function ChatActions(props: {
|
|
|
}) {
|
|
|
const config = useAppConfig();
|
|
|
const navigate = useNavigate();
|
|
|
+ const chatStore = useChatStore();
|
|
|
|
|
|
// switch themes
|
|
|
const theme = config.theme;
|
|
@@ -329,70 +382,83 @@ export function ChatActions(props: {
|
|
|
}
|
|
|
|
|
|
// stop all responses
|
|
|
- const couldStop = ControllerPool.hasPending();
|
|
|
- const stopAll = () => ControllerPool.stopAll();
|
|
|
+ const couldStop = ChatControllerPool.hasPending();
|
|
|
+ const stopAll = () => ChatControllerPool.stopAll();
|
|
|
|
|
|
return (
|
|
|
<div className={chatStyle["chat-input-actions"]}>
|
|
|
{couldStop && (
|
|
|
- <div
|
|
|
- className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ <ChatAction
|
|
|
onClick={stopAll}
|
|
|
- >
|
|
|
- <StopIcon />
|
|
|
- </div>
|
|
|
+ text={Locale.Chat.InputActions.Stop}
|
|
|
+ icon={<StopIcon />}
|
|
|
+ />
|
|
|
)}
|
|
|
{!props.hitBottom && (
|
|
|
- <div
|
|
|
- className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ <ChatAction
|
|
|
onClick={props.scrollToBottom}
|
|
|
- >
|
|
|
- <BottomIcon />
|
|
|
- </div>
|
|
|
+ text={Locale.Chat.InputActions.ToBottom}
|
|
|
+ icon={<BottomIcon />}
|
|
|
+ />
|
|
|
)}
|
|
|
{props.hitBottom && (
|
|
|
- <div
|
|
|
- className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ <ChatAction
|
|
|
onClick={props.showPromptModal}
|
|
|
- >
|
|
|
- <BrainIcon />
|
|
|
- </div>
|
|
|
+ text={Locale.Chat.InputActions.Settings}
|
|
|
+ icon={<SettingsIcon />}
|
|
|
+ />
|
|
|
)}
|
|
|
|
|
|
- <div
|
|
|
- className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ <ChatAction
|
|
|
onClick={nextTheme}
|
|
|
- >
|
|
|
- {theme === Theme.Auto ? (
|
|
|
- <AutoIcon />
|
|
|
- ) : theme === Theme.Light ? (
|
|
|
- <LightIcon />
|
|
|
- ) : theme === Theme.Dark ? (
|
|
|
- <DarkIcon />
|
|
|
- ) : null}
|
|
|
- </div>
|
|
|
+ text={Locale.Chat.InputActions.Theme[theme]}
|
|
|
+ icon={
|
|
|
+ <>
|
|
|
+ {theme === Theme.Auto ? (
|
|
|
+ <AutoIcon />
|
|
|
+ ) : theme === Theme.Light ? (
|
|
|
+ <LightIcon />
|
|
|
+ ) : theme === Theme.Dark ? (
|
|
|
+ <DarkIcon />
|
|
|
+ ) : null}
|
|
|
+ </>
|
|
|
+ }
|
|
|
+ />
|
|
|
|
|
|
- <div
|
|
|
- className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ <ChatAction
|
|
|
onClick={props.showPromptHints}
|
|
|
- >
|
|
|
- <PromptIcon />
|
|
|
- </div>
|
|
|
+ text={Locale.Chat.InputActions.Prompt}
|
|
|
+ icon={<PromptIcon />}
|
|
|
+ />
|
|
|
|
|
|
- <div
|
|
|
- className={`${chatStyle["chat-input-action"]} clickable`}
|
|
|
+ <ChatAction
|
|
|
onClick={() => {
|
|
|
navigate(Path.Masks);
|
|
|
}}
|
|
|
- >
|
|
|
- <MaskIcon />
|
|
|
- </div>
|
|
|
+ text={Locale.Chat.InputActions.Masks}
|
|
|
+ icon={<MaskIcon />}
|
|
|
+ />
|
|
|
+
|
|
|
+ <ChatAction
|
|
|
+ text={Locale.Chat.InputActions.Clear}
|
|
|
+ icon={<BreakIcon />}
|
|
|
+ onClick={() => {
|
|
|
+ chatStore.updateCurrentSession((session) => {
|
|
|
+ if (session.clearContextIndex === session.messages.length) {
|
|
|
+ session.clearContextIndex = undefined;
|
|
|
+ } else {
|
|
|
+ session.clearContextIndex = session.messages.length;
|
|
|
+ session.memoryPrompt = ""; // will clear memory
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ />
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
export function Chat() {
|
|
|
- type RenderMessage = Message & { preview?: boolean };
|
|
|
+ type RenderMessage = ChatMessage & { preview?: boolean };
|
|
|
|
|
|
const chatStore = useChatStore();
|
|
|
const [session, sessionIndex] = useChatStore((state) => [
|
|
@@ -402,6 +468,8 @@ export function Chat() {
|
|
|
const config = useAppConfig();
|
|
|
const fontSize = config.fontSize;
|
|
|
|
|
|
+ const [showExport, setShowExport] = useState(false);
|
|
|
+
|
|
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
|
const [userInput, setUserInput] = useState("");
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -485,23 +553,56 @@ export function Chat() {
|
|
|
|
|
|
// stop response
|
|
|
const onUserStop = (messageId: number) => {
|
|
|
- ControllerPool.stop(sessionIndex, messageId);
|
|
|
+ ChatControllerPool.stop(sessionIndex, messageId);
|
|
|
};
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ chatStore.updateCurrentSession((session) => {
|
|
|
+ const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
|
|
|
+ session.messages.forEach((m) => {
|
|
|
+ // check if should stop all stale messages
|
|
|
+ if (m.isError || new Date(m.date).getTime() < stopTiming) {
|
|
|
+ if (m.streaming) {
|
|
|
+ m.streaming = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (m.content.length === 0) {
|
|
|
+ m.isError = true;
|
|
|
+ m.content = prettyObject({
|
|
|
+ error: true,
|
|
|
+ message: "empty response",
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // auto sync mask config from global config
|
|
|
+ if (session.mask.syncGlobalConfig) {
|
|
|
+ console.log("[Mask] syncing from global, name = ", session.mask.name);
|
|
|
+ session.mask.modelConfig = { ...config.modelConfig };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, []);
|
|
|
+
|
|
|
// check if should send message
|
|
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
|
// if ArrowUp and no userInput, fill with last input
|
|
|
- if (e.key === "ArrowUp" && userInput.length <= 0) {
|
|
|
+ if (
|
|
|
+ e.key === "ArrowUp" &&
|
|
|
+ userInput.length <= 0 &&
|
|
|
+ !(e.metaKey || e.altKey || e.ctrlKey)
|
|
|
+ ) {
|
|
|
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
|
|
e.preventDefault();
|
|
|
return;
|
|
|
}
|
|
|
- if (shouldSubmit(e)) {
|
|
|
+ if (shouldSubmit(e) && promptHints.length === 0) {
|
|
|
doSubmit(userInput);
|
|
|
e.preventDefault();
|
|
|
}
|
|
|
};
|
|
|
- const onRightClick = (e: any, message: Message) => {
|
|
|
+ const onRightClick = (e: any, message: ChatMessage) => {
|
|
|
// copy to clipboard
|
|
|
if (selectOrCopy(e.currentTarget, message.content)) {
|
|
|
e.preventDefault();
|
|
@@ -548,7 +649,9 @@ export function Chat() {
|
|
|
inputRef.current?.focus();
|
|
|
};
|
|
|
|
|
|
- const context: RenderMessage[] = session.mask.context.slice();
|
|
|
+ const context: RenderMessage[] = session.mask.hideContext
|
|
|
+ ? []
|
|
|
+ : session.mask.context.slice();
|
|
|
|
|
|
const accessStore = useAccessStore();
|
|
|
|
|
@@ -563,6 +666,12 @@ export function Chat() {
|
|
|
context.push(copiedHello);
|
|
|
}
|
|
|
|
|
|
+ // clear context index = context length + index in messages
|
|
|
+ const clearContextIndex =
|
|
|
+ (session.clearContextIndex ?? -1) >= 0
|
|
|
+ ? session.clearContextIndex! + context.length
|
|
|
+ : -1;
|
|
|
+
|
|
|
// preview messages
|
|
|
const messages = context
|
|
|
.concat(session.messages as RenderMessage[])
|
|
@@ -602,9 +711,13 @@ export function Chat() {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const clientConfig = useMemo(() => getClientConfig(), []);
|
|
|
+
|
|
|
const location = useLocation();
|
|
|
const isChat = location.pathname === Path.Chat;
|
|
|
+
|
|
|
const autoFocus = !isMobileScreen || isChat; // only focus in chat page
|
|
|
+ const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
|
|
|
|
|
|
useCommand({
|
|
|
fill: setUserInput,
|
|
@@ -615,7 +728,7 @@ export function Chat() {
|
|
|
|
|
|
return (
|
|
|
<div className={styles.chat} key={session.id}>
|
|
|
- <div className="window-header">
|
|
|
+ <div className="window-header" data-tauri-drag-region>
|
|
|
<div className="window-header-title">
|
|
|
<div
|
|
|
className={`window-header-main-title " ${styles["chat-body-title"]}`}
|
|
@@ -649,14 +762,11 @@ export function Chat() {
|
|
|
bordered
|
|
|
title={Locale.Chat.Actions.Export}
|
|
|
onClick={() => {
|
|
|
- exportMessages(
|
|
|
- session.messages.filter((msg) => !msg.isError),
|
|
|
- session.topic,
|
|
|
- );
|
|
|
+ setShowExport(true);
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
- {!isMobileScreen && (
|
|
|
+ {showMaxIcon && (
|
|
|
<div className="window-action-button">
|
|
|
<IconButton
|
|
|
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
|
@@ -697,86 +807,91 @@ export function Chat() {
|
|
|
!(message.preview || message.content.length === 0);
|
|
|
const showTyping = message.preview || message.streaming;
|
|
|
|
|
|
+ const shouldShowClearContextDivider = i === clearContextIndex - 1;
|
|
|
+
|
|
|
return (
|
|
|
- <div
|
|
|
- key={i}
|
|
|
- className={
|
|
|
- isUser ? styles["chat-message-user"] : styles["chat-message"]
|
|
|
- }
|
|
|
- >
|
|
|
- <div className={styles["chat-message-container"]}>
|
|
|
- <div className={styles["chat-message-avatar"]}>
|
|
|
- {message.role === "user" ? (
|
|
|
- <Avatar avatar={config.avatar} />
|
|
|
- ) : (
|
|
|
- <MaskAvatar mask={session.mask} />
|
|
|
- )}
|
|
|
- </div>
|
|
|
- {showTyping && (
|
|
|
- <div className={styles["chat-message-status"]}>
|
|
|
- {Locale.Chat.Typing}
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ key={i}
|
|
|
+ className={
|
|
|
+ isUser ? styles["chat-message-user"] : styles["chat-message"]
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <div className={styles["chat-message-container"]}>
|
|
|
+ <div className={styles["chat-message-avatar"]}>
|
|
|
+ {message.role === "user" ? (
|
|
|
+ <Avatar avatar={config.avatar} />
|
|
|
+ ) : (
|
|
|
+ <MaskAvatar mask={session.mask} />
|
|
|
+ )}
|
|
|
</div>
|
|
|
- )}
|
|
|
- <div className={styles["chat-message-item"]}>
|
|
|
- {showActions && (
|
|
|
- <div className={styles["chat-message-top-actions"]}>
|
|
|
- {message.streaming ? (
|
|
|
- <div
|
|
|
- className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => onUserStop(message.id ?? i)}
|
|
|
- >
|
|
|
- {Locale.Chat.Actions.Stop}
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- <div
|
|
|
- className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => onDelete(message.id ?? i)}
|
|
|
- >
|
|
|
- {Locale.Chat.Actions.Delete}
|
|
|
- </div>
|
|
|
+ {showTyping && (
|
|
|
+ <div className={styles["chat-message-status"]}>
|
|
|
+ {Locale.Chat.Typing}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className={styles["chat-message-item"]}>
|
|
|
+ {showActions && (
|
|
|
+ <div className={styles["chat-message-top-actions"]}>
|
|
|
+ {message.streaming ? (
|
|
|
<div
|
|
|
className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => onResend(message.id ?? i)}
|
|
|
+ onClick={() => onUserStop(message.id ?? i)}
|
|
|
>
|
|
|
- {Locale.Chat.Actions.Retry}
|
|
|
+ {Locale.Chat.Actions.Stop}
|
|
|
</div>
|
|
|
- </>
|
|
|
- )}
|
|
|
-
|
|
|
- <div
|
|
|
- className={styles["chat-message-top-action"]}
|
|
|
- onClick={() => copyToClipboard(message.content)}
|
|
|
- >
|
|
|
- {Locale.Chat.Actions.Copy}
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className={styles["chat-message-top-action"]}
|
|
|
+ onClick={() => onDelete(message.id ?? i)}
|
|
|
+ >
|
|
|
+ {Locale.Chat.Actions.Delete}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ className={styles["chat-message-top-action"]}
|
|
|
+ onClick={() => onResend(message.id ?? i)}
|
|
|
+ >
|
|
|
+ {Locale.Chat.Actions.Retry}
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div
|
|
|
+ className={styles["chat-message-top-action"]}
|
|
|
+ onClick={() => copyToClipboard(message.content)}
|
|
|
+ >
|
|
|
+ {Locale.Chat.Actions.Copy}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <Markdown
|
|
|
+ content={message.content}
|
|
|
+ loading={
|
|
|
+ (message.preview || message.content.length === 0) &&
|
|
|
+ !isUser
|
|
|
+ }
|
|
|
+ onContextMenu={(e) => onRightClick(e, message)}
|
|
|
+ onDoubleClickCapture={() => {
|
|
|
+ if (!isMobileScreen) return;
|
|
|
+ setUserInput(message.content);
|
|
|
+ }}
|
|
|
+ fontSize={fontSize}
|
|
|
+ parentRef={scrollRef}
|
|
|
+ defaultShow={i >= messages.length - 10}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {!isUser && !message.preview && (
|
|
|
+ <div className={styles["chat-message-actions"]}>
|
|
|
+ <div className={styles["chat-message-action-date"]}>
|
|
|
+ {message.date.toLocaleString()}
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
- <Markdown
|
|
|
- content={message.content}
|
|
|
- loading={
|
|
|
- (message.preview || message.content.length === 0) &&
|
|
|
- !isUser
|
|
|
- }
|
|
|
- onContextMenu={(e) => onRightClick(e, message)}
|
|
|
- onDoubleClickCapture={() => {
|
|
|
- if (!isMobileScreen) return;
|
|
|
- setUserInput(message.content);
|
|
|
- }}
|
|
|
- fontSize={fontSize}
|
|
|
- parentRef={scrollRef}
|
|
|
- defaultShow={i >= messages.length - 10}
|
|
|
- />
|
|
|
</div>
|
|
|
- {!isUser && !message.preview && (
|
|
|
- <div className={styles["chat-message-actions"]}>
|
|
|
- <div className={styles["chat-message-action-date"]}>
|
|
|
- {message.date.toLocaleString()}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ {shouldShowClearContextDivider && <ClearContextDivider />}
|
|
|
+ </>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
@@ -789,7 +904,14 @@ export function Chat() {
|
|
|
scrollToBottom={scrollToBottom}
|
|
|
hitBottom={hitBottom}
|
|
|
showPromptHints={() => {
|
|
|
+ // Click again to close
|
|
|
+ if (promptHints.length > 0) {
|
|
|
+ setPromptHints([]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
inputRef.current?.focus();
|
|
|
+ setUserInput("/");
|
|
|
onSearch("");
|
|
|
}}
|
|
|
/>
|
|
@@ -815,6 +937,10 @@ export function Chat() {
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ {showExport && (
|
|
|
+ <ExportMessageModal onClose={() => setShowExport(false)} />
|
|
|
+ )}
|
|
|
</div>
|
|
|
);
|
|
|
}
|