Refactor Chat (#1108)
* 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
This commit is contained in:
@@ -1,45 +1,32 @@
|
||||
import {
|
||||
ArrowUpIcon,
|
||||
CheckIcon,
|
||||
LoaderIcon,
|
||||
MicIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
SendIcon,
|
||||
StepForwardIcon,
|
||||
TextIcon,
|
||||
TypeIcon,
|
||||
WandIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
ScrollArea,
|
||||
Separator,
|
||||
Textarea,
|
||||
toast,
|
||||
} from "@renderer/components/ui";
|
||||
import { ReactElement, useContext, useEffect, useRef, useState } from "react";
|
||||
import { Button, Textarea } from "@renderer/components/ui";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { LiveAudioVisualizer } from "react-audio-visualize";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
ChatProviderContext,
|
||||
ChatSessionProviderContext,
|
||||
HotKeysSettingsProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { t } from "i18next";
|
||||
import autosize from "autosize";
|
||||
import { LoaderSpin } from "@renderer/components";
|
||||
import { useAiCommand } from "@renderer/hooks";
|
||||
import { formatDateTime } from "@renderer/lib/utils";
|
||||
import { md5 } from "js-md5";
|
||||
import { ChatSuggestionButton } from "@renderer/components";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { ChatTypeEnum } from "@/types/enums";
|
||||
|
||||
export const ChatInput = () => {
|
||||
const { currentChat } = useContext(ChatProviderContext);
|
||||
const {
|
||||
chat,
|
||||
submitting,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
@@ -56,7 +43,7 @@ export const ChatInput = () => {
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const submitRef = useRef<HTMLButtonElement>(null);
|
||||
const [inputMode, setInputMode] = useState<"text" | "audio">("audio");
|
||||
const [inputMode, setInputMode] = useState<"text" | "audio">("text");
|
||||
const [content, setContent] = useState("");
|
||||
const { currentHotkeys } = useContext(HotKeysSettingsProviderContext);
|
||||
|
||||
@@ -82,7 +69,7 @@ export const ChatInput = () => {
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects
|
||||
.get(`chat-input-mode-${currentChat.id}`)
|
||||
.get(`chat-input-mode-${chat.id}`)
|
||||
.then((cachedInputMode) => {
|
||||
if (cachedInputMode) {
|
||||
setInputMode(cachedInputMode as typeof inputMode);
|
||||
@@ -91,7 +78,7 @@ export const ChatInput = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects.set(`chat-input-mode-${currentChat.id}`, inputMode);
|
||||
EnjoyApp.cacheObjects.set(`chat-input-mode-${chat.id}`, inputMode);
|
||||
}, [inputMode]);
|
||||
|
||||
useHotkeys(
|
||||
@@ -113,7 +100,7 @@ export const ChatInput = () => {
|
||||
currentHotkeys.PlayNextSegment,
|
||||
() => {
|
||||
if (shadowing) return;
|
||||
askAgent();
|
||||
askAgent({ force: true });
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
@@ -122,7 +109,7 @@ export const ChatInput = () => {
|
||||
|
||||
if (isRecording) {
|
||||
return (
|
||||
<div className="w-full flex justify-center">
|
||||
<div className="z-10 w-full flex justify-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<LiveAudioVisualizer
|
||||
mediaRecorder={mediaRecorder}
|
||||
@@ -140,7 +127,7 @@ export const ChatInput = () => {
|
||||
{String(recordingTime % 60).padStart(2, "0")}
|
||||
</span>
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
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"
|
||||
@@ -156,14 +143,14 @@ export const ChatInput = () => {
|
||||
>
|
||||
{isPaused ? (
|
||||
<PlayIcon
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("continue")}
|
||||
fill="white"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
) : (
|
||||
<PauseIcon
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("pause")}
|
||||
fill="white"
|
||||
className="w-4 h-4"
|
||||
@@ -171,7 +158,7 @@ export const ChatInput = () => {
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
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"
|
||||
@@ -186,9 +173,9 @@ export const ChatInput = () => {
|
||||
|
||||
if (inputMode === "text") {
|
||||
return (
|
||||
<div className="w-full flex items-end gap-2 px-2">
|
||||
<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-input-tooltip"
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("audioInput")}
|
||||
disabled={submitting}
|
||||
onClick={() => setInputMode("audio")}
|
||||
@@ -205,45 +192,53 @@ export const ChatInput = () => {
|
||||
disabled={submitting}
|
||||
placeholder={t("pressEnterToSend")}
|
||||
data-testid="chat-input"
|
||||
className="leading-6 bg-muted h-9 text-muted-foreground rounded-lg text-base px-3 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"
|
||||
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-input-tooltip"
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("send")}
|
||||
onClick={() => onCreateMessage(content).then(() => setContent(""))}
|
||||
onClick={() =>
|
||||
onCreateMessage(content, { onSuccess: () => setContent("") })
|
||||
}
|
||||
disabled={submitting || !content}
|
||||
className=""
|
||||
variant="ghost"
|
||||
className="rounded-full shadow w-8 h-8"
|
||||
variant="default"
|
||||
size="icon"
|
||||
>
|
||||
{submitting ? (
|
||||
<LoaderIcon className="w-6 h-6 animate-spin" />
|
||||
) : (
|
||||
<SendIcon className="w-6 h-6" />
|
||||
<ArrowUpIcon className="w-6 h-6" />
|
||||
)}
|
||||
</Button>
|
||||
<ChatSuggestionButton asChild>
|
||||
{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-input-tooltip"
|
||||
data-tooltip-content={t("suggestion")}
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("continue")}
|
||||
disabled={submitting}
|
||||
onClick={() => askAgent({ force: true })}
|
||||
className=""
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<WandIcon className="w-6 h-6" />
|
||||
<StepForwardIcon className="w-6 h-6" />
|
||||
</Button>
|
||||
</ChatSuggestionButton>
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-content={t("continue")}
|
||||
disabled={submitting}
|
||||
onClick={() => askAgent()}
|
||||
className=""
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<StepForwardIcon className="w-6 h-6" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -251,22 +246,22 @@ export const ChatInput = () => {
|
||||
return (
|
||||
<div className="w-full flex items-center gap-4 justify-center relative">
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("textInput")}
|
||||
disabled={submitting}
|
||||
onClick={() => setInputMode("text")}
|
||||
className="rounded-full shadow w-8 h-8"
|
||||
className="rounded-full shadow-lg w-8 h-8"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
>
|
||||
<TextIcon className="w-4 h-4" />
|
||||
<TypeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-id={`${chat.id}-tooltip`}
|
||||
data-tooltip-content={t("record")}
|
||||
disabled={submitting}
|
||||
onClick={startRecording}
|
||||
className="rounded-full shadow w-10 h-10"
|
||||
className="rounded-full shadow-lg w-10 h-10"
|
||||
size="icon"
|
||||
>
|
||||
{submitting ? (
|
||||
@@ -275,167 +270,20 @@ export const ChatInput = () => {
|
||||
<MicIcon className="w-6 h-6" />
|
||||
)}
|
||||
</Button>
|
||||
<ChatSuggestionButton />
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-content={t("continue")}
|
||||
disabled={submitting}
|
||||
onClick={() => askAgent()}
|
||||
className="absolute right-4 rounded-full shadow w-8 h-8"
|
||||
variant="default"
|
||||
size="icon"
|
||||
>
|
||||
<StepForwardIcon className="w-4 h-4" />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatSuggestionButton = (props: {
|
||||
asChild?: boolean;
|
||||
children?: ReactElement;
|
||||
}) => {
|
||||
const { currentChat } = useContext(ChatProviderContext);
|
||||
const { chatMessages, onCreateMessage } = useContext(
|
||||
ChatSessionProviderContext
|
||||
);
|
||||
const [suggestions, setSuggestions] = useState<
|
||||
{ text: string; explaination: string }[]
|
||||
>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
|
||||
const { chatSuggestion } = useAiCommand();
|
||||
|
||||
const context = `I'm ${
|
||||
currentChat.members.find((member) => member.user).user.name
|
||||
}.
|
||||
|
||||
[Chat Topic]
|
||||
${currentChat.topic}
|
||||
|
||||
[Chat Members]
|
||||
${currentChat.members.map((m) => {
|
||||
if (m.user) {
|
||||
return `- ${m.user.name} (${m.config.introduction})[It's me]`;
|
||||
} else if (m.agent) {
|
||||
return `- ${m.agent.name} (${m.agent.introduction})`;
|
||||
}
|
||||
})}
|
||||
|
||||
[Chat History]
|
||||
${chatMessages
|
||||
.filter((m) => m.state === "completed")
|
||||
.map(
|
||||
(message) =>
|
||||
`- ${(message.member.user || message.member.agent).name}: ${
|
||||
message.content
|
||||
}(${formatDateTime(message.createdAt)})`
|
||||
)
|
||||
.join("\n")}
|
||||
`;
|
||||
|
||||
const contextCacheKey = `chat-suggestion-${md5(
|
||||
chatMessages
|
||||
.filter((m) => m.state === "completed")
|
||||
.map((m) => m.content)
|
||||
.join("\n")
|
||||
)}`;
|
||||
|
||||
const suggest = async () => {
|
||||
setLoading(true);
|
||||
chatSuggestion(context, {
|
||||
cacheKey: contextCacheKey,
|
||||
})
|
||||
.then((res) => setSuggestions(res.suggestions))
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open && !suggestions?.length) {
|
||||
suggest();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects.get(contextCacheKey).then((result) => {
|
||||
if (result && result?.suggestions) {
|
||||
setSuggestions(result.suggestions as typeof suggestions);
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
});
|
||||
}, [contextCacheKey]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{props.asChild ? (
|
||||
{ ...props.children }
|
||||
) : (
|
||||
<Button
|
||||
data-tooltip-id="chat-input-tooltip"
|
||||
data-tooltip-content={t("suggestion")}
|
||||
className="rounded-full shadow w-8 h-8"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
>
|
||||
<WandIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" className="bg-muted w-full max-w-screen-md">
|
||||
{loading || suggestions.length === 0 ? (
|
||||
<LoaderSpin />
|
||||
) : (
|
||||
<ScrollArea className="h-72 px-3">
|
||||
<div className="select-text grid gap-6">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<div key={index} className="grid gap-4">
|
||||
<div className="text-sm">{suggestion.explaination}</div>
|
||||
<div className="px-4 py-2 rounded bg-background flex items-end justify-between space-x-2">
|
||||
<div className="font-serif">{suggestion.text}</div>
|
||||
<div>
|
||||
<Button
|
||||
data-tooltip-id="global-tooltip"
|
||||
data-tooltip-content={t("send")}
|
||||
variant="default"
|
||||
size="icon"
|
||||
className="rounded-full w-6 h-6"
|
||||
onClick={() =>
|
||||
onCreateMessage(suggestion.text).finally(() =>
|
||||
setOpen(false)
|
||||
)
|
||||
}
|
||||
>
|
||||
<SendIcon className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
disabled={loading}
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => suggest()}
|
||||
>
|
||||
{t("refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
<PopoverArrow />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user