feat: rename topic
This commit is contained in:
parent
4b17e4cd16
commit
ceb816bc2a
@ -13,6 +13,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@ -7,21 +7,16 @@ import Sidebar from './components/app/Sidebar'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import { ConfigProvider, theme } from 'antd'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import { AntdThemeConfig } from './config/antd'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#00b96b',
|
||||
borderRadius: 5,
|
||||
colorBgContainer: '#f6ffed'
|
||||
},
|
||||
algorithm: [theme.darkAlgorithm, theme.compactAlgorithm]
|
||||
}}>
|
||||
<ConfigProvider theme={AntdThemeConfig}>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<BrowserRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
@ -30,6 +25,7 @@ function App(): JSX.Element {
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</ConfigProvider>
|
||||
|
||||
167
src/renderer/src/components/Layout/index.ts
Normal file
167
src/renderer/src/components/Layout/index.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ContainerProps {
|
||||
padding?: string
|
||||
}
|
||||
|
||||
type PxValue = number | string
|
||||
|
||||
export interface BoxProps {
|
||||
width?: PxValue
|
||||
height?: PxValue
|
||||
w?: PxValue
|
||||
h?: PxValue
|
||||
color?: string
|
||||
background?: string
|
||||
flex?: string | number
|
||||
position?: string
|
||||
left?: PxValue
|
||||
top?: PxValue
|
||||
right?: PxValue
|
||||
bottom?: PxValue
|
||||
opacity?: string | number
|
||||
borderRadius?: PxValue
|
||||
border?: string
|
||||
gap?: PxValue
|
||||
mt?: PxValue
|
||||
marginTop?: PxValue
|
||||
mb?: PxValue
|
||||
marginBottom?: PxValue
|
||||
ml?: PxValue
|
||||
marginLeft?: PxValue
|
||||
mr?: PxValue
|
||||
marginRight?: PxValue
|
||||
m?: string
|
||||
margin?: string
|
||||
pt?: PxValue
|
||||
paddingTop?: PxValue
|
||||
pb?: PxValue
|
||||
paddingBottom?: PxValue
|
||||
pl?: PxValue
|
||||
paddingLeft?: PxValue
|
||||
pr?: PxValue
|
||||
paddingRight?: PxValue
|
||||
p?: string
|
||||
padding?: string
|
||||
}
|
||||
|
||||
export interface StackProps extends BoxProps {
|
||||
justifyContent?: 'center' | 'flex-start' | 'flex-end' | 'space-between'
|
||||
alignItems?: 'center' | 'flex-start' | 'flex-end' | 'space-between'
|
||||
flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse'
|
||||
}
|
||||
|
||||
export interface ButtonProps extends StackProps {
|
||||
color?: string
|
||||
isDisabled?: boolean
|
||||
isLoading?: boolean
|
||||
background?: string
|
||||
border?: string
|
||||
fontSize?: string
|
||||
}
|
||||
|
||||
const cssRegex = /(px|vw|vh|%|auto)$/g
|
||||
|
||||
const getElementValue = (value?: PxValue) => {
|
||||
if (!value) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value + 'px'
|
||||
}
|
||||
|
||||
if (value.match(cssRegex)) {
|
||||
return value
|
||||
}
|
||||
|
||||
return value + 'px'
|
||||
}
|
||||
|
||||
export const Box = styled.div<BoxProps>`
|
||||
width: ${(props) => (props.width || props.w ? getElementValue(props.width ?? props.w) : 'auto')};
|
||||
height: ${(props) => (props.height || props.h ? getElementValue(props.height || props.h) : 'auto')};
|
||||
color: ${(props) => props.color || 'default'};
|
||||
background: ${(props) => props.background || 'default'};
|
||||
flex: ${(props) => props.flex || 'none'};
|
||||
position: ${(props) => props.position || 'default'};
|
||||
left: ${(props) => getElementValue(props.left) || 'auto'};
|
||||
right: ${(props) => getElementValue(props.right) || 'auto'};
|
||||
bottom: ${(props) => getElementValue(props.bottom) || 'auto'};
|
||||
top: ${(props) => getElementValue(props.top) || 'auto'};
|
||||
gap: ${(p) => (p.gap ? getElementValue(p.gap) : 0)};
|
||||
opacity: ${(props) => props.opacity ?? 1};
|
||||
border-radius: ${(props) => getElementValue(props.borderRadius) || 0};
|
||||
box-sizing: border-box;
|
||||
border: ${(props) => props?.border || 'none'};
|
||||
gap: ${(p) => (p.gap ? getElementValue(p.gap) : 0)};
|
||||
margin: ${(props) => (props.m || props.margin ? props.m ?? props.margin : 'none')};
|
||||
margin-top: ${(props) => (props.mt || props.marginTop ? getElementValue(props.mt || props.marginTop) : 'default')};
|
||||
margin-bottom: ${(props) =>
|
||||
props.mb || props.marginBottom ? getElementValue(props.mb ?? props.marginBottom) : 'default'};
|
||||
margin-left: ${(props) => (props.ml || props.marginLeft ? getElementValue(props.ml ?? props.marginLeft) : 'default')};
|
||||
margin-right: ${(props) =>
|
||||
props.mr || props.marginRight ? getElementValue(props.mr ?? props.marginRight) : 'default'};
|
||||
padding: ${(props) => (props.p || props.padding ? props.p ?? props.padding : 'none')};
|
||||
padding-top: ${(props) => (props.pt || props.paddingTop ? getElementValue(props.pt ?? props.paddingTop) : 'auto')};
|
||||
padding-bottom: ${(props) =>
|
||||
props.pb || props.paddingBottom ? getElementValue(props.pb ?? props.paddingBottom) : 'auto'};
|
||||
padding-left: ${(props) => (props.pl || props.paddingLeft ? getElementValue(props.pl ?? props.paddingLeft) : 'auto')};
|
||||
padding-right: ${(props) =>
|
||||
props.pr || props.paddingRight ? getElementValue(props.pr ?? props.paddingRight) : 'auto'};
|
||||
`
|
||||
|
||||
export const Stack = styled(Box)<StackProps>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => props.justifyContent ?? 'flex-start'};
|
||||
align-items: ${(props) => props.alignItems ?? 'flex-start'};
|
||||
flex-direction: ${(props) => props.flexDirection ?? 'row'};
|
||||
`
|
||||
|
||||
export const Center = styled(Stack)<StackProps>`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const HStack = styled(Stack)<StackProps>`
|
||||
flex-direction: row;
|
||||
`
|
||||
|
||||
export const HSpaceBetweenStack = styled(HStack)<StackProps>`
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
export const VStack = styled(Stack)<StackProps>`
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const BaseTypography = styled(Box)<{
|
||||
fontSize?: number
|
||||
lineHeight?: string
|
||||
fontWeigth?: number | string
|
||||
color?: string
|
||||
textAlign?: string
|
||||
}>`
|
||||
font-size: ${(props) => (props.fontSize ? getElementValue(props.fontSize) : '16px')};
|
||||
line-height: ${(props) => (props.lineHeight ? getElementValue(props.lineHeight) : 'normal')};
|
||||
font-weight: ${(props) => props.fontWeigth || 'normal'};
|
||||
color: ${(props) => props.color || '#fff'};
|
||||
text-align: ${(props) => props.textAlign || 'left'};
|
||||
`
|
||||
|
||||
export const TypographyNormal = styled(BaseTypography)`
|
||||
font-family: 'Poppins';
|
||||
`
|
||||
|
||||
export const TypographyBold = styled(BaseTypography)`
|
||||
font-family: 'Poppins Bold';
|
||||
`
|
||||
|
||||
export const Container = styled.main<ContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
padding: ${(p) => p.padding ?? '0 18px'};
|
||||
`
|
||||
75
src/renderer/src/components/Popups/PromptPopup.tsx
Normal file
75
src/renderer/src/components/Popups/PromptPopup.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { Input, InputProps, Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { TopView } from '../TopView'
|
||||
import { Box } from '../Layout'
|
||||
|
||||
interface PromptPopupShowParams {
|
||||
title: string
|
||||
message: string
|
||||
defaultValue?: string
|
||||
inputPlaceholder?: string
|
||||
inputProps?: InputProps
|
||||
}
|
||||
|
||||
interface Props extends PromptPopupShowParams {
|
||||
resolve: (value: string) => void
|
||||
}
|
||||
|
||||
const PromptPopupContainer: React.FC<Props> = ({
|
||||
title,
|
||||
message,
|
||||
defaultValue = '',
|
||||
inputPlaceholder = '',
|
||||
inputProps = {},
|
||||
resolve
|
||||
}) => {
|
||||
const [value, setValue] = useState(defaultValue)
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={title} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}>
|
||||
<Box mb={8}>{message}</Box>
|
||||
<Input
|
||||
placeholder={inputPlaceholder}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
allowClear
|
||||
autoFocus
|
||||
onPressEnter={onOk}
|
||||
{...inputProps}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class PromptPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
}
|
||||
static show(props: PromptPopupShowParams) {
|
||||
return new Promise<string>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
<PromptPopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
64
src/renderer/src/components/TopView/index.tsx
Normal file
64
src/renderer/src/components/TopView/index.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { findIndex, pullAt } from 'lodash'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
let id = 0
|
||||
let onPop = () => {}
|
||||
let onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {}
|
||||
let onHide = ({ key }: { key: number }) => {}
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
type ElementItem = {
|
||||
key: number
|
||||
element: React.FC | React.ReactNode
|
||||
}
|
||||
|
||||
const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
const [elements, setElements] = useState<ElementItem[]>([])
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elements]
|
||||
views.pop()
|
||||
setElements(views)
|
||||
}
|
||||
|
||||
onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
|
||||
setElements(elements.concat([{ element, key }]))
|
||||
}
|
||||
|
||||
onHide = ({ key }: { key: number }) => {
|
||||
const views = [...elements]
|
||||
pullAt(views, findIndex(views, { key }))
|
||||
setElements(views)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{elements.length > 0 && (
|
||||
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
|
||||
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
|
||||
{elements.map(({ element: Element, key }) =>
|
||||
typeof Element === 'function' ? <Element key={`TOPVIEW_${key}`} /> : Element
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const TopView = {
|
||||
show: (element: React.FC | React.ReactNode) => {
|
||||
id = id + 1
|
||||
onShow({ element, key: id })
|
||||
return id
|
||||
},
|
||||
hide: (key: number) => {
|
||||
onHide({ key })
|
||||
},
|
||||
pop: onPop
|
||||
}
|
||||
|
||||
export default TopViewContainer
|
||||
9
src/renderer/src/config/antd.ts
Normal file
9
src/renderer/src/config/antd.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { theme, ThemeConfig } from 'antd'
|
||||
|
||||
export const AntdThemeConfig: ThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#00b96b',
|
||||
borderRadius: 5
|
||||
},
|
||||
algorithm: [theme.darkAlgorithm, theme.compactAlgorithm]
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addTopic as _addTopic,
|
||||
removeTopic as _removeTopic,
|
||||
updateTopic as _updateTopic,
|
||||
addAgent,
|
||||
removeAgent,
|
||||
updateAgent
|
||||
@ -34,10 +35,13 @@ export function useAgent(id: string) {
|
||||
return {
|
||||
agent,
|
||||
addTopic: (topic: Topic) => {
|
||||
dispatch(_addTopic({ agentId: agent?.id!, topic }))
|
||||
dispatch(_addTopic({ agentId: agent?.id, topic }))
|
||||
},
|
||||
removeTopic: (topic: Topic) => {
|
||||
dispatch(_removeTopic({ agentId: agent?.id!, topic }))
|
||||
dispatch(_removeTopic({ agentId: agent?.id, topic }))
|
||||
},
|
||||
updateTopic: (topic: Topic) => {
|
||||
dispatch(_updateTopic({ agentId: agent?.id, topic }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Agent } from '@renderer/types'
|
||||
import { FC, useState } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Inputbar from './Inputbar'
|
||||
import Conversations from './Conversations'
|
||||
@ -15,6 +15,10 @@ const Chat: FC<Props> = (props) => {
|
||||
const { agent } = useAgent(props.agent.id)
|
||||
const [activeTopic, setActiveTopic] = useState(agent.topics[0])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTopic(agent.topics[0])
|
||||
}, [agent])
|
||||
|
||||
if (!agent) {
|
||||
return null
|
||||
}
|
||||
|
||||
@ -18,22 +18,16 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||
|
||||
const { id: topicId } = topic
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const _messages = [...messages, message]
|
||||
setMessages(_messages)
|
||||
|
||||
const topic = {
|
||||
id: topicId,
|
||||
name: 'Default Topic',
|
||||
localforage.setItem(`topic:${topic.id}`, {
|
||||
...topic,
|
||||
messages: _messages
|
||||
}
|
||||
|
||||
localforage.setItem<Topic>(`topic:${topicId}`, topic)
|
||||
})
|
||||
},
|
||||
[topicId, messages]
|
||||
[messages, topic]
|
||||
)
|
||||
|
||||
const fetchChatCompletion = useCallback(
|
||||
@ -49,7 +43,7 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
|
||||
role: 'agent',
|
||||
content: '',
|
||||
agentId: agent.id,
|
||||
topicId,
|
||||
topicId: topic.id,
|
||||
createdAt: 'now'
|
||||
}
|
||||
|
||||
@ -66,7 +60,7 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
|
||||
|
||||
return _message
|
||||
},
|
||||
[agent.id, topicId]
|
||||
[agent.id, topic]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -85,10 +79,10 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const topic = await localforage.getItem<Topic>(`topic:${topicId}`)
|
||||
setMessages(topic ? topic.messages : [])
|
||||
const _topic = await localforage.getItem<Topic>(`topic:${topic.id}`)
|
||||
setMessages(_topic ? _topic.messages : [])
|
||||
})
|
||||
}, [topicId])
|
||||
}, [topic])
|
||||
|
||||
useEffect(() => hljs.highlightAll())
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAgent } from '@renderer/hooks/useAgents'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { Agent, Topic } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { FC, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
@ -11,6 +14,40 @@ interface Props {
|
||||
|
||||
const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
|
||||
const { showRightSidebar } = useShowRightSidebar()
|
||||
const currentTopic = useRef<Topic | null>(null)
|
||||
const { removeTopic, updateTopic } = useAgent(agent.id)
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: 'AI Rename',
|
||||
key: 'ai-rename'
|
||||
},
|
||||
{
|
||||
label: 'Rename',
|
||||
key: 'rename',
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: 'Rename Topic',
|
||||
message: 'Please enter the new name',
|
||||
defaultValue: currentTopic.current?.name || ''
|
||||
})
|
||||
if (name && currentTopic.current && currentTopic.current?.name !== name) {
|
||||
updateTopic({ ...currentTopic.current, name })
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
onClick() {
|
||||
if (agent.topics.length === 1) return
|
||||
currentTopic.current && removeTopic(currentTopic.current)
|
||||
currentTopic.current = null
|
||||
setActiveTopic(agent.topics[0])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (!showRightSidebar) {
|
||||
return null
|
||||
@ -18,13 +55,17 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
|
||||
|
||||
return (
|
||||
<Container className={showRightSidebar ? '' : 'collapsed'}>
|
||||
<TopicTitle>Topics ({agent.topics.length})</TopicTitle>
|
||||
{agent.topics.map((topic) => (
|
||||
<TopicListItem
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
trigger={['contextMenu']}
|
||||
key={topic.id}
|
||||
className={topic.id === activeTopic?.id ? 'active' : ''}
|
||||
onClick={() => setActiveTopic(topic)}>
|
||||
onOpenChange={(open) => open && (currentTopic.current = topic)}>
|
||||
<TopicListItem className={topic.id === activeTopic?.id ? 'active' : ''} onClick={() => setActiveTopic(topic)}>
|
||||
{topic.name}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
@ -42,7 +83,7 @@ const Container = styled.div`
|
||||
`
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 8px 15px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
@ -55,4 +96,11 @@ const TopicListItem = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const TopicTitle = styled.div`
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
export default TopicList
|
||||
|
||||
@ -44,10 +44,20 @@ const agentsSlice = createSlice({
|
||||
}
|
||||
: agent
|
||||
)
|
||||
},
|
||||
updateTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => {
|
||||
state.agents = state.agents.map((agent) =>
|
||||
agent.id === action.payload.agentId
|
||||
? {
|
||||
...agent,
|
||||
topics: agent.topics.map((topic) => (topic.id === action.payload.topic.id ? action.payload.topic : topic))
|
||||
}
|
||||
: agent
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { addAgent, removeAgent, updateAgent, addTopic, removeTopic } = agentsSlice.actions
|
||||
export const { addAgent, removeAgent, updateAgent, addTopic, removeTopic, updateTopic } = agentsSlice.actions
|
||||
|
||||
export default agentsSlice.reducer
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user