mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-06 09:23:45 +08:00
Merge branch 'main' into tts-stt
This commit is contained in:
@@ -31,6 +31,7 @@ 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";
|
||||
|
||||
@@ -44,6 +45,8 @@ 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 {
|
||||
ChatMessage,
|
||||
@@ -56,6 +59,7 @@ import {
|
||||
useAppConfig,
|
||||
DEFAULT_TOPIC,
|
||||
ModelType,
|
||||
usePluginStore,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@@ -67,6 +71,8 @@ import {
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
isDalle3,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
isFirefox,
|
||||
} from "../utils";
|
||||
|
||||
@@ -103,7 +109,6 @@ import {
|
||||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
ServiceProvider,
|
||||
Plugin,
|
||||
} from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||
@@ -114,6 +119,8 @@ import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
import { ClientApi } from "../client/api";
|
||||
import { createTTSPlayer } from "../utils/audio";
|
||||
import {
|
||||
@@ -204,7 +211,7 @@ function PromptToast(props: {
|
||||
|
||||
return (
|
||||
<div className={styles["prompt-toast"]} key="prompt-toast">
|
||||
{props.showToast && (
|
||||
{props.showToast && context.length > 0 && (
|
||||
<div
|
||||
className={styles["prompt-toast-inner"] + " clickable"}
|
||||
role="button"
|
||||
@@ -453,11 +460,13 @@ export function ChatActions(props: {
|
||||
showPromptHints: () => void;
|
||||
hitBottom: boolean;
|
||||
uploading: boolean;
|
||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUserInput: (input: string) => void;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
const chatStore = useChatStore();
|
||||
const pluginStore = usePluginStore();
|
||||
|
||||
// switch themes
|
||||
const theme = config.theme;
|
||||
@@ -518,6 +527,8 @@ export function ChatActions(props: {
|
||||
const currentStyle =
|
||||
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useEffect(() => {
|
||||
const show = isVisionModel(currentModel);
|
||||
setShowUploadImage(show);
|
||||
@@ -528,8 +539,8 @@ export function ChatActions(props: {
|
||||
|
||||
// if current model is not available
|
||||
// switch to first available model
|
||||
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
|
||||
if (isUnavaliableModel && models.length > 0) {
|
||||
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];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
@@ -671,7 +682,7 @@ export function ChatActions(props: {
|
||||
items={models.map((m) => ({
|
||||
title: `${m.displayName}${
|
||||
m?.provider?.providerName
|
||||
? "(" + m?.provider?.providerName + ")"
|
||||
? " (" + m?.provider?.providerName + ")"
|
||||
: ""
|
||||
}`,
|
||||
value: `${m.name}@${m?.provider?.providerName}`,
|
||||
@@ -780,34 +791,44 @@ export function ChatActions(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChatAction
|
||||
onClick={() => setShowPluginSelector(true)}
|
||||
text={Locale.Plugin.Name}
|
||||
icon={<PluginIcon />}
|
||||
/>
|
||||
{showPlugins(currentProviderName, currentModel) && (
|
||||
<ChatAction
|
||||
onClick={() => {
|
||||
if (pluginStore.getAll().length == 0) {
|
||||
navigate(Path.Plugins);
|
||||
} else {
|
||||
setShowPluginSelector(true);
|
||||
}
|
||||
}}
|
||||
text={Locale.Plugin.Name}
|
||||
icon={<PluginIcon />}
|
||||
/>
|
||||
)}
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
multiple
|
||||
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
|
||||
items={[
|
||||
{
|
||||
title: Locale.Plugin.Artifacts,
|
||||
value: Plugin.Artifacts,
|
||||
},
|
||||
]}
|
||||
items={pluginStore.getAll().map((item) => ({
|
||||
title: `${item?.title}@${item?.version}`,
|
||||
value: item?.id,
|
||||
}))}
|
||||
onClose={() => setShowPluginSelector(false)}
|
||||
onSelection={(s) => {
|
||||
const plugin = s[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.plugin = s;
|
||||
session.mask.plugin = s as string[];
|
||||
});
|
||||
if (plugin) {
|
||||
showToast(plugin);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isMobileScreen && (
|
||||
<ChatAction
|
||||
onClick={() => props.setShowShortcutKeyModal(true)}
|
||||
text={Locale.Chat.ShortcutKey.Title}
|
||||
icon={<ShortcutkeyIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{config.sttConfig.enable && (
|
||||
<ChatAction
|
||||
onClick={async () =>
|
||||
@@ -891,6 +912,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutKeyModal(props: { onClose: () => void }) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
const shortcuts = [
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.newChat,
|
||||
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
|
||||
},
|
||||
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastCode,
|
||||
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastMessage,
|
||||
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.showShortcutKey,
|
||||
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Chat.ShortcutKey.Title}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
<IconButton
|
||||
type="primary"
|
||||
text={Locale.UI.Confirm}
|
||||
icon={<ConfirmIcon />}
|
||||
key="ok"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div className={styles["shortcut-key-container"]}>
|
||||
<div className={styles["shortcut-key-grid"]}>
|
||||
{shortcuts.map((shortcut, index) => (
|
||||
<div key={index} className={styles["shortcut-key-item"]}>
|
||||
<div className={styles["shortcut-key-title"]}>
|
||||
{shortcut.title}
|
||||
</div>
|
||||
<div className={styles["shortcut-key-keys"]}>
|
||||
{shortcut.keys.map((key, i) => (
|
||||
<div key={i} className={styles["shortcut-key"]}>
|
||||
<span>{key}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function _Chat() {
|
||||
type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
|
||||
@@ -1003,7 +1085,7 @@ function _Chat() {
|
||||
.onUserInput(userInput, attachImages)
|
||||
.then(() => setIsLoading(false));
|
||||
setAttachImages([]);
|
||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||
chatStore.setLastInput(userInput);
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
if (!isMobileScreen) inputRef.current?.focus();
|
||||
@@ -1069,7 +1151,7 @@ function _Chat() {
|
||||
userInput.length <= 0 &&
|
||||
!(e.metaKey || e.altKey || e.ctrlKey)
|
||||
) {
|
||||
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
||||
setUserInput(chatStore.lastInput ?? "");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -1480,6 +1562,70 @@ function _Chat() {
|
||||
setAttachImages(images);
|
||||
}
|
||||
|
||||
// 快捷键 shortcut keys
|
||||
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: any) => {
|
||||
// 打开新聊天 command + shift + o
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "o"
|
||||
) {
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
chatStore.newSession();
|
||||
navigate(Path.Chat);
|
||||
}, 10);
|
||||
}
|
||||
// 聚焦聊天输入 shift + esc
|
||||
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.preventDefault();
|
||||
const copyCodeButton =
|
||||
document.querySelectorAll<HTMLElement>(".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.preventDefault();
|
||||
const lastNonUserMessage = messages
|
||||
.filter((message) => message.role !== "user")
|
||||
.pop();
|
||||
if (lastNonUserMessage) {
|
||||
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
|
||||
copyToClipboard(lastMessageContent);
|
||||
}
|
||||
}
|
||||
// 展示快捷键 command + /
|
||||
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
|
||||
event.preventDefault();
|
||||
setShowShortcutKeyModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [messages, chatStore, navigate]);
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -1508,6 +1654,17 @@ function _Chat() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<ReloadIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.RefreshTitle}
|
||||
onClick={() => {
|
||||
showToast(Locale.Chat.Actions.RefreshToast);
|
||||
chatStore.summarizeSession(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isMobileScreen && (
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
@@ -1704,11 +1861,31 @@ function _Chat() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showTyping && (
|
||||
{message?.tools?.length == 0 && showTyping && (
|
||||
<div className={styles["chat-message-status"]}>
|
||||
{Locale.Chat.Typing}
|
||||
</div>
|
||||
)}
|
||||
{/*@ts-ignore*/}
|
||||
{message?.tools?.length > 0 && (
|
||||
<div className={styles["chat-message-tools"]}>
|
||||
{message?.tools?.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className={styles["chat-message-tool"]}
|
||||
>
|
||||
{tool.isError === false ? (
|
||||
<ConfirmIcon />
|
||||
) : tool.isError === true ? (
|
||||
<CloseIcon />
|
||||
) : (
|
||||
<LoadingButtonIcon />
|
||||
)}
|
||||
<span>{tool?.function?.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
<Markdown
|
||||
key={message.streaming ? "loading" : "done"}
|
||||
@@ -1718,7 +1895,7 @@ function _Chat() {
|
||||
message.content.length === 0 &&
|
||||
!isUser
|
||||
}
|
||||
onContextMenu={(e) => onRightClick(e, message)}
|
||||
// onContextMenu={(e) => onRightClick(e, message)} // hard to use
|
||||
onDoubleClickCapture={() => {
|
||||
if (!isMobileScreen) return;
|
||||
setUserInput(getMessageTextContent(message));
|
||||
@@ -1795,6 +1972,7 @@ function _Chat() {
|
||||
setUserInput("/");
|
||||
onSearch("");
|
||||
}}
|
||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||
setUserInput={setUserInput}
|
||||
/>
|
||||
<label
|
||||
@@ -1867,6 +2045,10 @@ function _Chat() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showShortcutKeyModal && (
|
||||
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user