feat: 智能体改进:名称、上下文支持、模型参数支持 #59

This commit is contained in:
kangfenmao 2024-10-21 23:18:21 +08:00
parent fe2e3bfc36
commit 43b9298329
49 changed files with 1166 additions and 642 deletions

6
.gitignore vendored
View File

@ -19,12 +19,6 @@ lerna-debug.log*
*.sln
*.sw?
# NPM
npm/*/*
!npm/*/dist
!npm/*/package.json
!npm/*/*.js
# Yarn
.pnp.*
.yarn/*

1
npm/artifacts/README.md Normal file
View File

@ -0,0 +1 @@
# Cherry Studio Artifacts

View File

@ -0,0 +1,19 @@
{
"name": "@cherry-studio/artifacts",
"version": "0.1.0",
"description": "Cherry Studio Artifacts",
"main": "index.js",
"homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"artifacts"
],
"author": "kangfenmao",
"license": "ISC"
}

View File

@ -0,0 +1,108 @@
:root {
/* 莫兰迪色系:使用柔和、低饱和度的颜色 */
--primary-color: #b6b5a7; /* 莫兰迪灰褐色,用于背景文字 */
--secondary-color: #9a8f8f; /* 莫兰迪灰棕色,用于标题背景 */
--accent-color: #c5b4a0; /* 莫兰迪淡棕色,用于强调元素 */
--background-color: #e8e3de; /* 莫兰迪米色,用于页面背景 */
--text-color: #5b5b5b; /* 莫兰迪深灰色,用于主要文字 */
--light-text-color: #8c8c8c; /* 莫兰迪中灰色,用于次要文字 */
--divider-color: #d1cbc3; /* 莫兰迪浅灰色,用于分隔线 */
}
body,
html {
margin: 0;
padding: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--background-color); /* 使用莫兰迪米色作为页面背景 */
font-family: 'Noto Sans SC', sans-serif;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要文字颜色 */
}
.card {
width: 300px;
height: 500px;
background-color: #f2ede9; /* 莫兰迪浅米色,用于卡片背景 */
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.header {
background-color: var(--secondary-color); /* 使用莫兰迪灰棕色作为标题背景 */
color: #f2ede9; /* 浅色文字与深色背景形成对比 */
padding: 20px;
text-align: left;
position: relative;
z-index: 1;
}
h1 {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
margin: 0;
font-weight: 700;
}
.content {
padding: 30px 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.word {
text-align: left;
margin-bottom: 20px;
}
.word-main {
font-family: 'Noto Serif SC', serif;
font-size: 36px;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要词汇颜色 */
margin-bottom: 10px;
position: relative;
}
.word-main::after {
content: '';
position: absolute;
left: 0;
bottom: -5px;
width: 50px;
height: 3px;
background-color: var(--accent-color); /* 使用莫兰迪淡棕色作为下划线 */
}
.word-sub {
font-size: 14px;
color: var(--light-text-color); /* 使用莫兰迪中灰色作为次要文字颜色 */
margin: 5px 0;
}
.divider {
width: 100%;
height: 1px;
background-color: var(--divider-color); /* 使用莫兰迪浅灰色作为分隔线 */
margin: 20px 0;
}
.explanation {
font-size: 18px;
line-height: 1.6;
text-align: left;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.quote {
position: relative;
padding-left: 20px;
border-left: 3px solid var(--accent-color); /* 使用莫兰迪淡棕色作为引用边框 */
}
.background-text {
position: absolute;
font-size: 150px;
color: rgba(182, 181, 167, 0.15); /* 使用莫兰迪灰褐色的透明版本作为背景文字 */
z-index: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}

View File

@ -8,7 +8,8 @@
"homepage": "https://github.com/kangfenmao/cherry-studio",
"workspaces": {
"packages": [
"local"
"local",
"npm/*"
]
},
"scripts": {

View File

@ -9,7 +9,6 @@ import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { ThemeProvider } from './context/ThemeProvider'
import AgentEditPage from './pages/agents/AgentEditPage'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
@ -31,7 +30,6 @@ function App(): JSX.Element {
<Route path="/" element={<HomePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/agents/:id" element={<AgentEditPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/messages/*" element={<HistoryPage />} />

View File

@ -10,7 +10,6 @@
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
border-radius: 4px;
&:hover {
background: var(--color-scrollbar-thumb-hover);
}

View File

@ -0,0 +1,158 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'
import { Assistant, AssistantMessage, AssistantSettings } from '@renderer/types'
import { Button, Card, Col, Divider, Form as FormAntd, FormInstance, Row, Space, Switch } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: Partial<AssistantSettings>) => void
}
const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const formRef = useRef<FormInstance>(null)
const [messages, setMessagess] = useState<AssistantMessage[]>(assistant?.messages || [])
const [hideMessages, setHideMessages] = useState(assistant?.settings?.hideMessages || false)
const onSave = () => {
// 检查是否有空对话组
for (let i = 0; i < messages.length; i += 2) {
const userContent = messages[i].content.trim()
const assistantContent = messages[i + 1]?.content.trim()
if (userContent === '' || assistantContent === '') {
window.modal.error({
centered: true,
content: t('agents.edit.message.empty.content')
})
return
}
}
// 过滤掉空消息并将消息分组
const filteredMessagess = messages.reduce((acc, conv, index) => {
if (index % 2 === 0) {
const userContent = conv.content.trim()
const assistantContent = messages[index + 1]?.content.trim()
if (userContent !== '' || assistantContent !== '') {
acc.push({ role: 'user', content: userContent }, { role: 'assistant', content: assistantContent })
}
}
return acc
}, [] as AssistantMessage[])
updateAssistant({
...assistant,
messages: filteredMessagess
})
window.message.success({ content: t('message.save.success.title'), key: 'save-messages' })
}
const addMessages = () => {
setMessagess([...messages, { role: 'user', content: '' }, { role: 'assistant', content: '' }])
}
const updateMessages = (index: number, role: 'user' | 'assistant', content: string) => {
const newMessagess = [...messages]
newMessagess[index] = { role, content }
setMessagess(newMessagess)
}
const deleteMessages = (index: number) => {
const newMessagess = [...messages]
newMessagess.splice(index, 2) // 删除用户和助手的对话
setMessagess(newMessagess)
}
return (
<Container>
<Form ref={formRef} layout="vertical" form={form} labelAlign="right" colon={false}>
<Form.Item label={t('agents.edit.settings.hide_preset_messages')}>
<Switch
checked={hideMessages}
onChange={(checked) => {
setHideMessages(checked)
updateAssistantSettings({ hideMessages: checked })
}}
/>
</Form.Item>
<Divider style={{ marginBottom: 15 }} />
<Form.Item label={t('agents.edit.message.group.title')}>
{messages.map(
(_, index) =>
index % 2 === 0 && (
<Card
size="small"
key={index}
style={{ marginBottom: 16 }}
title={`${t('agents.edit.message.group.title')} #${index / 2 + 1}`}
extra={<Button icon={<DeleteOutlined />} type="text" danger onClick={() => deleteMessages(index)} />}>
<Row gutter={16} align="middle" style={{ marginBottom: 16 }}>
<Col span={3}>
<label>{t('agents.edit.message.user.title')}</label>
</Col>
<Col span={21}>
<TextArea
value={messages[index].content}
onChange={(e) => updateMessages(index, 'user', e.target.value)}
placeholder={t('agents.edit.message.user.placeholder')}
rows={1}
/>
</Col>
</Row>
<Row gutter={16} align="top">
<Col span={3}>
<label>{t('agents.edit.message.assistant.title')}</label>
</Col>
<Col span={21}>
<TextArea
value={messages[index + 1]?.content || ''}
onChange={(e) => updateMessages(index + 1, 'assistant', e.target.value)}
placeholder={t('agents.edit.message.assistant.placeholder')}
rows={3}
/>
</Col>
</Row>
</Card>
)
)}
<Space>
<Button icon={<PlusOutlined />} onClick={addMessages}>
{t('agents.edit.message.add.title')}
</Button>
</Space>
</Form.Item>
<Divider style={{ marginBottom: 15 }} />
<Form.Item>
{messages.length > 0 && (
<Button type="primary" onClick={onSave}>
{t('common.save')}
</Button>
)}
</Form.Item>
</Form>
<div style={{ minHeight: 50 }} />
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding-top: 10px;
`
const Form = styled(FormAntd)`
.ant-form-item-no-colon {
font-weight: 500;
}
`
export default AssistantMessagesSettings

View File

@ -1,81 +1,100 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { PlusOutlined, 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 { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { SettingRow } 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 { Button, Col, Divider, Row, Slider, Switch, Tooltip } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ModelAvatar from '../Avatar/ModelAvatar'
import SelectModelPopup from '../Popups/SelectModelPopup'
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: Partial<AssistantSettings>) => void
}
const AssistantModelSettings: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
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 [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
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 })
updateAssistantSettings({ temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ contextCount: value })
updateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ maxTokens: value })
updateAssistantSettings({ 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
}
})
setEnableMaxTokens(false)
setMaxTokens(0)
setStreamOutput(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])
const onSelectModel = async () => {
const selectedModel = await SelectModelPopup.show({ model: assistant?.model })
if (selectedModel) {
setDefaultModel(selectedModel)
updateAssistant({
...assistant,
defaultModel: selectedModel
})
}
}
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }}>
<Label style={{ marginBottom: 10 }}>{t('assistants.settings.default_model')}</Label>
<Col span={24}>
<HStack alignItems="center">
<Button
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
onClick={onSelectModel}>
{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}
</Button>
</HStack>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<Label>
{t('assistants.settings.auto_reset_model')}{' '}
<Tooltip title={t('assistants.settings.auto_reset_model.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
<Switch
value={autoResetModel}
onChange={(checked) => {
setAutoResetModel(checked)
updateAssistantSettings({ autoResetModel: checked })
}}
/>
</SettingRow>
<Divider style={{ margin: '10px 0' }} />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
@ -95,10 +114,12 @@ const AssistantModelSettings: FC<Props> = (props) => {
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
<Label>
{t('chat.settings.conext_count')}{' '}
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
@ -123,7 +144,7 @@ const AssistantModelSettings: FC<Props> = (props) => {
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
updateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
@ -141,18 +162,17 @@ const AssistantModelSettings: FC<Props> = (props) => {
</Col>
</Row>
<SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
<Label>{t('model.stream_output')}</Label>
<Switch
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
updateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<HStack
justifyContent="flex-end"
style={{ marginTop: 20, padding: '10px 0', borderTop: '0.5px solid var(--color-border)' }}>
<Divider style={{ margin: '15px 0' }} />
<HStack justifyContent="flex-end">
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
{t('chat.settings.reset')}
</Button>
@ -170,8 +190,8 @@ const Container = styled.div`
`
const Label = styled.p`
margin: 0;
margin-right: 5px;
font-weight: 500;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
@ -180,8 +200,4 @@ const QuestionIcon = styled(QuestionCircleOutlined)`
color: var(--color-text-3);
`
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export default AssistantModelSettings

View File

@ -1,15 +1,20 @@
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Box, HStack, VStack } from '../Layout'
import { Box, HStack } from '../Layout'
const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void }> = (props) => {
const { assistant, updateAssistant } = useAssistant(props.assistant.id)
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: AssistantSettings) => void
onOk: () => void
}
const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant, onOk }) => {
const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt)
const { t } = useTranslation()
@ -17,11 +22,10 @@ const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void
const onUpdate = () => {
const _assistant = { ...assistant, name, prompt }
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
return (
<VStack flex={1}>
<Container>
<Box mb={8} style={{ fontWeight: 'bold' }}>
{t('common.name')}
</Box>
@ -43,12 +47,20 @@ const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }}
/>
<HStack width="100%" justifyContent="flex-end" mt="10px">
<Button type="primary" onClick={props.onOk}>
<Button type="primary" onClick={onOk}>
{t('common.close')}
</Button>
</HStack>
</VStack>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding: 5px;
`
export default AssistantPromptSettings

View File

@ -1,4 +1,5 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAgent } from '@renderer/hooks/useAgents'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Menu, Modal } from 'antd'
import { useState } from 'react'
@ -7,6 +8,7 @@ import styled from 'styled-components'
import { HStack } from '../Layout'
import { TopView } from '../TopView'
import AssistantMessagesSettings from './AssistantMessagesSettings'
import AssistantModelSettings from './AssistantModelSettings'
import AssistantPromptSettings from './AssistantPromptSettings'
@ -18,32 +20,43 @@ interface Props extends AssistantSettingPopupShowParams {
resolve: (assistant: Assistant) => void
}
const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => {
const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, ...props }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [menu, setMenu] = useState('prompt')
const { theme } = useTheme()
const _useAssistant = useAssistant(props.assistant.id)
const _useAgent = useAgent(props.assistant.id)
const isAgent = props.assistant.type === 'agent'
const assistant = isAgent ? _useAgent.agent : _useAssistant.assistant
const updateAssistant = isAgent ? _useAgent.updateAgent : _useAssistant.updateAssistant
const updateAssistantSettings = isAgent ? _useAgent.updateAgentSettings : _useAssistant.updateAssistantSettings
const onOk = () => {
setOpen(false)
}
const handleCancel = () => {
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
const afterClose = () => {
resolve(assistant)
}
const items = [
{
key: 'prompt',
label: t('assistants.prompt_settings')
label: t('assistants.settings.prompt')
},
{
key: 'model',
label: t('assistants.model_settings')
label: t('assistants.settings.model')
},
{
key: 'messages',
label: t('assistants.settings.preset_messages')
}
]
@ -51,21 +64,19 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
<StyledModal
open={open}
onOk={onOk}
onCancel={handleCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
onClose={onCancel}
onCancel={onCancel}
afterClose={afterClose}
footer={null}
title={assistant.name}
transitionName="ant-move-down"
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)' }
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 }
}}
width="70vw"
height="80vh"
@ -81,8 +92,28 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
/>
</LeftMenu>
<Settings>
{menu === 'prompt' && <AssistantPromptSettings assistant={assistant} onOk={onOk} />}
{menu === 'model' && <AssistantModelSettings assistant={assistant} />}
{menu === 'prompt' && (
<AssistantPromptSettings
assistant={assistant}
updateAssistant={updateAssistant}
updateAssistantSettings={updateAssistantSettings}
onOk={onOk}
/>
)}
{menu === 'model' && (
<AssistantModelSettings
assistant={assistant}
updateAssistant={updateAssistant}
updateAssistantSettings={updateAssistantSettings}
/>
)}
{menu === 'messages' && (
<AssistantMessagesSettings
assistant={assistant}
updateAssistant={updateAssistant}
updateAssistantSettings={updateAssistantSettings}
/>
)}
</Settings>
</HStack>
</StyledModal>
@ -111,7 +142,7 @@ const StyledModal = styled(Modal)`
}
.ant-menu-item {
height: 36px;
border-radius: 4px;
border-radius: 6px;
color: var(--color-text-2);
display: flex;
align-items: center;
@ -132,11 +163,7 @@ const StyledModal = styled(Modal)`
}
`
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide('AssistantSettingPopup')
}
export default class AssistantSettingsPopup {
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
TopView.show(
@ -144,10 +171,10 @@ export default class AssistantSettingPopup {
{...props}
resolve={(v) => {
resolve(v)
this.hide()
TopView.hide('AssistantSettingsPopup')
}}
/>,
'AssistantSettingPopup'
'AssistantSettingsPopup'
)
})
}

