diff --git a/package.json b/package.json index dad0d1df..5a587237 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "electron-devtools-installer": "^3.2.0", "electron-vite": "^2.0.0", "emittery": "^1.0.3", + "emoji-picker-element": "^1.22.1", "eslint": "^8.56.0", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 28348bc5..81a582f0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -5,7 +5,7 @@ import { PersistGate } from 'redux-persist/integration/react' import Sidebar from './components/app/Sidebar' import TopViewContainer from './components/TopView' -import AppsPage from './pages/apps/AppsPage' +import AgentsPage from './pages/agents/AgentsPage' import HomePage from './pages/home/HomePage' import SettingsPage from './pages/settings/SettingsPage' import TranslatePage from './pages/translate/TranslatePage' @@ -23,7 +23,7 @@ function App(): JSX.Element { } /> - } /> + } /> } /> } /> diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index 0972e0d2..e763c3aa 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -230,3 +230,7 @@ } } } + +emoji-picker { + --border-size: 0; +} diff --git a/src/renderer/src/components/DragableList/index.tsx b/src/renderer/src/components/DragableList/index.tsx new file mode 100644 index 00000000..2b5d81e9 --- /dev/null +++ b/src/renderer/src/components/DragableList/index.tsx @@ -0,0 +1,49 @@ +import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd' +import { droppableReorder } from '@renderer/utils' +import { FC } from 'react' + +interface Props { + list: T[] + children: (item: T, index: number) => React.ReactNode + onUpdate: (list: T[]) => void + onDragStart?: () => void + onDragEnd?: () => void +} + +const DragableList: FC> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => { + const _onDragEnd = (result: DropResult) => { + onDragEnd?.() + if (result.destination) { + const sourceIndex = result.source.index + const destIndex = result.destination.index + const reorderAgents = droppableReorder(list, sourceIndex, destIndex) + onUpdate(reorderAgents) + } + } + + return ( + + + {(provided) => ( + + {list.map((item, index) => ( + + {(provided) => ( + + {children(item, index)} + + )} + + ))} + + )} + + + ) +} + +export default DragableList diff --git a/src/renderer/src/components/EmojiPicker/index.tsx b/src/renderer/src/components/EmojiPicker/index.tsx new file mode 100644 index 00000000..1668b884 --- /dev/null +++ b/src/renderer/src/components/EmojiPicker/index.tsx @@ -0,0 +1,25 @@ +import { useTheme } from '@renderer/providers/ThemeProvider' +import { FC, useEffect, useRef } from 'react' + +interface Props { + onEmojiClick: (emoji: string) => void +} + +const EmojiPicker: FC = ({ onEmojiClick }) => { + const { theme } = useTheme() + const ref = useRef(null) + + useEffect(() => { + if (ref.current) { + ref.current.addEventListener('emoji-click', (event: any) => { + event.stopPropagation() + onEmojiClick(event.detail.emoji.unicode) + }) + } + }, [onEmojiClick]) + + // @ts-ignore next-line + return +} + +export default EmojiPicker diff --git a/src/renderer/src/components/Popups/AssistantSettingPopup.tsx b/src/renderer/src/components/Popups/AssistantSettingPopup.tsx index f6612f83..27d48eae 100644 --- a/src/renderer/src/components/Popups/AssistantSettingPopup.tsx +++ b/src/renderer/src/components/Popups/AssistantSettingPopup.tsx @@ -57,18 +57,19 @@ const AssistantSettingPopupContainer: React.FC = ({ assistant, resolve }) export default class AssistantSettingPopup { static topviewId = 0 static hide() { - TopView.hide(this.topviewId) + TopView.hide('AssistantSettingPopup') } static show(props: AssistantSettingPopupShowParams) { return new Promise((resolve) => { - this.topviewId = TopView.show( + TopView.show( { resolve(v) this.hide() }} - /> + />, + 'AssistantSettingPopup' ) }) } diff --git a/src/renderer/src/components/Popups/PromptPopup.tsx b/src/renderer/src/components/Popups/PromptPopup.tsx index 3fc15629..dc630553 100644 --- a/src/renderer/src/components/Popups/PromptPopup.tsx +++ b/src/renderer/src/components/Popups/PromptPopup.tsx @@ -58,18 +58,19 @@ const PromptPopupContainer: React.FC = ({ export default class PromptPopup { static topviewId = 0 static hide() { - TopView.hide(this.topviewId) + TopView.hide('PromptPopup') } static show(props: PromptPopupShowParams) { return new Promise((resolve) => { - this.topviewId = TopView.show( + TopView.show( { resolve(v) this.hide() }} - /> + />, + 'PromptPopup' ) }) } diff --git a/src/renderer/src/components/Popups/TemplatePopup.tsx b/src/renderer/src/components/Popups/TemplatePopup.tsx index 1befc411..9b961648 100644 --- a/src/renderer/src/components/Popups/TemplatePopup.tsx +++ b/src/renderer/src/components/Popups/TemplatePopup.tsx @@ -37,18 +37,19 @@ const PopupContainer: React.FC = ({ title, resolve }) => { export default class TemplatePopup { static topviewId = 0 static hide() { - TopView.hide(this.topviewId) + TopView.hide('TemplatePopup') } static show(props: ShowParams) { return new Promise((resolve) => { - this.topviewId = TopView.show( + TopView.show( { resolve(v) this.hide() }} - /> + />, + 'TemplatePopup' ) }) } diff --git a/src/renderer/src/components/TopView/index.tsx b/src/renderer/src/components/TopView/index.tsx index 33bdae0d..ec5a1381 100644 --- a/src/renderer/src/components/TopView/index.tsx +++ b/src/renderer/src/components/TopView/index.tsx @@ -1,87 +1,101 @@ -import { useAppInit } from '@renderer/hooks/useAppInit' import { message, Modal } from 'antd' -import { findIndex, pullAt } from 'lodash' -import React, { useEffect, useState } from 'react' +import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' + +import { Box } from '../Layout' -let id = 0 let onPop = () => {} - -let onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => { +let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => { element - key + id } - -let onHide = ({ key }: { key: number }) => { - key +let onHide = (id: string) => { + id } +let onHideAll = () => {} interface Props { children?: React.ReactNode } type ElementItem = { - key: number + id: string element: React.FC | React.ReactNode } const TopViewContainer: React.FC = ({ children }) => { const [elements, setElements] = useState([]) + const elementsRef = useRef([]) + elementsRef.current = elements + const [messageApi, messageContextHolder] = message.useMessage() const [modal, modalContextHolder] = Modal.useModal() - useAppInit() - - onPop = () => { - const views = [...elements] - views.pop() - setElements(views) - } - - onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => { - setElements(elements.concat([{ element, key }])) - } - - onHide = ({ key }: { key: number }) => { - const views = [...elements] - pullAt(views, findIndex(views, { key })) - setElements(views) - } - useEffect(() => { window.message = messageApi window.modal = modal }, [messageApi, modal]) + onPop = () => { + console.debug('[TopView] onPop') + const views = [...elementsRef.current] + views.pop() + elementsRef.current = views + setElements(elementsRef.current) + } + + onShow = ({ element, id }: ElementItem) => { + console.debug('[TopView] onShow', id) + + if (!elementsRef.current.find((el) => el.id === id)) { + elementsRef.current = elementsRef.current.concat([{ element, id }]) + setElements(elementsRef.current) + } + } + + onHide = (id: string) => { + console.debug('[TopView] onHide', id, elementsRef.current) + elementsRef.current = elementsRef.current.filter((el) => el.id !== id) + setElements(elementsRef.current) + } + + onHideAll = () => { + console.debug('[TopView] onHideAll') + setElements([]) + elementsRef.current = [] + } + + const FullScreenContainer: React.FC = useCallback(({ children }) => { + return ( + + + {children} + + ) + }, []) + + console.debug( + '[TopView]', + elements.map((el) => [el.id, el.element]) + ) + return ( <> {children} {messageContextHolder} {modalContextHolder} - {elements.length > 0 && ( - - - {elements.map(({ element: Element, key }) => - typeof Element === 'function' ? ( - - ) : ( - {Element} - ) - )} - - )} + {elements.map(({ element: Element, id }) => ( + + {typeof Element === 'function' ? : Element} + + ))} > ) } export const TopView = { - show: (element: React.FC | React.ReactNode) => { - id = id + 1 - onShow({ element, key: id }) - return id - }, - hide: (key: number) => { - onHide({ key }) - }, + show: (element: React.FC | React.ReactNode, id: string) => onShow({ element, id }), + hide: (id: string) => onHide(id), + clear: () => onHideAll(), pop: onPop } diff --git a/src/renderer/src/config/assistants.json b/src/renderer/src/config/agents.json similarity index 92% rename from src/renderer/src/config/assistants.json rename to src/renderer/src/config/agents.json index 8f14908f..6d2a3e24 100644 --- a/src/renderer/src/config/assistants.json +++ b/src/renderer/src/config/agents.json @@ -1,7 +1,7 @@ [ { "id": 1, - "name": "🎯 产品经理 - Product Manager", + "name": "产品经理 - Product Manager", "emoji": "🎯", "group": "职业", "prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。", @@ -9,15 +9,15 @@ }, { "id": 2, - "name": "🎯 策略产品经理 - Strategy Product Manager", - "emoji": "🎯", + "name": "策略产品经理 - Strategy Product Manager", + "emoji": "🎯 ", "group": "职业", "prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。", "description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。" }, { "id": 3, - "name": "👥 社群运营 - Community Operations", + "name": "社群运营 - Community Operations", "emoji": "👥", "group": "职业", "prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。", @@ -25,7 +25,7 @@ }, { "id": 4, - "name": "✍️ 内容运营 - Content Operations", + "name": "内容运营 - Content Operations", "emoji": "✍️", "group": "职业", "prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。", @@ -33,7 +33,7 @@ }, { "id": 5, - "name": "🛍️ 商家运营 - Merchant Operations", + "name": "商家运营 - Merchant Operations", "emoji": "🛍️", "group": "职业", "prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。", @@ -41,7 +41,7 @@ }, { "id": 6, - "name": "🚀 产品运营 - Product Operations", + "name": "产品运营 - Product Operations", "emoji": "🚀", "group": "职业", "prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n", @@ -49,15 +49,15 @@ }, { "id": 7, - "name": "💼 销售运营 - Sales Operations", - "emoji": "🎓", + "name": "销售运营 - Sales Operations", + "emoji": "💼", "group": "职业", "prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。", "description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。" }, { "id": 8, - "name": "👨💻 用户运营 - User Operations", + "name": "用户运营 - User Operations", "emoji": "👨💻", "group": "职业", "prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n", @@ -65,7 +65,7 @@ }, { "id": 9, - "name": "📢 市场营销 - Marketing", + "name": "市场营销 - Marketing", "emoji": "📢", "group": "职业", "prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。", @@ -73,7 +73,7 @@ }, { "id": 10, - "name": "📈 商业数据分析 - Business Data Analysis", + "name": "商业数据分析 - Business Data Analysis", "emoji": "📈", "group": "职业", "prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。", @@ -81,7 +81,7 @@ }, { "id": 11, - "name": "🗂️ 项目管理 - Project Management", + "name": "项目管理 - Project Management", "emoji": "🗂️", "group": "职业", "prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。", @@ -89,7 +89,7 @@ }, { "id": 12, - "name": "🔎 SEO专家 - SEO Expert", + "name": "SEO专家 - SEO Expert", "emoji": "🔎", "group": "职业", "prompt": "你现在是一名知识丰富的SEO专家,你了解搜索引擎的工作原理,熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。", @@ -97,7 +97,7 @@ }, { "id": 13, - "name": "💻 网站运营数据分析 - Website Operations Data Analysis", + "name": "网站运营数据分析 - Website Operations Data Analysis", "emoji": "💻", "group": "职业", "prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n", @@ -105,7 +105,7 @@ }, { "id": 14, - "name": "📊 数据分析师 - Data Analyst", + "name": "数据分析师 - Data Analyst", "emoji": "📊", "group": "职业", "prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。", @@ -113,7 +113,7 @@ }, { "id": 15, - "name": "🖥️ 前端工程师 - Frontend Engineer", + "name": "前端工程师 - Frontend Engineer", "emoji": "🖥️", "group": "职业", "prompt": "你现在是一名专业的前端工程师,你对HTML、CSS、JavaScript等前端技术有深入的了解,能够制作和优化用户界面。你能够解决浏览器兼容性问题,提升网页性能,并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n", @@ -121,7 +121,7 @@ }, { "id": 16, - "name": "🛠️ 运维工程师 - Operations Engineer", + "name": "运维工程师 - Operations Engineer", "emoji": "🛠️", "group": "职业", "prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。", @@ -129,7 +129,7 @@ }, { "id": 17, - "name": "💻 开发工程师 - Software Engineer", + "name": "开发工程师 - Software Engineer", "emoji": "💻", "group": "职业", "prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。", @@ -137,7 +137,7 @@ }, { "id": 18, - "name": "🧪 测试工程师 - Test Engineer", + "name": "测试工程师 - Test Engineer", "emoji": "🧪", "group": "职业", "prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。", @@ -145,7 +145,7 @@ }, { "id": 19, - "name": "👥 HR人力资源管理 - Human Resources Management", + "name": "HR人力资源管理 - Human Resources Management", "emoji": "👥", "group": "职业", "prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。", @@ -153,7 +153,7 @@ }, { "id": 20, - "name": "📋 行政 - Administration", + "name": "行政 - Administration", "emoji": "📋", "group": "职业", "prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。", @@ -161,7 +161,7 @@ }, { "id": 21, - "name": "💰 财务顾问 - Financial Advisor", + "name": "财务顾问 - Financial Advisor", "emoji": "💰", "group": "职业", "prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。", @@ -169,7 +169,7 @@ }, { "id": 22, - "name": "🩺 医生 - Doctor", + "name": "医生 - Doctor", "emoji": "🩺", "group": "职业", "prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。", @@ -177,7 +177,7 @@ }, { "id": 23, - "name": "✒️ 编辑 - Editor", + "name": "编辑 - Editor", "emoji": "✒️", "group": "职业", "prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n", @@ -185,7 +185,7 @@ }, { "id": 24, - "name": "🧠 哲学家 - Philosopher", + "name": "哲学家 - Philosopher", "emoji": "🧠", "group": "职业", "prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n", @@ -193,7 +193,7 @@ }, { "id": 25, - "name": "🛒 采购 - Procurement", + "name": "采购 - Procurement", "emoji": "🛒", "group": "职业", "prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n", @@ -201,7 +201,7 @@ }, { "id": 26, - "name": "⚖️ 法务 - Legal Affairs", + "name": "法务 - Legal Affairs", "emoji": "⚖️", "group": "职业", "prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。", @@ -209,7 +209,7 @@ }, { "id": 27, - "name": "🇨🇳 翻译成中文 - Chinese", + "name": "翻译成中文 - Chinese", "emoji": "🇨🇳", "group": "语言", "prompt": "你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。", @@ -217,7 +217,7 @@ }, { "id": 28, - "name": "🌐 翻译成英文 - English", + "name": "翻译成英文 - English", "emoji": "🌐", "group": "语言", "prompt": "你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。", @@ -225,7 +225,7 @@ }, { "id": 29, - "name": "📕 英语单词背诵助手", + "name": "英语单词背诵助手", "emoji": "📕", "group": "语言", "prompt": "- 版本:0.1\n- 语言:中文\n- 描述:您是一位语言专家,擅长阐释英语词汇的复杂性。您的角色是将复杂的英语单词分解为简单的概念,提供易懂的英语解释,提供中文翻译,并提供助记设备以帮助记忆。\n\n技能\n1. 分析高级英语单词的拼写、发音和含义。\n2. 使用简单的英语词汇进行解释,然后提供中文翻译。\n3. 使用音标联想、形象联想和词源等记忆技巧。\n4. 创作高质量的句子,以示范单词在语境中的使用。\n\n规则\n1. 总是以使用简单的英语词汇进行解释为开头。\n2. 在适当的时候,保持解释和例句的清晰、准确和幽默。\n3. 确保助记设备与记忆相关且有效。\n\n工作流程\n1. 问候用户并询问他们感兴趣的英语单词。\n2. 分解单词,分析其拼写、发音和复杂含义。\n3. 用简单的英语词汇解释,使含义更易理解。\n4. 提供单词的中文翻译和简单的英语解释。\n5. 针对单词的特点提供个性化的助记策略。\n6. 使用单词构建高质量、信息丰富且引人入胜的句子。\n\n初始化\n作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。", @@ -233,7 +233,7 @@ }, { "id": 30, - "name": "📖 文章总结 - Summarize", + "name": "文章总结 - Summarize", "emoji": "📖", "group": "阅读", "prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复", diff --git a/src/renderer/src/hooks/useAgents.ts b/src/renderer/src/hooks/useAgents.ts new file mode 100644 index 00000000..2e91c846 --- /dev/null +++ b/src/renderer/src/hooks/useAgents.ts @@ -0,0 +1,17 @@ +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' + +export function useAgents() { + const agents = useSelector((state: RootState) => state.agents.agents) + const dispatch = useDispatch() + + return { + 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)) + } +} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 197a4e84..87431b38 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -25,7 +25,8 @@ const resources = { provider: 'Provider', you: 'You', save: 'Save', - footnotes: 'References' + footnotes: 'References', + select: 'Select' }, button: { add: 'Add', @@ -48,9 +49,7 @@ const resources = { 'switch.disabled': 'Switching is disabled while the assistant is generating' }, chat: { - save: 'Save' - }, - assistant: { + save: 'Save', 'default.name': '😀 Default Assistant', 'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away", 'default.topic.name': 'Default Topic', @@ -83,8 +82,18 @@ const resources = { 'settings.max': 'Max', 'suggestions.title': 'Suggested Questions' }, - apps: { - title: 'Agents' + agents: { + title: 'Agents', + my_agents: 'My Agents', + 'add.title': 'Add Agent', + 'edit.title': 'Edit Agent', + 'add.name': 'Name', + 'add.name.placeholder': 'Enter name', + 'add.prompt': 'Prompt', + 'add.prompt.placeholder': 'Enter prompt', + 'add.button': 'Add', + 'manage.title': 'Manage Agents', + 'delete.popup.content': 'Are you sure you want to delete this agent?' }, provider: { openai: 'OpenAI', @@ -212,7 +221,8 @@ const resources = { regenerate: '重新生成', provider: '提供商', you: '用户', - footnote: '引用内容' + footnote: '引用内容', + select: '选择' }, button: { add: '添加', @@ -235,9 +245,7 @@ const resources = { 'switch.disabled': '模型回复完成后才能切换' }, chat: { - save: '保存' - }, - assistant: { + save: '保存', 'default.name': '😃 默认助手 - Assistant', 'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。', 'default.topic.name': '默认话题', @@ -271,8 +279,18 @@ const resources = { 'settings.max': '不限', 'suggestions.title': '建议的问题' }, - apps: { - title: '智能体' + agents: { + title: '智能体', + my_agents: '我的智能体', + 'add.title': '添加智能体', + 'edit.title': '编辑智能体', + 'add.name': '名称', + 'add.name.placeholder': '输入名称', + 'add.prompt': '提示词', + 'add.prompt.placeholder': '输入提示词', + 'add.button': '添加', + 'manage.title': '管理智能体', + 'delete.popup.content': '确定要删除此智能体吗?' }, provider: { openai: 'OpenAI', diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx new file mode 100644 index 00000000..67333914 --- /dev/null +++ b/src/renderer/src/pages/agents/AgentsPage.tsx @@ -0,0 +1,128 @@ +import { UnorderedListOutlined } from '@ant-design/icons' +import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' +import { HStack } from '@renderer/components/Layout' +import Agents from '@renderer/config/agents.json' +import { useAgents } from '@renderer/hooks/useAgents' +import { useAssistants } from '@renderer/hooks/useAssistant' +import { getDefaultAssistant } from '@renderer/services/assistant' +import { Agent } from '@renderer/types' +import { Col, Row, Typography } from 'antd' +import { find, groupBy } from 'lodash' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import AgentCard from './components/AgentCard' +import ManageAgentsPopup from './components/ManageAgentsPopup' +import UserAgents from './components/UserAgents' + +const { Title } = Typography + +const AppsPage: FC = () => { + const { assistants, addAssistant } = useAssistants() + const { agents } = useAgents() + const agentGroups = groupBy( + Agents.map((a) => ({ ...a, id: String(a.id) })), + 'group' + ) + const { t } = useTranslation() + + const onAddAgentConfirm = (agent: Agent) => { + const added = find(assistants, { id: agent.id }) + + window.modal.confirm({ + title: agent.emoji + ' ' + agent.name, + content: agent.description || agent.prompt, + icon: null, + closable: true, + maskClosable: true, + okButtonProps: { type: 'primary', disabled: Boolean(added) }, + okText: added ? t('button.added') : t('button.add'), + onOk: () => onAddAgent(agent) + }) + } + + const onAddAgent = (agent: Agent) => { + addAssistant({ + ...getDefaultAssistant(), + ...agent, + name: agent.emoji ? agent.emoji + ' ' + agent.name : agent.name, + id: String(agent.id) + }) + window.message.success({ + content: t('message.assistant.added.content'), + key: 'agent-added', + style: { marginTop: '5vh' } + }) + } + + return ( + + + {t('agents.title')} + + + + + {t('agents.my_agents')} + {agents.length > 0 && } + + + {Object.keys(agentGroups).map((group) => ( + + + {group} + + + {agentGroups[group].map((agent, index) => { + return ( + + onAddAgentConfirm(agent)} agent={agent as any} /> + + ) + })} + + + ))} + + + + + ) +} + +const Container = styled.div` + display: flex; + flex: 1; + flex-direction: column; + height: 100%; +` + +const ContentContainer = styled.div` + display: flex; + flex: 1; + flex-direction: row; + justify-content: center; + height: 100%; + overflow-y: scroll; + background-color: var(--color-background); +` + +const AssistantsContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + height: calc(100vh - var(--navbar-height)); + padding: 20px; + max-width: 1000px; +` + +const ManageIcon = styled(UnorderedListOutlined)` + font-size: 18px; + color: var(--color-icon); + cursor: pointer; + margin-bottom: 0.5em; + margin-left: 0.5em; +` + +export default AppsPage diff --git a/src/renderer/src/pages/agents/components/AddAgentPopup.tsx b/src/renderer/src/pages/agents/components/AddAgentPopup.tsx new file mode 100644 index 00000000..54b1d64a --- /dev/null +++ b/src/renderer/src/pages/agents/components/AddAgentPopup.tsx @@ -0,0 +1,135 @@ +import 'emoji-picker-element' + +import EmojiPicker from '@renderer/components/EmojiPicker' +import { TopView } from '@renderer/components/TopView' +import { useAgents } from '@renderer/hooks/useAgents' +import { Agent } from '@renderer/types' +import { getLeadingEmoji, uuid } from '@renderer/utils' +import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd' +import TextArea from 'antd/es/input/TextArea' +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' + +interface Props { + agent?: Agent + resolve: (data: Agent | null) => void +} + +type FieldType = { + id: string + name: string + prompt: string +} + +const PopupContainer: React.FC = ({ agent, resolve }) => { + const [open, setOpen] = useState(true) + const [form] = Form.useForm() + const { t } = useTranslation() + const { addAgent, updateAgent } = useAgents() + const formRef = useRef(null) + const [emoji, setEmoji] = useState(agent?.emoji) + + const onFinish = (values: FieldType) => { + const _emoji = emoji || getLeadingEmoji(values.name) + + if (values.name.trim() === '' || values.prompt.trim() === '') { + return + } + + if (agent) { + const _agent = { + ...agent, + name: values.name, + emoji: _emoji, + prompt: values.prompt + } + updateAgent(_agent) + resolve(_agent) + setOpen(false) + return + } + + const _agent = { + id: uuid(), + name: values.name, + emoji: _emoji, + prompt: values.prompt, + group: 'user' + } + + addAgent(_agent) + resolve(_agent) + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve(null) + } + + useEffect(() => { + if (agent) { + form.setFieldsValue({ + name: agent.name, + prompt: agent.prompt + }) + } + }, [agent, form]) + + return ( + formRef.current?.submit()} + onCancel={onCancel} + maskClosable={false} + afterClose={onClose} + okText={agent ? t('common.save') : t('agents.add.button')}> + + + } trigger="click" arrow> + {emoji}}>{t('common.select')} + + + + + + + + + + + ) +} + +export default class AddAgentPopup { + static topviewId = 0 + static hide() { + TopView.hide('AddAgentPopup') + } + static show(agent?: Agent) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'AddAgentPopup' + ) + }) + } +} diff --git a/src/renderer/src/pages/agents/components/AgentCard.tsx b/src/renderer/src/pages/agents/components/AgentCard.tsx new file mode 100644 index 00000000..cf262432 --- /dev/null +++ b/src/renderer/src/pages/agents/components/AgentCard.tsx @@ -0,0 +1,81 @@ +import { Agent } from '@renderer/types' +import { Col, Typography } from 'antd' +import styled from 'styled-components' + +interface Props { + agent: Agent + onClick?: () => void +} + +const { Title } = Typography + +const AgentCard: React.FC = ({ agent, onClick }) => { + return ( + + {agent.emoji && {agent.emoji}} + + + + {agent.name} + + + {agent.prompt} + + + ) +} + +const Container = styled.div` + display: flex; + flex-direction: row; + margin-bottom: 16px; + background-color: var(--color-background-soft); + border: 0.5px solid var(--color-border); + border-radius: 10px; + padding: 15px; + position: relative; + cursor: pointer; + transition: all 0.2s ease-in-out; + &:hover { + background-color: var(--color-background-mute); + } +` +const EmojiHeader = styled.div` + width: 25px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-right: 5px; + font-size: 25px; + line-height: 25px; +` + +const AgentHeader = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +` + +const AgentName = styled(Title)` + font-size: 18px; + line-height: 1.2; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + color: var(--color-white); + font-weight: 900; +` + +const AgentCardPrompt = styled.div` + color: #666; + margin-top: 6px; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +` + +export default AgentCard diff --git a/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx b/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx new file mode 100644 index 00000000..87e28838 --- /dev/null +++ b/src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx @@ -0,0 +1,109 @@ +import { DeleteOutlined, EditOutlined, MenuOutlined } from '@ant-design/icons' +import DragableList from '@renderer/components/DragableList' +import { Box, HStack } from '@renderer/components/Layout' +import { TopView } from '@renderer/components/TopView' +import { useAgents } from '@renderer/hooks/useAgents' +import { Empty, Modal, Popconfirm } from 'antd' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import AddAgentPopup from './AddAgentPopup' + +const PopupContainer: React.FC = () => { + const [open, setOpen] = useState(true) + const { t } = useTranslation() + const { agents, removeAgent, updateAgents } = useAgents() + + const onOk = () => { + setOpen(false) + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = async () => { + ManageAgentsPopup.hide() + } + + useEffect(() => { + if (agents.length === 0) { + setOpen(false) + } + }, [agents]) + + return ( + + + {agents.length > 0 && ( + + {(item) => ( + + + {item.emoji} {item.name} + + + removeAgent(item)}> + + + AddAgentPopup.show(item)} /> + + + + )} + + )} + {agents.length === 0 && } + + + ) +} + +const Container = styled.div` + padding: 16px; + height: 50vh; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +` + +const AgentItem = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 8px; + border-radius: 8px; + 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 class ManageAgentsPopup { + static topviewId = 0 + static hide() { + TopView.hide('ManageAgentsPopup') + } + static show() { + TopView.show(, 'ManageAgentsPopup') + } +} diff --git a/src/renderer/src/pages/agents/components/UserAgents.tsx b/src/renderer/src/pages/agents/components/UserAgents.tsx new file mode 100644 index 00000000..880c1345 --- /dev/null +++ b/src/renderer/src/pages/agents/components/UserAgents.tsx @@ -0,0 +1,57 @@ +import { PlusOutlined } from '@ant-design/icons' +import { useAgents } from '@renderer/hooks/useAgents' +import { Agent } from '@renderer/types' +import { Col, Row } from 'antd' +import { FC } from 'react' +import styled from 'styled-components' + +import AddAssistantPopup from './AddAgentPopup' +import AgentCard from './AgentCard' + +interface Props { + onAdd: (agent: Agent) => void +} + +const UserAgents: FC = ({ onAdd }) => { + const { agents } = useAgents() + + const onAddMyAgentClick = () => { + AddAssistantPopup.show() + } + + return ( + + {agents.map((agent) => ( + + onAdd(agent)} /> + + ))} + + + + + + + ) +} + +const AssistantCardContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + border: 1px dashed var(--color-border); + border-radius: 10px; + cursor: pointer; + min-height: 84px; + .anticon { + font-size: 16px; + color: var(--color-icon); + } + &:hover { + background-color: var(--color-background-soft); + } +` + +export default UserAgents diff --git a/src/renderer/src/pages/apps/AppsPage.tsx b/src/renderer/src/pages/apps/AppsPage.tsx deleted file mode 100644 index 6fdff1e9..00000000 --- a/src/renderer/src/pages/apps/AppsPage.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' -import SYSTEM_ASSISTANTS from '@renderer/config/assistants.json' -import { useAssistants } from '@renderer/hooks/useAssistant' -import { getDefaultAssistant } from '@renderer/services/assistant' -import { SystemAssistant } from '@renderer/types' -import { Col, Row, Typography } from 'antd' -import { find, groupBy } from 'lodash' -import { FC } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - -const { Title } = Typography - -const AppsPage: FC = () => { - const { assistants, addAssistant } = useAssistants() - const assistantGroups = groupBy( - SYSTEM_ASSISTANTS.map((a) => ({ ...a, id: String(a.id) })), - 'group' - ) - const { t } = useTranslation() - - const onAddAssistantConfirm = (assistant: SystemAssistant) => { - const added = find(assistants, { id: assistant.id }) - - window.modal.confirm({ - title: assistant.name, - content: assistant.description || assistant.prompt, - icon: null, - closable: true, - maskClosable: true, - okButtonProps: { type: 'primary', disabled: Boolean(added) }, - okText: added ? t('button.added') : t('button.add'), - onOk: () => onAddAssistant(assistant) - }) - } - - const onAddAssistant = (assistant: SystemAssistant) => { - addAssistant({ - ...getDefaultAssistant(), - ...assistant, - id: String(assistant.id) - }) - window.message.success({ - content: t('message.assistant.added.content'), - key: 'assistant-added', - style: { marginTop: '5vh' } - }) - } - - return ( - - - {t('apps.title')} - - - - {Object.keys(assistantGroups).map((group) => ( - - - {group} - - - {assistantGroups[group].map((assistant, index) => { - return ( - - onAddAssistantConfirm(assistant)}> - {assistant.emoji} - - - - {assistant.name.replace(assistant.emoji + ' ', '')} - - - {assistant.prompt} - - - - ) - })} - - - ))} - - - - - ) -} - -const Container = styled.div` - display: flex; - flex: 1; - flex-direction: column; - height: 100%; -` - -const ContentContainer = styled.div` - display: flex; - flex: 1; - flex-direction: row; - justify-content: center; - height: 100%; - overflow-y: scroll; - background-color: var(--color-background); -` - -const AssistantsContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - height: calc(100vh - var(--navbar-height)); - padding: 20px; - max-width: 1000px; -` - -const AssistantCard = styled.div` - display: flex; - flex-direction: row; - margin-bottom: 16px; - background-color: var(--color-background-soft); - border: 0.5px solid var(--color-border); - border-radius: 10px; - padding: 15px; - position: relative; - cursor: pointer; - transition: all 0.2s ease-in-out; - &:hover { - background-color: var(--color-background-mute); - } -` -const EmojiHeader = styled.div` - width: 25px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - margin-right: 5px; - font-size: 25px; - line-height: 25px; -` - -const AssistantHeader = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; -` - -const AssistantName = styled(Title)` - font-size: 18px; - line-height: 1.2; - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - color: var(--color-white); - font-weight: 900; -` - -const AssistantCardPrompt = styled.div` - color: #666; - margin-top: 6px; - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; -` - -export default AppsPage diff --git a/src/renderer/src/pages/home/components/Assistants.tsx b/src/renderer/src/pages/home/components/Assistants.tsx index 26dd031e..3798e967 100644 --- a/src/renderer/src/pages/home/components/Assistants.tsx +++ b/src/renderer/src/pages/home/components/Assistants.tsx @@ -113,9 +113,7 @@ const Assistants: FC = ({ activeAssistant, setActiveAssistant, onCreateAs onSwitchAssistant(assistant)} className={assistant.id === activeAssistant?.id ? 'active' : ''}> - - {assistant.name || t('assistant.default.name')} - + {assistant.name || t('chat.default.name')} diff --git a/src/renderer/src/pages/home/components/Messages.tsx b/src/renderer/src/pages/home/components/Messages.tsx index c06b1d75..0d2df042 100644 --- a/src/renderer/src/pages/home/components/Messages.tsx +++ b/src/renderer/src/pages/home/components/Messages.tsx @@ -31,7 +31,7 @@ const Messages: FC = ({ assistant, topic }) => { () => ({ id: 'assistant', role: 'assistant', - content: assistant.description || assistant.prompt || t('assistant.default.description'), + content: assistant.description || assistant.prompt || t('chat.default.description'), assistantId: assistant.id, topicId: topic.id, status: 'pending', @@ -50,7 +50,7 @@ const Messages: FC = ({ assistant, topic }) => { ) const autoRenameTopic = useCallback(async () => { - if (topic.name === t('assistant.default.topic.name') && messages.length >= 2) { + if (topic.name === t('chat.default.topic.name') && messages.length >= 2) { const summaryText = await fetchMessagesSummary({ messages, assistant }) summaryText && updateTopic({ ...topic, name: summaryText }) } diff --git a/src/renderer/src/pages/home/components/NavigationCenter.tsx b/src/renderer/src/pages/home/components/NavigationCenter.tsx index f638c5fb..21c54527 100644 --- a/src/renderer/src/pages/home/components/NavigationCenter.tsx +++ b/src/renderer/src/pages/home/components/NavigationCenter.tsx @@ -27,7 +27,7 @@ const NavigationCenter: FC = ({ activeAssistant }) => { )} - {removeLeadingEmoji(assistant?.name) || t('assistant.default.name')} + {removeLeadingEmoji(assistant?.name) || t('chat.default.name')} ) diff --git a/src/renderer/src/pages/home/components/input/Inputbar.tsx b/src/renderer/src/pages/home/components/input/Inputbar.tsx index 2ed326bc..b91a6909 100644 --- a/src/renderer/src/pages/home/components/input/Inputbar.tsx +++ b/src/renderer/src/pages/home/components/input/Inputbar.tsx @@ -148,45 +148,45 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { setExpend(!expended)}> - + - + EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}> - + EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}> - + } - okText={t('assistant.input.clear')}> + okText={t('chat.input.clear')}> - + setExpend(!expended)}> {expended ? : } {showInputEstimatedTokens && ( - + {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} - + ↑{`${inputTokenCount} / ${estimateTokenCount}`} @@ -194,7 +194,7 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { {generating && ( - + @@ -207,7 +207,7 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { value={text} onChange={(e) => setText(e.target.value)} onKeyDown={handleKeyDown} - placeholder={t('assistant.input.placeholder')} + placeholder={t('chat.input.placeholder')} autoFocus contextMenu="true" variant="borderless" diff --git a/src/renderer/src/pages/home/components/input/SendMessageButton.tsx b/src/renderer/src/pages/home/components/input/SendMessageButton.tsx index a45352b2..18a2f742 100644 --- a/src/renderer/src/pages/home/components/input/SendMessageButton.tsx +++ b/src/renderer/src/pages/home/components/input/SendMessageButton.tsx @@ -15,13 +15,13 @@ const SendMessageButton: FC = ({ sendMessage }) => { const sendSettingItems: MenuProps['items'] = [ { - label: `Enter ${t('assistant.input.send')}`, + label: `Enter ${t('chat.input.send')}`, key: 'Enter', icon: , onClick: () => setSendMessageShortcut('Enter') }, { - label: `Shift+Enter ${t('assistant.input.send')}`, + label: `Shift+Enter ${t('chat.input.send')}`, key: 'Shift+Enter', icon: , onClick: () => setSendMessageShortcut('Shift+Enter') @@ -36,7 +36,7 @@ const SendMessageButton: FC = ({ sendMessage }) => { arrow menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }} style={{ width: 'auto' }}> - {t('assistant.input.send')} + {t('chat.input.send')} ) diff --git a/src/renderer/src/pages/home/components/sidebar/SettingsTab.tsx b/src/renderer/src/pages/home/components/sidebar/SettingsTab.tsx index 2666a111..59d37dda 100644 --- a/src/renderer/src/pages/home/components/sidebar/SettingsTab.tsx +++ b/src/renderer/src/pages/home/components/sidebar/SettingsTab.tsx @@ -80,14 +80,14 @@ const SettingsTab: FC = (props) => { {t('settings.messages.model.title')}{' '} - + - {t('assistant.settings.temperature')} - + {t('chat.settings.temperature')} + @@ -114,8 +114,8 @@ const SettingsTab: FC = (props) => { - {t('assistant.settings.conext_count')} - + {t('chat.settings.conext_count')} + @@ -124,7 +124,7 @@ const SettingsTab: FC = (props) => { = ({ assistant: _assistant, activeTopic, setActiveTop (topic: Topic) => { const menus: MenuProps['items'] = [ { - label: t('assistant.topics.auto_rename'), + label: t('chat.topics.auto_rename'), key: 'auto-rename', icon: , async onClick() { @@ -41,12 +41,12 @@ const TopicsTab: FC = ({ assistant: _assistant, activeTopic, setActiveTop } }, { - label: t('assistant.topics.edit.title'), + label: t('chat.topics.edit.title'), key: 'rename', icon: , async onClick() { const name = await PromptPopup.show({ - title: t('assistant.topics.edit.title'), + title: t('chat.topics.edit.title'), message: '', defaultValue: topic?.name || '' }) diff --git a/src/renderer/src/pages/settings/AssistantSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings.tsx index b05fb77e..fabe0237 100644 --- a/src/renderer/src/pages/settings/AssistantSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings.tsx @@ -82,8 +82,8 @@ const AssistantSettings: FC = () => { {t('settings.assistant.model_params')} - {t('assistant.settings.temperature')} - + {t('chat.settings.temperature')} + @@ -110,8 +110,8 @@ const AssistantSettings: FC = () => { - {t('assistant.settings.conext_count')} - + {t('chat.settings.conext_count')} + @@ -120,7 +120,7 @@ const AssistantSettings: FC = () => { { - {t('assistant.settings.reset')} + {t('chat.settings.reset')} ) diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index 7500f9bc..837fd6fe 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -1,5 +1,6 @@ import useAvatar from '@renderer/hooks/useAvatar' import { useSettings } from '@renderer/hooks/useSettings' +import i18n from '@renderer/i18n' import LocalStorage from '@renderer/services/storage' import { useAppDispatch } from '@renderer/store' import { setAvatar } from '@renderer/store/runtime' @@ -23,6 +24,7 @@ const GeneralSettings: FC = () => { const onSelectLanguage = (value: string) => { dispatch(setLanguage(value)) localStorage.setItem('language', value) + i18n.changeLanguage(value) } const onSetProxyUrl = () => { diff --git a/src/renderer/src/pages/settings/components/AddModelPopup.tsx b/src/renderer/src/pages/settings/components/AddModelPopup.tsx index aa1039dc..2f91bc4e 100644 --- a/src/renderer/src/pages/settings/components/AddModelPopup.tsx +++ b/src/renderer/src/pages/settings/components/AddModelPopup.tsx @@ -115,18 +115,19 @@ const PopupContainer: React.FC = ({ title, provider, resolve }) => { export default class AddModelPopup { static topviewId = 0 static hide() { - TopView.hide(this.topviewId) + TopView.hide('AddModelPopup') } static show(props: ShowParams) { return new Promise((resolve) => { - this.topviewId = TopView.show( + TopView.show( { resolve(v) this.hide() }} - /> + />, + 'AddModelPopup' ) }) } diff --git a/src/renderer/src/pages/settings/components/AddProviderPopup.tsx b/src/renderer/src/pages/settings/components/AddProviderPopup.tsx index 5356122b..77d7490f 100644 --- a/src/renderer/src/pages/settings/components/AddProviderPopup.tsx +++ b/src/renderer/src/pages/settings/components/AddProviderPopup.tsx @@ -54,18 +54,19 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { export default class AddProviderPopup { static topviewId = 0 static hide() { - TopView.hide(this.topviewId) + TopView.hide('AddProviderPopup') } static show(provider?: Provider) { return new Promise((resolve) => { - this.topviewId = TopView.show( + TopView.show( { resolve(v) this.hide() }} - /> + />, + 'AddProviderPopup' ) }) } diff --git a/src/renderer/src/pages/settings/components/EditModelsPopup.tsx b/src/renderer/src/pages/settings/components/EditModelsPopup.tsx index 47dac56b..2e79fd98 100644 --- a/src/renderer/src/pages/settings/components/EditModelsPopup.tsx +++ b/src/renderer/src/pages/settings/components/EditModelsPopup.tsx @@ -224,18 +224,19 @@ const Question = styled(QuestionCircleOutlined)` export default class EditModelsPopup { static topviewId = 0 static hide() { - TopView.hide(this.topviewId) + TopView.hide('EditModelsPopup') } static show(props: ShowParams) { return new Promise((resolve) => { - this.topviewId = TopView.show( + TopView.show( { resolve(v) this.hide() }} - /> + />, + 'EditModelsPopup' ) }) } diff --git a/src/renderer/src/services/assistant.ts b/src/renderer/src/services/assistant.ts index c106f014..afb814e3 100644 --- a/src/renderer/src/services/assistant.ts +++ b/src/renderer/src/services/assistant.ts @@ -6,7 +6,7 @@ import { uuid } from '@renderer/utils' export function getDefaultAssistant(): Assistant { return { id: 'default', - name: i18n.t('assistant.default.name'), + name: i18n.t('chat.default.name'), prompt: '', topics: [getDefaultTopic()] } @@ -15,7 +15,7 @@ export function getDefaultAssistant(): Assistant { export function getDefaultTopic(): Topic { return { id: uuid(), - name: i18n.t('assistant.default.topic.name'), + name: i18n.t('chat.default.topic.name'), messages: [] } } diff --git a/src/renderer/src/store/agents.ts b/src/renderer/src/store/agents.ts new file mode 100644 index 00000000..d68fa2c1 --- /dev/null +++ b/src/renderer/src/store/agents.ts @@ -0,0 +1,33 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { Agent } from '@renderer/types' + +export interface AgentsState { + agents: Agent[] +} + +const initialState: AgentsState = { + agents: [] +} + +const runtimeSlice = createSlice({ + name: 'agents', + initialState, + reducers: { + addAgent: (state, action: PayloadAction) => { + state.agents.push(action.payload) + }, + removeAgent: (state, action: PayloadAction) => { + state.agents = state.agents.filter((a) => a.id !== action.payload.id) + }, + updateAgent: (state, action: PayloadAction) => { + state.agents = state.agents.map((a) => (a.id === action.payload.id ? action.payload : a)) + }, + updateAgents: (state, action: PayloadAction) => { + state.agents = action.payload + } + } +}) + +export const { addAgent, removeAgent, updateAgent, updateAgents } = runtimeSlice.actions + +export default runtimeSlice.reducer diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 7e5bbb5a..9fd27699 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -3,6 +3,7 @@ import { useDispatch, useSelector, useStore } from 'react-redux' import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist' import storage from 'redux-persist/lib/storage' +import agents from './agents' import assistants from './assistants' import llm from './llm' import migrate from './migrate' @@ -13,6 +14,7 @@ const rootReducer = combineReducers({ assistants, settings, llm, + agents, runtime }) @@ -20,7 +22,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 17, + version: 18, blacklist: ['runtime'], migrate }, diff --git a/src/renderer/src/store/llm.ts b/src/renderer/src/store/llm.ts index 566a978a..fdfd1eff 100644 --- a/src/renderer/src/store/llm.ts +++ b/src/renderer/src/store/llm.ts @@ -136,7 +136,7 @@ const initialState: LlmState = { } const settingsSlice = createSlice({ - name: 'settings', + name: 'llm', initialState, reducers: { updateProvider: (state, action: PayloadAction) => { diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index d35cdf6a..9ecf60e1 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -271,6 +271,14 @@ const migrateConfig = { theme: 'auto' } } + }, + '18': (state: RootState) => { + return { + ...state, + agents: { + agents: [] + } + } } } diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index 572c805e..cc01aadb 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -12,7 +12,7 @@ const initialState: RuntimeState = { } const runtimeSlice = createSlice({ - name: 'settings', + name: 'runtime', initialState, reducers: { setAvatar: (state, action: PayloadAction) => { diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index fbfe4852..3298591d 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -3,9 +3,10 @@ import OpenAI from 'openai' export type Assistant = { id: string name: string - description?: string prompt: string topics: Topic[] + emoji?: string + description?: string model?: Model settings?: AssistantSettings } @@ -60,7 +61,7 @@ export type Model = { description?: string } -export type SystemAssistant = { +export type Agent = { id: string name: string emoji: string diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index ff81cb4e..986654b3 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -101,6 +101,12 @@ export function removeLeadingEmoji(str: string): string { return str.replace(emojiRegex, '') } +export function getLeadingEmoji(str: string): string { + const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u + const match = str.match(emojiRegex) + return match ? match[0] : '' +} + export function isFreeModel(model: Model) { return (model.id + model.name).toLocaleLowerCase().includes('free') } diff --git a/yarn.lock b/yarn.lock index 57b3de42..49b8f8c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3469,6 +3469,7 @@ __metadata: electron-vite: "npm:^2.0.0" electron-window-state: "npm:^5.0.3" emittery: "npm:^1.0.3" + emoji-picker-element: "npm:^1.22.1" eslint: "npm:^8.56.0" eslint-plugin-react: "npm:^7.34.3" eslint-plugin-react-hooks: "npm:^4.6.2" @@ -4251,6 +4252,13 @@ __metadata: languageName: node linkType: hard +"emoji-picker-element@npm:^1.22.1": + version: 1.22.1 + resolution: "emoji-picker-element@npm:1.22.1" + checksum: 10c0/3fbd6b5498796b4d46cc641a0276e934c683f2c0a63a00aef0082e7b2acc6711b4acab49c0cf38aaa12bf3bcd126aad1355afbf4580917f7aad5f2fb2ffa0d9c + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0"