* modify chat table & migrate * refactor layout * update layout * actions for chats & agents * refactor chat form * refactor chat form * update chat form * rename * refactor types & locales * refactor tts engine * refactor * fix config * refactor chat form * refactor chat member form * fixing * refactor ask agent * chat in conversation * fix chat message update * may update chat member * update chat member from message * refacto group propmt * chat member gpt settings * update ui * more config for chat * add locales * update chat agent form * add locales for agent form * update UI * auto reply for text input * update chat * update chat input * rename colomns * update chat * udpate agent message * add chat member * add/remove chat member * fix chat member * refactor * auto update chat name * fix chat update * refactor chat column * fix chat * add agent loading * use fresh new prompt when ask agent * add chat forwarder * refactor chat * fix * add copilot * toggle copilot * add copilot chat * avoid open the same chat at the same time * update copilot header * add agent introduction in the first place of chat * rename column * update style * refactor * invoke all agents in group after asking * chat sidebar collopse * may select chat in copilot * update ui * new chat from agent * upgrade deps * refactor chat & chatAgent * add limit for chat member create * update chat form * update db & migration * fix up * fix group chat * fix panel warning * display chat agent type * tts message * fit tts agent * refactor * chat fowarder * update UI * setup default values for tts agent * fix chat member add/remove * edit tts agent * display chat date * Fix UI * add system message * refactor * fix hook * refactor * touch chat&agent when new message created * fix auto reply * migrate conversation to chat * add migrate api * fix migrate * update migrate * refactor * refactor * refactor * fix delete agent * add hotkey for copilot * fix bugs * upgrade deps * refactor tts hook * stop auto playback when azure transcribed * refactor * clean up * fix UI * fix conversation migrate * handle error * update model * declare types * audo backup db file when started * fix db backup * refactor db migration * fix UI * refactor * fix chat auto update name * fix authorization lost when hot reload * refactor * refactor * fix tts form * keep agent avatar up to date * clean code
290 lines
8.3 KiB
TypeScript
290 lines
8.3 KiB
TypeScript
import {
|
|
ArrowUpIcon,
|
|
CheckIcon,
|
|
LoaderIcon,
|
|
MicIcon,
|
|
PauseIcon,
|
|
PlayIcon,
|
|
StepForwardIcon,
|
|
TypeIcon,
|
|
WandIcon,
|
|
XIcon,
|
|
} from "lucide-react";
|
|
import { Button, Textarea } from "@renderer/components/ui";
|
|
import { useContext, useEffect, useRef, useState } from "react";
|
|
import { LiveAudioVisualizer } from "react-audio-visualize";
|
|
import {
|
|
AppSettingsProviderContext,
|
|
ChatSessionProviderContext,
|
|
HotKeysSettingsProviderContext,
|
|
} from "@renderer/context";
|
|
import { t } from "i18next";
|
|
import autosize from "autosize";
|
|
import { ChatSuggestionButton } from "@renderer/components";
|
|
import { useHotkeys } from "react-hotkeys-hook";
|
|
import { ChatTypeEnum } from "@/types/enums";
|
|
|
|
export const ChatInput = () => {
|
|
const {
|
|
chat,
|
|
submitting,
|
|
startRecording,
|
|
stopRecording,
|
|
cancelRecording,
|
|
togglePauseResume,
|
|
isRecording,
|
|
mediaRecorder,
|
|
recordingTime,
|
|
isPaused,
|
|
askAgent,
|
|
onCreateMessage,
|
|
shadowing,
|
|
} = useContext(ChatSessionProviderContext);
|
|
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
const submitRef = useRef<HTMLButtonElement>(null);
|
|
const [inputMode, setInputMode] = useState<"text" | "audio">("text");
|
|
const [content, setContent] = useState("");
|
|
const { currentHotkeys } = useContext(HotKeysSettingsProviderContext);
|
|
|
|
useEffect(() => {
|
|
if (!inputRef.current) return;
|
|
|
|
autosize(inputRef.current);
|
|
|
|
inputRef.current.addEventListener("keypress", (event) => {
|
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
event.preventDefault();
|
|
submitRef.current?.click();
|
|
}
|
|
});
|
|
|
|
inputRef.current.focus();
|
|
|
|
return () => {
|
|
inputRef.current?.removeEventListener("keypress", () => {});
|
|
autosize.destroy(inputRef.current);
|
|
};
|
|
}, [inputRef.current]);
|
|
|
|
useEffect(() => {
|
|
EnjoyApp.cacheObjects
|
|
.get(`chat-input-mode-${chat.id}`)
|
|
.then((cachedInputMode) => {
|
|
if (cachedInputMode) {
|
|
setInputMode(cachedInputMode as typeof inputMode);
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
EnjoyApp.cacheObjects.set(`chat-input-mode-${chat.id}`, inputMode);
|
|
}, [inputMode]);
|
|
|
|
useHotkeys(
|
|
currentHotkeys.StartOrStopRecording,
|
|
() => {
|
|
if (shadowing) return;
|
|
if (isRecording) {
|
|
stopRecording();
|
|
} else {
|
|
startRecording();
|
|
}
|
|
},
|
|
{
|
|
preventDefault: true,
|
|
}
|
|
);
|
|
|
|
useHotkeys(
|
|
currentHotkeys.PlayNextSegment,
|
|
() => {
|
|
if (shadowing) return;
|
|
askAgent({ force: true });
|
|
},
|
|
{
|
|
preventDefault: true,
|
|
}
|
|
);
|
|
|
|
if (isRecording) {
|
|
return (
|
|
<div className="z-10 w-full flex justify-center">
|
|
<div className="flex items-center space-x-2">
|
|
<LiveAudioVisualizer
|
|
mediaRecorder={mediaRecorder}
|
|
barWidth={2}
|
|
gap={2}
|
|
width={140}
|
|
height={30}
|
|
fftSize={512}
|
|
maxDecibels={-10}
|
|
minDecibels={-80}
|
|
smoothingTimeConstant={0.4}
|
|
/>
|
|
<span className="text-sm text-muted-foreground">
|
|
{Math.floor(recordingTime / 60)}:
|
|
{String(recordingTime % 60).padStart(2, "0")}
|
|
</span>
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("cancel")}
|
|
onClick={cancelRecording}
|
|
className="rounded-full shadow w-8 h-8 bg-red-500 hover:bg-red-600"
|
|
variant="secondary"
|
|
size="icon"
|
|
>
|
|
<XIcon fill="white" className="w-4 h-4 text-white" />
|
|
</Button>
|
|
<Button
|
|
onClick={togglePauseResume}
|
|
className="rounded-full shadow w-8 h-8"
|
|
size="icon"
|
|
>
|
|
{isPaused ? (
|
|
<PlayIcon
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("continue")}
|
|
fill="white"
|
|
className="w-4 h-4"
|
|
/>
|
|
) : (
|
|
<PauseIcon
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("pause")}
|
|
fill="white"
|
|
className="w-4 h-4"
|
|
/>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("finish")}
|
|
onClick={stopRecording}
|
|
className="rounded-full bg-green-500 hover:bg-green-600 shadow w-8 h-8"
|
|
size="icon"
|
|
>
|
|
<CheckIcon className="w-4 h-4 text-white" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (inputMode === "text") {
|
|
return (
|
|
<div className="z-10 w-full flex items-end gap-2 px-2 py-2 bg-muted mx-4 rounded-3xl shadow-lg">
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("audioInput")}
|
|
disabled={submitting}
|
|
onClick={() => setInputMode("audio")}
|
|
variant="ghost"
|
|
className=""
|
|
size="icon"
|
|
>
|
|
<MicIcon className="w-6 h-6" />
|
|
</Button>
|
|
<Textarea
|
|
ref={inputRef}
|
|
value={content}
|
|
onChange={(e) => setContent(e.target.value)}
|
|
disabled={submitting}
|
|
placeholder={t("pressEnterToSend")}
|
|
data-testid="chat-input"
|
|
className="flex-1 h-8 text-muted-foreground rounded-lg text-sm leading-7 px-0 py-1 shadow-none focus-visible:outline-0 focus-visible:ring-0 border-none min-h-[2.25rem] max-h-[70vh] scrollbar-thin !overflow-x-hidden"
|
|
/>
|
|
<Button
|
|
ref={submitRef}
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("send")}
|
|
onClick={() =>
|
|
onCreateMessage(content, { onSuccess: () => setContent("") })
|
|
}
|
|
disabled={submitting || !content}
|
|
className="rounded-full shadow w-8 h-8"
|
|
variant="default"
|
|
size="icon"
|
|
>
|
|
{submitting ? (
|
|
<LoaderIcon className="w-6 h-6 animate-spin" />
|
|
) : (
|
|
<ArrowUpIcon className="w-6 h-6" />
|
|
)}
|
|
</Button>
|
|
{chat.config.enableChatAssistant && (
|
|
<ChatSuggestionButton chat={chat} asChild>
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("suggestion")}
|
|
className="rounded-full w-8 h-8"
|
|
variant="ghost"
|
|
size="icon"
|
|
>
|
|
<WandIcon className="w-6 h-6" />
|
|
</Button>
|
|
</ChatSuggestionButton>
|
|
)}
|
|
|
|
{chat.type === ChatTypeEnum.GROUP && (
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("continue")}
|
|
disabled={submitting}
|
|
onClick={() => askAgent({ force: true })}
|
|
className=""
|
|
variant="ghost"
|
|
size="icon"
|
|
>
|
|
<StepForwardIcon className="w-6 h-6" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full flex items-center gap-4 justify-center relative">
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("textInput")}
|
|
disabled={submitting}
|
|
onClick={() => setInputMode("text")}
|
|
className="rounded-full shadow-lg w-8 h-8"
|
|
variant="secondary"
|
|
size="icon"
|
|
>
|
|
<TypeIcon className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("record")}
|
|
disabled={submitting}
|
|
onClick={startRecording}
|
|
className="rounded-full shadow-lg w-10 h-10"
|
|
size="icon"
|
|
>
|
|
{submitting ? (
|
|
<LoaderIcon className="w-6 h-6 animate-spin" />
|
|
) : (
|
|
<MicIcon className="w-6 h-6" />
|
|
)}
|
|
</Button>
|
|
{chat.config.enableChatAssistant && <ChatSuggestionButton chat={chat} />}
|
|
{chat.type === ChatTypeEnum.GROUP && (
|
|
<Button
|
|
data-tooltip-id={`${chat.id}-tooltip`}
|
|
data-tooltip-content={t("continue")}
|
|
disabled={submitting}
|
|
onClick={() => askAgent({ force: true })}
|
|
className="rounded-full shadow-lg w-8 h-8"
|
|
variant="default"
|
|
size="icon"
|
|
>
|
|
<StepForwardIcon className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|