From 50b1f7db12fce10eeab7a83d25a9abe423f21809 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 24 Jun 2023 22:32:46 +0800 Subject: [PATCH 1/4] feat: right-click to copy message to user input --- app/components/chat.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 35f2804668b..d4e1dcd8483 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -605,6 +605,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(); } }; From 5d06fa217cfde63439ef2c5b09cc71703e7d7f90 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 24 Jun 2023 22:41:13 +0800 Subject: [PATCH 2/4] feat: close #2013 add switch model button to chat actions --- app/components/chat.tsx | 21 +++++++++++++++++++++ app/icons/robot.svg | 1 + 2 files changed, 22 insertions(+) create mode 100644 app/icons/robot.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index d4e1dcd8483..505af77da23 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -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 { @@ -385,6 +387,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 (
{couldStop && ( @@ -453,6 +468,12 @@ export function ChatActions(props: { }); }} /> + + } + />
); } diff --git a/app/icons/robot.svg b/app/icons/robot.svg new file mode 100644 index 00000000000..62dd9dc8d84 --- /dev/null +++ b/app/icons/robot.svg @@ -0,0 +1 @@ + From ae1ef3215b45ae373044f0cba370307279d1ff7a Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 24 Jun 2023 23:38:11 +0800 Subject: [PATCH 3/4] feat: close #628 add chat commands --- app/command.ts | 43 ++++++++++++++++++++++++++++++++++++++ app/components/chat.tsx | 38 ++++++++++++++++++++++++++------- app/components/sidebar.tsx | 7 ++----- app/locales/cn.ts | 10 ++++++++- app/locales/en.ts | 10 ++++++++- app/store/chat.ts | 8 +++++++ 6 files changed, 102 insertions(+), 14 deletions(-) diff --git a/app/command.ts b/app/command.ts index 40bad92b3a9..ba3bb65389e 100644 --- a/app/command.ts +++ b/app/command.ts @@ -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 }; +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 505af77da23..e1011e42241 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -66,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"; @@ -208,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 @@ -510,16 +509,19 @@ export function Chat() { const [promptHints, setPromptHints] = useState([]); 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 @@ -543,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) => { @@ -552,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("/")) { @@ -563,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); diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index a43daedefb0..6392b962ae9 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -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); } } }; diff --git a/app/locales/cn.ts b/app/locales/cn.ts index e94c0332ec4..14ee7ec9960 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -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: { diff --git a/app/locales/en.ts b/app/locales/en.ts index 8e56147c827..1659ddb1f67 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -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: { diff --git a/app/store/chat.ts b/app/store/chat.ts index d420310051e..fa6296811bf 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -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; summarizeSession: () => void; @@ -200,6 +201,13 @@ export const useChatStore = create()( })); }, + 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); From 8915af9b6d7150f128e9fb99052300e20f9af311 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Sat, 24 Jun 2023 23:39:38 +0800 Subject: [PATCH 4/4] fixup --- app/locales/ar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/locales/ar.ts b/app/locales/ar.ts index 70bfb0cebc1..7a3eaa2b2ca 100644 --- a/app/locales/ar.ts +++ b/app/locales/ar.ts @@ -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: