From aa4ede4427c483b9d168e833ed9fe7e9a4366e3d Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 30 May 2024 16:20:50 +0800 Subject: [PATCH] feat: add localforage and conversations hook --- package.json | 1 + src/renderer/src/App.tsx | 23 ++--- src/renderer/src/assets/css/base.css | 17 +++- src/renderer/src/components/app/Navbar.tsx | 3 +- src/renderer/src/hooks/useConversactions.ts | 51 +++++++++++ src/renderer/src/init.ts | 13 +++ src/renderer/src/main.tsx | 1 + src/renderer/src/pages/home/HomePage.tsx | 87 ++++++++++++------- .../src/pages/home/components/Chat.tsx | 20 +++++ .../pages/home/components/Conversations.tsx | 70 +++++++++++++++ src/renderer/src/utils/index.ts | 46 ++++++++++ yarn.lock | 19 ++++ 12 files changed, 298 insertions(+), 53 deletions(-) create mode 100644 src/renderer/src/hooks/useConversactions.ts create mode 100644 src/renderer/src/init.ts create mode 100644 src/renderer/src/pages/home/components/Chat.tsx create mode 100644 src/renderer/src/pages/home/components/Conversations.tsx create mode 100644 src/renderer/src/utils/index.ts 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"