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"