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:
an-lee
2024-10-09 16:57:32 +08:00
committed by GitHub
parent 245a1ea461
commit d96c9ff773
96 changed files with 8387 additions and 4889 deletions

View File

@@ -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>
);
};