View File

@ -3,7 +3,7 @@ import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import { createAssistantFromAgent } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Agent, Assistant } from '@renderer/types'
import { Divider, Input, InputRef, Modal, Tag } from 'antd'
@ -26,35 +26,22 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { assistants, addAssistant } = useAssistants()
const inputRef = useRef<InputRef>(null)
const defaultAgent: Agent = useMemo(
() => ({
id: defaultAssistant.id,
name: defaultAssistant.name,
emoji: defaultAssistant.emoji || '',
prompt: defaultAssistant.prompt,
group: 'system'
}),
[defaultAssistant.emoji, defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
)
const agents = useMemo(() => {
const allAgents = [...userAgents, ...systemAgents] as Agent[]
const list = [defaultAgent, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
const list = [defaultAssistant, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
return searchText
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
: list
}, [assistants, defaultAgent, searchText, userAgents])
}, [assistants, defaultAssistant, searchText, userAgents])
const onCreateAssistant = (agent: Agent) => {
if (agent.id !== 'default') {
if (assistants.map((a) => a.id).includes(String(agent.id))) {
return
}
const onCreateAssistant = async (agent: Agent) => {
if (agent.id === 'default') {
addAssistant(agent)
return
}
const assistant = covertAgentToAssistant(agent)
const assistant = await createAssistantFromAgent(agent)
addAssistant(assistant)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
resolve(assistant)
setOpen(false)
@ -112,8 +99,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<HStack alignItems="center" gap={5}>
{agent.emoji} {agent.name}
</HStack>
{agent.group === 'system' && <Tag color="green">{t('agents.tag.system')}</Tag>}
{agent.group === 'user' && <Tag color="orange">{t('agents.tag.user')}</Tag>}
{agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
{agent.type === 'agent' && <Tag color="orange">{t('agents.tag.agent')}</Tag>}
</AgentItem>
))}
</Container>

View File

@ -80,7 +80,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
closeIcon={null}
footer={null}>

View File

@ -0,0 +1,48 @@
export const AGENT_PROMPT = `
Prompt Markdown Prompt使
## Role :
[]
## Background :
[]
## Preferences :
[]
## Profile :
- version: 0.2
- language: 中文
- description: [50 ]
## Goals :
[ 1]
[ 2]
...
## Constrains :
[ 1]
[ 2]
...
## Skills :
[ 1]
[ 2]
...
## Examples :
[ 1]
[ 2]
...
## OutputFormat :
[]
[]
...
## Initialization :
[], [], [], 使 [] .
`
export const SUMMARIZE_PROMPT =
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'

View File

@ -1,28 +1,28 @@
import { RootState } from '@renderer/store'
import { addAgent, removeAgent, updateAgent, updateAgents } from '@renderer/store/agents'
import { Agent } from '@renderer/types'
import { useDispatch, useSelector } from 'react-redux'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { addAgent, removeAgent, updateAgent, updateAgents, updateAgentSettings } from '@renderer/store/agents'
import { Agent, AssistantSettings } from '@renderer/types'
export function useAgents() {
const agents = useSelector((state: RootState) => state.agents.agents)
const dispatch = useDispatch()
const agents = useAppSelector((state) => state.agents.agents)
const dispatch = useAppDispatch()
return {
agents,
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents)),
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (agent: Agent) => dispatch(removeAgent(agent)),
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents))
removeAgent: (id: string) => dispatch(removeAgent({ id }))
}
}
export function useAgent(id: string) {
const agents = useSelector((state: RootState) => state.agents.agents)
const dispatch = useDispatch()
const agent = agents.find((a) => a.id === id)
const agent = useAppSelector((state) => state.agents.agents.find((a) => a.id === id) as Agent)
const dispatch = useAppDispatch()
return {
agent,
updateAgent: (agent: Agent) => dispatch(updateAgent(agent))
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
updateAgentSettings: (settings: Partial<AssistantSettings>) => {
dispatch(updateAgentSettings({ assistantId: agent.id, settings }))
}
}
}

