diff --git a/package.json b/package.json
index 3faa758e..fcf9c3d2 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 49b37330..3d97e20b 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -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,23 +9,15 @@ import '@fontsource/inter'
function App(): JSX.Element {
return (
-
-
-
- } />
- } />
- } />
-
-
-
+
+
+ } />
+ } />
+ } />
+
+
)
}
-const MainContainer = styled.main`
- display: flex;
- flex-direction: row;
- flex: 1;
-`
-
export default App
diff --git a/src/renderer/src/assets/css/base.css b/src/renderer/src/assets/css/base.css
index 480fe7b3..8fb18dc2 100644
--- a/src/renderer/src/assets/css/base.css
+++ b/src/renderer/src/assets/css/base.css
@@ -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;
}
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 28365a58..fcd5d9f8 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -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;
`
diff --git a/src/renderer/src/hooks/useConversactions.ts b/src/renderer/src/hooks/useConversactions.ts
new file mode 100644
index 00000000..2269a3e0
--- /dev/null
+++ b/src/renderer/src/hooks/useConversactions.ts
@@ -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([])
+ const [activeConversation, setActiveConversation] = useState()
+
+ // Use localforage to initialize conversations
+ useEffect(() => {
+ runAsyncFunction(async () => {
+ const conversations = await localforage.getItem('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
+ }
+}
diff --git a/src/renderer/src/init.ts b/src/renderer/src/init.ts
new file mode 100644
index 00000000..75b03a04
--- /dev/null
+++ b/src/renderer/src/init.ts
@@ -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()
diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx
index 9389d369..df428be3 100644
--- a/src/renderer/src/main.tsx
+++ b/src/renderer/src/main.tsx
@@ -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(
diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx
index 707e834a..ca2be215 100644
--- a/src/renderer/src/pages/home/HomePage.tsx
+++ b/src/renderer/src/pages/home/HomePage.tsx
@@ -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 (
-
+
-
- new
- new
+
+
+
+
Cherry AI
-
-
+
+
-
+
)
}
-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`
diff --git a/src/renderer/src/pages/home/components/Chat.tsx b/src/renderer/src/pages/home/components/Chat.tsx
new file mode 100644
index 00000000..0628d633
--- /dev/null
+++ b/src/renderer/src/pages/home/components/Chat.tsx
@@ -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 = ({ activeConversation }) => {
+ return {activeConversation?.lastMessage}
+}
+
+const Container = styled.div`
+ display: flex;
+ height: 100%;
+ flex: 1;
+ border-right: 1px solid #ffffff20;
+`
+
+export default Chat
diff --git a/src/renderer/src/pages/home/components/Conversations.tsx b/src/renderer/src/pages/home/components/Conversations.tsx
new file mode 100644
index 00000000..9ba78b3a
--- /dev/null
+++ b/src/renderer/src/pages/home/components/Conversations.tsx
@@ -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 = ({ conversations, activeConversation, onSelectConversation }) => {
+ return (
+
+ {conversations.map((conversation) => (
+ onSelectConversation(conversation)}
+ className={conversation.id === activeConversation?.id ? 'active' : ''}>
+ {conversation.lastMessageAt}
+ {conversation.name}
+ {conversation.lastMessage}
+
+ ))}
+
+ )
+}
+
+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
diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts
new file mode 100644
index 00000000..3666f058
--- /dev/null
+++ b/src/renderer/src/utils/index.ts
@@ -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, interval = 200, stopTimeout = 60000) => {
+ let timeout = false
+ const timer = setTimeout(() => (timeout = true), stopTimeout)
+
+ return (async function check(): Promise {
+ if (await fn()) {
+ clearTimeout(timer)
+ return Promise.resolve()
+ } else if (!timeout) {
+ return delay(interval / 1000).then(check)
+ } else {
+ return Promise.resolve()
+ }
+ })()
+}
diff --git a/yarn.lock b/yarn.lock
index e218b2c1..6f966c0b 100644
--- a/yarn.lock
+++ b/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"