feat: add localforage and conversations hook
This commit is contained in:
parent
78c13d7586
commit
aa4ede4427
@ -28,6 +28,7 @@
|
||||
"@fontsource/inter": "^5.0.18",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"localforage": "^1.10.0",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
"styled-components": "^6.1.11"
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import styled from 'styled-components'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import Statusbar from './components/app/Statusbar'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
@ -10,7 +9,6 @@ import '@fontsource/inter'
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<MainContainer>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
@ -18,15 +16,8 @@ function App(): JSX.Element {
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
<Statusbar />
|
||||
</MainContainer>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
const MainContainer = styled.main`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
export default App
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
@import 'https://at.alicdn.com/t/c/font_4563475_m52ypmvg0jc.css';
|
||||
@import 'https://at.alicdn.com/t/c/font_4563475_65n90fm9f93.css';
|
||||
|
||||
:root {
|
||||
--ev-c-white: #ffffff;
|
||||
@ -101,8 +101,17 @@ code {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@ -23,7 +23,8 @@ const NavbarContainer = styled.div`
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
51
src/renderer/src/hooks/useConversactions.ts
Normal file
51
src/renderer/src/hooks/useConversactions.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import localforage from 'localforage'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export type Conversation = {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
lastMessage: string
|
||||
lastMessageAt: string
|
||||
}
|
||||
|
||||
export default function useConversations() {
|
||||
const [conversations, setConversations] = useState<Conversation[]>([])
|
||||
const [activeConversation, setActiveConversation] = useState<Conversation>()
|
||||
|
||||
// Use localforage to initialize conversations
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const conversations = await localforage.getItem<Conversation[]>('conversations')
|
||||
conversations && setConversations(conversations)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Update localforage
|
||||
useEffect(() => {
|
||||
localforage.setItem('conversations', conversations)
|
||||
}, [conversations])
|
||||
|
||||
const addConversation = (conversation) => {
|
||||
setConversations([...conversations, conversation])
|
||||
}
|
||||
|
||||
const removeConversation = (conversationId) => {
|
||||
setConversations(conversations.filter((c) => c.id !== conversationId))
|
||||
}
|
||||
|
||||
const updateConversation = (conversation) => {
|
||||
setConversations(conversations.map((c) => (c.id === conversation.id ? conversation : c)))
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
activeConversation,
|
||||
setConversations,
|
||||
addConversation,
|
||||
removeConversation,
|
||||
updateConversation,
|
||||
setActiveConversation
|
||||
}
|
||||
}
|
||||
13
src/renderer/src/init.ts
Normal file
13
src/renderer/src/init.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import localforage from 'localforage'
|
||||
|
||||
function init() {
|
||||
localforage.config({
|
||||
driver: localforage.INDEXEDDB,
|
||||
name: 'CherryAI',
|
||||
version: 1.0,
|
||||
storeName: 'cherryai',
|
||||
description: 'Cherry AI storage'
|
||||
})
|
||||
}
|
||||
|
||||
init()
|
||||
@ -3,6 +3,7 @@ import './assets/css/base.css'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './init'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@ -1,67 +1,90 @@
|
||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { FC } from 'react'
|
||||
import useConversations from '@renderer/hooks/useConversactions'
|
||||
import { FC, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Conversations from './components/Conversations'
|
||||
import Chat from './components/Chat'
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { conversations, activeConversation, setActiveConversation, addConversation } = useConversations()
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeConversation) {
|
||||
setActiveConversation(conversations[0])
|
||||
}
|
||||
}, [activeConversation, conversations])
|
||||
|
||||
const onCreateConversation = () => {
|
||||
window.electron.ipcRenderer.send('storage.set', { key: 'conversations', value: [] })
|
||||
const _conversation = {
|
||||
// ID auto increment
|
||||
id: Math.random().toString(),
|
||||
name: 'New conversation',
|
||||
// placeholder url
|
||||
avatar: 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y',
|
||||
lastMessage: 'message',
|
||||
lastMessageAt: 'now'
|
||||
}
|
||||
addConversation(_conversation)
|
||||
setActiveConversation(_conversation)
|
||||
}
|
||||
|
||||
return (
|
||||
<MainContainer>
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarLeft style={{ justifyContent: 'space-between' }}>
|
||||
<NewButton onClick={onCreateConversation}>new</NewButton>
|
||||
<NewButton onClick={onCreateConversation}>new</NewButton>
|
||||
<NavbarLeft style={{ justifyContent: 'flex-end' }}>
|
||||
<NewButton onClick={onCreateConversation}>
|
||||
<i className="iconfont icon-a-addchat"></i>
|
||||
</NewButton>
|
||||
</NavbarLeft>
|
||||
<NavbarCenter>Cherry AI</NavbarCenter>
|
||||
<NavbarRight />
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<Conversations />
|
||||
<Chat />
|
||||
<Conversations
|
||||
conversations={conversations}
|
||||
activeConversation={activeConversation}
|
||||
onSelectConversation={setActiveConversation}
|
||||
/>
|
||||
<Chat activeConversation={activeConversation} />
|
||||
<Settings />
|
||||
</ContentContainer>
|
||||
</MainContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const MainContainer = styled.div`
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const NewButton = styled.button`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-background-soft);
|
||||
border: 1px solid var(--color-background-soft);
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
`
|
||||
|
||||
const Conversations = styled.div`
|
||||
display: flex;
|
||||
min-width: var(--conversations-width);
|
||||
border-right: 1px solid #ffffff20;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Chat = styled.div`
|
||||
const NewButton = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 4px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 1px solid #ffffff20;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
color: var(--color-icon);
|
||||
.iconfont {
|
||||
font-size: 22px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
cursor: pointer;
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const Settings = styled.div`
|
||||
|
||||
20
src/renderer/src/pages/home/components/Chat.tsx
Normal file
20
src/renderer/src/pages/home/components/Chat.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Conversation } from '@renderer/hooks/useConversactions'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
activeConversation?: Conversation
|
||||
}
|
||||
|
||||
const Chat: FC<Props> = ({ activeConversation }) => {
|
||||
return <Container>{activeConversation?.lastMessage}</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 1px solid #ffffff20;
|
||||
`
|
||||
|
||||
export default Chat
|
||||
70
src/renderer/src/pages/home/components/Conversations.tsx
Normal file
70
src/renderer/src/pages/home/components/Conversations.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import type { Conversation } from '@renderer/hooks/useConversactions'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
conversations: Conversation[]
|
||||
activeConversation?: Conversation
|
||||
onSelectConversation: (conversation: Conversation) => void
|
||||
}
|
||||
|
||||
const Conversations: FC<Props> = ({ conversations, activeConversation, onSelectConversation }) => {
|
||||
return (
|
||||
<Container>
|
||||
{conversations.map((conversation) => (
|
||||
<Conversation
|
||||
key={conversation.id}
|
||||
onClick={() => onSelectConversation(conversation)}
|
||||
className={conversation.id === activeConversation?.id ? 'active' : ''}>
|
||||
<ConversationTime>{conversation.lastMessageAt}</ConversationTime>
|
||||
<ConversationName>{conversation.name}</ConversationName>
|
||||
<ConversationLastMessage>{conversation.lastMessage}</ConversationLastMessage>
|
||||
</Conversation>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--conversations-width);
|
||||
border-right: 1px solid #ffffff20;
|
||||
height: calc(100vh - var(--navbar-height) - var(--status-bar-height));
|
||||
padding: 10px;
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const Conversation = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
cursor: pointer;
|
||||
}
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const ConversationTime = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
const ConversationName = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
const ConversationLastMessage = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
export default Conversations
|
||||
46
src/renderer/src/utils/index.ts
Normal file
46
src/renderer/src/utils/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export const runAsyncFunction = async (fn: () => void) => {
|
||||
await fn()
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否是 json 字符串
|
||||
* @param str 字符串
|
||||
*/
|
||||
export function isJSON(str: any): boolean {
|
||||
if (typeof str !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
return typeof JSON.parse(str) === 'object'
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const delay = (seconds: number) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true)
|
||||
}, seconds * 1000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Waiting fn return true
|
||||
**/
|
||||
export const waitAsyncFunction = (fn: () => Promise<any>, interval = 200, stopTimeout = 60000) => {
|
||||
let timeout = false
|
||||
const timer = setTimeout(() => (timeout = true), stopTimeout)
|
||||
|
||||
return (async function check(): Promise<any> {
|
||||
if (await fn()) {
|
||||
clearTimeout(timer)
|
||||
return Promise.resolve()
|
||||
} else if (!timeout) {
|
||||
return delay(interval / 1000).then(check)
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
})()
|
||||
}
|
||||
19
yarn.lock
19
yarn.lock
@ -2889,6 +2889,11 @@ ignore@^5.2.0, ignore@^5.2.4:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
|
||||
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
|
||||
|
||||
immediate@~3.0.5:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447"
|
||||
@ -3266,11 +3271,25 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
lie@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
localforage@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
|
||||
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
|
||||
dependencies:
|
||||
lie "3.1.1"
|
||||
|
||||
locate-path@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user