Prechádzať zdrojové kódy

feat: close #628 add chat commands

Yidadaa 1 rok pred
rodič
commit
ae1ef3215b

+ 43 - 0
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 };
+}

+ 31 - 7
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<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
@@ -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);

+ 2 - 5
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);
         }
       }
     };

+ 9 - 1
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: {

+ 9 - 1
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: {

+ 8 - 0
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<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);