From c4f5ff9a29f2d368184f8287efab06c20e92776a Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Tue, 22 Apr 2025 22:40:48 +0800 Subject: [PATCH] chore: update --- .../ai-employees/AIEmployeesProvider.tsx | 12 +- .../ai-employees/chatbox/AIEmployeeHeader.tsx | 40 +- .../ai-employees/chatbox/ChatBoxContext.tsx | 31 +- .../ai-employees/chatbox/ChatButton.tsx | 49 +- .../client/ai-employees/chatbox/Sender.tsx | 3 +- .../ai-employees/chatbox/SenderPrefix.tsx | 11 +- .../initializer/AIEmployeeButton.tsx | 40 +- .../ai-employees/initializer/AIEmployees.tsx | 2 +- .../ai-employees/manager/AvatarSelect.tsx | 44 +- .../client/ai-employees/manager/Employees.tsx | 662 +++++++++++++----- .../ai-employees/manager/ProfileSettings.tsx | 5 +- .../selector/AISelectorProvider.tsx | 4 +- .../settings/AIEmployeeButton.tsx | 7 +- .../src/client/ai-employees/types.ts | 1 - .../plugin-ai/src/client/schemas/llms.ts | 1 - .../src/server/collections/ai-employees.ts | 12 + .../server/collections/users-ai-employees.ts | 22 + .../plugin-ai/src/server/collections/users.ts | 27 + .../@nocobase/plugin-ai/src/server/plugin.ts | 3 + .../src/server/resource/aiEmployees.ts | 39 ++ 20 files changed, 719 insertions(+), 296 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-ai/src/server/collections/users-ai-employees.ts create mode 100644 packages/plugins/@nocobase/plugin-ai/src/server/collections/users.ts create mode 100644 packages/plugins/@nocobase/plugin-ai/src/server/resource/aiEmployees.ts diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx index f3e1694251..129c635cf7 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { createContext } from 'react'; import { ChatBoxProvider } from './chatbox/ChatBoxProvider'; import { useAPIClient, useRequest } from '@nocobase/client'; @@ -46,12 +46,18 @@ export const useAIEmployeesContext = () => { () => api .resource('aiEmployees') - .list() + .listByUser() .then((res) => res?.data?.data), { ready: !aiEmployees, onSuccess: (aiEmployees) => setAIEmployees(aiEmployees), }, ); - return { aiEmployees, service }; + const aiEmployeesMap = useMemo(() => { + return (aiEmployees || []).reduce((acc, aiEmployee) => { + acc[aiEmployee.username] = aiEmployee; + return acc; + }, {}); + }, [aiEmployees]); + return { aiEmployees, aiEmployeesMap, service }; }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeHeader.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeHeader.tsx index 4b00a83a2e..cdaae80c09 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeHeader.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/AIEmployeeHeader.tsx @@ -7,9 +7,8 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { useCallback } from 'react'; -import { List, Popover, Button, Avatar, Divider } from 'antd'; -import { useToken } from '@nocobase/client'; +import React from 'react'; +import { List, Popover, Button, Avatar } from 'antd'; import { useAIEmployeesContext } from '../AIEmployeesProvider'; import { useT } from '../../locale'; import { useChatBoxContext } from './ChatBoxContext'; @@ -17,10 +16,6 @@ import { avatars } from '../avatars'; import { css } from '@emotion/css'; import { Sender } from '@ant-design/x'; import { ProfileCard } from '../ProfileCard'; -import { AIEmployee } from '../types'; -import { uid } from '@formily/shared'; -import { useChatMessages } from './ChatMessagesProvider'; -import { useChatConversations } from './ChatConversationsProvider'; export const AIEmployeeHeader: React.FC = () => { const { @@ -28,36 +23,7 @@ export const AIEmployeeHeader: React.FC = () => { aiEmployees, } = useAIEmployeesContext(); const t = useT(); - const { setMessages, addMessage } = useChatMessages(); - const { currentConversation } = useChatConversations(); - const setCurrentEmployee = useChatBoxContext('setCurrentEmployee'); - const setSenderPlaceholder = useChatBoxContext('setSenderPlaceholder'); - const setSenderValue = useChatBoxContext('setSenderValue'); - const senderRef = useChatBoxContext('senderRef'); - const infoForm = useChatBoxContext('infoForm'); - const switchAIEmployee = useCallback( - (aiEmployee: AIEmployee) => { - const greetingMsg = { - key: uid(), - role: aiEmployee.username, - content: { - type: 'greeting' as const, - content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }), - }, - }; - setCurrentEmployee(aiEmployee); - setSenderPlaceholder(aiEmployee.chatSettings?.senderPlaceholder); - infoForm.reset(); - senderRef.current?.focus(); - if (!currentConversation) { - setMessages([greetingMsg]); - } else { - addMessage(greetingMsg); - setSenderValue(''); - } - }, - [currentConversation, infoForm], - ); + const switchAIEmployee = useChatBoxContext('switchAIEmployee'); return ( diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxContext.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxContext.tsx index 15b53f2d65..4eb8dbe1c7 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxContext.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxContext.tsx @@ -42,6 +42,7 @@ type ChatBoxContextValues = { setSenderPlaceholder: React.Dispatch>; startNewConversation: () => void; triggerShortcut: (options: ShortcutOptions) => void; + switchAIEmployee: (aiEmployee: AIEmployee) => void; send(opts: SendOptions): void; }; @@ -113,7 +114,7 @@ export const useSetChatBoxContext = () => { const [expanded, setExpanded] = useState(false); const [showConversations, setShowConversations] = useState(false); const [currentEmployee, setCurrentEmployee] = useState(null); - const { setMessages, sendMessages } = useChatMessages(); + const { setMessages, sendMessages, addMessage } = useChatMessages(); const { currentConversation, setCurrentConversation, conversationsService } = useChatConversations(); const [senderValue, setSenderValue] = useState(''); const [senderPlaceholder, setSenderPlaceholder] = useState(''); @@ -156,6 +157,33 @@ export const useSetChatBoxContext = () => { senderRef.current?.focus(); }, [currentEmployee]); + const switchAIEmployee = useCallback( + (aiEmployee: AIEmployee) => { + setCurrentEmployee(aiEmployee); + setSenderValue(''); + if (aiEmployee) { + const greetingMsg = { + key: uid(), + role: aiEmployee.username, + content: { + type: 'greeting' as const, + content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }), + }, + }; + setSenderPlaceholder(aiEmployee.chatSettings?.senderPlaceholder); + senderRef.current?.focus(); + if (!currentConversation) { + setMessages([greetingMsg]); + } else { + addMessage(greetingMsg); + } + } else { + setMessages([]); + } + }, + [currentConversation], + ); + const triggerShortcut = useCallback( (options: ShortcutOptions) => { const { aiEmployee, message, autoSend } = options; @@ -238,6 +266,7 @@ export const useSetChatBoxContext = () => { setSenderPlaceholder, startNewConversation, triggerShortcut, + switchAIEmployee, send, }; }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx index aefa944fba..3f46330ab7 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatButton.tsx @@ -7,51 +7,20 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { FloatButton, Avatar, Dropdown } from 'antd'; import icon from '../icon.svg'; import { css } from '@emotion/css'; import { useChatBoxContext } from './ChatBoxContext'; import { useAIEmployeesContext } from '../AIEmployeesProvider'; import { avatars } from '../avatars'; -import { useT } from '../../locale'; -import { useChatMessages } from './ChatMessagesProvider'; -import { useChatConversations } from './ChatConversationsProvider'; -import { AIEmployee } from '../types'; -import { uid } from '@formily/shared'; export const ChatButton: React.FC = () => { - const t = useT(); const { aiEmployees } = useAIEmployeesContext(); + const open = useChatBoxContext('open'); const setOpen = useChatBoxContext('setOpen'); - const setCurrentEmployee = useChatBoxContext('setCurrentEmployee'); - const { setMessages, addMessage } = useChatMessages(); - const { currentConversation } = useChatConversations(); - const setSenderPlaceholder = useChatBoxContext('setSenderPlaceholder'); - const setSenderValue = useChatBoxContext('setSenderValue'); - const senderRef = useChatBoxContext('senderRef'); - const switchAIEmployee = useCallback( - (aiEmployee: AIEmployee) => { - const greetingMsg = { - key: uid(), - role: aiEmployee.username, - content: { - type: 'greeting' as const, - content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }), - }, - }; - setCurrentEmployee(aiEmployee); - setSenderPlaceholder(aiEmployee.chatSettings?.senderPlaceholder); - senderRef.current?.focus(); - if (!currentConversation) { - setMessages([greetingMsg]); - } else { - addMessage(greetingMsg); - setSenderValue(''); - } - }, - [currentConversation], - ); + const switchAIEmployee = useChatBoxContext('switchAIEmployee'); + const items = useMemo(() => { return aiEmployees?.map((employee) => ({ key: employee.username, @@ -89,7 +58,7 @@ export const ChatButton: React.FC = () => { padding: 0; } .ant-float-btn .ant-float-btn-body .ant-float-btn-content .ant-float-btn-icon { - width: 40px; + width: 36px; } `} > @@ -98,15 +67,15 @@ export const ChatButton: React.FC = () => { icon={ } - // onClick={() => { - // setOpen(false); - // }} + onClick={() => { + setOpen(!open); + }} shape="square" /> diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx index 9d06e19075..fcbe3526d3 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Sender.tsx @@ -16,6 +16,7 @@ import { SenderHeader } from './SenderHeader'; import { SenderFooter } from './SenderFooter'; import { useChatConversations } from './ChatConversationsProvider'; import { useChatMessages } from './ChatMessagesProvider'; +import { AIEmployeeHeader } from './AIEmployeeHeader'; export const Sender: React.FC = () => { const t = useT(); @@ -60,7 +61,7 @@ export const Sender: React.FC = () => { } onCancel={cancelRequest} prefix={} - // header={!currentConversation ? : null} + header={!currentEmployee ? : null} loading={responseLoading} footer={({ components }) => } disabled={!currentEmployee} diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/SenderPrefix.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/SenderPrefix.tsx index fd5ce515ba..2569704903 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/SenderPrefix.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/SenderPrefix.tsx @@ -14,5 +14,14 @@ import { Avatar } from 'antd'; export const SenderPrefix: React.FC = () => { const currentEmployee = useChatBoxContext('currentEmployee'); - return currentEmployee ? : null; + const switchAIEmployee = useChatBoxContext('switchAIEmployee'); + return currentEmployee ? ( + switchAIEmployee(null)} + /> + ) : null; }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployeeButton.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployeeButton.tsx index a601db0dad..50d036de28 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployeeButton.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployeeButton.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { Avatar, Popover, Button } from 'antd'; +import { Avatar, Popover, Button, Spin } from 'antd'; import { avatars } from '../avatars'; import { SortableItem, @@ -21,6 +21,7 @@ import { useFieldSchema } from '@formily/react'; import { useChatBoxContext } from '../chatbox/ChatBoxContext'; import { AIEmployee } from '../types'; import { ProfileCard } from '../ProfileCard'; +import { useAIEmployeesContext } from '../AIEmployeesProvider'; async function replaceVariables(template, variables, localVariables = {}) { const regex = /\{\{\s*(.*?)\s*\}\}/g; @@ -58,20 +59,24 @@ async function replaceVariables(template, variables, localVariables = {}) { } export const AIEmployeeButton: React.FC<{ - aiEmployee: AIEmployee; + username: string; taskDesc?: string; autoSend?: boolean; message: { type: string; content: string; }; - infoForm: any; -}> = withDynamicSchemaProps(({ aiEmployee, taskDesc, message, infoForm: infoFormValues, autoSend }) => { +}> = withDynamicSchemaProps(({ username, taskDesc, message, autoSend }) => { const triggerShortcut = useChatBoxContext('triggerShortcut'); const fieldSchema = useFieldSchema(); const { render } = useSchemaToolbarRender(fieldSchema); const variables = useVariables(); const localVariables = useLocalVariables(); + const { + aiEmployeesMap, + service: { loading }, + } = useAIEmployeesContext(); + const aiEmployee = aiEmployeesMap[username]; return ( - }> - - - {render()} + + }> + + + {render()} + ); }); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployees.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployees.tsx index 4964935c63..f8021772cd 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployees.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployees.tsx @@ -41,7 +41,7 @@ const getAIEmployeesInitializer = () => ({ 'x-toolbar': 'ActionSchemaToolbar', 'x-settings': 'aiEmployees:button', 'x-component-props': { - aiEmployee, + username: aiEmployee.username, }, }); }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/AvatarSelect.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/AvatarSelect.tsx index 6770a392b0..06eca7bff5 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/AvatarSelect.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/AvatarSelect.tsx @@ -10,11 +10,11 @@ import React, { useEffect } from 'react'; import cls from 'classnames'; import { useToken, useUploadStyles } from '@nocobase/client'; -import useUploadStyle from 'antd/es/upload/style'; import { css } from '@emotion/css'; import { useField } from '@formily/react'; import { Field } from '@formily/core'; import { avatars } from '../avatars'; +import { List, Avatar as AntdAvatar } from 'antd'; export const Avatar: React.FC<{ srcs: [string, string][]; @@ -22,11 +22,9 @@ export const Avatar: React.FC<{ selectable?: boolean; highlightItem?: string; onClick?: (name: string) => void; -}> = ({ srcs, size, selectable, highlightItem, onClick }) => { +}> = ({ srcs, size = 'large', selectable, highlightItem, onClick }) => { const { token } = useToken(); const { wrapSSR, hashId, componentCls: prefixCls } = useUploadStyles(); - const useUploadStyleVal = (useUploadStyle as any).default ? (useUploadStyle as any).default : useUploadStyle; - useUploadStyleVal(prefixCls); const list = srcs?.map(([src, name], index) => ( @@ -61,6 +59,44 @@ export const Avatar: React.FC<{ )) || []; + return ( + { + return ( + onClick && onClick(name)} + /> + ); + }} + /> + ); + return wrapSSR(
{ +const AIEmployeeForm: React.FC<{ + edit?: boolean; +}> = ({ edit }) => { return ( , - }, - { - key: 'chat', - label: 'Chat settings', - children: , + children: , + forceRender: true, }, // { + // key: 'chat', + // label: 'Chat settings', + // children: , + // }, + // { // key: 'skills', // label: 'Skills', // // children: ( @@ -130,6 +153,7 @@ const AIEmployeeForm: React.FC = () => { key: 'modelSettings', label: 'Model Settings', children: , + forceRender: true, }, ]} /> @@ -182,7 +206,7 @@ const useCreateActionProps = () => { const { message } = App.useApp(); const form = useForm(); const api = useAPIClient(); - const { refresh } = useContext(EmployeeContext); + const { refresh } = useDataBlockRequest(); const t = useT(); return { @@ -205,18 +229,20 @@ const useEditActionProps = () => { const { setVisible } = useActionContext(); const { message } = App.useApp(); const form = useForm(); + const resource = useDataBlockResource(); + const { refresh } = useDataBlockRequest(); + const collection = useCollection(); + const filterTk = collection.getFilterTargetKey(); const t = useT(); - const { refresh } = useContext(EmployeeContext); - const api = useAPIClient(); return { type: 'primary', async onClick() { await form.submit(); const values = form.values; - await api.resource('aiEmployees').update({ + await resource.update({ values, - filterByTk: values.username, + filterByTk: values[filterTk], }); refresh(); message.success(t('Saved successfully')); @@ -226,196 +252,462 @@ const useEditActionProps = () => { }; }; -export const Employees: React.FC = () => { +// export const Employees: React.FC = () => { +// const t = useT(); +// const { message, modal } = App.useApp(); +// const { token } = useToken(); +// const api = useAPIClient(); +// const { data, loading, refresh } = useRequest(() => +// api +// .resource('aiEmployees') +// .list() +// .then((res) => res?.data?.data), +// ); +// +// const del = (username: string) => { +// modal.confirm({ +// title: t('Delete AI employee'), +// content: t('Are you sure to delete this employee?'), +// onOk: async () => { +// await api.resource('aiEmployees').destroy({ +// filterByTk: username, +// }); +// message.success(t('Deleted successfully')); +// refresh(); +// }, +// }); +// }; +// +// return ( +// +//
+//
+// +//
+//
+// +// +// +// +//
+//
+// {loading ? ( +// +// ) : data && data.length ? ( +// +// {data.map((employee) => ( +// +// +// , +// }, +// properties: { +// drawer: { +// type: 'void', +// 'x-component': 'Action.Drawer', +// title: 'Edit AI employee', +// 'x-decorator': 'FormV2', +// 'x-use-decorator-props': 'useEditFormProps', +// properties: { +// form: { +// type: 'void', +// 'x-component': 'AIEmployeeForm', +// 'x-component-props': { +// edit: true, +// }, +// }, +// footer: { +// type: 'void', +// 'x-component': 'Action.Drawer.Footer', +// properties: { +// close: { +// title: 'Cancel', +// 'x-component': 'Action', +// 'x-component-props': { +// type: 'default', +// }, +// 'x-use-component-props': 'useCancelActionProps', +// }, +// submit: { +// title: 'Submit', +// 'x-component': 'Action', +// 'x-component-props': { +// type: 'primary', +// }, +// 'x-use-component-props': 'useEditActionProps', +// }, +// }, +// }, +// }, +// }, +// }, +// }} +// />, +// del(employee.username)} />, +// ]} +// > +// : null} +// title={employee.nickname} +// description={ +// <> +// {employee.position && ( +// +// {employee.position} +// +// )} +// +// {employee.bio} +// +// +// } +// /> +// +// +// +// ))} +// +// ) : ( +// +// )} +//
+// ); +// }; + +const Avatar: React.FC = (props) => { + const field = useField(); + if (!field.value) { + return null; + } + return ; +}; + +export const Employees = () => { const t = useT(); - const { message, modal } = App.useApp(); - const { token } = useToken(); - const api = useAPIClient(); - const { data, loading, refresh } = useRequest(() => - api - .resource('aiEmployees') - .list() - .then((res) => res?.data?.data), - ); - - const del = (username: string) => { - modal.confirm({ - title: t('Delete AI employee'), - content: t('Are you sure to delete this employee?'), - onOk: async () => { - await api.resource('aiEmployees').destroy({ - filterByTk: username, - }); - message.success(t('Deleted successfully')); - refresh(); - }, - }); - }; - return ( - -
-
- -
-
- - - + - -
-
- {loading ? ( - - ) : data && data.length ? ( - - {data.map((employee) => ( - - - , + table: { + type: 'array', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'username', + rowSelection: { + type: 'checkbox', + }, + }, + properties: { + column0: { + type: 'void', + title: t('Avatar'), + 'x-component': 'TableV2.Column', + properties: { + avatar: { + type: 'string', + 'x-component': 'Avatar', }, - properties: { - drawer: { - type: 'void', - 'x-component': 'Action.Drawer', - title: 'Edit AI employee', - 'x-decorator': 'FormV2', - 'x-use-decorator-props': 'useEditFormProps', - properties: { - form: { - type: 'void', - 'x-component': 'AIEmployeeForm', + }, + }, + column1: { + type: 'void', + title: t('Username'), + 'x-component': 'TableV2.Column', + properties: { + username: { + type: 'string', + 'x-component': 'Input', + 'x-pattern': 'readPretty', + }, + }, + }, + column2: { + type: 'void', + title: t('Nickname'), + 'x-component': 'TableV2.Column', + properties: { + nickname: { + type: 'string', + 'x-component': 'Input', + 'x-pattern': 'readPretty', + }, + }, + }, + column3: { + type: 'void', + title: t('Position'), + 'x-component': 'TableV2.Column', + properties: { + position: { + type: 'string', + 'x-component': 'Input', + 'x-pattern': 'readPretty', + }, + }, + }, + column4: { + type: 'void', + title: t('Bio'), + 'x-component': 'TableV2.Column', + properties: { + bio: { + type: 'string', + 'x-component': 'Input.TextArea', + 'x-component-props': { + ellipsis: true, + }, + 'x-pattern': 'readPretty', + }, + }, + }, + column5: { + type: 'void', + title: 'Actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + properties: { + actions: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + edit: { + type: 'void', + title: 'Edit', + 'x-action': 'update', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', }, - footer: { - type: 'void', - 'x-component': 'Action.Drawer.Footer', - properties: { - close: { - title: 'Cancel', - 'x-component': 'Action', - 'x-component-props': { - type: 'default', + properties: { + drawer: { + type: 'void', + title: t('Edit AI employee'), + 'x-component': 'Action.Drawer', + 'x-decorator': 'FormV2', + 'x-use-decorator-props': 'useEditFormProps', + properties: { + form: { + type: 'void', + 'x-component': 'AIEmployeeForm', + 'x-component-props': { + edit: true, + }, }, - 'x-use-component-props': 'useCancelActionProps', - }, - submit: { - title: 'Submit', - 'x-component': 'Action', - 'x-component-props': { - type: 'primary', + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + close: { + title: t('Cancel'), + 'x-component': 'Action', + 'x-use-component-props': 'useCancelActionProps', + }, + submit: { + title: 'Submit', + 'x-component': 'Action', + 'x-use-component-props': 'useEditActionProps', + }, + }, }, - 'x-use-component-props': 'useEditActionProps', }, }, }, }, + destroy: { + type: 'void', + title: '{{ t("Delete") }}', + 'x-action': 'destroy', + 'x-component': 'Action.Link', + 'x-use-component-props': 'useDestroyActionProps', + 'x-component-props': { + confirm: { + title: "{{t('Delete AI employee')}}", + content: "{{t('Are you sure you want to delete this AI employee?')}}", + }, + }, + }, }, }, - }} - />, - del(employee.username)} />, - ]} - > - : null} - title={employee.nickname} - description={ - <> - {employee.position && ( - - {employee.position} - - )} - - {employee.bio} - - - } - /> - - - - ))} - - ) : ( - - )} -
+ }, + }, + }, + }, + }, + }, + }, + }} + /> + ); }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ProfileSettings.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ProfileSettings.tsx index 099ec2fd94..8d490a51e6 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ProfileSettings.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ProfileSettings.tsx @@ -12,7 +12,9 @@ import React from 'react'; import { AvatarSelect } from './AvatarSelect'; import { useT } from '../../locale'; -export const ProfileSettings: React.FC = () => { +export const ProfileSettings: React.FC<{ + edit?: boolean; +}> = ({ edit }) => { const t = useT(); return ( { 'x-decorator': 'FormItem', 'x-component': 'Input', required: true, + 'x-disabled': edit, }, nickname: { type: 'string', diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/selector/AISelectorProvider.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/selector/AISelectorProvider.tsx index e05cfae7dd..211cf2d977 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/selector/AISelectorProvider.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/selector/AISelectorProvider.tsx @@ -23,7 +23,9 @@ export const useAISelectionContext = () => { return useContext(AISelectionContext); }; -export const AISelectionProvider: React.FC = (props) => { +export const AISelectionProvider: React.FC<{ + children: React.ReactNode; +}> = (props) => { const [selectable, setSelectable] = useState(''); const [selector, setSelector] = useState(null); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/settings/AIEmployeeButton.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/settings/AIEmployeeButton.tsx index a0fc82ba73..327b994dff 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/settings/AIEmployeeButton.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/settings/AIEmployeeButton.tsx @@ -19,6 +19,7 @@ import { useAISelectionContext } from '../selector/AISelectorProvider'; import { AIEmployee } from '../types'; import { AIVariableRawTextArea } from './AIVariableRawTextArea'; import { useFieldSchema } from '@formily/react'; +import { useAIEmployeesContext } from '../AIEmployeesProvider'; const SettingsForm: React.FC<{ form: any; @@ -150,9 +151,11 @@ export const aiEmployeeButtonSettings = new SchemaSettings({ const t = useT(); const { dn } = useSchemaSettings(); const [open, setOpen] = useState(false); - const aiEmployee = dn.getSchemaAttribute('x-component-props.aiEmployee') || {}; + const username = dn.getSchemaAttribute('x-component-props.username') || {}; const form = useMemo(() => createForm({}), []); const { selectable } = useAISelectionContext(); + const { aiEmployeesMap } = useAIEmployeesContext(); + const aiEmployee = aiEmployeesMap[username]; return (
e.stopPropagation()}> @@ -176,7 +179,7 @@ export const aiEmployeeButtonSettings = new SchemaSettings({ dn.deepMerge({ 'x-uid': dn.getSchemaAttribute('x-uid'), 'x-component-props': { - aiEmployee, + username, message, taskDesc, autoSend, diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/types.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/types.ts index 42b7f69092..06f5f6695d 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/types.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/types.ts @@ -79,6 +79,5 @@ export type ResendOptions = { export type ShortcutOptions = { aiEmployee: AIEmployee; message: { type: MessageType; content: string }; - infoFormValues: any; autoSend: boolean; }; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/schemas/llms.ts b/packages/plugins/@nocobase/plugin-ai/src/client/schemas/llms.ts index c945abc595..1a4cff6066 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/schemas/llms.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client/schemas/llms.ts @@ -187,7 +187,6 @@ export const llmsSchema = { 'x-component': 'Action.Link', 'x-component-props': { openMode: 'drawer', - icon: 'EditOutlined', }, properties: { drawer: { diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-employees.ts b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-employees.ts index 1c654d249b..3824dd4680 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-employees.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-employees.ts @@ -13,5 +13,17 @@ import aiEmployees from '../../collections/ai-employees'; export default defineCollection({ migrationRules: ['overwrite', 'schema-only'], autoGenId: false, + sortable: true, ...aiEmployees, + fields: [ + ...aiEmployees.fields, + { + name: 'userConfigs', + type: 'hasMany', + target: 'usersAiEmployees', + sourceKey: 'username', + foreignKey: 'aiEmployee', + onDelete: 'CASCADE', + }, + ], }); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/collections/users-ai-employees.ts b/packages/plugins/@nocobase/plugin-ai/src/server/collections/users-ai-employees.ts new file mode 100644 index 0000000000..c68ed0c60a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/collections/users-ai-employees.ts @@ -0,0 +1,22 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + name: 'usersAiEmployees', + migrationRules: ['schema-only'], + fields: [ + { type: 'sort', name: 'sort' }, + { + type: 'text', + name: 'prompt', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/collections/users.ts b/packages/plugins/@nocobase/plugin-ai/src/server/collections/users.ts new file mode 100644 index 0000000000..b61130b90a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/collections/users.ts @@ -0,0 +1,27 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { extendCollection } from '@nocobase/database'; + +export default extendCollection({ + name: 'users', + fields: [ + { + type: 'belongsToMany', + name: 'aiEmployees', + target: 'aiEmployees', + foreignKey: 'userId', + otherKey: 'aiEmployee', + onDelete: 'CASCADE', + sourceKey: 'id', + targetKey: 'username', + through: 'usersAiEmployees', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts index c9f7179c65..e2d3272aac 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts @@ -17,6 +17,7 @@ import { LLMInstruction } from './workflow/nodes/llm'; import aiConversations from './resource/aiConversations'; import { AIEmployeesManager } from './ai-employees/ai-employees-manager'; import Snowflake from './snowflake'; +import { listByUser } from './resource/aiEmployees'; export class PluginAIServer extends Plugin { aiManager = new AIManager(); @@ -40,6 +41,7 @@ export class PluginAIServer extends Plugin { this.app.resourceManager.define(aiResource); this.app.resourceManager.define(aiConversations); + this.app.resourceManager.registerActionHandler('aiEmployees:listByUser', listByUser); this.app.acl.registerSnippet({ name: `pm.${this.name}.llm-services`, actions: ['ai:*', 'llmServices:*'], @@ -48,6 +50,7 @@ export class PluginAIServer extends Plugin { name: `pm.${this.name}.ai-employees`, actions: ['aiEmployees:*'], }); + this.app.acl.allow('aiEmployees', 'listByUser', 'loggedIn'); this.app.acl.allow('aiConversations', '*', 'loggedIn'); const workflowSnippet = this.app.acl.snippetManager.snippets.get('pm.workflow.workflows'); if (workflowSnippet) { diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiEmployees.ts b/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiEmployees.ts new file mode 100644 index 0000000000..57c36529e9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiEmployees.ts @@ -0,0 +1,39 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Context, Next } from '@nocobase/actions'; + +export const listByUser = async (ctx: Context, next: Next) => { + const user = ctx.auth.user; + const model = ctx.db.getModel('aiEmployees'); + const sequelize = ctx.db.sequelize; + const rows = await model.findAll({ + include: [ + { + model: ctx.db.getModel('usersAiEmployees'), + as: 'userConfigs', + required: false, + where: { userId: user.id }, + }, + ], + order: [ + [sequelize.literal('CASE WHEN userConfigs.sort IS NOT NULL THEN 0 ELSE 1 END'), 'ASC'], + [sequelize.fn('COALESCE', sequelize.col('userConfigs.sort'), sequelize.col('aiEmployees.sort')), 'ASC'], + ], + }); + ctx.body = rows.map((row) => ({ + username: row.username, + nickname: row.nickname, + position: row.position, + avatar: row.avatar, + bio: row.bio, + greeting: row.greeting, + })); + await next(); +};