From 79d9a37eae78a2b92407e4d7666eef20ae53c280 Mon Sep 17 00:00:00 2001 From: an-lee Date: Fri, 26 Jul 2024 11:33:29 +0800 Subject: [PATCH 01/64] add chats page --- enjoy/src/i18n/en.json | 1 + enjoy/src/i18n/zh-CN.json | 1 + enjoy/src/renderer/components/misc/sidebar.tsx | 9 +++++++++ enjoy/src/renderer/pages/chats.tsx | 3 +++ enjoy/src/renderer/router.tsx | 5 +++++ 5 files changed, 19 insertions(+) create mode 100644 enjoy/src/renderer/pages/chats.tsx diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 297b2bd32..9baa6db6a 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -133,6 +133,7 @@ }, "sidebar": { "home": "Home", + "chats": "Chats", "courses": "Courses", "community": "Community", "audios": "Audios", diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index fb59bc714..b714b69ba 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -133,6 +133,7 @@ }, "sidebar": { "home": "主页", + "chats": "聊天", "courses": "课程", "community": "社区", "audios": "音频", diff --git a/enjoy/src/renderer/components/misc/sidebar.tsx b/enjoy/src/renderer/components/misc/sidebar.tsx index a7e607df4..64994d237 100644 --- a/enjoy/src/renderer/components/misc/sidebar.tsx +++ b/enjoy/src/renderer/components/misc/sidebar.tsx @@ -32,6 +32,7 @@ import { NotebookPenIcon, SpeechIcon, GraduationCapIcon, + MessagesSquareIcon, } from "lucide-react"; import { useLocation, Link } from "react-router-dom"; import { t } from "i18next"; @@ -72,6 +73,14 @@ export const Sidebar = () => { Icon={HomeIcon} /> + + Chats; +} diff --git a/enjoy/src/renderer/router.tsx b/enjoy/src/renderer/router.tsx index 054dab255..a63b38d3f 100644 --- a/enjoy/src/renderer/router.tsx +++ b/enjoy/src/renderer/router.tsx @@ -23,6 +23,7 @@ import PronunciationAssessmentsNew from "./pages/pronunciation-assessments/new"; import Courses from "./pages/courses/index"; import Course from "./pages/courses/show"; import Chapter from "./pages/courses/chapter"; +import Chats from "./pages/chats"; export default createHashRouter([ { @@ -31,6 +32,10 @@ export default createHashRouter([ errorElement: , children: [ { index: true, element: }, + { + path: "/chats", + element: , + }, { path: "/courses", element: , From fb56b484ad452539b1abfade8db5618f2164104e Mon Sep 17 00:00:00 2001 From: an-lee Date: Fri, 26 Jul 2024 17:13:29 +0800 Subject: [PATCH 02/64] add basic layout and types --- .../components/chats/chat-sidebar.tsx | 47 ++++++++++++++++ enjoy/src/renderer/components/chats/chat.tsx | 25 +++++++++ enjoy/src/renderer/components/chats/index.ts | 2 + enjoy/src/renderer/components/index.ts | 1 + enjoy/src/renderer/pages/chats.tsx | 11 +++- enjoy/src/types/chat.d.ts | 55 +++++++++++++++++++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 enjoy/src/renderer/components/chats/chat-sidebar.tsx create mode 100644 enjoy/src/renderer/components/chats/chat.tsx create mode 100644 enjoy/src/renderer/components/chats/index.ts create mode 100644 enjoy/src/types/chat.d.ts diff --git a/enjoy/src/renderer/components/chats/chat-sidebar.tsx b/enjoy/src/renderer/components/chats/chat-sidebar.tsx new file mode 100644 index 000000000..06111bca4 --- /dev/null +++ b/enjoy/src/renderer/components/chats/chat-sidebar.tsx @@ -0,0 +1,47 @@ +import { PlusIcon } from "lucide-react"; +import { + Avatar, + AvatarFallback, + Button, + Input, + ScrollArea, +} from "@renderer/components/ui"; +import { t } from "i18next"; +import { useContext } from "react"; +import { AppSettingsProviderContext } from "@renderer/context"; + +export const ChatSidebar = () => { + const { user } = useContext(AppSettingsProviderContext); + return ( + +
+ + +
+ +
+
+
+ Let's talk about the weather today. It's so hot outside. +
+
+ + + A + + + + B + + + + {user.name} + +
+
+
+
+ ); +}; diff --git a/enjoy/src/renderer/components/chats/chat.tsx b/enjoy/src/renderer/components/chats/chat.tsx new file mode 100644 index 000000000..1201ea78c --- /dev/null +++ b/enjoy/src/renderer/components/chats/chat.tsx @@ -0,0 +1,25 @@ +import { MicIcon, SettingsIcon } from "lucide-react"; +import { Button, ScrollArea } from "@renderer/components/ui"; +import { t } from "i18next"; + +export const Chat = () => { + return ( + +
+ New Group(4) + +
+
+
+
+
+ +
+
+
+ ); +}; diff --git a/enjoy/src/renderer/components/chats/index.ts b/enjoy/src/renderer/components/chats/index.ts new file mode 100644 index 000000000..d8ae75716 --- /dev/null +++ b/enjoy/src/renderer/components/chats/index.ts @@ -0,0 +1,2 @@ +export * from "./chat"; +export * from "./chat-sidebar"; diff --git a/enjoy/src/renderer/components/index.ts b/enjoy/src/renderer/components/index.ts index c28fb7796..98a56132c 100644 --- a/enjoy/src/renderer/components/index.ts +++ b/enjoy/src/renderer/components/index.ts @@ -1,4 +1,5 @@ export * from "./audios"; +export * from "./chats"; export * from "./conversations"; export * from "./courses"; export * from "./llm-chats"; diff --git a/enjoy/src/renderer/pages/chats.tsx b/enjoy/src/renderer/pages/chats.tsx index 0bfa7d37b..fe155bb58 100644 --- a/enjoy/src/renderer/pages/chats.tsx +++ b/enjoy/src/renderer/pages/chats.tsx @@ -1,3 +1,12 @@ +import { Chat, ChatSidebar } from "@renderer/components"; + export default function Chats() { - return <>Chats; + return ( +
+ +
+ +
+
+ ); } diff --git a/enjoy/src/types/chat.d.ts b/enjoy/src/types/chat.d.ts new file mode 100644 index 000000000..280a08d4a --- /dev/null +++ b/enjoy/src/types/chat.d.ts @@ -0,0 +1,55 @@ +type ChatType = { + id: string; + name: string; + topic: string; + prompt: string; + language: string; + digest: string; + createdAt: string; + updatedAt: string; + sessions: ChatSessionType[]; + members: ChatMemberType[]; +}; + +type ChatMemberType = { + id: string; + state: "active" | "passive"; + type: "User" | "Agent"; +}; + +type ChatAgentType = { + id: string; + name: string; + avatarUrl: string; + introduction: string; + language: string; + config: { + engine: "enjoyai" | "openai" | "ollama"; + model: string; + prompt: string; + temperature?: number; + ttsEngine: "enjoyai" | "openai"; + ttsModel: string; + ttsVoice: string; + [key: string]: any; + }; +}; + +type ChatSessionType = { + id: string; + chatId: string; + createdAt: string; + updatedAt: string; + chat: ChatType; + messages: ChatMessageType[]; +}; + +type ChatMessageType = { + id: string; + memberId: string; + sessionId: string; + content: string; + state: "pending" | "completed"; + createdAt: string; + updatedAt: string; +}; From 7aba56cf0db1347e3c706b4939893f0eafc169cb Mon Sep 17 00:00:00 2001 From: an-lee Date: Fri, 26 Jul 2024 17:47:03 +0800 Subject: [PATCH 03/64] add chat context --- .../components/chats/chat-sidebar.tsx | 51 ++++++++++++------- enjoy/src/renderer/components/chats/chat.tsx | 20 ++++++-- enjoy/src/renderer/context/chat-provider.tsx | 35 +++++++++++++ enjoy/src/renderer/context/index.ts | 1 + enjoy/src/renderer/pages/chats.tsx | 13 +++-- enjoy/src/types/chat.d.ts | 14 ++++- 6 files changed, 105 insertions(+), 29 deletions(-) create mode 100644 enjoy/src/renderer/context/chat-provider.tsx diff --git a/enjoy/src/renderer/components/chats/chat-sidebar.tsx b/enjoy/src/renderer/components/chats/chat-sidebar.tsx index 06111bca4..a341200d4 100644 --- a/enjoy/src/renderer/components/chats/chat-sidebar.tsx +++ b/enjoy/src/renderer/components/chats/chat-sidebar.tsx @@ -8,10 +8,16 @@ import { } from "@renderer/components/ui"; import { t } from "i18next"; import { useContext } from "react"; -import { AppSettingsProviderContext } from "@renderer/context"; +import { + AppSettingsProviderContext, + ChatProviderContext, +} from "@renderer/context"; export const ChatSidebar = () => { const { user } = useContext(AppSettingsProviderContext); + const { chats, currentChat, setCurrentChat } = + useContext(ChatProviderContext); + return (
@@ -21,26 +27,33 @@ export const ChatSidebar = () => {
+ {chats.length === 0 && ( +
+ {t("noData")} +
+ )}
-
-
- Let's talk about the weather today. It's so hot outside. + {chats.map((chat) => ( +
setCurrentChat(chat)} + > +
+ {chat.name}({chat.members.length}) +
+
+ {chat.members.map((member) => ( + + + {member.name[0]} + + ))} +
-
- - - A - - - - B - - - - {user.name} - -
-
+ ))}
); diff --git a/enjoy/src/renderer/components/chats/chat.tsx b/enjoy/src/renderer/components/chats/chat.tsx index 1201ea78c..1c3f89100 100644 --- a/enjoy/src/renderer/components/chats/chat.tsx +++ b/enjoy/src/renderer/components/chats/chat.tsx @@ -1,8 +1,20 @@ import { MicIcon, SettingsIcon } from "lucide-react"; import { Button, ScrollArea } from "@renderer/components/ui"; import { t } from "i18next"; +import { ChatProviderContext } from "@renderer/context"; +import { useContext } from "react"; export const Chat = () => { + const { currentChat } = useContext(ChatProviderContext); + + if (!currentChat) { + return ( +
+ {t("noChatSelected")} +
+ ); + } + return (
@@ -11,11 +23,13 @@ export const Chat = () => {
-
-
+
-
diff --git a/enjoy/src/renderer/context/chat-provider.tsx b/enjoy/src/renderer/context/chat-provider.tsx new file mode 100644 index 000000000..ffb9fbe4a --- /dev/null +++ b/enjoy/src/renderer/context/chat-provider.tsx @@ -0,0 +1,35 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type ChatProviderState = { + chats: ChatType[]; + currentChat: ChatType; + setCurrentChat: (chat: ChatType) => void; +}; + +const initialState: ChatProviderState = { + chats: [], + currentChat: null, + setCurrentChat: () => null, +}; + +export const ChatProviderContext = + createContext(initialState); + +export const ChatProvider = ({ children }: { children: React.ReactNode }) => { + const [chats, setChats] = useState([]); + const [currentChat, setCurrentChat] = useState(null); + + useEffect(() => { + if (currentChat) return; + + setCurrentChat(chats[0]); + }, [chats]); + + return ( + + {children} + + ); +}; diff --git a/enjoy/src/renderer/context/index.ts b/enjoy/src/renderer/context/index.ts index caf8dec3a..808b065c1 100644 --- a/enjoy/src/renderer/context/index.ts +++ b/enjoy/src/renderer/context/index.ts @@ -1,5 +1,6 @@ export * from "./ai-settings-provider"; export * from "./app-settings-provider"; +export * from "./chat-provider"; export * from "./course-provider"; export * from "./db-provider"; export * from './hotkeys-settings-provider' diff --git a/enjoy/src/renderer/pages/chats.tsx b/enjoy/src/renderer/pages/chats.tsx index fe155bb58..44e0da1bb 100644 --- a/enjoy/src/renderer/pages/chats.tsx +++ b/enjoy/src/renderer/pages/chats.tsx @@ -1,12 +1,15 @@ import { Chat, ChatSidebar } from "@renderer/components"; +import { ChatProvider } from "@renderer/context"; export default function Chats() { return ( -
- -
- + +
+ +
+ +
-
+ ); } diff --git a/enjoy/src/types/chat.d.ts b/enjoy/src/types/chat.d.ts index 280a08d4a..28cb297f6 100644 --- a/enjoy/src/types/chat.d.ts +++ b/enjoy/src/types/chat.d.ts @@ -13,9 +13,19 @@ type ChatType = { type ChatMemberType = { id: string; + chatId: string; + userId: string; state: "active" | "passive"; - type: "User" | "Agent"; -}; +} & ( + | ({ + userType: "User"; + user: ChatUserType; + } & UserType) + | ({ + userType: "Agent"; + agent: ChatAgentType; + } & ChatAgentType) +); type ChatAgentType = { id: string; From 18e7f1da3042331f2e01361f4ce5756713675e63 Mon Sep 17 00:00:00 2001 From: an-lee Date: Fri, 26 Jul 2024 19:04:51 +0800 Subject: [PATCH 04/64] add more components --- .../components/chats/chat-agent-form.tsx | 3 ++ .../renderer/components/chats/chat-agents.tsx | 3 ++ .../renderer/components/chats/chat-form.tsx | 3 ++ .../components/chats/chat-sidebar.tsx | 44 +++++++++++++++++-- enjoy/src/renderer/components/chats/index.ts | 3 ++ 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 enjoy/src/renderer/components/chats/chat-agent-form.tsx create mode 100644 enjoy/src/renderer/components/chats/chat-agents.tsx create mode 100644 enjoy/src/renderer/components/chats/chat-form.tsx diff --git a/enjoy/src/renderer/components/chats/chat-agent-form.tsx b/enjoy/src/renderer/components/chats/chat-agent-form.tsx new file mode 100644 index 000000000..23f65b449 --- /dev/null +++ b/enjoy/src/renderer/components/chats/chat-agent-form.tsx @@ -0,0 +1,3 @@ +export const ChatAgentForm = () => { + return <>Agent Form; +}; diff --git a/enjoy/src/renderer/components/chats/chat-agents.tsx b/enjoy/src/renderer/components/chats/chat-agents.tsx new file mode 100644 index 000000000..bb76b207d --- /dev/null +++ b/enjoy/src/renderer/components/chats/chat-agents.tsx @@ -0,0 +1,3 @@ +export const ChatAgents = () => { + return
Agents
; +}; diff --git a/enjoy/src/renderer/components/chats/chat-form.tsx b/enjoy/src/renderer/components/chats/chat-form.tsx new file mode 100644 index 000000000..efcedabc5 --- /dev/null +++ b/enjoy/src/renderer/components/chats/chat-form.tsx @@ -0,0 +1,3 @@ +export const ChatForm = () => { + return <>chat Form; +}; diff --git a/enjoy/src/renderer/components/chats/chat-sidebar.tsx b/enjoy/src/renderer/components/chats/chat-sidebar.tsx index a341200d4..6fee233ea 100644 --- a/enjoy/src/renderer/components/chats/chat-sidebar.tsx +++ b/enjoy/src/renderer/components/chats/chat-sidebar.tsx @@ -3,28 +3,52 @@ import { Avatar, AvatarFallback, Button, + Dialog, + DialogContent, + DialogTitle, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, Input, ScrollArea, } from "@renderer/components/ui"; import { t } from "i18next"; -import { useContext } from "react"; +import { useContext, useState } from "react"; import { AppSettingsProviderContext, ChatProviderContext, } from "@renderer/context"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { ChatForm } from "./chat-form"; +import { ChatAgents } from "./chat-agents"; export const ChatSidebar = () => { const { user } = useContext(AppSettingsProviderContext); const { chats, currentChat, setCurrentChat } = useContext(ChatProviderContext); + const [displayChatForm, setDisplayChatForm] = useState(false); + const [displayAgentForm, setDisplayAgentForm] = useState(false); + return (
- + + + + + + setDisplayChatForm(true)}> + {t("addChat")} + + setDisplayAgentForm(true)}> + {t("addAgent")} + + +
{chats.length === 0 && ( @@ -55,6 +79,18 @@ export const ChatSidebar = () => {
))}
+ + + + + + + + + + + +
); }; diff --git a/enjoy/src/renderer/components/chats/index.ts b/enjoy/src/renderer/components/chats/index.ts index d8ae75716..2fc419323 100644 --- a/enjoy/src/renderer/components/chats/index.ts +++ b/enjoy/src/renderer/components/chats/index.ts @@ -1,2 +1,5 @@ export * from "./chat"; +export * from "./chat-agents"; +export * from "./chat-agent-form"; +export * from "./chat-form"; export * from "./chat-sidebar"; From c3a6df6f1f1eef044c86c978e9a405a0e594be8d Mon Sep 17 00:00:00 2001 From: an-lee Date: Fri, 26 Jul 2024 21:04:03 +0800 Subject: [PATCH 05/64] add agent form --- .../components/chats/chat-agent-form.tsx | 364 +++++++++++++++++- .../renderer/components/chats/chat-agents.tsx | 73 +++- .../renderer/components/chats/chat-form.tsx | 86 ++++- .../components/chats/chat-sidebar.tsx | 2 +- enjoy/src/types/chat.d.ts | 2 +- 5 files changed, 520 insertions(+), 7 deletions(-) diff --git a/enjoy/src/renderer/components/chats/chat-agent-form.tsx b/enjoy/src/renderer/components/chats/chat-agent-form.tsx index 23f65b449..75de82d3d 100644 --- a/enjoy/src/renderer/components/chats/chat-agent-form.tsx +++ b/enjoy/src/renderer/components/chats/chat-agent-form.tsx @@ -1,3 +1,363 @@ -export const ChatAgentForm = () => { - return <>Agent Form; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Avatar, + Button, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Textarea, +} from "@renderer/components/ui"; +import { t } from "i18next"; +import { LANGUAGES } from "@/constants"; +import { + AISettingsProviderContext, + AppSettingsProviderContext, +} from "@renderer/context"; +import { useContext, useEffect, useState } from "react"; +import { GPT_PROVIDERS, TTS_PROVIDERS } from "@renderer/components"; + +export const ChatAgentForm = (props: { agent?: ChatAgentType }) => { + const { agent } = props; + const { learningLanguage, webApi } = useContext(AppSettingsProviderContext); + const { openai } = useContext(AISettingsProviderContext); + const [gptProviders, setGptProviders] = useState(GPT_PROVIDERS); + const [ttsProviders, setTtsProviders] = useState(TTS_PROVIDERS); + + const agentFormSchema = z.object({ + name: z.string(), + introduction: z.string(), + language: z.string(), + engine: z.enum(["enjoyai", "openai", "ollama"]), + model: z.string(), + prompt: z.string(), + temperature: z.number().optional(), + ttsEngine: z.enum(["enjoyai", "openai"]), + ttsModel: z.string(), + ttsVoice: z.string(), + }); + + const form = useForm>({ + resolver: zodResolver(agentFormSchema), + values: agent || { + name: "Edgar", + language: learningLanguage, + engine: "enjoyai", + model: "gpt-4o", + temperature: 0.7, + ttsEngine: "enjoyai", + ttsModel: "azure/speech", + ttsVoice: ttsProviders?.enjoyai?.voices?.["azure"].find( + (voice: any) => voice?.language === learningLanguage + )?.value, + }, + }); + + const onSubmit = form.handleSubmit((data) => { + console.log(data); + }); + + const refreshGptProviders = async () => { + let providers = GPT_PROVIDERS; + + try { + const config = await webApi.config("gpt_providers"); + providers = Object.assign(providers, config); + } catch (e) { + console.warn(`Failed to fetch remote GPT config: ${e.message}`); + } + + try { + const response = await fetch(providers["ollama"]?.baseUrl + "/api/tags"); + providers["ollama"].models = (await response.json()).models.map( + (m: any) => m.name + ); + } catch (e) { + console.warn(`No ollama server found: ${e.message}`); + } + + if (openai.models) { + providers["openai"].models = openai.models.split(","); + } + + setGptProviders({ ...providers }); + }; + + const refreshTtsProviders = async () => { + let providers = TTS_PROVIDERS; + + try { + const config = await webApi.config("tts_providers_v2"); + providers = Object.assign(providers, config); + } catch (e) { + console.warn(`Failed to fetch remote TTS config: ${e.message}`); + } + + setTtsProviders({ ...providers }); + }; + + useEffect(() => { + refreshGptProviders(); + refreshTtsProviders(); + }, []); + + return ( +
+ +
{agent?.id ? t("editAgent") : t("newAgent")}
+
+ + {form.watch("name")} + + ( + + {t("name")} + + + + )} + /> + + ( + + {t("introduction")} +