View File

@ -58,7 +58,7 @@ export function useAssistant(id: string) {
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
updateAssistantSettings: (settings: AssistantSettings) => {
updateAssistantSettings: (settings: Partial<AssistantSettings>) => {
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
}
}

View File

@ -62,7 +62,8 @@
"upgrade.success.title": "Upgrade successfully",
"upgrade.success.content": "Please restart the application to complete the upgrade",
"upgrade.success.button": "Restart",
"topic.added": "New topic added"
"topic.added": "New topic added",
"save.success.title": "Saved successfully"
},
"chat": {
"save": "Save",
@ -74,8 +75,6 @@
"topics.edit.title": "Edit Name",
"topics.edit.placeholder": "Enter new name",
"topics.clear.title": "Clear Messages",
"topics.delete.all.title": "Delete all topics",
"topics.delete.all.content": "Are you sure you want to delete all topics?",
"topics.move_to": "Move to",
"topics.list": "Topic List",
"topics.export.title": "Export",
@ -118,8 +117,19 @@
"title": "Assistants",
"abbr": "Assistant",
"search": "Search assistants...",
"prompt_settings": "Prompt Settings",
"model_settings": "Model Settings"
"settings.prompt": "Prompt Settings",
"settings.model": "Model Settings",
"settings.preset_messages": "Preset Messages",
"settings.default_model": "Default Model",
"settings.auto_reset_model": "Auto Reset Model",
"settings.auto_reset_model.tip": "Automatically reset the model when a new topic is created.",
"edit.title": "Edit Assistant",
"copy.title": "Copy Assistant",
"clear.title": "Clear topics",
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
"saveto.title": "Save to agent",
"delete.title": "Delete Assistant",
"delete.content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?"
},
"model": {
"stream_output": "Stream Output",
@ -136,18 +146,28 @@
"agents": {
"title": "Agents",
"my_agents": "My Agents",
"add.title": "Add Agent",
"add.title": "Create Agent",
"edit.title": "Edit Agent",
"add.name": "Name",
"add.name.placeholder": "Enter name",
"add.prompt": "Prompt",
"add.prompt.placeholder": "Enter prompt",
"add.button": "Add",
"add.button": "Add to Assistant",
"manage.title": "Manage Agents",
"delete.popup.content": "Are you sure you want to delete this agent?",
"tag.default": "Default",
"tag.system": "System",
"tag.user": "Mine"
"tag.agent": "Agent",
"edit.message.title": "Preset messages",
"edit.message.add.title": "Add",
"edit.message.group.title": "Message Group",
"edit.message.assistant.title": "Assistant",
"edit.message.assistant.placeholder": "Enter assistant message",
"edit.message.user.title": "User",
"edit.message.user.placeholder": "Enter user message",
"edit.message.empty.content": "Conversation input content cannot be empty",
"edit.model.select.title": "Select Model",
"edit.settings.hide_preset_messages": "Hide Preset Message"
},
"minapp": {
"title": "MinApp"
@ -211,6 +231,9 @@
"general.backup.button": "Backup",
"general.restore.button": "Restore",
"general.view_webdav_settings": "View WebDAV settings",
"general.reset.title": "Data Reset",
"general.reset.button": "Reset",
"general.manually_check_update.title": "Manually Check for updates",
"data.webdav.title": "WebDAV",
"data.webdav.host": "WebDAV Host",
"data.webdav.host.placeholder": "http://localhost:8080",
@ -220,11 +243,6 @@
"data.webdav.path.placeholder": "/backup",
"data.webdav.backup.button": "Backup to WebDAV",
"data.webdav.restore.button": "Restore from WebDAV",
"general.reset.title": "Data Reset",
"general.reset.button": "Reset",
"general.check_update_setting": "Check for updates",
"general.manual_update_check": "Check for updates manually",
"general.auto_update_check": "Check for updates automatically",
"advanced.title": "Advanced Settings",
"advanced.click_assistant_switch_to_topics": "Auto switch to topic",
"provider.api_key": "API Key",
@ -285,7 +303,8 @@
"font_size.title": "Message Font Size",
"topic.position": "Topic Position",
"topic.position.left": "Left",
"topic.position.right": "Right"
"topic.position.right": "Right",
"topic.show.time": "Show Topic Time"
},
"translate": {
"title": "Translation",

View File

@ -62,7 +62,8 @@
"upgrade.success.title": "升级成功",
"upgrade.success.content": "重启应用以完成升级",
"upgrade.success.button": "重启",
"topic.added": "话题添加成功"
"topic.added": "话题添加成功",
"save.success.title": "保存成功"
},
"chat": {
"save": "保存",
@ -74,8 +75,6 @@
"topics.edit.title": "编辑话题名",
"topics.edit.placeholder": "输入新名称",
"topics.clear.title": "清空消息",
"topics.delete.all.title": "删除所有话题",
"topics.delete.all.content": "确定要删除所有话题吗?",
"topics.move_to": "移动到",
"topics.list": "话题列表",
"topics.export.title": "导出",
@ -118,8 +117,19 @@
"title": "助手",
"abbr": "助手",
"search": "搜索助手",
"prompt_settings": "提示词设置",
"model_settings": "模型设置"
"settings.prompt": "提示词设置",
"settings.model": "模型设置",
"settings.preset_messages": "预设消息",
"settings.default_model": "默认模型",
"settings.auto_reset_model": "自动重置模型",
"settings.auto_reset_model.tip": "创建新话题时自动重置模型",
"edit.title": "编辑助手",
"copy.title": "复制助手",
"clear.title": "清空话题",
"clear.content": "清空话题会删除助手下所有话题和文件,确定要继续吗?",
"saveto.title": "保存到智能体",
"delete.title": "删除助手",
"delete.content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?"
},
"model": {
"stream_output": "流式输出",
@ -136,18 +146,28 @@
"agents": {
"title": "智能体",
"my_agents": "我的智能体",
"add.title": "添加智能体",
"add.title": "创建智能体",
"edit.title": "编辑智能体",
"add.name": "名称",
"add.name.placeholder": "输入名称",
"add.prompt": "提示词",
"add.prompt.placeholder": "输入提示词",
"add.button": "添加",
"add.button": "添加到助手",
"manage.title": "管理智能体",
"delete.popup.content": "确定要删除此智能体吗?",
"tag.default": "默认",
"tag.system": "系统",
"tag.user": "我的"
"tag.agent": "智能体",
"edit.message.title": "预设消息",
"edit.message.add.title": "添加",
"edit.message.group.title": "消息组",
"edit.message.assistant.title": "助手",
"edit.message.assistant.placeholder": "输入助手消息",
"edit.message.user.title": "用户",
"edit.message.user.placeholder": "输入用户消息",
"edit.message.empty.content": "会话输入内容不能为空",
"edit.model.select.title": "选择模型",
"edit.settings.hide_preset_messages": "隐藏预设消息"
},
"minapp": {
"title": "小程序"
@ -213,6 +233,7 @@
"general.reset.title": "重置数据",
"general.reset.button": "重置",
"general.view_webdav_settings": "查看 WebDAV 设置",
"general.manually_check_update.title": "手动检查更新",
"data.webdav.title": "WebDAV",
"data.webdav.host": "WebDAV 地址",
"data.webdav.host.placeholder": "http://localhost:8080",
@ -222,9 +243,6 @@
"data.webdav.path.placeholder": "/backup",
"data.webdav.backup.button": "备份到 WebDAV",
"data.webdav.restore.button": "从 WebDAV 恢复",
"general.check_update_setting": "更新设置",
"general.manual_update_check": "手动检查更新",
"general.auto_update_check": "自动检查更新",
"advanced.title": "高级设置",
"advanced.click_assistant_switch_to_topics": "点击助手切换到话题",
"provider.api_key": "API 密钥",
@ -285,7 +303,8 @@
"font_size.title": "消息字体大小",
"topic.position": "话题位置",
"topic.position.left": "左侧",
"topic.position.right": "右侧"
"topic.position.right": "右侧",
"topic.show.time": "显示话题时间"
},
"translate": {
"title": "翻译",

View File

@ -62,7 +62,8 @@
"upgrade.success.title": "升級成功",
"upgrade.success.content": "請重新啟動應用以完成升級",
"upgrade.success.button": "重新啟動",
"topic.added": "新話題已添加"
"topic.added": "新話題已添加",
"save.success.title": "保存成功"
},
"chat": {
"save": "保存",
@ -74,8 +75,6 @@
"topics.edit.title": "編輯名稱",
"topics.edit.placeholder": "輸入新名稱",
"topics.clear.title": "清空消息",
"topics.delete.all.title": "刪除所有話題",
"topics.delete.all.content": "確定要刪除所有話題嗎?",
"topics.move_to": "移動到",
"topics.list": "話題列表",
"topics.export.title": "匯出",
@ -118,8 +117,19 @@
"title": "助手",
"abbr": "助",
"search": "搜尋助手...",
"prompt_settings": "提示詞設定",
"model_settings": "模型設定"
"settings.prompt": "提示詞設定",
"settings.model": "模型設定",
"settings.preset_messages": "預設訊息",
"settings.default_model": "預設模型",
"settings.auto_reset_model": "自動重置模型",
"settings.auto_reset_model.tip": "每次新的話題時自動重置模型",
"edit.title": "編輯助手",
"copy.title": "複製助手",
"clear.title": "清空話題",
"clear.content": "清空話題會刪除助手下所有主題和文件,確定要繼續嗎?",
"saveto.title": "儲存到智能體",
"delete.title": "删除助手",
"delete.content": "删除助手会删除所有该助手下的话题和文件,确定要繼續吗?"
},
"model": {
"stream_output": "串流輸出",
@ -136,18 +146,28 @@
"agents": {
"title": "智能體",
"my_agents": "我的智能體",
"add.title": "添加智能體",
"add.title": "创建智能體",
"edit.title": "編輯智能體",
"add.name": "名稱",
"add.name.placeholder": "輸入名稱",
"add.prompt": "提示詞",
"add.prompt.placeholder": "輸入提示詞",
"add.button": "添加",
"add.button": "添加到助手",
"manage.title": "管理智能體",
"delete.popup.content": "確定要刪除此智能體嗎?",
"tag.default": "預設",
"tag.system": "系統",
"tag.user": "我的"
"tag.agent": "智能体",
"edit.message.title": "預設訊息",
"edit.message.add.title": "添加",
"edit.message.group.title": "訊息組",
"edit.message.assistant.title": "助手",
"edit.message.assistant.placeholder": "輸入助手消息",
"edit.message.user.title": "用戶",
"edit.message.user.placeholder": "輸入用戶消息",
"edit.message.empty.content": "會話輸入內容不能為空",
"edit.model.select.title": "選擇模型",
"edit.settings.hide_preset_messages": "隱藏預設消息"
},
"minapp": {
"title": "小程序"
@ -211,6 +231,9 @@
"general.backup.button": "備份",
"general.restore.button": "復原",
"general.view_webdav_settings": "查看 WebDAV 設定",
"general.reset.title": "資料重置",
"general.reset.button": "重置",
"general.manually_check_update.title": "手動檢查更新",
"data.webdav.title": "WebDAV",
"data.webdav.host": "WebDAV 主機位址",
"data.webdav.host.placeholder": "http://localhost:8080",
@ -220,11 +243,6 @@
"data.webdav.path.placeholder": "/backup",
"data.webdav.backup.button": "從 WebDAV 備份",
"data.webdav.restore.button": "從 WebDAV 恢復",
"general.reset.title": "資料重置",
"general.reset.button": "重置",
"general.check_update_setting": "更新設定",
"general.manual_update_check": "手動檢查更新",
"general.auto_update_check": "自動檢查更新",
"advanced.title": "進階設定",
"advanced.click_assistant_switch_to_topics": "點擊助手切換到話題",
"provider.api_key": "API 密鑰",
@ -285,7 +303,8 @@
"font_size.title": "訊息字體大小",
"topic.position": "話題位置",
"topic.position.left": "左側",
"topic.position.right": "右側"
"topic.position.right": "右側",
"topic.show.time": "顯示話題時間"
},
"translate": {
"title": "翻譯",

View File

@ -1,161 +0,0 @@
import { LoadingOutlined, ThunderboltOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { useAgent, useAgents } from '@renderer/hooks/useAgents'
import { fetchGenerate } from '@renderer/services/api'
import { syncAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { getLeadingEmoji } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Popover } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useParams } from 'react-router'
import styled from 'styled-components'
type FieldType = {
id: string
name: string
prompt: string
}
const AgentEditPage: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const { agent } = useAgent(id!)
const [form] = Form.useForm()
const formRef = useRef<FormInstance>(null)
const { addAgent, updateAgent } = useAgents()
const [emoji, setEmoji] = useState(agent?.emoji)
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const onFinish = (values: FieldType) => {
const _emoji = emoji || getLeadingEmoji(values.name)
if (values.name.trim() === '' || values.prompt.trim() === '') {
return
}
const _agent = {
...agent,
name: values.name,
emoji: _emoji,
prompt: values.prompt
} as Agent
updateAgent(_agent)
syncAgentToAssistant(_agent)
navigate(-1)
}
const handleButtonClick = async () => {
const prompt = `你是一个专业的 prompt 优化助手我会给你一段prompt你需要帮我优化它仅回复优化后的 prompt 不要添加任何解释,使用 [CRISPE提示框架] 回复。`
const name = formRef.current?.getFieldValue('name')
const content = formRef.current?.getFieldValue('prompt')
const promptText = content || name
if (!promptText) {
return
}
if (content) {
navigator.clipboard.writeText(content)
}
setLoading(true)
try {
const prefixedContent = `请帮我优化下面这段 prompt使用 CRISPE 提示框架,请使用 Markdown 格式回复,不要使用 codeblock: ${promptText}`
const generatedText = await fetchGenerate({ prompt, content: prefixedContent })
formRef.current?.setFieldValue('prompt', generatedText)
} catch (error) {
console.error('Error fetching data:', error)
}
setLoading(false)
}
useEffect(() => {
if (agent) {
form.setFieldsValue({
name: agent.name,
prompt: agent.prompt
})
}
}, [agent, form])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.edit.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<Form
ref={formRef}
layout="vertical"
form={form}
labelAlign="left"
colon={false}
style={{ width: '100%' }}
onFinish={onFinish}>
<Form.Item name="name" label="Emoji">
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} arrow placement="rightBottom">
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
</Popover>
</Form.Item>
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
</Form.Item>
<Form.Item
name="prompt"
label={
<>
{t('agents.add.prompt')}{' '}
<Button
size="small"
style={{ marginLeft: 5 }}
type="text"
icon={loading ? <LoadingOutlined /> : <ThunderboltOutlined />}
onClick={handleButtonClick}
disabled={loading}
/>
</>
}
rules={[{ required: true }]}
style={{ position: 'relative' }}>
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} />
</Form.Item>
<Form.Item wrapperCol={{ span: 16 }}>
<Button type="primary" htmlType="submit">
{t('common.save')}
</Button>
<Button type="link" onClick={() => navigate(-1)}>
{t('common.cancel')}
</Button>
</Form.Item>
</Form>
<div style={{ minHeight: 50 }} />
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 20px;
overflow-y: scroll;
`
export default AgentEditPage

View File

@ -0,0 +1,161 @@
import { DeleteOutlined, EditOutlined, MoreOutlined, PlusOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import { HStack } from '@renderer/components/Layout'
import { useAgents } from '@renderer/hooks/useAgents'
import { createAssistantFromAgent } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { Button, Dropdown, Typography } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AddAgentPopup from './components/AddAgentPopup'
const { Title } = Typography
interface Props {
onClick: (agent: Agent) => void
}
const Agents: React.FC<Props> = ({ onClick }) => {
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const [dragging, setDragging] = useState(false)
const getMenuItems = useCallback(
(agent: Agent) =>
[
{
label: t('agents.edit.title'),
key: 'edit',
icon: <EditOutlined />,
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
},
{
label: t('agents.add.title'),
key: 'add',
icon: <PlusOutlined />,
onClick: () => createAssistantFromAgent(agent)
},
{ type: 'divider' },
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
window.modal.confirm({
centered: true,
content: t('agents.delete.popup.content'),
onOk: () => removeAgent(agent.id)
})
}
}
] as ItemType[],
[removeAgent, t]
)
return (
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
{agents.length > 0 && (
<DragableList
list={agents}
onUpdate={updateAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent: Agent) => (
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['contextMenu']}>
<AgentItem onClick={() => onClick(agent)}>
<HStack alignItems="center" justifyContent="space-between" h="36px">
<AgentItemName className="text-nowrap">
{agent.emoji} {agent.name}
</AgentItemName>
<ActionButton className="actions" gap="15px" onClick={(e) => e.stopPropagation()}>
<Dropdown menu={{ items: getMenuItems(agent) }} trigger={['hover']}>
<MoreOutlined style={{ cursor: 'pointer' }} />
</Dropdown>
</ActionButton>
</HStack>
<AgentItemPrompt>{agent.prompt}</AgentItemPrompt>
</AgentItem>
</Dropdown>
)}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
</Container>
)
}
const Container = styled.div`
padding: 15px;
display: flex;
flex-direction: column;
width: 280px;
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
overflow-y: scroll;
`
const AgentItem = styled.div`
display: flex;
flex-direction: column;
padding: 0 12px;
min-height: 38px;
border-radius: 10px;
user-select: none;
margin-bottom: 12px;
padding-bottom: 12px;
border: 0.5px solid var(--color-border);
transition: all 0.2s ease-in-out;
cursor: pointer;
&:hover {
.actions {
display: flex;
}
}
&:hover {
border: 0.5px solid var(--color-primary);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
`
const AgentItemName = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const AgentItemPrompt = styled.div`
font-size: 12px;
color: var(--color-text-soft);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: -5px;
color: var(--color-text-3);
`
const ActionButton = styled(HStack)`
align-items: center;
justify-content: center;
display: none;
background-color: var(--color-background-soft);
width: 24px;
height: 24px;
border-radius: 12px;
font-size: 16px;
color: var(--color-icon);
`
export default Agents

View File

@ -1,48 +1,56 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { VStack } from '@renderer/components/Layout'
import Agents from '@renderer/config/agents.json'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import SystemAgents from '@renderer/config/agents.json'
import { createAssistantFromAgent } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Col, Row, Typography } from 'antd'
import { find, groupBy } from 'lodash'
import { groupBy, omit } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import Agents from './Agents'
import AgentCard from './components/AgentCard'
import MyAgents from './components/MyAgents'
const { Title } = Typography
const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const agentGroups = groupBy(Agents, 'group')
const AgentsPage: FC = () => {
const agentGroups = groupBy(SystemAgents, 'group')
const { t } = useTranslation()
const onAddAgentConfirm = (agent: Agent) => {
const added = find(assistants, { id: agent.id })
const getAgentName = (agent: Agent) => {
return agent.emoji ? agent.emoji + ' ' + agent.name : agent.name
}
const onAddAgentConfirm = (agent: Agent) => {
window.modal.confirm({
title: agent.emoji + ' ' + agent.name,
content: (agent.description || agent.prompt).substring(0, 1000) + '...',
title: getAgentName(agent),
content: (
<AgentPrompt>
<ReactMarkdown className="markdown">{agent.description || agent.prompt}</ReactMarkdown>
</AgentPrompt>
),
width: 600,
icon: null,
closable: true,
maskClosable: true,
centered: true,
okButtonProps: { type: 'primary', disabled: Boolean(added) },
okText: added ? t('button.added') : t('button.add'),
onOk: () => onAddAgent(agent)
okButtonProps: { type: 'primary' },
okText: t('agents.add.button'),
onOk: () => createAssistantFromAgent(agent)
})
}
const onAddAgent = (agent: Agent) => {
addAssistant(covertAgentToAssistant(agent))
window.message.success({
content: t('message.assistant.added.content'),
key: 'agent-added',
style: { marginTop: '5vh' }
})
const getAgentFromSystemAgent = (agent: (typeof SystemAgents)[number]) => {
return {
...omit(agent, 'group'),
name: agent.name,
id: uuid(),
topics: [],
type: 'agent'
}
}
return (
@ -51,7 +59,7 @@ const AppsPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<MyAgents onClick={onAddAgentConfirm} />
<Agents onClick={onAddAgentConfirm} />
<AssistantsContainer>
<VStack style={{ flex: 1 }}>
{Object.keys(agentGroups)
@ -65,7 +73,10 @@ const AppsPage: FC = () => {
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))}
agent={agent as any}
/>
</Col>
)
})}
@ -104,4 +115,10 @@ const AssistantsContainer = styled.div`
overflow-y: scroll;
`
export default AppsPage
const AgentPrompt = styled.div`
max-height: 60vh;
overflow-y: scroll;
max-width: 560px;
`
export default AgentsPage

View File

@ -3,8 +3,10 @@ import 'emoji-picker-element'
import { LoadingOutlined, ThunderboltOutlined } from '@ant-design/icons'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { TopView } from '@renderer/components/TopView'
import { AGENT_PROMPT } from '@renderer/config/prompts'
import { useAgents } from '@renderer/hooks/useAgents'
import { fetchGenerate } from '@renderer/services/api'
import { getDefaultModel } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd'
@ -38,12 +40,15 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
return
}
const _agent = {
const _agent: Agent = {
id: uuid(),
name: values.name,
emoji: _emoji,
prompt: values.prompt,
group: 'user'
defaultModel: getDefaultModel(),
type: 'agent',
topics: [],
messages: []
}
addAgent(_agent)
@ -60,8 +65,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
}
const handleButtonClick = async () => {
const prompt = `你是一个专业的 prompt 优化助手我会给你一段prompt你需要帮我优化它仅回复优化后的 prompt 不要添加任何解释,使用 [CRISPE提示框架] 回复。`
const name = formRef.current?.getFieldValue('name')
const content = formRef.current?.getFieldValue('prompt')
const promptText = content || name
@ -77,8 +80,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
setLoading(true)
try {
const prefixedContent = `请帮我优化下面这段 prompt使用 CRISPE 提示框架,请使用 Markdown 格式回复,不要使用 codeblock: ${promptText}`
const generatedText = await fetchGenerate({ prompt, content: prefixedContent })
const generatedText = await fetchGenerate({
prompt: AGENT_PROMPT,
content: promptText
})
formRef.current?.setFieldValue('prompt', generatedText)
} catch (error) {
console.error('Error fetching data:', error)
@ -95,7 +100,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onCancel={onCancel}
maskClosable={false}
afterClose={onClose}
okText={t('agents.add.button')}
okText={t('agents.add.title')}
centered>
<Form
ref={formRef}

View File

@ -32,7 +32,8 @@ const Container = styled.div`
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: var(--color-background-mute);
border: 0.5px solid var(--color-primary);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
`
const EmojiHeader = styled.div`
@ -69,6 +70,7 @@ const AgentCardPrompt = styled.div`
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: pre-wrap;
font-size: 12px;
`

View File

@ -1,98 +0,0 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import { Box, HStack } from '@renderer/components/Layout'
import { useAgents } from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Button, Popconfirm, Typography } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import AddAgentPopup from './AddAgentPopup'
const { Title } = Typography
interface Props {
onClick: (agent: Agent) => void
}
const MyAssistants: React.FC<Props> = ({ onClick }) => {
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const [dragging, setDragging] = useState(false)
const navigate = useNavigate()
return (
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
<Title level={5} style={{ marginLeft: 10 }}>
{t('agents.my_agents')}
</Title>
{agents.length > 0 && (
<DragableList
list={agents}
onUpdate={updateAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent) => (
<AgentItem onClick={() => onClick(agent)}>
<Box mr={8}>
{agent.emoji} {agent.name}
</Box>
<HStack gap="15px" onClick={(e) => e.stopPropagation()}>
<Popconfirm
title={t('agents.delete.popup.content')}
placement="bottom"
okButtonProps={{ danger: true }}
onConfirm={() => removeAgent(agent)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>
<EditOutlined style={{ cursor: 'pointer' }} onClick={() => navigate(`/agents/${agent.id}`)} />
</HStack>
</AgentItem>
)}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
</Container>
)
}
const Container = styled.div`
padding: 15px 10px;
display: flex;
flex-direction: column;
width: var(--assistants-width);
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
overflow-y: scroll;
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: 20px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default MyAssistants

View File

@ -118,6 +118,7 @@ const Header = styled.div`
align-items: center;
justify-content: center;
padding: 8px 20px;
padding-top: 10px;
width: 100%;
position: relative;
`

View File

@ -13,7 +13,7 @@ import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime, useShowTopics } from '@renderer/hooks/useStore'
import { getDefaultTopic } from '@renderer/services/assistant'
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import FileManager from '@renderer/services/file'
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/tokens'
@ -45,7 +45,7 @@ let _files: FileType[] = []
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false)
const { addTopic, model } = useAssistant(assistant.id)
const { addTopic, model, setModel } = useAssistant(assistant.id)
const { sendMessageShortcut, fontSize, pasteLongTextAsFile, showInputEstimatedTokens } = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
@ -127,14 +127,20 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
}
const addNewTopic = useCallback(() => {
const addNewTopic = useCallback(async () => {
const topic = getDefaultTopic(assistant.id)
await db.topics.add({ id: topic.id, messages: [] })
await addAssistantMessagesToTopic({ assistant, topic })
// Reset to assistant default model
if (assistant.settings?.autoResetModel) {
assistant.defaultModel && setModel(assistant.defaultModel)
}
addTopic(topic)
setActiveTopic(topic)
db.topics.add({ id: topic.id, messages: [] })
}, [addTopic, assistant.id, setActiveTopic])
}, [addTopic, assistant, setActiveTopic, setModel])
const clearTopic = async () => {
if (generating) {
@ -270,9 +276,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
_setEstimateTokenCount(tokensCount)
setContextCount(contextCount)
}),
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, () => {
addNewTopic()
})
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [addNewTopic])

View File

@ -20,11 +20,20 @@ interface Props {
total?: number
lastMessage?: boolean
showMenu?: boolean
hidePresetMessages?: boolean
onEditMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => void
}
const MessageItem: FC<Props> = ({ message, index, lastMessage, showMenu = true, onEditMessage, onDeleteMessage }) => {
const MessageItem: FC<Props> = ({
message,
index,
lastMessage,
showMenu = true,
hidePresetMessages,
onEditMessage,
onDeleteMessage
}) => {
const { t } = useTranslation()
const { assistant, setModel } = useAssistant(message.assistantId)
const model = useModel(message.modelId)
@ -59,6 +68,10 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, showMenu = true,
return () => unsubscribes.forEach((unsub) => unsub())
}, [message])
if (hidePresetMessages && message.isPreset) {
return null
}
if (message.type === 'clear') {
return (
<Divider dashed style={{ padding: '0 20px' }} plain>

View File

@ -227,6 +227,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
key={message.id}
message={message}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage}
/>

View File

@ -1,4 +1,4 @@
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@ -18,7 +18,7 @@ const Prompt: FC<Props> = ({ assistant }) => {
}
return (
<Container onClick={() => AssistantSettingPopup.show({ assistant })}>
<Container onClick={() => AssistantSettingsPopup.show({ assistant })}>
<Text>{prompt}</Text>
</Container>
)

View File

@ -1,6 +1,6 @@
import { FormOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import { HStack } from '@renderer/components/Layout'
import { isMac, isWindows } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -57,7 +57,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
<TitleText
style={{ marginRight: 10, cursor: 'pointer' }}
className="nodrag"
onClick={() => AssistantSettingPopup.show({ assistant })}>
onClick={() => AssistantSettingsPopup.show({ assistant })}>
{assistant.name}
</TitleText>
<SelectModelButton assistant={assistant} />

View File

@ -1,7 +1,8 @@
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { getDefaultTopic } from '@renderer/services/assistant'
@ -12,7 +13,7 @@ import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Dropdown, Input, InputRef } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { isEmpty, last } from 'lodash'
import { isEmpty, last, omit } from 'lodash'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -39,6 +40,7 @@ const Assistants: FC<Props> = ({
const searchRef = useRef<InputRef>(null)
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { addAgent } = useAgents()
const onDelete = useCallback(
(assistant: Assistant) => {
@ -53,13 +55,13 @@ const Assistants: FC<Props> = ({
(assistant: Assistant) =>
[
{
label: t('common.edit'),
label: t('assistants.edit.title'),
key: 'edit',
icon: <EditOutlined />,
onClick: () => AssistantSettingPopup.show({ assistant })
onClick: () => AssistantSettingsPopup.show({ assistant })
},
{
label: t('common.duplicate'),
label: t('assistants.copy.title'),
key: 'duplicate',
icon: <CopyIcon />,
onClick: async () => {
@ -69,29 +71,48 @@ const Assistants: FC<Props> = ({
}
},
{
label: t('chat.topics.delete.all.title'),
key: 'delete-all',
label: t('assistants.clear.title'),
key: 'clear',
icon: <MinusCircleOutlined />,
onClick: () => {
window.modal.confirm({
title: t('chat.topics.delete.all.title'),
content: t('chat.topics.delete.all.content'),
title: t('assistants.clear.title'),
content: t('assistants.clear.content'),
centered: true,
okButtonProps: { danger: true },
onOk: removeAllTopics
})
}
},
{
label: t('assistants.saveto.title'),
key: 'save-to-agent',
icon: <SaveOutlined />,
onClick: async () => {
const agent = omit(assistant, ['model', 'emoji'])
agent.id = uuid()
agent.type = 'agent'
addAgent(agent)
}
},
{ type: 'divider' },
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => onDelete(assistant)
onClick: () => {
window.modal.confirm({
title: t('assistants.delete.title'),
content: t('assistants.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => onDelete(assistant)
})
}
}
] as ItemType[],
[addAssistant, onDelete, removeAllTopics, setActiveAssistant, t]
[addAgent, addAssistant, onDelete, removeAllTopics, setActiveAssistant, t]
)
const onSwitchAssistant = useCallback(

View File

@ -49,13 +49,7 @@ const SettingsTab: FC<Props> = (props) => {
} = useSettings()
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
})
updateAssistantSettings(settings)
}
const onTemperatureChange = (value) => {

View File

@ -9,6 +9,7 @@ import {
import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
@ -16,6 +17,7 @@ import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import dayjs from 'dayjs'
import { findIndex } from 'lodash'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@ -32,6 +34,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const { showTopicTime } = useSettings()
const borderRadius = showTopicTime ? 12 : 17
const onDeleteTopic = useCallback(
(topic: Topic) => {
@ -172,8 +177,12 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const isActive = topic.id === activeTopic?.id
return (
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)}>
<TopicListItem
className={isActive ? 'active' : ''}
style={{ borderRadius }}
onClick={() => onSwitchTopic(topic)}>
<TopicName className="name">{topic.name.replace('`', '')}</TopicName>
{showTopicTime && <TopicTime>{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
{isActive && (
<MenuButton
className="menu"
@ -192,6 +201,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
)
}}
</DragableList>
<div style={{ minHeight: '10px' }}></div>
</Container>
)
}
@ -210,9 +220,8 @@ const TopicListItem = styled.div`
font-family: Ubuntu;
font-size: 13px;
display: flex;
flex-direction: row;
flex-direction: column;
justify-content: space-between;
align-items: center;
position: relative;
font-family: Ubuntu;
cursor: pointer;
@ -249,6 +258,11 @@ const TopicName = styled.div`
font-size: 13px;
`
const TopicTime = styled.div`
color: var(--color-text-3);
font-size: 11px;
`
const MenuButton = styled.div`
display: flex;
flex-direction: row;

View File

@ -3,8 +3,11 @@ import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from
import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setManualUpdateCheck } from '@renderer/store/settings'
import { runAsyncFunction } from '@renderer/utils'
import { Avatar, Button, Progress, Row, Tag } from 'antd'
import { Avatar, Button, Progress, Row, Switch, Tag } from 'antd'
import { ProgressInfo } from 'electron-updater'
import { debounce } from 'lodash'
import { FC, useEffect, useState } from 'react'
@ -20,6 +23,8 @@ const AboutSettings: FC = () => {
const [percent, setPercent] = useState(0)
const [checkUpdateLoading, setCheckUpdateLoading] = useState(false)
const [downloading, setDownloading] = useState(false)
const { manualUpdateCheck } = useSettings()
const dispatch = useAppDispatch()
const onCheckUpdate = debounce(
async () => {
@ -140,6 +145,11 @@ const AboutSettings: FC = () => {
</CheckUpdateButton>
</AboutHeader>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.manually_check_update.title')}</SettingRowTitle>
<Switch value={manualUpdateCheck} onChange={(v) => dispatch(setManualUpdateCheck(v))} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
<SoundOutlined />

View File

@ -24,7 +24,7 @@ const DataSettings: FC = () => {
<SettingRowTitle>{t('settings.data.webdav.title')}</SettingRowTitle>
<VStack gap="5px">
<Link to="/settings/data/webdav" style={{ color: 'var(--color-text-2)' }}>
{t('settings.general.view_webdav_settings')}
<Button>{t('settings.general.view_webdav_settings')}</Button>
</Link>
</VStack>
</SettingRow>

View File

@ -2,7 +2,7 @@ import { isMac } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { useAppDispatch } from '@renderer/store'
import { setClickAssistantToShowTopic, setLanguage, setManualUpdateCheck } from '@renderer/store/settings'
import { setClickAssistantToShowTopic, setLanguage, setShowTopicTime } from '@renderer/store/settings'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types'
import { isValidProxyUrl } from '@renderer/utils'
@ -19,8 +19,8 @@ const GeneralSettings: FC = () => {
theme,
windowStyle,
topicPosition,
showTopicTime,
clickAssistantToShowTopic,
manualUpdateCheck,
setTheme,
setWindowStyle,
setTopicPosition
@ -95,19 +95,6 @@ const GeneralSettings: FC = () => {
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.check_update_setting')}</SettingRowTitle>
<Select
defaultValue={manualUpdateCheck ?? false}
style={{ width: 180 }}
onChange={(v) => dispatch(setManualUpdateCheck(v))}
options={[
{ value: false, label: t('settings.general.auto_update_check') },
{ value: true, label: t('settings.general.manual_update_check') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
@ -145,6 +132,11 @@ const GeneralSettings: FC = () => {
<SettingDivider />
</>
)}
<SettingRow>
<SettingRowTitle>{t('settings.topic.show.time')}</SettingRowTitle>
<Switch checked={showTopicTime} onChange={(checked) => dispatch(setShowTopicTime(checked))} />
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}

View File

@ -116,7 +116,7 @@ const ProvidersList: FC = () => {
{getFirstCharacter(provider.name)}
</ProviderLogo>
)}
<ProviderItemName>
<ProviderItemName className="text-nowrap">
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
</ProviderItemName>
{provider.enabled && (
@ -196,9 +196,6 @@ const ProviderLogo = styled(Avatar)`
const ProviderItemName = styled.div`
margin-left: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
font-family: Ubuntu;
`

View File

@ -1,6 +1,7 @@
import Anthropic from '@anthropic-ai/sdk'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { SUMMARIZE_PROMPT } from '@renderer/config/prompts'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
import { EVENT_NAMES } from '@renderer/services/event'
import { filterContextMessages } from '@renderer/services/messages'
@ -136,22 +137,34 @@ export default class AnthropicProvider extends BaseProvider {
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
const userMessages = takeRight(messages, 5).map((message) => ({
role: message.role,
content: message.content
}))
const userMessages = takeRight(messages, 5)
.filter((message) => !message.isPreset)
.map((message) => ({
role: message.role,
content: message.content
}))
if (first(userMessages)?.role === 'assistant') {
userMessages.shift()
}
const userMessageContent = userMessages.reduce((prev, curr) => {
const content = curr.role === 'user' ? `User: ${curr.content}` : `Assistant: ${curr.content}`
return prev + (prev ? '\n' : '') + content
}, '')
const systemMessage = {
role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
content: SUMMARIZE_PROMPT
}
const userMessage = {
role: 'user',
content: userMessageContent
}
const message = await this.sdk.messages.create({
messages: userMessages as Anthropic.Messages.MessageParam[],
messages: [userMessage] as Anthropic.Messages.MessageParam[],
model: model.id,
system: systemMessage.content,
stream: false,
@ -165,16 +178,16 @@ export default class AnthropicProvider extends BaseProvider {
const model = getDefaultModel()
const message = await this.sdk.messages.create({
model: model.id,
system: prompt,
stream: false,
max_tokens: 4096,
messages: [
{
role: 'user',
content
}
],
model: model.id,
system: prompt,
stream: false,
max_tokens: 4096
]
})
return message.content[0].type === 'text' ? message.content[0].text : ''

View File

@ -7,6 +7,7 @@ import {
Part,
TextPart
} from '@google/generative-ai'
import { SUMMARIZE_PROMPT } from '@renderer/config/prompts'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
import { EVENT_NAMES } from '@renderer/services/event'
import { filterContextMessages } from '@renderer/services/messages'
@ -145,14 +146,26 @@ export default class GeminiProvider extends BaseProvider {
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
const userMessages = takeRight(messages, 5).map((message) => ({
role: message.role,
content: message.content
}))
const userMessages = takeRight(messages, 5)
.filter((message) => !message.isPreset)
.map((message) => ({
role: message.role,
content: message.content
}))
const userMessageContent = userMessages.reduce((prev, curr) => {
const content = curr.role === 'user' ? `User: ${curr.content}` : `Assistant: ${curr.content}`
return prev + (prev ? '\n' : '') + content
}, '')
const systemMessage = {
role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
content: SUMMARIZE_PROMPT
}
const userMessage = {
role: 'user',
content: userMessageContent
}
const geminiModel = this.sdk.getGenerativeModel({
@ -163,16 +176,9 @@ export default class GeminiProvider extends BaseProvider {
}
})
const lastUserMessage = userMessages.pop()
const chat = await geminiModel.startChat()
const chat = await geminiModel.startChat({
history: userMessages.map((message) => ({
role: message.role === 'user' ? 'user' : 'model',
parts: [{ text: message.content }]
}))
})
const { response } = await chat.sendMessage(lastUserMessage?.content!)
const { response } = await chat.sendMessage(userMessage.content)
return response.text()
}

View File

@ -1,11 +1,11 @@
import { isLocalAi } from '@renderer/config/env'
import { isSupportedModel, isVisionModel } from '@renderer/config/models'
import { SUMMARIZE_PROMPT } from '@renderer/config/prompts'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
import { EVENT_NAMES } from '@renderer/services/event'
import { filterContextMessages } from '@renderer/services/messages'
import { Assistant, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
import { removeQuotes } from '@renderer/utils'
import { first, takeRight } from 'lodash'
import { takeRight } from 'lodash'
import OpenAI, { AzureOpenAI } from 'openai'
import {
ChatCompletionContentPart,
@ -45,7 +45,7 @@ export default class OpenAIProvider extends BaseProvider {
}
private get isNotSupportFiles() {
const providers = ['deepseek', 'baichuan', 'minimax', 'yi', 'doubao']
const providers = ['deepseek', 'baichuan', 'minimax', 'doubao']
return providers.includes(this.provider.id)
}
@ -190,23 +190,35 @@ export default class OpenAIProvider extends BaseProvider {
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
const userMessages = takeRight(messages, 5).map((message) => ({
role: message.role,
content: message.content
}))
const userMessages = takeRight(messages, 5)
.filter((message) => !message.isPreset)
.map((message) => ({
role: message.role,
content: message.content
}))
const userMessageContent = userMessages.reduce((prev, curr) => {
const content = curr.role === 'user' ? `User: ${curr.content}` : `Assistant: ${curr.content}`
return prev + (prev ? '\n' : '') + content
}, '')
const systemMessage = {
role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
content: SUMMARIZE_PROMPT
}
const userMessage = {
role: 'user',
content: userMessageContent
}
// @ts-ignore key is not typed
const response = await this.sdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...(isLocalAi ? [first(userMessages)] : userMessages)] as ChatCompletionMessageParam[],
messages: [systemMessage, userMessage] as ChatCompletionMessageParam[],
stream: false,
max_tokens: 50,
keep_alive: this.keepAliveTime
keep_alive: this.keepAliveTime,
max_tokens: 1000
})
return removeQuotes(response.choices[0].message?.content?.substring(0, 50) || '')
@ -219,8 +231,8 @@ export default class OpenAIProvider extends BaseProvider {
model: model.id,
stream: false,
messages: [
{ role: 'user', content },
{ role: 'system', content: prompt }
{ role: 'system', content: prompt },
{ role: 'user', content }
]
})

View File

@ -1,17 +1,21 @@
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { updateAgent } from '@renderer/store/agents'
import { updateAssistant } from '@renderer/store/assistants'
import { Agent, Assistant, AssistantSettings, Model, Provider, Topic } from '@renderer/types'
import { getLeadingEmoji, removeLeadingEmoji, uuid } from '@renderer/utils'
import { addAssistant } from '@renderer/store/assistants'
import { Agent, Assistant, AssistantSettings, Message, Model, Provider, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { estimateMessageUsage } from './tokens'
export function getDefaultAssistant(): Assistant {
return {
id: 'default',
name: i18n.t('chat.default.name'),
prompt: '',
topics: [getDefaultTopic('default')]
topics: [getDefaultTopic('default')],
messages: [],
type: 'assistant'
}
}
@ -82,19 +86,9 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE,
enableMaxTokens: assistant?.settings?.enableMaxTokens ?? false,
maxTokens: getAssistantMaxTokens(),
streamOutput: assistant?.settings?.streamOutput ?? true
}
}
export function covertAgentToAssistant(agent: Agent): Assistant {
const id = agent.group === 'system' ? uuid() : String(agent.id)
return {
...getDefaultAssistant(),
...agent,
id,
topics: [getDefaultTopic(id)],
name: getAssistantNameWithAgent(agent),
settings: getDefaultAssistantSettings()
streamOutput: assistant?.settings?.streamOutput ?? true,
hideMessages: assistant?.settings?.hideMessages ?? false,
autoResetModel: assistant?.settings?.autoResetModel ?? false
}
}
@ -102,38 +96,58 @@ export function getAssistantNameWithAgent(agent: Agent) {
return agent.emoji ? agent.emoji + ' ' + agent.name : agent.name
}
export function syncAsistantToAgent(assistant: Assistant) {
const agents = store.getState().agents.agents
const agent = agents.find((a) => a.id === assistant.id)
if (agent) {
store.dispatch(
updateAgent({
...agent,
emoji: getLeadingEmoji(assistant.name),
name: removeLeadingEmoji(assistant.name),
prompt: assistant.prompt
})
)
}
}
export function syncAgentToAssistant(agent: Agent) {
const assistants = store.getState().assistants.assistants
const assistant = assistants.find((a) => a.id === agent.id)
if (assistant) {
store.dispatch(
updateAssistant({
...assistant,
name: getAssistantNameWithAgent(agent),
prompt: agent.prompt
})
)
}
}
export function getAssistantById(id: string) {
const assistants = store.getState().assistants.assistants
return assistants.find((a) => a.id === id)
}
export async function addAssistantMessagesToTopic({ assistant, topic }: { assistant: Assistant; topic: Topic }) {
const messages: Message[] = []
const defaultModel = getDefaultModel()
for (const msg of assistant?.messages || []) {
const message: Message = {
id: uuid(),
assistantId: assistant.id,
role: msg.role,
content: msg.content,
topicId: topic.id,
createdAt: new Date().toISOString(),
status: 'success',
modelId: assistant.defaultModel?.id || defaultModel.id,
type: 'text',
isPreset: true
}
message.usage = await estimateMessageUsage(message)
messages.push(message)
}
db.topics.put({ id: topic.id, messages }, topic.id)
return messages
}
export async function createAssistantFromAgent(agent: Agent) {
const assistantId = uuid()
const topic = getDefaultTopic(assistantId)
const assistant: Assistant = {
...agent,
id: assistantId,
name: agent.emoji ? agent.emoji + ' ' + agent.name : agent.name,
topics: [topic],
model: agent.defaultModel,
type: 'assistant'
}
store.dispatch(addAssistant(assistant))
await addAssistantMessagesToTopic({ assistant, topic })
window.message.success({
content: i18n.t('message.assistant.added.content'),
key: 'assistant-added'
})
return assistant
}

View File

@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Agent } from '@renderer/types'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { Agent, AssistantSettings } from '@renderer/types'
export interface AgentsState {
agents: Agent[]
@ -9,25 +10,49 @@ const initialState: AgentsState = {
agents: []
}
const runtimeSlice = createSlice({
const assistantsSlice = createSlice({
name: 'agents',
initialState,
reducers: {
updateAgents: (state, action: PayloadAction<Agent[]>) => {
state.agents = action.payload
},
addAgent: (state, action: PayloadAction<Agent>) => {
state.agents.push(action.payload)
},
removeAgent: (state, action: PayloadAction<Agent>) => {
state.agents = state.agents.filter((a) => a.id !== action.payload.id)
removeAgent: (state, action: PayloadAction<{ id: string }>) => {
state.agents = state.agents.filter((c) => c.id !== action.payload.id)
},
updateAgent: (state, action: PayloadAction<Agent>) => {
state.agents = state.agents.map((a) => (a.id === action.payload.id ? action.payload : a))
state.agents = state.agents.map((c) => (c.id === action.payload.id ? action.payload : c))
},
updateAgents: (state, action: PayloadAction<Agent[]>) => {
state.agents = action.payload
updateAgentSettings: (
state,
action: PayloadAction<{ assistantId: string; settings: Partial<AssistantSettings> }>
) => {
for (const agent of state.agents) {
const settings = action.payload.settings
if (agent.id === action.payload.assistantId) {
for (const key in settings) {
if (!agent.settings) {
agent.settings = {
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: 0,
streamOutput: true,
hideMessages: false,
autoResetModel: false
}
}
agent.settings[key] = settings[key]
}
}
}
}
}
})
export const { addAgent, removeAgent, updateAgent, updateAgents } = runtimeSlice.actions
export const { updateAgents, addAgent, removeAgent, updateAgent, updateAgentSettings } = assistantsSlice.actions
export default runtimeSlice.reducer
export default assistantsSlice.reducer

View File

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { TopicManager } from '@renderer/hooks/useTopic'
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
@ -33,15 +34,29 @@ const assistantsSlice = createSlice({
updateAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
},
updateAssistantSettings: (state, action: PayloadAction<{ assistantId: string; settings: AssistantSettings }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
settings: action.payload.settings
updateAssistantSettings: (
state,
action: PayloadAction<{ assistantId: string; settings: Partial<AssistantSettings> }>
) => {
for (const assistant of state.assistants) {
const settings = action.payload.settings
if (assistant.id === action.payload.assistantId) {
for (const key in settings) {
if (!assistant.settings) {
assistant.settings = {
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: 0,
streamOutput: true,
hideMessages: false,
autoResetModel: false
}
}
: assistant
)
assistant.settings[key] = settings[key]
}
}
}
},
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
const topic = action.payload.topic

View File

@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 32,
version: 33,
blacklist: ['runtime'],
migrate
},

View File

@ -1,6 +1,7 @@
import { SYSTEM_MODELS } from '@renderer/config/models'
import i18n from '@renderer/i18n'
import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist'
@ -568,6 +569,31 @@ const migrateConfig = {
]
}
}
},
'33': (state: RootState) => {
state.assistants.defaultAssistant.type = 'assistant'
state.agents.agents.forEach((agent) => {
agent.type = 'agent'
// @ts-ignore eslint-disable-next-line
delete agent.group
})
return {
...state,
assistants: {
...state.assistants,
assistants: [...state.assistants.assistants].map((assistant) => {
// @ts-ignore eslint-disable-next-line
delete assistant.group
return {
...assistant,
id: assistant.id.length === 36 ? assistant.id : uuid(),
type: assistant.type === 'system' ? assistant.type : 'assistant'
}
})
}
}
}
}

View File

@ -17,6 +17,7 @@ export interface SettingsState {
windowStyle: 'transparent' | 'opaque'
fontSize: number
topicPosition: 'left' | 'right'
showTopicTime: boolean
pasteLongTextAsFile: boolean
clickAssistantToShowTopic: boolean
manualUpdateCheck: boolean
@ -43,7 +44,8 @@ const initialState: SettingsState = {
windowStyle: 'transparent',
fontSize: 14,
topicPosition: 'right',
pasteLongTextAsFile: true,
showTopicTime: false,
pasteLongTextAsFile: false,
clickAssistantToShowTopic: false,
manualUpdateCheck: false,
renderInputMessageAsMarkdown: true,
@ -104,6 +106,9 @@ const settingsSlice = createSlice({
setTopicPosition: (state, action: PayloadAction<'left' | 'right'>) => {
state.topicPosition = action.payload
},
setShowTopicTime: (state, action: PayloadAction<boolean>) => {
state.showTopicTime = action.payload
},
setPasteLongTextAsFile: (state, action: PayloadAction<boolean>) => {
state.pasteLongTextAsFile = action.payload
},
@ -150,6 +155,7 @@ export const {
setFontSize,
setWindowStyle,
setTopicPosition,
setShowTopicTime,
setPasteLongTextAsFile,
setRenderInputMessageAsMarkdown,
setClickAssistantToShowTopic,

View File

@ -5,10 +5,18 @@ export type Assistant = {
name: string
prompt: string
topics: Topic[]
type: string
emoji?: string
description?: string
model?: Model
defaultModel?: Model
settings?: AssistantSettings
messages?: AssistantMessage[]
}
export type AssistantMessage = {
role: 'user' | 'assistant'
content: string
}
export type AssistantSettings = {
@ -17,8 +25,12 @@ export type AssistantSettings = {
maxTokens: number | undefined
enableMaxTokens: boolean
streamOutput: boolean
hideMessages: boolean
autoResetModel: boolean
}
export type Agent = Omit<Assistant, 'model'>
export type Message = {
id: string
assistantId: string
@ -32,6 +44,7 @@ export type Message = {
images?: string[]
usage?: OpenAI.Completions.CompletionUsage
type?: 'text' | '@' | 'clear'
isPreset?: boolean
}
export type Topic = {
@ -70,15 +83,6 @@ export type Model = {
description?: string
}
export type Agent = {
id: string
name: string
emoji: string
description?: string
prompt: string
group: string
}
export type Suggestion = {
content: string
}

View File

@ -401,6 +401,12 @@ __metadata:
languageName: node
linkType: hard
"@cherry-studio/artifacts@workspace:npm/artifacts":
version: 0.0.0-use.local
resolution: "@cherry-studio/artifacts@workspace:npm/artifacts"
languageName: unknown
linkType: soft
"@ctrl/tinycolor@npm:^3.6.1":
version: 3.6.1
resolution: "@ctrl/tinycolor@npm:3.6.1"
@ -11506,11 +11512,11 @@ __metadata:
"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.6.2#optional!builtin<compat/typescript>":
version: 5.6.2
resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin<compat/typescript>::version=5.6.2&hash=379a07"
resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin<compat/typescript>::version=5.6.2&hash=8c6c40"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/e6c1662e4852e22fe4bbdca471dca3e3edc74f6f1df043135c44a18a7902037023ccb0abdfb754595ca9028df8920f2f8492c00fc3cbb4309079aae8b7de71cd
checksum: 10c0/94eb47e130d3edd964b76da85975601dcb3604b0c848a36f63ac448d0104e93819d94c8bdf6b07c00120f2ce9c05256b8b6092d23cf5cf1c6fa911159e4d572f
languageName: node
linkType: hard