feat: add assistant setting popup

This commit is contained in:
kangfenmao 2024-09-29 17:40:57 +08:00
parent 33b83bf242
commit 32cdfbbfb0
11 changed files with 416 additions and 124 deletions

View File

@ -0,0 +1,187 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Col, Row, Slider, Switch, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
}
const AssistantModelSettings: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const { t } = useTranslation()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings({
temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens,
streamOutput: settings.streamOutput ?? streamOutput
})
}
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ maxTokens: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS,
streamOutput: true
}
})
}
useEffect(() => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
setStreamOutput(assistant?.settings?.streamOutput ?? true)
}, [assistant])
return (
<Container>
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={20}
onChange={setConextCount}
onChangeComplete={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<Row align="middle" justify="space-between">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
disabled={!enableMaxTokens}
min={0}
max={32000}
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
</Row>
<SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
<Switch
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<HStack
justifyContent="flex-end"
style={{ marginTop: 20, padding: '10px 0', borderTop: '0.5px solid var(--color-border)' }}>
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
{t('chat.settings.reset')}
</Button>
</HStack>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding-bottom: 10px;
`
const Label = styled.p`
margin: 0;
margin-right: 5px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 12px;
cursor: pointer;
color: var(--color-text-3);
`
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export default AssistantModelSettings

View File

@ -0,0 +1,47 @@
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box, VStack } from '../Layout'
const AssistantPromptSettings: React.FC<{ assistant: Assistant }> = (props) => {
const { assistant, updateAssistant } = useAssistant(props.assistant.id)
const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt)
const { t } = useTranslation()
const onUpdate = () => {
const _assistant = { ...assistant, name, prompt }
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
return (
<VStack flex={1}>
<Box mb={8}>{t('common.name')}</Box>
<Input
placeholder={t('common.assistant') + t('common.name')}
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={onUpdate}
/>
<Box mt={8} mb={8}>
{t('common.prompt')}
</Box>
<TextArea
rows={10}
placeholder={t('common.assistant') + t('common.prompt')}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onBlur={onUpdate}
style={{ minHeight: 'calc(80vh - 150px)', maxHeight: 'calc(80vh - 150px)' }}
/>
</VStack>
)
}
export default AssistantPromptSettings

View File

@ -0,0 +1,154 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { Assistant } from '@renderer/types'
import { Menu, Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { HStack } from '../Layout'
import { TopView } from '../TopView'
import AssistantModelSettings from './AssistantModelSettings'
import AssistantPromptSettings from './AssistantPromptSettings'
interface AssistantSettingPopupShowParams {
assistant: Assistant
}
interface Props extends AssistantSettingPopupShowParams {
resolve: (assistant: Assistant) => void
}
const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [menu, setMenu] = useState('prompt')
const { theme } = useTheme()
const onOk = () => {
setOpen(false)
}
const handleCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(assistant)
}
const items = [
{
key: 'prompt',
label: t('assistants.prompt_settings')
},
{
key: 'model',
label: t('assistants.model_settings')
}
]
return (
<StyledModal
open={open}
onOk={onOk}
onCancel={handleCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
footer={null}
title={assistant.name}
styles={{
content: {
padding: 0,
overflow: 'hidden',
border: '1px solid var(--color-border)',
background: 'var(--color-background)'
},
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 },
mask: { background: theme === 'light' ? 'rgba(255,255,255, 0.8)' : 'rgba(0,0,0, 0.8)' }
}}
width="70vw"
height="80vh"
centered>
<HStack>
<LeftMenu>
<Menu
style={{ width: 220, padding: 5, background: 'transparent' }}
defaultSelectedKeys={['prompt']}
mode="vertical"
items={items}
onSelect={({ key }) => setMenu(key as string)}
/>
</LeftMenu>
<Settings>
{menu === 'prompt' && <AssistantPromptSettings assistant={assistant} />}
{menu === 'model' && <AssistantModelSettings assistant={assistant} />}
</Settings>
</HStack>
</StyledModal>
)
}
const LeftMenu = styled.div`
background-color: var(--color-background);
height: calc(80vh - 20px);
border-right: 0.5px solid var(--color-border);
`
const Settings = styled.div`
flex: 1;
padding: 10px 20px;
height: calc(80vh - 20px);
overflow-y: scroll;
`
const StyledModal = styled(Modal)`
.ant-modal-title {
font-size: 14px;
}
.ant-modal-close {
top: 4px;
}
.ant-menu-item {
height: 36px;
border-radius: 4px;
color: var(--color-text-2);
display: flex;
align-items: center;
.ant-menu-title-content {
line-height: 36px;
}
}
.ant-menu-item-active {
background-color: var(--color-background-soft) !important;
transition: none;
}
.ant-menu-item-selected {
background-color: var(--color-background-soft);
.ant-menu-title-content {
color: var(--color-text-1);
font-weight: 500;
}
}
`
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AssistantSettingPopup'
)
})
}
}

View File

@ -1,84 +0,0 @@
import { Assistant } from '@renderer/types'
import { Input, Modal } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface AssistantSettingPopupShowParams {
assistant: Assistant
}
interface Props extends AssistantSettingPopupShowParams {
resolve: (assistant: Assistant) => void
}
const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => {
const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt)
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
}
const handleCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({ ...assistant, name, prompt })
}
return (
<Modal
title={assistant.name}
open={open}
onOk={onOk}
onCancel={handleCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
centered>
<Box mb={8}>{t('common.name')}</Box>
<Input
placeholder={t('common.assistant') + t('common.name')}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Box mt={8} mb={8}>
{t('common.prompt')}
</Box>
<TextArea
rows={10}
placeholder={t('common.assistant') + t('common.prompt')}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</Modal>
)
}
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AssistantSettingPopup'
)
})
}
}

View File

@ -20,11 +20,14 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
trackBg: 'transparent',
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
boxShadowTertiary: undefined
},
Menu: {
activeBarBorderWidth: 0,
darkItemBg: 'transparent'
}
},
token: {
colorPrimary: '#00b96b',
borderRadius: 6
colorPrimary: '#00b96b'
}
}}>
{children}

View File

@ -110,7 +110,9 @@
"assistants": {
"title": "Assistants",
"abbr": "Assistant",
"search": "Search assistants..."
"search": "Search assistants...",
"prompt_settings": "Prompt Settings",
"model_settings": "Model Settings"
},
"model": {
"stream_output": "Stream Output"

View File

@ -110,7 +110,9 @@
"assistants": {
"title": "助手",
"abbr": "助手",
"search": "搜索助手"
"search": "搜索助手",
"prompt_settings": "提示词设置",
"model_settings": "模型设置"
},
"model": {
"stream_output": "流式输出"

View File

@ -110,7 +110,9 @@
"assistants": {
"title": "助手",
"abbr": "助",
"search": "搜尋助手..."
"search": "搜尋助手...",
"prompt_settings": "提示詞設定",
"model_settings": "模型設定"
},
"model": {
"stream_output": "串流輸出"

View File

@ -1,10 +1,10 @@
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSearching } from '@renderer/store/runtime'
@ -34,7 +34,7 @@ const Assistants: FC<Props> = ({
const generating = useAppSelector((state) => state.runtime.generating)
const [search, setSearch] = useState('')
const [dragging, setDragging] = useState(false)
const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id)
const { removeAllTopics } = useAssistant(activeAssistant.id)
const { clickAssistantToShowTopic, topicPosition } = useSettings()
const searchRef = useRef<InputRef>(null)
const { t } = useTranslation()
@ -49,15 +49,6 @@ const Assistants: FC<Props> = ({
[assistants, onCreateDefaultAssistant, removeAssistant, setActiveAssistant]
)
const onEditAssistant = useCallback(
async (assistant: Assistant) => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
},
[updateAssistant]
)
const getMenuItems = useCallback(
(assistant: Assistant) =>
[
@ -65,7 +56,7 @@ const Assistants: FC<Props> = ({
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
onClick: () => onEditAssistant(assistant)
onClick: () => AssistantSettingPopup.show({ assistant })
},
{
label: t('common.duplicate'),
@ -100,7 +91,7 @@ const Assistants: FC<Props> = ({
onClick: () => onDelete(assistant)
}
] as ItemType[],
[addAssistant, onDelete, onEditAssistant, removeAllTopics, setActiveAssistant, t]
[addAssistant, onDelete, removeAllTopics, setActiveAssistant, t]
)
const onSwitchAssistant = useCallback(

View File

@ -1,6 +1,4 @@
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -12,22 +10,15 @@ interface Props {
const Prompt: FC<Props> = ({ assistant }) => {
const { t } = useTranslation()
const { updateAssistant } = useAssistant(assistant.id)
const prompt = assistant.prompt || t('chat.default.description')
const onEdit = async () => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
if (!prompt) {
return null
}
return (
<Container onClick={onEdit}>
<Container onClick={() => AssistantSettingPopup.show({ assistant })}>
<Text>{prompt}</Text>
</Container>
)

View File

@ -1,14 +1,14 @@
import { FormOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import { HStack } from '@renderer/components/Layout'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { isMac, isWindows } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types'
import { Switch } from 'antd'
@ -25,19 +25,13 @@ interface Props {
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
const { assistant, updateAssistant, addTopic } = useAssistant(activeAssistant.id)
const { assistant, addTopic } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { theme, toggleTheme } = useTheme()
const { topicPosition } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation()
const onEditAssistant = useCallback(async () => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}, [assistant, updateAssistant])
const addNewTopic = useCallback(() => {
const topic = getDefaultTopic()
addTopic(topic)
@ -68,7 +62,10 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
<i className="iconfont icon-show-sidebar" />
</NewButton>
)}
<TitleText style={{ marginRight: 10, cursor: 'pointer' }} className="nodrag" onClick={onEditAssistant}>
<TitleText
style={{ marginRight: 10, cursor: 'pointer' }}
className="nodrag"
onClick={() => AssistantSettingPopup.show({ assistant })}>
{assistant.name}
</TitleText>
<SelectModelButton assistant={assistant} />