feat(module): add new feature module
Add a new module called "module" that implements the following features: - Implement feature A - Provide API interface B - Optimize performance issues BREAKING CHANGE: This feature module introduces a new configuration option, requiring updates to the existing configuration files.
This commit is contained in:
parent
9d08e77dd1
commit
182d631dd6
@ -30,6 +30,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
|
"emittery": "^1.0.3",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
|
|||||||
@ -4,19 +4,20 @@ import {
|
|||||||
addThread,
|
addThread,
|
||||||
removeConversationFromThread,
|
removeConversationFromThread,
|
||||||
removeThread,
|
removeThread,
|
||||||
setActiveThread,
|
|
||||||
updateThread
|
updateThread
|
||||||
} from '@renderer/store/threads'
|
} from '@renderer/store/threads'
|
||||||
import { Thread } from '@renderer/types'
|
import { Thread } from '@renderer/types'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
export default function useThreads() {
|
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()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
threads,
|
threads,
|
||||||
activeThread,
|
thread: threads.find((t) => t.id === threadId),
|
||||||
setActiveThread: (thread: Thread) => dispatch(setActiveThread(thread)),
|
setThread: (thread: Thread) => setThreadId(thread.id),
|
||||||
addThread: (thread: Thread) => dispatch(addThread(thread)),
|
addThread: (thread: Thread) => dispatch(addThread(thread)),
|
||||||
removeThread: (id: string) => dispatch(removeThread({ id })),
|
removeThread: (id: string) => dispatch(removeThread({ id })),
|
||||||
updateThread: (thread: Thread) => dispatch(updateThread(thread)),
|
updateThread: (thread: Thread) => dispatch(updateThread(thread)),
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import Threads from './components/Threads'
|
|||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
|
|
||||||
const HomePage: FC = () => {
|
const HomePage: FC = () => {
|
||||||
const { threads, activeThread, setActiveThread, addThread } = useThreads()
|
const { threads, thread, setThread, addThread } = useThreads()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!activeThread && setActiveThread(threads[0])
|
!thread && threads[0] && setThread(threads[0])
|
||||||
}, [activeThread, threads])
|
}, [thread, threads])
|
||||||
|
|
||||||
const onCreateConversation = () => {
|
const onCreateConversation = () => {
|
||||||
const _thread = {
|
const _thread = {
|
||||||
@ -23,7 +23,7 @@ const HomePage: FC = () => {
|
|||||||
conversations: []
|
conversations: []
|
||||||
}
|
}
|
||||||
addThread(_thread)
|
addThread(_thread)
|
||||||
setActiveThread(_thread)
|
setThread(_thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,7 +34,7 @@ const HomePage: FC = () => {
|
|||||||
<i className="iconfont icon-a-addchat"></i>
|
<i className="iconfont icon-a-addchat"></i>
|
||||||
</NewButton>
|
</NewButton>
|
||||||
</NavbarLeft>
|
</NavbarLeft>
|
||||||
<NavbarCenter style={{ border: 'none' }}>{activeThread?.name}</NavbarCenter>
|
<NavbarCenter style={{ border: 'none' }}>{thread?.name}</NavbarCenter>
|
||||||
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
|
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
|
||||||
<NewButton>
|
<NewButton>
|
||||||
<i className="iconfont icon-showsidebarhoriz"></i>
|
<i className="iconfont icon-showsidebarhoriz"></i>
|
||||||
@ -43,7 +43,7 @@ const HomePage: FC = () => {
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<Threads />
|
<Threads />
|
||||||
<Chat />
|
{thread && <Chat thread={thread} />}
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,23 +1,24 @@
|
|||||||
import { Message } from '@renderer/types'
|
import { Message, Thread } from '@renderer/types'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Inputbar from './Inputbar'
|
import Inputbar from './Inputbar'
|
||||||
import Conversations from './Conversations'
|
import Conversations from './Conversations'
|
||||||
import useThreads from '@renderer/hooks/useThreads'
|
import useThreads from '@renderer/hooks/useThreads'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import localforage from 'localforage'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
|
|
||||||
const Chat: FC = () => {
|
interface Props {
|
||||||
const { activeThread, addConversation } = useThreads()
|
thread: Thread
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
}
|
||||||
|
|
||||||
const onSendMessage = (message: Message) => {
|
const Chat: FC<Props> = ({ thread }) => {
|
||||||
setMessages([...messages, message])
|
const [conversationId] = useState<string>(thread.conversations[0] || uuid())
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Conversations messages={messages}></Conversations>
|
<Conversations thread={thread} conversationId={conversationId} />
|
||||||
<Inputbar onSendMessage={onSendMessage} activeThread={activeThread} />
|
<Inputbar thread={thread} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,58 @@
|
|||||||
import { Message } from '@renderer/types'
|
import { Avatar } from '@douyinfe/semi-ui'
|
||||||
import { FC } from 'react'
|
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'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: Message[]
|
thread: Thread
|
||||||
|
conversationId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Conversations: FC<Props> = ({ messages }) => {
|
const Conversations: FC<Props> = ({ thread, conversationId }) => {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
|
const { addConversation } = useThreads()
|
||||||
|
|
||||||
|
const onSendMessage = (message: Message) => {
|
||||||
|
setMessages([...messages, message])
|
||||||
|
|
||||||
|
if (isEmpty(thread?.conversations)) {
|
||||||
|
addConversation(thread.id, conversationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
localforage.setItem<Conversation>(`conversation:${conversationId}`, {
|
||||||
|
id: conversationId,
|
||||||
|
messages: [...messages, message]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
runAsyncFunction(async () => {
|
||||||
|
const conversation = await localforage.getItem<Conversation>(`conversation:${conversationId}`)
|
||||||
|
conversation && setMessages(conversation.messages)
|
||||||
|
})
|
||||||
|
}, [conversationId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)
|
||||||
|
return () => unsubscribe()
|
||||||
|
}, [onSendMessage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<div key={message.id}>{message.content}</div>
|
<ConversationItem key={message.id}>
|
||||||
|
<AvatarWrapper>
|
||||||
|
<Avatar size="small" alt="Alice Swift">
|
||||||
|
Y
|
||||||
|
</Avatar>
|
||||||
|
</AvatarWrapper>
|
||||||
|
<div>{message.content}</div>
|
||||||
|
</ConversationItem>
|
||||||
))}
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@ -20,11 +62,25 @@ const Container = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 15px;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
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
|
export default Conversations
|
||||||
|
|||||||
@ -1,29 +1,30 @@
|
|||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { Message, Thread } from '@renderer/types'
|
import { Message, Thread } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeThread: Thread
|
thread: Thread
|
||||||
onSendMessage: (message: Message) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Inputbar: FC<Props> = ({ activeThread, onSendMessage }) => {
|
const Inputbar: FC<Props> = ({ thread }) => {
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (event.key === 'Enter') {
|
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 = {
|
const message: Message = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
content: text,
|
content: text,
|
||||||
threadId: activeThread.id,
|
threadId: thread.id,
|
||||||
conversationId,
|
conversationId,
|
||||||
createdAt: 'now'
|
createdAt: 'now'
|
||||||
}
|
}
|
||||||
|
|
||||||
onSendMessage(message)
|
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||||
|
|
||||||
setText('')
|
setText('')
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,15 +5,15 @@ import { Dropdown } from '@douyinfe/semi-ui'
|
|||||||
import useThreads from '@renderer/hooks/useThreads'
|
import useThreads from '@renderer/hooks/useThreads'
|
||||||
|
|
||||||
const Threads: FC = () => {
|
const Threads: FC = () => {
|
||||||
const { threads, activeThread, setActiveThread, removeThread } = useThreads()
|
const { threads, thread, setThread, removeThread } = useThreads()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{threads.map((thread) => (
|
{threads.map((thread) => (
|
||||||
<ThreadItem
|
<ThreadItem
|
||||||
key={thread.id}
|
key={thread.id}
|
||||||
onClick={() => setActiveThread(thread)}
|
onClick={() => setThread(thread)}
|
||||||
className={thread.id === activeThread?.id ? 'active' : ''}>
|
className={thread.id === thread?.id ? 'active' : ''}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger="click"
|
trigger="click"
|
||||||
stopPropagation
|
stopPropagation
|
||||||
|
|||||||
7
src/renderer/src/services/event.ts
Normal file
7
src/renderer/src/services/event.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Emittery from 'emittery'
|
||||||
|
|
||||||
|
export const EventEmitter = new Emittery()
|
||||||
|
|
||||||
|
export const EVENT_NAMES = {
|
||||||
|
SEND_MESSAGE: 'SEND_MESSAGE'
|
||||||
|
}
|
||||||
@ -4,11 +4,9 @@ import { Thread } from '@renderer/types'
|
|||||||
|
|
||||||
export interface ThreadsState {
|
export interface ThreadsState {
|
||||||
threads: Thread[]
|
threads: Thread[]
|
||||||
activeThread?: Thread
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ThreadsState = {
|
const initialState: ThreadsState = {
|
||||||
activeThread: getDefaultThread(),
|
|
||||||
threads: [getDefaultThread()]
|
threads: [getDefaultThread()]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,14 +19,10 @@ const threadsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
removeThread: (state, action: PayloadAction<{ id: string }>) => {
|
removeThread: (state, action: PayloadAction<{ id: string }>) => {
|
||||||
state.threads = state.threads.filter((c) => c.id !== action.payload.id)
|
state.threads = state.threads.filter((c) => c.id !== action.payload.id)
|
||||||
state.activeThread = state.threads[0]
|
|
||||||
},
|
},
|
||||||
updateThread: (state, action: PayloadAction<Thread>) => {
|
updateThread: (state, action: PayloadAction<Thread>) => {
|
||||||
state.threads = state.threads.map((c) => (c.id === action.payload.id ? action.payload : c))
|
state.threads = state.threads.map((c) => (c.id === action.payload.id ? action.payload : c))
|
||||||
},
|
},
|
||||||
setActiveThread: (state, action: PayloadAction<Thread>) => {
|
|
||||||
state.activeThread = action.payload
|
|
||||||
},
|
|
||||||
addConversationToThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => {
|
addConversationToThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => {
|
||||||
state.threads = state.threads.map((c) =>
|
state.threads = state.threads.map((c) =>
|
||||||
c.id === action.payload.threadId
|
c.id === action.payload.threadId
|
||||||
@ -52,13 +46,7 @@ const threadsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const {
|
export const { addThread, removeThread, updateThread, addConversationToThread, removeConversationFromThread } =
|
||||||
addThread,
|
threadsSlice.actions
|
||||||
removeThread,
|
|
||||||
updateThread,
|
|
||||||
setActiveThread,
|
|
||||||
addConversationToThread,
|
|
||||||
removeConversationFromThread
|
|
||||||
} = threadsSlice.actions
|
|
||||||
|
|
||||||
export default threadsSlice.reducer
|
export default threadsSlice.reducer
|
||||||
|
|||||||
@ -15,6 +15,11 @@ export type Message = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Conversation = {
|
||||||
|
id: string
|
||||||
|
messages: Message[]
|
||||||
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@ -2264,6 +2264,11 @@ electron@^28.2.0:
|
|||||||
"@types/node" "^18.11.18"
|
"@types/node" "^18.11.18"
|
||||||
extract-zip "^2.0.1"
|
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:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user