diff --git a/app/command.ts b/app/command.ts
index 40bad92b..ba3bb653 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 35f28046..e1011e42 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 {
@@ -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 (
{couldStop && (
@@ -453,6 +467,12 @@ export function ChatActions(props: {
});
}}
/>
+
+ }
+ />
);
}
@@ -489,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
@@ -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();
}
};
diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx
index a43daede..6392b962 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/icons/robot.svg b/app/icons/robot.svg
new file mode 100644
index 00000000..62dd9dc8
--- /dev/null
+++ b/app/icons/robot.svg
@@ -0,0 +1 @@
+
diff --git a/app/locales/ar.ts b/app/locales/ar.ts
index 70bfb0ce..7a3eaa2b 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:
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index e94c0332..14ee7ec9 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 8e56147c..1659ddb1 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 d4203100..fa629681 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);