Merge pull request #2118 from Yidadaa/bugfix-0624

feat: #2013 #628 switch model button & chat commands
This commit is contained in:
Yifei Zhang 2023-06-24 23:40:54 +08:00 committed by GitHub
commit 7ee062e1de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 130 additions and 16 deletions

View File

@ -1,4 +1,5 @@
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import Locale from "./locales";
type Command = (param: string) => void; type Command = (param: string) => void;
interface Commands { interface Commands {
@ -26,3 +27,45 @@ export function useCommand(commands: Commands = {}) {
setSearchParams(searchParams); 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 };
}

View File

@ -27,6 +27,7 @@ import DarkIcon from "../icons/dark.svg";
import AutoIcon from "../icons/auto.svg"; import AutoIcon from "../icons/auto.svg";
import BottomIcon from "../icons/bottom.svg"; import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg"; import StopIcon from "../icons/pause.svg";
import RobotIcon from "../icons/robot.svg";
import { import {
ChatMessage, ChatMessage,
@ -38,6 +39,7 @@ import {
Theme, Theme,
useAppConfig, useAppConfig,
DEFAULT_TOPIC, DEFAULT_TOPIC,
ALL_MODELS,
} from "../store"; } from "../store";
import { import {
@ -64,7 +66,7 @@ import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
import { Avatar } from "./emoji"; import { Avatar } from "./emoji";
import { MaskAvatar, MaskConfig } from "./mask"; import { MaskAvatar, MaskConfig } from "./mask";
import { useMaskStore } from "../store/mask"; import { useMaskStore } from "../store/mask";
import { useCommand } from "../command"; import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter"; import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
@ -206,8 +208,7 @@ export function PromptHints(props: {
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (noPrompts) return; if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
if (e.metaKey || e.altKey || e.ctrlKey) {
return; return;
} }
// arrow up / down to select prompt // arrow up / down to select prompt
@ -385,6 +386,19 @@ export function ChatActions(props: {
const couldStop = ChatControllerPool.hasPending(); const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll(); 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 ( return (
<div className={chatStyle["chat-input-actions"]}> <div className={chatStyle["chat-input-actions"]}>
{couldStop && ( {couldStop && (
@ -453,6 +467,12 @@ export function ChatActions(props: {
}); });
}} }}
/> />
<ChatAction
onClick={nextModel}
text={currentModel}
icon={<RobotIcon />}
/>
</div> </div>
); );
} }
@ -489,16 +509,19 @@ export function Chat() {
const [promptHints, setPromptHints] = useState<Prompt[]>([]); const [promptHints, setPromptHints] = useState<Prompt[]>([]);
const onSearch = useDebouncedCallback( const onSearch = useDebouncedCallback(
(text: string) => { (text: string) => {
setPromptHints(promptStore.search(text)); const matchedPrompts = promptStore.search(text);
setPromptHints(matchedPrompts);
}, },
100, 100,
{ leading: true, trailing: true }, { leading: true, trailing: true },
); );
const onPromptSelect = (prompt: Prompt) => { const onPromptSelect = (prompt: Prompt) => {
setPromptHints([]); setTimeout(() => {
inputRef.current?.focus(); setPromptHints([]);
setTimeout(() => setUserInput(prompt.content), 60); setUserInput(prompt.content);
inputRef.current?.focus();
}, 30);
}; };
// auto grow input // auto grow input
@ -522,6 +545,19 @@ export function Chat() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(measure, [userInput]); 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 // only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30; const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => { const onInput = (text: string) => {
@ -531,6 +567,8 @@ export function Chat() {
// clear search results // clear search results
if (n === 0) { if (n === 0) {
setPromptHints([]); setPromptHints([]);
} else if (text.startsWith(ChatCommandPrefix)) {
setPromptHints(chatCommands.search(text));
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion // check if need to trigger auto completion
if (text.startsWith("/")) { if (text.startsWith("/")) {
@ -542,6 +580,13 @@ export function Chat() {
const doSubmit = (userInput: string) => { const doSubmit = (userInput: string) => {
if (userInput.trim() === "") return; if (userInput.trim() === "") return;
const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) {
setUserInput("");
setPromptHints([]);
matchCommand.invoke();
return;
}
setIsLoading(true); setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false)); chatStore.onUserInput(userInput).then(() => setIsLoading(false));
localStorage.setItem(LAST_INPUT_KEY, userInput); localStorage.setItem(LAST_INPUT_KEY, userInput);
@ -605,6 +650,10 @@ export function Chat() {
const onRightClick = (e: any, message: ChatMessage) => { const onRightClick = (e: any, message: ChatMessage) => {
// copy to clipboard // copy to clipboard
if (selectOrCopy(e.currentTarget, message.content)) { if (selectOrCopy(e.currentTarget, message.content)) {
if (userInput.length === 0) {
setUserInput(message.content);
}
e.preventDefault(); e.preventDefault();
} }
}; };

View File

@ -38,13 +38,10 @@ function useHotKey() {
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (e.metaKey || e.altKey || e.ctrlKey) { 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") { if (e.key === "ArrowUp") {
chatStore.selectSession(limit(i - 1)); chatStore.nextSession(-1);
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
chatStore.selectSession(limit(i + 1)); chatStore.nextSession(1);
} }
} }
}; };

1
app/icons/robot.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const ar: LocaleType = { const ar: PartialLocaleType = {
WIP: "قريبًا...", WIP: "قريبًا...",
Error: { Error: {
Unauthorized: Unauthorized:

View File

@ -27,6 +27,14 @@ const cn = {
Retry: "重试", Retry: "重试",
Delete: "删除", Delete: "删除",
}, },
Commands: {
new: "新建聊天",
newm: "从面具新建聊天",
next: "下一个聊天",
prev: "上一个聊天",
clear: "清除上下文",
del: "删除聊天",
},
InputActions: { InputActions: {
Stop: "停止响应", Stop: "停止响应",
ToBottom: "滚到最新", ToBottom: "滚到最新",
@ -47,7 +55,7 @@ const cn = {
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行"; inputHints += "Shift + Enter 换行";
} }
return inputHints + "/ 触发补全"; return inputHints + "/ 触发补全: 触发命令";
}, },
Send: "发送", Send: "发送",
Config: { Config: {

View File

@ -28,6 +28,14 @@ const en: LocaleType = {
Retry: "Retry", Retry: "Retry",
Delete: "Delete", 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: { InputActions: {
Stop: "Stop", Stop: "Stop",
ToBottom: "To Latest", ToBottom: "To Latest",
@ -48,7 +56,7 @@ const en: LocaleType = {
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter to wrap"; inputHints += ", Shift + Enter to wrap";
} }
return inputHints + ", / to search prompts"; return inputHints + ", / to search prompts, : to use commands";
}, },
Send: "Send", Send: "Send",
Config: { Config: {

View File

@ -85,6 +85,7 @@ interface ChatStore {
newSession: (mask?: Mask) => void; newSession: (mask?: Mask) => void;
deleteSession: (index: number) => void; deleteSession: (index: number) => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
nextSession: (delta: number) => void;
onNewMessage: (message: ChatMessage) => void; onNewMessage: (message: ChatMessage) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
summarizeSession: () => 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) { deleteSession(index) {
const deletingLastSession = get().sessions.length === 1; const deletingLastSession = get().sessions.length === 1;
const deletedSession = get().sessions.at(index); const deletedSession = get().sessions.at(index);