- {(provided) => (
+ {provided => (
@@ -155,8 +159,8 @@ export function ChatList(props: { narrow?: boolean }) {
}}
onDelete={async () => {
if (
- (!props.narrow && !isMobileScreen) ||
- (await showConfirm(Locale.Home.DeleteChat))
+ (!props.narrow && !isMobileScreen)
+ || (await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(i);
}
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
index 7560d0305..f556aa868 100644
--- a/app/components/chat.module.scss
+++ b/app/components/chat.module.scss
@@ -235,12 +235,7 @@
animation: slide-in ease 0.3s;
- $linear: linear-gradient(
- to right,
- rgba(0, 0, 0, 0),
- rgba(0, 0, 0, 1),
- rgba(0, 0, 0, 0)
- );
+ $linear: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
mask-image: $linear;
@mixin show {
@@ -510,13 +505,8 @@
}
@media screen and (min-width: 600px) {
- $max-image-width: calc(
- calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
- );
- $image-width: calc(
- calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
- var(--image-count)
- );
+ $max-image-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count));
+ $image-width: calc(calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 / var(--image-count));
.chat-message-item-image-multi {
width: $image-width;
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 51fe74fe7..762f47e04 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -1,91 +1,123 @@
-import { useDebouncedCallback } from "use-debounce";
+import type {
+ RefObject,
+} from 'react';
+import type { ClientApi, MultimodalContent } from '../client/api';
+
+import type {
+ ChatMessage,
+ ModelType,
+} from '../store';
+import type { Prompt } from '../store/prompt';
+import type { DalleQuality, DalleSize, DalleStyle } from '../typing';
+import { RealtimeChat } from '@/app/components/realtime-chat';
+import { uploadImage as uploadImageRemote } from '@/app/utils/chat';
+import clsx from 'clsx';
+import { isEmpty } from 'lodash-es';
+import dynamic from 'next/dynamic';
import React, {
- useState,
- useRef,
+ Fragment,
+ useCallback,
useEffect,
useMemo,
- useCallback,
- Fragment,
- RefObject,
-} from "react";
-
-import SendWhiteIcon from "../icons/send-white.svg";
-import BrainIcon from "../icons/brain.svg";
-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 SpeakIcon from "../icons/speak.svg";
-import SpeakStopIcon from "../icons/speak-stop.svg";
-import LoadingIcon from "../icons/three-dots.svg";
-import LoadingButtonIcon from "../icons/loading.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 DeleteIcon from "../icons/clear.svg";
-import PinIcon from "../icons/pin.svg";
-import EditIcon from "../icons/rename.svg";
-import ConfirmIcon from "../icons/confirm.svg";
-import CloseIcon from "../icons/close.svg";
-import CancelIcon from "../icons/cancel.svg";
-import ImageIcon from "../icons/image.svg";
-
-import LightIcon from "../icons/light.svg";
-import DarkIcon from "../icons/dark.svg";
-import AutoIcon from "../icons/auto.svg";
-import BottomIcon from "../icons/bottom.svg";
-import StopIcon from "../icons/pause.svg";
-import RobotIcon from "../icons/robot.svg";
-import SizeIcon from "../icons/size.svg";
-import QualityIcon from "../icons/hd.svg";
-import StyleIcon from "../icons/palette.svg";
-import PluginIcon from "../icons/plugin.svg";
-import ShortcutkeyIcon from "../icons/shortcutkey.svg";
-import ReloadIcon from "../icons/reload.svg";
-import HeadphoneIcon from "../icons/headphone.svg";
+ useRef,
+ useState,
+} from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useDebouncedCallback } from 'use-debounce';
+import { ChatControllerPool } from '../client/controller';
+import { ChatCommandPrefix, useChatCommand, useCommand } from '../command';
+import { getClientConfig } from '../config/client';
+import {
+ CHAT_PAGE_SIZE,
+ DEFAULT_TTS_ENGINE,
+ ModelProvider,
+ Path,
+ REQUEST_TIMEOUT_MS,
+ ServiceProvider,
+ UNFINISHED_INPUT,
+} from '../constant';
+import AutoIcon from '../icons/auto.svg';
+import BottomIcon from '../icons/bottom.svg';
+import BrainIcon from '../icons/brain.svg';
+import BreakIcon from '../icons/break.svg';
+import CancelIcon from '../icons/cancel.svg';
+import SettingsIcon from '../icons/chat-settings.svg';
+import DeleteIcon from '../icons/clear.svg';
+import CloseIcon from '../icons/close.svg';
+import ConfirmIcon from '../icons/confirm.svg';
+
+import CopyIcon from '../icons/copy.svg';
+import DarkIcon from '../icons/dark.svg';
+import QualityIcon from '../icons/hd.svg';
+import HeadphoneIcon from '../icons/headphone.svg';
+import ImageIcon from '../icons/image.svg';
+import LightIcon from '../icons/light.svg';
+import LoadingButtonIcon from '../icons/loading.svg';
+import MaskIcon from '../icons/mask.svg';
+import MaxIcon from '../icons/max.svg';
+import MinIcon from '../icons/min.svg';
+import StyleIcon from '../icons/palette.svg';
+import StopIcon from '../icons/pause.svg';
+import PinIcon from '../icons/pin.svg';
+import PluginIcon from '../icons/plugin.svg';
+
+import PromptIcon from '../icons/prompt.svg';
+
+import ResetIcon from '../icons/reload.svg';
+
+import ReloadIcon from '../icons/reload.svg';
+
+import RenameIcon from '../icons/rename.svg';
+import EditIcon from '../icons/rename.svg';
+import ReturnIcon from '../icons/return.svg';
+import RobotIcon from '../icons/robot.svg';
+
+import SendWhiteIcon from '../icons/send-white.svg';
+import ExportIcon from '../icons/share.svg';
+
+import ShortcutkeyIcon from '../icons/shortcutkey.svg';
+import SizeIcon from '../icons/size.svg';
+import SpeakIcon from '../icons/speak.svg';
+import SpeakStopIcon from '../icons/speak-stop.svg';
+import LoadingIcon from '../icons/three-dots.svg';
+import Locale from '../locales';
import {
- ChatMessage,
- SubmitKey,
- useChatStore,
BOT_HELLO,
createMessage,
- useAccessStore,
- Theme,
- useAppConfig,
DEFAULT_TOPIC,
- ModelType,
+ SubmitKey,
+ Theme,
+ useAccessStore,
+ useAppConfig,
+ useChatStore,
usePluginStore,
-} from "../store";
-
+} from '../store';
+import { useMaskStore } from '../store/mask';
+import { usePromptStore } from '../store/prompt';
import {
- copyToClipboard,
- selectOrCopy,
autoGrowTextArea,
- useMobileScreen,
- getMessageTextContent,
+ copyToClipboard,
getMessageImages,
- isVisionModel,
+ getMessageTextContent,
isDalle3,
- showPlugins,
+ isVisionModel,
safeLocalStorage,
-} from "../utils";
+ selectOrCopy,
+ showPlugins,
+ useMobileScreen,
+} from '../utils';
+import { createTTSPlayer } from '../utils/audio';
+import { prettyObject } from '../utils/format';
+import { useAllModels } from '../utils/hooks';
+import { getModelProvider } from '../utils/model';
-import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
-
-import dynamic from "next/dynamic";
-
-import { ChatControllerPool } from "../client/controller";
-import { DalleSize, DalleQuality, DalleStyle } from "../typing";
-import { Prompt, usePromptStore } from "../store/prompt";
-import Locale from "../locales";
-
-import { IconButton } from "./button";
-import styles from "./chat.module.scss";
+import { MsEdgeTTS, OUTPUT_FORMAT } from '../utils/ms_edge_tts';
+import { IconButton } from './button';
+import styles from './chat.module.scss';
+import { Avatar } from './emoji';
+import { ExportMessageModal } from './exporter';
+import { ContextPrompts, MaskAvatar, MaskConfig } from './mask';
import {
List,
ListItem,
@@ -94,41 +126,13 @@ import {
showConfirm,
showPrompt,
showToast,
-} from "./ui-lib";
-import { useNavigate } from "react-router-dom";
-import {
- CHAT_PAGE_SIZE,
- DEFAULT_TTS_ENGINE,
- ModelProvider,
- Path,
- REQUEST_TIMEOUT_MS,
- UNFINISHED_INPUT,
- ServiceProvider,
-} from "../constant";
-import { Avatar } from "./emoji";
-import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
-import { useMaskStore } from "../store/mask";
-import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
-import { prettyObject } from "../utils/format";
-import { ExportMessageModal } from "./exporter";
-import { getClientConfig } from "../config/client";
-import { useAllModels } from "../utils/hooks";
-import { MultimodalContent } from "../client/api";
-
-import { ClientApi } from "../client/api";
-import { createTTSPlayer } from "../utils/audio";
-import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
-
-import { isEmpty } from "lodash-es";
-import { getModelProvider } from "../utils/model";
-import { RealtimeChat } from "@/app/components/realtime-chat";
-import clsx from "clsx";
+} from './ui-lib';
const localStorage = safeLocalStorage();
const ttsPlayer = createTTSPlayer();
-const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
+const Markdown = dynamic(async () => (await import('./markdown')).Markdown, {
loading: () => ,
});
@@ -153,7 +157,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
if (await showConfirm(Locale.Memory.ResetConfirm)) {
chatStore.updateTargetSession(
session,
- (session) => (session.memoryPrompt = ""),
+ session => (session.memoryPrompt = ''),
);
}
}}
@@ -179,22 +183,26 @@ export function SessionConfigModel(props: { onClose: () => void }) {
updater(mask);
chatStore.updateTargetSession(
session,
- (session) => (session.mask = mask),
+ session => (session.mask = mask),
);
}}
shouldSyncFromGlobal
extraListItems={
- session.mask.modelConfig.sendMemory ? (
-
- ) : (
- <>>
- )
+ session.mask.modelConfig.sendMemory
+ ? (
+
+
+ )
+ : (
+ <>>
+ )
}
- >
+ >
+
);
@@ -210,15 +218,15 @@ function PromptToast(props: {
const context = session.mask.context;
return (
-
+
{props.showToast && context.length > 0 && (
props.setShowModal(true)}
>
-
+
{Locale.Context.Toast(context.length)}
@@ -243,31 +251,33 @@ function useSubmitHandler() {
isComposing.current = false;
};
- window.addEventListener("compositionstart", onCompositionStart);
- window.addEventListener("compositionend", onCompositionEnd);
+ window.addEventListener('compositionstart', onCompositionStart);
+ window.addEventListener('compositionend', onCompositionEnd);
return () => {
- window.removeEventListener("compositionstart", onCompositionStart);
- window.removeEventListener("compositionend", onCompositionEnd);
+ window.removeEventListener('compositionstart', onCompositionStart);
+ window.removeEventListener('compositionend', onCompositionEnd);
};
}, []);
const shouldSubmit = (e: React.KeyboardEvent
) => {
// Fix Chinese input method "Enter" on Safari
- if (e.keyCode == 229) return false;
- if (e.key !== "Enter") return false;
- if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current))
- return false;
+ if (e.keyCode == 229)
+ { return false; }
+ if (e.key !== 'Enter')
+ { return false; }
+ if (e.key === 'Enter' && (e.nativeEvent.isComposing || isComposing.current))
+ { 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)
+ (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)
);
};
@@ -277,7 +287,7 @@ function useSubmitHandler() {
};
}
-export type RenderPrompt = Pick;
+export type RenderPrompt = Pick;
export function PromptHints(props: {
prompts: RenderPrompt[];
@@ -306,15 +316,15 @@ export function PromptHints(props: {
);
setSelectIndex(nextIndex);
selectedRef.current?.scrollIntoView({
- block: "center",
+ block: 'center',
});
};
- if (e.key === "ArrowUp") {
+ if (e.key === 'ArrowUp') {
changeIndex(1);
- } else if (e.key === "ArrowDown") {
+ } else if (e.key === 'ArrowDown') {
changeIndex(-1);
- } else if (e.key === "Enter") {
+ } else if (e.key === 'Enter') {
const selectedPrompt = props.prompts.at(selectIndex);
if (selectedPrompt) {
props.onPromptSelect(selectedPrompt);
@@ -322,27 +332,28 @@ export function PromptHints(props: {
}
};
- window.addEventListener("keydown", onKeyDown);
+ window.addEventListener('keydown', onKeyDown);
- return () => window.removeEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener('keydown', onKeyDown);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.prompts.length, selectIndex]);
- if (noPrompts) return null;
+ if (noPrompts)
+ { return null; }
return (
-
+
{props.prompts.map((prompt, i) => (
props.onPromptSelect(prompt)}
onMouseEnter={() => setSelectIndex(i)}
>
-
{prompt.title}
-
{prompt.content}
+
{prompt.title}
+
{prompt.content}
))}
@@ -355,16 +366,15 @@ function ClearContextDivider() {
return (
chatStore.updateTargetSession(
session,
- (session) => (session.clearContextIndex = undefined),
- )
- }
+ session => (session.clearContextIndex = undefined),
+ )}
>
-
{Locale.Context.Clear}
-
+
{Locale.Context.Clear}
+
{Locale.Context.Revert}
@@ -384,7 +394,8 @@ export function ChatAction(props: {
});
function updateWidth() {
- if (!iconRef.current || !textRef.current) return;
+ if (!iconRef.current || !textRef.current)
+ { return; }
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
const textWidth = getWidth(textRef.current);
const iconWidth = getWidth(iconRef.current);
@@ -396,7 +407,7 @@ export function ChatAction(props: {
return (
{
props.onClick();
setTimeout(updateWidth, 1);
@@ -405,15 +416,15 @@ export function ChatAction(props: {
onTouchStart={updateWidth}
style={
{
- "--icon-width": `${width.icon}px`,
- "--full-width": `${width.full}px`,
+ '--icon-width': `${width.icon}px`,
+ '--full-width': `${width.full}px`,
} as React.CSSProperties
}
>
-
+
{props.icon}
-
@@ -478,7 +489,7 @@ export function ChatActions(props: {
const themeIndex = themes.indexOf(theme);
const nextIndex = (themeIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
- config.update((config) => (config.theme = nextTheme));
+ config.update(config => (config.theme = nextTheme));
}
// stop all responses
@@ -487,17 +498,17 @@ export function ChatActions(props: {
// switch model
const currentModel = session.mask.modelConfig.model;
- const currentProviderName =
- session.mask.modelConfig?.providerName || ServiceProvider.OpenAI;
+ const currentProviderName
+ = session.mask.modelConfig?.providerName || ServiceProvider.OpenAI;
const allModels = useAllModels();
const models = useMemo(() => {
- const filteredModels = allModels.filter((m) => m.available);
- const defaultModel = filteredModels.find((m) => m.isDefault);
+ const filteredModels = allModels.filter(m => m.available);
+ const defaultModel = filteredModels.find(m => m.isDefault);
if (defaultModel) {
const arr = [
defaultModel,
- ...filteredModels.filter((m) => m !== defaultModel),
+ ...filteredModels.filter(m => m !== defaultModel),
];
return arr;
} else {
@@ -506,11 +517,11 @@ export function ChatActions(props: {
}, [allModels]);
const currentModelName = useMemo(() => {
const model = models.find(
- (m) =>
- m.name == currentModel &&
- m?.provider?.providerName == currentProviderName,
+ m =>
+ m.name == currentModel
+ && m?.provider?.providerName == currentProviderName,
);
- return model?.displayName ?? "";
+ return model?.displayName ?? '';
}, [models, currentModel, currentProviderName]);
const [showModelSelector, setShowModelSelector] = useState(false);
const [showPluginSelector, setShowPluginSelector] = useState(false);
@@ -519,12 +530,12 @@ export function ChatActions(props: {
const [showSizeSelector, setShowSizeSelector] = useState(false);
const [showQualitySelector, setShowQualitySelector] = useState(false);
const [showStyleSelector, setShowStyleSelector] = useState(false);
- const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
- const dalle3Qualitys: DalleQuality[] = ["standard", "hd"];
- const dalle3Styles: DalleStyle[] = ["vivid", "natural"];
- const currentSize = session.mask.modelConfig?.size ?? "1024x1024";
- const currentQuality = session.mask.modelConfig?.quality ?? "standard";
- const currentStyle = session.mask.modelConfig?.style ?? "vivid";
+ const dalle3Sizes: DalleSize[] = ['1024x1024', '1792x1024', '1024x1792'];
+ const dalle3Qualitys: DalleQuality[] = ['standard', 'hd'];
+ const dalle3Styles: DalleStyle[] = ['vivid', 'natural'];
+ const currentSize = session.mask.modelConfig?.size ?? '1024x1024';
+ const currentQuality = session.mask.modelConfig?.quality ?? 'standard';
+ const currentStyle = session.mask.modelConfig?.style ?? 'vivid';
const isMobileScreen = useMobileScreen();
@@ -538,17 +549,17 @@ export function ChatActions(props: {
// if current model is not available
// switch to first available model
- const isUnavailableModel = !models.some((m) => m.name === currentModel);
+ const isUnavailableModel = !models.some(m => m.name === currentModel);
if (isUnavailableModel && models.length > 0) {
// show next model to default model if exist
- let nextModel = models.find((model) => model.isDefault) || models[0];
+ const nextModel = models.find(model => model.isDefault) || models[0];
chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.model = nextModel.name;
session.mask.modelConfig.providerName = nextModel?.provider
?.providerName as ServiceProvider;
});
showToast(
- nextModel?.provider?.providerName == "ByteDance"
+ nextModel?.provider?.providerName == 'ByteDance'
? nextModel.displayName
: nextModel.name,
);
@@ -556,7 +567,7 @@ export function ChatActions(props: {
}, [chatStore, currentModel, models, session]);
return (
-
+
<>
{couldStop && (
- {theme === Theme.Auto ? (
-
- ) : theme === Theme.Light ? (
-
- ) : theme === Theme.Dark ? (
-
- ) : null}
+ {theme === Theme.Auto
+ ? (
+
+ )
+ : theme === Theme.Light
+ ? (
+
+ )
+ : theme === Theme.Dark
+ ? (
+
+ )
+ : null}
>
- }
+ )}
/>
({
+ items={models.map(m => ({
title: `${m.displayName}${
m?.provider?.providerName
- ? " (" + m?.provider?.providerName + ")"
- : ""
+ ? ` (${m?.provider?.providerName})`
+ : ''
}`,
value: `${m.name}@${m?.provider?.providerName}`,
}))}
onClose={() => setShowModelSelector(false)}
onSelection={(s) => {
- if (s.length === 0) return;
+ if (s.length === 0)
+ { return; }
const [model, providerName] = getModelProvider(s[0]);
chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.model = model as ModelType;
- session.mask.modelConfig.providerName =
- providerName as ServiceProvider;
+ session.mask.modelConfig.providerName
+ = providerName as ServiceProvider;
session.mask.syncGlobalConfig = false;
});
- if (providerName == "ByteDance") {
+ if (providerName == 'ByteDance') {
const selectedModel = models.find(
- (m) =>
- m.name == model &&
- m?.provider?.providerName == providerName,
+ m =>
+ m.name == model
+ && m?.provider?.providerName == providerName,
);
- showToast(selectedModel?.displayName ?? "");
+ showToast(selectedModel?.displayName ?? '');
} else {
showToast(model);
}
@@ -684,13 +702,14 @@ export function ChatActions(props: {
{showSizeSelector && (
({
+ items={dalle3Sizes.map(m => ({
title: m,
value: m,
}))}
onClose={() => setShowSizeSelector(false)}
onSelection={(s) => {
- if (s.length === 0) return;
+ if (s.length === 0)
+ { return; }
const size = s[0];
chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.size = size;
@@ -711,13 +730,14 @@ export function ChatActions(props: {
{showQualitySelector && (
({
+ items={dalle3Qualitys.map(m => ({
title: m,
value: m,
}))}
onClose={() => setShowQualitySelector(false)}
onSelection={(q) => {
- if (q.length === 0) return;
+ if (q.length === 0)
+ { return; }
const quality = q[0];
chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.quality = quality;
@@ -738,13 +758,14 @@ export function ChatActions(props: {
{showStyleSelector && (
({
+ items={dalle3Styles.map(m => ({
title: m,
value: m,
}))}
onClose={() => setShowStyleSelector(false)}
onSelection={(s) => {
- if (s.length === 0) return;
+ if (s.length === 0)
+ { return; }
const style = s[0];
chatStore.updateTargetSession(session, (session) => {
session.mask.modelConfig.style = style;
@@ -771,7 +792,7 @@ export function ChatActions(props: {
({
+ items={pluginStore.getAll().map(item => ({
title: `${item?.title}@${item?.version}`,
value: item?.id,
}))}
@@ -792,11 +813,11 @@ export function ChatActions(props: {
/>
)}
>
-
+
{config.realtimeConfig.enable && (
props.setShowChatSidePanel(true)}
- text={"Realtime Chat"}
+ text="Realtime Chat"
icon={}
/>
)}
@@ -832,7 +853,7 @@ export function EditMessageModal(props: { onClose: () => void }) {
onClick={() => {
chatStore.updateTargetSession(
session,
- (session) => (session.messages = messages),
+ session => (session.messages = messages),
);
props.onClose();
}}
@@ -847,13 +868,13 @@ export function EditMessageModal(props: { onClose: () => void }) {
+ onInput={e =>
chatStore.updateTargetSession(
session,
- (session) => (session.topic = e.currentTarget.value),
- )
- }
- >
+ session => (session.topic = e.currentTarget.value),
+ )}
+ >
+
void }) {
export function DeleteImageButton(props: { deleteImage: () => void }) {
return (
-
+
);
}
export function ShortcutKeyModal(props: { onClose: () => void }) {
- const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
+ const isMac = navigator.platform.toUpperCase().includes('MAC');
const shortcuts = [
{
title: Locale.Chat.ShortcutKey.newChat,
- keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
+ keys: isMac ? ['⌘', 'Shift', 'O'] : ['Ctrl', 'Shift', 'O'],
},
- { title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
+ { title: Locale.Chat.ShortcutKey.focusInput, keys: ['Shift', 'Esc'] },
{
title: Locale.Chat.ShortcutKey.copyLastCode,
- keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
+ keys: isMac ? ['⌘', 'Shift', ';'] : ['Ctrl', 'Shift', ';'],
},
{
title: Locale.Chat.ShortcutKey.copyLastMessage,
- keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
+ keys: isMac ? ['⌘', 'Shift', 'C'] : ['Ctrl', 'Shift', 'C'],
},
{
title: Locale.Chat.ShortcutKey.showShortcutKey,
- keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
+ keys: isMac ? ['⌘', '/'] : ['Ctrl', '/'],
},
];
return (
@@ -915,16 +936,16 @@ export function ShortcutKeyModal(props: { onClose: () => void }) {
/>,
]}
>
-
-
+
+
{shortcuts.map((shortcut, index) => (
-
-
+
+
{shortcut.title}
-
+
{shortcut.keys.map((key, i) => (
-
+
{key}
))}
@@ -950,28 +971,29 @@ function _Chat() {
const [showExport, setShowExport] = useState(false);
const inputRef = useRef
(null);
- const [userInput, setUserInput] = useState("");
+ const [userInput, setUserInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const scrollRef = useRef(null);
const isScrolledToBottom = scrollRef?.current
? Math.abs(
- scrollRef.current.scrollHeight -
- (scrollRef.current.scrollTop + scrollRef.current.clientHeight),
- ) <= 1
+ scrollRef.current.scrollHeight
+ - (scrollRef.current.scrollTop + scrollRef.current.clientHeight),
+ ) <= 1
: false;
const isAttachWithTop = useMemo(() => {
const lastMessage = scrollRef.current?.lastElementChild as HTMLElement;
// if scrolllRef is not ready or no message, return false
- if (!scrollRef?.current || !lastMessage) return false;
- const topDistance =
- lastMessage!.getBoundingClientRect().top -
- scrollRef.current.getBoundingClientRect().top;
+ if (!scrollRef?.current || !lastMessage)
+ { return false; }
+ const topDistance
+ = lastMessage!.getBoundingClientRect().top
+ - scrollRef.current.getBoundingClientRect().top;
// leave some space for user question
return topDistance < 100;
}, [scrollRef?.current?.scrollHeight]);
- const isTyping = userInput !== "";
+ const isTyping = userInput !== '';
// if user is typing, should auto scroll to bottom
// if user is not typing, should auto scroll to bottom only if already at bottom
@@ -1027,7 +1049,7 @@ function _Chat() {
clear: () =>
chatStore.updateTargetSession(
session,
- (session) => (session.clearContextIndex = session.messages.length),
+ session => (session.clearContextIndex = session.messages.length),
),
fork: () => chatStore.forkSession(),
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
@@ -1046,18 +1068,19 @@ function _Chat() {
setPromptHints(chatCommands.search(text));
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion
- if (text.startsWith("/")) {
- let searchText = text.slice(1);
+ if (text.startsWith('/')) {
+ const searchText = text.slice(1);
onSearch(searchText);
}
}
};
const doSubmit = (userInput: string) => {
- if (userInput.trim() === "" && isEmpty(attachImages)) return;
+ if (userInput.trim() === '' && isEmpty(attachImages))
+ { return; }
const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) {
- setUserInput("");
+ setUserInput('');
setPromptHints([]);
matchCommand.invoke();
return;
@@ -1068,9 +1091,10 @@ function _Chat() {
.then(() => setIsLoading(false));
setAttachImages([]);
chatStore.setLastInput(userInput);
- setUserInput("");
+ setUserInput('');
setPromptHints([]);
- if (!isMobileScreen) inputRef.current?.focus();
+ if (!isMobileScreen)
+ { inputRef.current?.focus(); }
setAutoScroll(true);
};
@@ -1082,7 +1106,7 @@ function _Chat() {
if (matchedChatCommand.matched) {
// if user is selecting a chat command, just trigger it
matchedChatCommand.invoke();
- setUserInput("");
+ setUserInput('');
} else {
// or fill the prompt
setUserInput(prompt.content);
@@ -1110,7 +1134,7 @@ function _Chat() {
m.isError = true;
m.content = prettyObject({
error: true,
- message: "empty response",
+ message: 'empty response',
});
}
}
@@ -1118,7 +1142,7 @@ function _Chat() {
// auto sync mask config from global config
if (session.mask.syncGlobalConfig) {
- console.log("[Mask] syncing from global, name = ", session.mask.name);
+ console.log('[Mask] syncing from global, name = ', session.mask.name);
session.mask.modelConfig = { ...config.modelConfig };
}
});
@@ -1129,11 +1153,11 @@ function _Chat() {
const onInputKeyDown = (e: React.KeyboardEvent) => {
// if ArrowUp and no userInput, fill with last input
if (
- e.key === "ArrowUp" &&
- userInput.length <= 0 &&
- !(e.metaKey || e.altKey || e.ctrlKey)
+ e.key === 'ArrowUp'
+ && userInput.length <= 0
+ && !(e.metaKey || e.altKey || e.ctrlKey)
) {
- setUserInput(chatStore.lastInput ?? "");
+ setUserInput(chatStore.lastInput ?? '');
e.preventDefault();
return;
}
@@ -1156,8 +1180,8 @@ function _Chat() {
const deleteMessage = (msgId?: string) => {
chatStore.updateTargetSession(
session,
- (session) =>
- (session.messages = session.messages.filter((m) => m.id !== msgId)),
+ session =>
+ (session.messages = session.messages.filter(m => m.id !== msgId)),
);
};
@@ -1173,31 +1197,31 @@ function _Chat() {
// 4. resend the user's input
const resendingIndex = session.messages.findIndex(
- (m) => m.id === message.id,
+ m => m.id === message.id,
);
if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
- console.error("[Chat] failed to find resending message", message);
+ console.error('[Chat] failed to find resending message', message);
return;
}
let userMessage: ChatMessage | undefined;
let botMessage: ChatMessage | undefined;
- if (message.role === "assistant") {
+ if (message.role === 'assistant') {
// if it is resending a bot's message, find the user input for it
botMessage = message;
for (let i = resendingIndex; i >= 0; i -= 1) {
- if (session.messages[i].role === "user") {
+ if (session.messages[i].role === 'user') {
userMessage = session.messages[i];
break;
}
}
- } else if (message.role === "user") {
+ } else if (message.role === 'user') {
// if it is resending a user's input, find the bot's response
userMessage = message;
for (let i = resendingIndex; i < session.messages.length; i += 1) {
- if (session.messages[i].role === "assistant") {
+ if (session.messages[i].role === 'assistant') {
botMessage = session.messages[i];
break;
}
@@ -1205,7 +1229,7 @@ function _Chat() {
}
if (userMessage === undefined) {
- console.error("[Chat] failed to resend", message);
+ console.error('[Chat] failed to resend', message);
return;
}
@@ -1222,9 +1246,8 @@ function _Chat() {
};
const onPinMessage = (message: ChatMessage) => {
- chatStore.updateTargetSession(session, (session) =>
- session.mask.context.push(message),
- );
+ chatStore.updateTargetSession(session, session =>
+ session.mask.context.push(message));
showToast(Locale.Chat.Actions.PinToastContent, {
text: Locale.Chat.Actions.PinToastAction,
@@ -1242,13 +1265,14 @@ function _Chat() {
ttsPlayer.stop();
setSpeechStatus(false);
} else {
- var api: ClientApi;
+ let api: ClientApi;
+ // @ts-ignore
api = new ClientApi(ModelProvider.GPT);
const config = useAppConfig.getState();
setSpeechLoading(true);
ttsPlayer.init();
let audioBuffer: ArrayBuffer;
- const { markdownToTxt } = require("markdown-to-txt");
+ const { markdownToTxt } = require('markdown-to-txt');
const textContent = markdownToTxt(text);
if (config.ttsConfig.engine !== DEFAULT_TTS_ENGINE) {
const edgeVoiceName = accessStore.edgeVoiceName();
@@ -1272,7 +1296,7 @@ function _Chat() {
setSpeechStatus(false);
})
.catch((e) => {
- console.error("[OpenAI Speech]", e);
+ console.error('[OpenAI Speech]', e);
showToast(prettyObject(e));
setSpeechStatus(false);
})
@@ -1285,8 +1309,8 @@ function _Chat() {
}, [session.mask.context, session.mask.hideContext]);
if (
- context.length === 0 &&
- session.messages.at(0)?.content !== BOT_HELLO.content
+ context.length === 0
+ && session.messages.at(0)?.content !== BOT_HELLO.content
) {
const copiedHello = Object.assign({}, BOT_HELLO);
if (!accessStore.isAuthorized()) {
@@ -1304,8 +1328,8 @@ function _Chat() {
? [
{
...createMessage({
- role: "assistant",
- content: "……",
+ role: 'assistant',
+ content: '……',
}),
preview: true,
},
@@ -1317,7 +1341,7 @@ function _Chat() {
? [
{
...createMessage({
- role: "user",
+ role: 'user',
content: userInput,
}),
preview: true,
@@ -1356,8 +1380,8 @@ function _Chat() {
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
- const isHitBottom =
- bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
+ const isHitBottom
+ = bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
@@ -1377,8 +1401,8 @@ function _Chat() {
}
// clear context index = context length + index in messages
- const clearContextIndex =
- (session.clearContextIndex ?? -1) >= 0
+ const clearContextIndex
+ = (session.clearContextIndex ?? -1) >= 0
? session.clearContextIndex! + context.length - msgRenderIndex
: -1;
@@ -1395,16 +1419,18 @@ function _Chat() {
doSubmit(text);
},
code: (text) => {
- if (accessStore.disableFastLink) return;
- console.log("[Command] got code from url: ", text);
- showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
+ if (accessStore.disableFastLink)
+ { return; }
+ console.log('[Command] got code from url: ', text);
+ showConfirm(`${Locale.URLCommand.Code}code = ${text}`).then((res) => {
if (res) {
- accessStore.update((access) => (access.accessCode = text));
+ accessStore.update(access => (access.accessCode = text));
}
});
},
settings: (text) => {
- if (accessStore.disableFastLink) return;
+ if (accessStore.disableFastLink)
+ { return; }
try {
const payload = JSON.parse(text) as {
@@ -1412,27 +1438,28 @@ function _Chat() {
url?: string;
};
- console.log("[Command] got settings from url: ", payload);
+ console.log('[Command] got settings from url: ', payload);
if (payload.key || payload.url) {
showConfirm(
- Locale.URLCommand.Settings +
- `\n${JSON.stringify(payload, null, 4)}`,
+ `${Locale.URLCommand.Settings
+ }\n${JSON.stringify(payload, null, 4)}`,
).then((res) => {
- if (!res) return;
+ if (!res)
+ { return; }
if (payload.key) {
accessStore.update(
- (access) => (access.openaiApiKey = payload.key!),
+ access => (access.openaiApiKey = payload.key!),
);
}
if (payload.url) {
- accessStore.update((access) => (access.openaiUrl = payload.url!));
+ accessStore.update(access => (access.openaiUrl = payload.url!));
}
- accessStore.update((access) => (access.useCustomConfig = true));
+ accessStore.update(access => (access.useCustomConfig = true));
});
}
} catch {
- console.error("[Command] failed to get settings from url: ", text);
+ console.error('[Command] failed to get settings from url: ', text);
}
},
});
@@ -1452,7 +1479,7 @@ function _Chat() {
const dom = inputRef.current;
return () => {
- localStorage.setItem(key, dom?.value ?? "");
+ localStorage.setItem(key, dom?.value ?? '');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -1465,7 +1492,7 @@ function _Chat() {
}
const items = (event.clipboardData || window.clipboardData).items;
for (const item of items) {
- if (item.kind === "file" && item.type.startsWith("image/")) {
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
event.preventDefault();
const file = item.getAsFile();
if (file) {
@@ -1506,10 +1533,10 @@ function _Chat() {
images.push(
...(await new Promise((res, rej) => {
- const fileInput = document.createElement("input");
- fileInput.type = "file";
- fileInput.accept =
- "image/png, image/jpeg, image/webp, image/heic, image/heif";
+ const fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept
+ = 'image/png, image/jpeg, image/webp, image/heic, image/heif';
fileInput.multiple = true;
fileInput.onchange = (event: any) => {
setUploading(true);
@@ -1521,8 +1548,8 @@ function _Chat() {
.then((dataUrl) => {
imagesData.push(dataUrl);
if (
- imagesData.length === 3 ||
- imagesData.length === files.length
+ imagesData.length === 3
+ || imagesData.length === files.length
) {
setUploading(false);
res(imagesData);
@@ -1552,9 +1579,9 @@ function _Chat() {
const handleKeyDown = (event: any) => {
// 打开新聊天 command + shift + o
if (
- (event.metaKey || event.ctrlKey) &&
- event.shiftKey &&
- event.key.toLowerCase() === "o"
+ (event.metaKey || event.ctrlKey)
+ && event.shiftKey
+ && event.key.toLowerCase() === 'o'
) {
event.preventDefault();
setTimeout(() => {
@@ -1563,32 +1590,32 @@ function _Chat() {
}, 10);
}
// 聚焦聊天输入 shift + esc
- else if (event.shiftKey && event.key.toLowerCase() === "escape") {
+ else if (event.shiftKey && event.key.toLowerCase() === 'escape') {
event.preventDefault();
inputRef.current?.focus();
}
// 复制最后一个代码块 command + shift + ;
else if (
- (event.metaKey || event.ctrlKey) &&
- event.shiftKey &&
- event.code === "Semicolon"
+ (event.metaKey || event.ctrlKey)
+ && event.shiftKey
+ && event.code === 'Semicolon'
) {
event.preventDefault();
- const copyCodeButton =
- document.querySelectorAll(".copy-code-button");
+ const copyCodeButton
+ = document.querySelectorAll('.copy-code-button');
if (copyCodeButton.length > 0) {
copyCodeButton[copyCodeButton.length - 1].click();
}
}
// 复制最后一个回复 command + shift + c
else if (
- (event.metaKey || event.ctrlKey) &&
- event.shiftKey &&
- event.key.toLowerCase() === "c"
+ (event.metaKey || event.ctrlKey)
+ && event.shiftKey
+ && event.key.toLowerCase() === 'c'
) {
event.preventDefault();
const lastNonUserMessage = messages
- .filter((message) => message.role !== "user")
+ .filter(message => message.role !== 'user')
.pop();
if (lastNonUserMessage) {
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
@@ -1596,16 +1623,16 @@ function _Chat() {
}
}
// 展示快捷键 command + /
- else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
+ else if ((event.metaKey || event.ctrlKey) && event.key === '/') {
event.preventDefault();
setShowShortcutKeyModal(true);
}
};
- window.addEventListener("keydown", handleKeyDown);
+ window.addEventListener('keydown', handleKeyDown);
return () => {
- window.removeEventListener("keydown", handleKeyDown);
+ window.removeEventListener('keydown', handleKeyDown);
};
}, [messages, chatStore, navigate]);
@@ -1617,7 +1644,7 @@ function _Chat() {
{isMobileScreen && (
-
+
}
bordered
@@ -1629,12 +1656,12 @@ function _Chat() {
)}
setIsEditingMessage(true)}
>
@@ -1686,7 +1713,7 @@ function _Chat() {
aria={Locale.Chat.Actions.FullScreen}
onClick={() => {
config.update(
- (config) => (config.tightBorder = !config.tightBorder),
+ config => (config.tightBorder = !config.tightBorder),
);
}}
/>
@@ -1700,12 +1727,12 @@ function _Chat() {
setShowModal={setShowPromptModal}
/>
-
-
+
+
onChatBodyScroll(e.currentTarget)}
+ onScroll={e => onChatBodyScroll(e.currentTarget)}
onMouseDown={() => inputRef.current?.blur()}
onTouchStart={() => {
inputRef.current?.blur();
@@ -1713,30 +1740,30 @@ function _Chat() {
}}
>
{messages.map((message, i) => {
- const isUser = message.role === "user";
+ const isUser = message.role === 'user';
const isContext = i < context.length;
- const showActions =
- i > 0 &&
- !(message.preview || message.content.length === 0) &&
- !isContext;
+ const showActions
+ = i > 0
+ && !(message.preview || message.content.length === 0)
+ && !isContext;
const showTyping = message.preview || message.streaming;
- const shouldShowClearContextDivider =
- i === clearContextIndex - 1;
+ const shouldShowClearContextDivider
+ = i === clearContextIndex - 1;
return (
-
-
-
-
+
+
+
+
}
aria={Locale.Chat.Actions.Edit}
@@ -1746,16 +1773,16 @@ function _Chat() {
getMessageTextContent(message),
10,
);
- let newContent: string | MultimodalContent[] =
- newMessage;
+ let newContent: string | MultimodalContent[]
+ = newMessage;
const images = getMessageImages(message);
if (images.length > 0) {
newContent = [
- { type: "text", text: newMessage },
+ { type: 'text', text: newMessage },
];
for (let i = 0; i < images.length; i++) {
newContent.push({
- type: "image_url",
+ type: 'image_url',
image_url: {
url: images[i],
},
@@ -1767,141 +1794,153 @@ function _Chat() {
(session) => {
const m = session.mask.context
.concat(session.messages)
- .find((m) => m.id === message.id);
+ .find(m => m.id === message.id);
if (m) {
m.content = newContent;
}
},
);
}}
- >
+ >
+
- {isUser ? (
-
- ) : (
- <>
- {["system"].includes(message.role) ? (
-
- ) : (
-
+ {isUser
+ ? (
+
+ )
+ : (
+ <>
+ {['system'].includes(message.role)
+ ? (
+
+ )
+ : (
+
+ )}
+ >
)}
- >
- )}
{!isUser && (
-
+
{message.model}
)}
{showActions && (
-
-
- {message.streaming ? (
-
}
- onClick={() => onUserStop(message.id ?? i)}
- />
- ) : (
- <>
-
}
- onClick={() => onResend(message)}
- />
-
-
}
- onClick={() => onDelete(message.id ?? i)}
- />
-
-
}
- onClick={() => onPinMessage(message)}
- />
-
}
- onClick={() =>
- copyToClipboard(
- getMessageTextContent(message),
- )
- }
- />
- {config.ttsConfig.enable && (
+
+
+ {message.streaming
+ ? (
- ) : (
-
- )
- }
- onClick={() =>
- openaiSpeech(
- getMessageTextContent(message),
- )
- }
+ text={Locale.Chat.Actions.Stop}
+ icon={}
+ onClick={() => onUserStop(message.id ?? i)}
/>
+ )
+ : (
+ <>
+ }
+ onClick={() => onResend(message)}
+ />
+
+ }
+ onClick={() => onDelete(message.id ?? i)}
+ />
+
+ }
+ onClick={() => onPinMessage(message)}
+ />
+ }
+ onClick={() =>
+ copyToClipboard(
+ getMessageTextContent(message),
+ )}
+ />
+ {config.ttsConfig.enable && (
+
+ )
+ : (
+
+ )
+ }
+ onClick={() =>
+ openaiSpeech(
+ getMessageTextContent(message),
+ )}
+ />
+ )}
+ >
)}
- >
- )}
)}
{message?.tools?.length == 0 && showTyping && (
-
+
{Locale.Chat.Typing}
)}
- {/*@ts-ignore*/}
+ {/* @ts-ignore */}
{message?.tools?.length > 0 && (
-
- {message?.tools?.map((tool) => (
+
+ {message?.tools?.map(tool => (
- {tool.isError === false ? (
-
- ) : tool.isError === true ? (
-
- ) : (
-
- )}
+ {tool.isError === false
+ ? (
+
+ )
+ : tool.isError === true
+ ? (
+
+ )
+ : (
+
+ )}
{tool?.function?.name}
))}
)}
-
+
onRightClick(e, message)} // hard to use
onDoubleClickCapture={() => {
- if (!isMobileScreen) return;
+ if (!isMobileScreen)
+ { return; }
setUserInput(getMessageTextContent(message));
}}
fontSize={fontSize}
@@ -1911,17 +1950,17 @@ function _Chat() {
/>
{getMessageImages(message).length == 1 && (
)}
{getMessageImages(message).length > 1 && (
{message?.audio_url && (
-
+
)}
-
+
{isContext
? Locale.Chat.IsContext
: message.date.toLocaleString()}
@@ -1959,7 +1998,7 @@ function _Chat() {
);
})}
-
+