diff --git a/package.json b/package.json index e6dada30..889e82c0 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@reduxjs/toolkit": "^2.2.5", "electron-updater": "^6.1.7", "electron-window-state": "^5.0.3", + "emittery": "^1.0.3", "localforage": "^1.10.0", "lodash": "^4.17.21", "react-redux": "^9.1.2", diff --git a/src/renderer/src/hooks/useThreads.ts b/src/renderer/src/hooks/useThreads.ts index da55bbfb..34d55ecb 100644 --- a/src/renderer/src/hooks/useThreads.ts +++ b/src/renderer/src/hooks/useThreads.ts @@ -4,19 +4,20 @@ import { addThread, removeConversationFromThread, removeThread, - setActiveThread, updateThread } from '@renderer/store/threads' import { Thread } from '@renderer/types' +import { useState } from 'react' export default function useThreads() { - const { threads, activeThread } = useAppSelector((state) => state.threads) + const { threads } = useAppSelector((state) => state.threads) + const [threadId, setThreadId] = useState(threads[0]?.id) const dispatch = useAppDispatch() return { threads, - activeThread, - setActiveThread: (thread: Thread) => dispatch(setActiveThread(thread)), + thread: threads.find((t) => t.id === threadId), + setThread: (thread: Thread) => setThreadId(thread.id), addThread: (thread: Thread) => dispatch(addThread(thread)), removeThread: (id: string) => dispatch(removeThread({ id })), updateThread: (thread: Thread) => dispatch(updateThread(thread)), diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index b28d3563..6b507909 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -7,11 +7,11 @@ import Threads from './components/Threads' import { uuid } from '@renderer/utils' const HomePage: FC = () => { - const { threads, activeThread, setActiveThread, addThread } = useThreads() + const { threads, thread, setThread, addThread } = useThreads() useEffect(() => { - !activeThread && setActiveThread(threads[0]) - }, [activeThread, threads]) + !thread && threads[0] && setThread(threads[0]) + }, [thread, threads]) const onCreateConversation = () => { const _thread = { @@ -23,7 +23,7 @@ const HomePage: FC = () => { conversations: [] } addThread(_thread) - setActiveThread(_thread) + setThread(_thread) } return ( @@ -34,7 +34,7 @@ const HomePage: FC = () => { - {activeThread?.name} + {thread?.name} @@ -43,7 +43,7 @@ const HomePage: FC = () => { - + {thread && } ) diff --git a/src/renderer/src/pages/home/components/Chat.tsx b/src/renderer/src/pages/home/components/Chat.tsx index 17c95698..feb1db44 100644 --- a/src/renderer/src/pages/home/components/Chat.tsx +++ b/src/renderer/src/pages/home/components/Chat.tsx @@ -1,23 +1,24 @@ -import { Message } from '@renderer/types' -import { FC, useEffect, useState } from 'react' +import { Message, Thread } from '@renderer/types' +import { FC, useState } from 'react' import styled from 'styled-components' import Inputbar from './Inputbar' import Conversations from './Conversations' import useThreads from '@renderer/hooks/useThreads' +import { isEmpty } from 'lodash' +import localforage from 'localforage' import { uuid } from '@renderer/utils' -const Chat: FC = () => { - const { activeThread, addConversation } = useThreads() - const [messages, setMessages] = useState([]) +interface Props { + thread: Thread +} - const onSendMessage = (message: Message) => { - setMessages([...messages, message]) - } +const Chat: FC = ({ thread }) => { + const [conversationId] = useState(thread.conversations[0] || uuid()) return ( - - + + ) } diff --git a/src/renderer/src/pages/home/components/Conversations.tsx b/src/renderer/src/pages/home/components/Conversations.tsx index f2b8633a..0fd9966d 100644 --- a/src/renderer/src/pages/home/components/Conversations.tsx +++ b/src/renderer/src/pages/home/components/Conversations.tsx @@ -1,16 +1,58 @@ -import { Message } from '@renderer/types' -import { FC } from 'react' +import { Avatar } from '@douyinfe/semi-ui' +import useThreads from '@renderer/hooks/useThreads' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' +import { Conversation, Message, Thread } from '@renderer/types' +import { runAsyncFunction } from '@renderer/utils' +import localforage from 'localforage' +import { isEmpty } from 'lodash' +import { FC, useEffect, useState } from 'react' import styled from 'styled-components' interface Props { - messages: Message[] + thread: Thread + conversationId: string } -const Conversations: FC = ({ messages }) => { +const Conversations: FC = ({ thread, conversationId }) => { + const [messages, setMessages] = useState([]) + const { addConversation } = useThreads() + + const onSendMessage = (message: Message) => { + setMessages([...messages, message]) + + if (isEmpty(thread?.conversations)) { + addConversation(thread.id, conversationId) + } + + localforage.setItem(`conversation:${conversationId}`, { + id: conversationId, + messages: [...messages, message] + }) + } + + useEffect(() => { + runAsyncFunction(async () => { + const conversation = await localforage.getItem(`conversation:${conversationId}`) + conversation && setMessages(conversation.messages) + }) + }, [conversationId]) + + useEffect(() => { + const unsubscribe = EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage) + return () => unsubscribe() + }, [onSendMessage]) + return ( {messages.map((message) => ( -
{message.content}
+ + + + Y + + +
{message.content}
+
))}
) @@ -20,11 +62,25 @@ const Container = styled.div` display: flex; flex-direction: column; flex: 1; - padding: 15px; overflow-y: scroll; &::-webkit-scrollbar { display: none; } ` +const ConversationItem = styled.div` + display: flex; + flex-direction: row; + padding: 10px 15px; + position: relative; + cursor: pointer; + &:hover { + background-color: var(--color-background-soft); + } +` + +const AvatarWrapper = styled.div` + margin-right: 10px; +` + export default Conversations diff --git a/src/renderer/src/pages/home/components/Inputbar.tsx b/src/renderer/src/pages/home/components/Inputbar.tsx index 155c5536..d8ce4c7b 100644 --- a/src/renderer/src/pages/home/components/Inputbar.tsx +++ b/src/renderer/src/pages/home/components/Inputbar.tsx @@ -1,29 +1,30 @@ +import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { Message, Thread } from '@renderer/types' import { uuid } from '@renderer/utils' import { FC, useState } from 'react' import styled from 'styled-components' interface Props { - activeThread: Thread - onSendMessage: (message: Message) => void + thread: Thread } -const Inputbar: FC = ({ activeThread, onSendMessage }) => { +const Inputbar: FC = ({ thread }) => { const [text, setText] = useState('') const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { - const conversationId = activeThread.conversations[0] ? activeThread.conversations[0] : uuid() + const conversationId = thread.conversations[0] ? thread.conversations[0] : uuid() const message: Message = { id: uuid(), content: text, - threadId: activeThread.id, + threadId: thread.id, conversationId, createdAt: 'now' } - onSendMessage(message) + EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message) + setText('') event.preventDefault() } diff --git a/src/renderer/src/pages/home/components/Threads.tsx b/src/renderer/src/pages/home/components/Threads.tsx index 77b2e88b..1ae95ef0 100644 --- a/src/renderer/src/pages/home/components/Threads.tsx +++ b/src/renderer/src/pages/home/components/Threads.tsx @@ -5,15 +5,15 @@ import { Dropdown } from '@douyinfe/semi-ui' import useThreads from '@renderer/hooks/useThreads' const Threads: FC = () => { - const { threads, activeThread, setActiveThread, removeThread } = useThreads() + const { threads, thread, setThread, removeThread } = useThreads() return ( {threads.map((thread) => ( setActiveThread(thread)} - className={thread.id === activeThread?.id ? 'active' : ''}> + onClick={() => setThread(thread)} + className={thread.id === thread?.id ? 'active' : ''}> ) => { state.threads = state.threads.filter((c) => c.id !== action.payload.id) - state.activeThread = state.threads[0] }, updateThread: (state, action: PayloadAction) => { state.threads = state.threads.map((c) => (c.id === action.payload.id ? action.payload : c)) }, - setActiveThread: (state, action: PayloadAction) => { - state.activeThread = action.payload - }, addConversationToThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => { state.threads = state.threads.map((c) => c.id === action.payload.threadId @@ -52,13 +46,7 @@ const threadsSlice = createSlice({ } }) -export const { - addThread, - removeThread, - updateThread, - setActiveThread, - addConversationToThread, - removeConversationFromThread -} = threadsSlice.actions +export const { addThread, removeThread, updateThread, addConversationToThread, removeConversationFromThread } = + threadsSlice.actions export default threadsSlice.reducer diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 931b7361..2062aa41 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -15,6 +15,11 @@ export type Message = { createdAt: string } +export type Conversation = { + id: string + messages: Message[] +} + export type User = { id: string name: string diff --git a/yarn.lock b/yarn.lock index eb73e26c..bf1a9c39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2264,6 +2264,11 @@ electron@^28.2.0: "@types/node" "^18.11.18" extract-zip "^2.0.1" +emittery@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-1.0.3.tgz#c9d2a9c689870f15251bb13b31c67715c26d69ac" + integrity sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"