forked from XiaoMo/ChatGPT-Next-Web
Merge pull request #2118 from Yidadaa/bugfix-0624
feat: #2013 #628 switch model button & chat commands
This commit is contained in:
commit
7ee062e1de
@ -1,4 +1,5 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import Locale from "./locales";
|
||||
|
||||
type Command = (param: string) => void;
|
||||
interface Commands {
|
||||
@ -26,3 +27,45 @@ export function useCommand(commands: Commands = {}) {
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatCommands {
|
||||
new?: Command;
|
||||
newm?: Command;
|
||||
next?: Command;
|
||||
prev?: Command;
|
||||
clear?: Command;
|
||||
del?: Command;
|
||||
}
|
||||
|
||||
export const ChatCommandPrefix = ":";
|
||||
|
||||
export function useChatCommand(commands: ChatCommands = {}) {
|
||||
function extract(userInput: string) {
|
||||
return (
|
||||
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
|
||||
) as keyof ChatCommands;
|
||||
}
|
||||
|
||||
function search(userInput: string) {
|
||||
const input = extract(userInput);
|
||||
const desc = Locale.Chat.Commands;
|
||||
return Object.keys(commands)
|
||||
.filter((c) => c.startsWith(input))
|
||||
.map((c) => ({
|
||||
title: desc[c as keyof ChatCommands],
|
||||
content: ChatCommandPrefix + c,
|
||||
}));
|
||||
}
|
||||
|
||||
function match(userInput: string) {
|
||||
const command = extract(userInput);
|
||||
const matched = typeof commands[command] === "function";
|
||||
|
||||
return {
|
||||
matched,
|
||||
invoke: () => matched && commands[command]!(userInput),
|
||||
};
|
||||
}
|
||||
|
||||
return { match, search };
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ 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 {
|
||||
ChatMessage,
|
||||
@ -38,6 +39,7 @@ import {
|
||||
Theme,
|
||||
useAppConfig,
|
||||
DEFAULT_TOPIC,
|
||||
ALL_MODELS,
|
||||
} from "../store";
|
||||
|
||||
import {
|
||||
@ -64,7 +66,7 @@ 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 { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
@ -206,8 +208,7 @@ export function PromptHints(props: {
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (noPrompts) return;
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
// arrow up / down to select prompt
|
||||
@ -385,6 +386,19 @@ export function ChatActions(props: {
|
||||
const couldStop = ChatControllerPool.hasPending();
|
||||
const stopAll = () => ChatControllerPool.stopAll();
|
||||
|
||||
// switch model
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
function nextModel() {
|
||||
const models = ALL_MODELS.filter((m) => m.available).map((m) => m.name);
|
||||
const modelIndex = models.indexOf(currentModel);
|
||||
const nextIndex = (modelIndex + 1) % models.length;
|
||||
const nextModel = models[nextIndex];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.mask.modelConfig.model = nextModel;
|
||||
session.mask.syncGlobalConfig = false;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={chatStyle["chat-input-actions"]}>
|
||||
{couldStop && (
|
||||
@ -453,6 +467,12 @@ export function ChatActions(props: {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<ChatAction
|
||||
onClick={nextModel}
|
||||
text={currentModel}
|
||||
icon={<RobotIcon />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -489,16 +509,19 @@ export function Chat() {
|
||||
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
||||
const onSearch = useDebouncedCallback(
|
||||
(text: string) => {
|
||||
setPromptHints(promptStore.search(text));
|
||||
const matchedPrompts = promptStore.search(text);
|
||||
setPromptHints(matchedPrompts);
|
||||
},
|
||||
100,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
const onPromptSelect = (prompt: Prompt) => {
|
||||
setPromptHints([]);
|
||||
inputRef.current?.focus();
|
||||
setTimeout(() => setUserInput(prompt.content), 60);
|
||||
setTimeout(() => {
|
||||
setPromptHints([]);
|
||||
setUserInput(prompt.content);
|
||||
inputRef.current?.focus();
|
||||
}, 30);
|
||||
};
|
||||
|
||||
// auto grow input
|
||||
@ -522,6 +545,19 @@ export function Chat() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(measure, [userInput]);
|
||||
|
||||
// chat commands shortcuts
|
||||
const chatCommands = useChatCommand({
|
||||
new: () => chatStore.newSession(),
|
||||
newm: () => navigate(Path.NewChat),
|
||||
prev: () => chatStore.nextSession(-1),
|
||||
next: () => chatStore.nextSession(1),
|
||||
clear: () =>
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.clearContextIndex = session.messages.length),
|
||||
),
|
||||
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
|
||||
});
|
||||
|
||||
// only search prompts when user input is short
|
||||
const SEARCH_TEXT_LIMIT = 30;
|
||||
const onInput = (text: string) => {
|
||||
@ -531,6 +567,8 @@ export function Chat() {
|
||||
// clear search results
|
||||
if (n === 0) {
|
||||
setPromptHints([]);
|
||||
} else if (text.startsWith(ChatCommandPrefix)) {
|
||||
setPromptHints(chatCommands.search(text));
|
||||
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||
// check if need to trigger auto completion
|
||||
if (text.startsWith("/")) {
|
||||
@ -542,6 +580,13 @@ export function Chat() {
|
||||
|
||||
const doSubmit = (userInput: string) => {
|
||||
if (userInput.trim() === "") return;
|
||||
const matchCommand = chatCommands.match(userInput);
|
||||
if (matchCommand.matched) {
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
matchCommand.invoke();
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||
@ -605,6 +650,10 @@ export function Chat() {
|
||||
const onRightClick = (e: any, message: ChatMessage) => {
|
||||
// copy to clipboard
|
||||
if (selectOrCopy(e.currentTarget, message.content)) {
|
||||
if (userInput.length === 0) {
|
||||
setUserInput(message.content);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
@ -38,13 +38,10 @@ function useHotKey() {
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
const n = chatStore.sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = chatStore.currentSessionIndex;
|
||||
if (e.key === "ArrowUp") {
|
||||
chatStore.selectSession(limit(i - 1));
|
||||
chatStore.nextSession(-1);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
chatStore.selectSession(limit(i + 1));
|
||||
chatStore.nextSession(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
1
app/icons/robot.svg
Normal file
1
app/icons/robot.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
@ -1,7 +1,7 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { LocaleType } from "./index";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
|
||||
const ar: LocaleType = {
|
||||
const ar: PartialLocaleType = {
|
||||
WIP: "قريبًا...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
|
@ -27,6 +27,14 @@ const cn = {
|
||||
Retry: "重试",
|
||||
Delete: "删除",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
newm: "从面具新建聊天",
|
||||
next: "下一个聊天",
|
||||
prev: "上一个聊天",
|
||||
clear: "清除上下文",
|
||||
del: "删除聊天",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "停止响应",
|
||||
ToBottom: "滚到最新",
|
||||
@ -47,7 +55,7 @@ const cn = {
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ",Shift + Enter 换行";
|
||||
}
|
||||
return inputHints + ",/ 触发补全";
|
||||
return inputHints + ",/ 触发补全,: 触发命令";
|
||||
},
|
||||
Send: "发送",
|
||||
Config: {
|
||||
|
@ -28,6 +28,14 @@ const en: LocaleType = {
|
||||
Retry: "Retry",
|
||||
Delete: "Delete",
|
||||
},
|
||||
Commands: {
|
||||
new: "Start a new chat",
|
||||
newm: "Start a new chat with mask",
|
||||
next: "Next Chat",
|
||||
prev: "Previous Chat",
|
||||
clear: "Clear Context",
|
||||
del: "Delete Chat",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Stop",
|
||||
ToBottom: "To Latest",
|
||||
@ -48,7 +56,7 @@ const en: LocaleType = {
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter to wrap";
|
||||
}
|
||||
return inputHints + ", / to search prompts";
|
||||
return inputHints + ", / to search prompts, : to use commands";
|
||||
},
|
||||
Send: "Send",
|
||||
Config: {
|
||||
|
@ -85,6 +85,7 @@ interface ChatStore {
|
||||
newSession: (mask?: Mask) => void;
|
||||
deleteSession: (index: number) => void;
|
||||
currentSession: () => ChatSession;
|
||||
nextSession: (delta: number) => void;
|
||||
onNewMessage: (message: ChatMessage) => void;
|
||||
onUserInput: (content: string) => Promise<void>;
|
||||
summarizeSession: () => void;
|
||||
@ -200,6 +201,13 @@ export const useChatStore = create<ChatStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
nextSession(delta) {
|
||||
const n = get().sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = get().currentSessionIndex;
|
||||
get().selectSession(limit(i + delta));
|
||||
},
|
||||
|
||||
deleteSession(index) {
|
||||
const deletingLastSession = get().sessions.length === 1;
|
||||
const deletedSession = get().sessions.at(index);
|
||||
|
Loading…
Reference in New Issue
Block a user