|
|
|
|
@ -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) => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setPromptHints([]);
|
|
|
|
|
setUserInput(prompt.content);
|
|
|
|
|
inputRef.current?.focus();
|
|
|
|
|
setTimeout(() => setUserInput(prompt.content), 60);
|
|
|
|
|
}, 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();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|