chore: update

This commit is contained in:
xilesun 2025-04-22 22:40:48 +08:00
parent 861f0e5f02
commit c4f5ff9a29
20 changed files with 719 additions and 296 deletions

View File

@ -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 };
};

View File

@ -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 (
<Sender.Header closable={false}>

View File

@ -42,6 +42,7 @@ type ChatBoxContextValues = {
setSenderPlaceholder: React.Dispatch<React.SetStateAction<string>>;
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<AIEmployee>(null);
const { setMessages, sendMessages } = useChatMessages();
const { setMessages, sendMessages, addMessage } = useChatMessages();
const { currentConversation, setCurrentConversation, conversationsService } = useChatConversations();
const [senderValue, setSenderValue] = useState<string>('');
const [senderPlaceholder, setSenderPlaceholder] = useState<string>('');
@ -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,
};
};

View File

@ -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={
<Avatar
src={icon}
size={40}
size={36}
style={{
marginBottom: '4px',
}}
/>
}
// onClick={() => {
// setOpen(false);
// }}
onClick={() => {
setOpen(!open);
}}
shape="square"
/>
</Dropdown>

View File

@ -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={<SenderPrefix />}
// header={!currentConversation ? <SenderHeader /> : null}
header={!currentEmployee ? <AIEmployeeHeader /> : null}
loading={responseLoading}
footer={({ components }) => <SenderFooter components={components} />}
disabled={!currentEmployee}

View File

@ -14,5 +14,14 @@ import { Avatar } from 'antd';
export const SenderPrefix: React.FC = () => {
const currentEmployee = useChatBoxContext('currentEmployee');
return currentEmployee ? <Avatar src={avatars(currentEmployee.avatar)} /> : null;
const switchAIEmployee = useChatBoxContext('switchAIEmployee');
return currentEmployee ? (
<Avatar
src={avatars(currentEmployee.avatar)}
style={{
cursor: 'pointer',
}}
onClick={() => switchAIEmployee(null)}
/>
) : null;
};

View File

@ -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 (
<SortableItem
@ -92,10 +97,10 @@ export const AIEmployeeButton: React.FC<{
aiEmployee,
message: msg,
autoSend,
infoFormValues,
});
}}
>
<Spin spinning={loading}>
<Popover content={<ProfileCard taskDesc={taskDesc} aiEmployee={aiEmployee} />}>
<Button
shape="circle"
@ -104,10 +109,11 @@ export const AIEmployeeButton: React.FC<{
height: '36px',
}}
>
<Avatar src={avatars(aiEmployee.avatar)} size={36} />
{aiEmployee && <Avatar src={avatars(aiEmployee.avatar)} size={36} />}
</Button>
</Popover>
{render()}
</Spin>
</SortableItem>
);
});

View File

@ -41,7 +41,7 @@ const getAIEmployeesInitializer = () => ({
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'aiEmployees:button',
'x-component-props': {
aiEmployee,
username: aiEmployee.username,
},
});
};

View File

@ -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<{
</div>
)) || [];
return (
<List
itemLayout="horizontal"
dataSource={srcs}
renderItem={([src, name]) => {
return (
<AntdAvatar
size={size === 'small' ? 32 : 80}
className={cls(
css`
margin: 2px;
border-radius: ${size === 'small' ? '4px' : '8px'};
border: 1px solid ${token.colorBorder};
padding: 1px;
`,
highlightItem === name
? css`
border-color: ${token.colorPrimary} !important;
`
: '',
selectable
? css`
cursor: pointer;
&:hover {
border-color: ${token.colorPrimary} !important;
}
`
: '',
)}
src={src}
shape="square"
onClick={() => onClick && onClick(name)}
/>
);
}}
/>
);
return wrapSSR(
<div
className={cls(

View File

@ -8,13 +8,31 @@
*/
import React, { createContext, useContext, useMemo } from 'react';
import { Card, Row, Col, Avatar, Input, Space, Button, Tabs, App, Spin, Empty, Typography, Tag } from 'antd';
import {
Card,
Row,
Col,
Avatar as AntdAvatar,
Input,
Space,
Button,
Tabs,
App,
Spin,
Empty,
Typography,
Tag,
} from 'antd';
import {
CollectionRecordProvider,
ExtendCollectionsProvider,
SchemaComponent,
useAPIClient,
useActionContext,
useCollection,
useCollectionRecordData,
useDataBlockRequest,
useDataBlockResource,
useRequest,
useToken,
} from '@nocobase/client';
@ -22,32 +40,37 @@ import { useT } from '../../locale';
const { Meta } = Card;
import { css } from '@emotion/css';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { useForm } from '@formily/react';
import { createForm } from '@formily/core';
import { useForm, useField } from '@formily/react';
import { createForm, Field } from '@formily/core';
import { uid } from '@formily/shared';
import { avatars } from '../avatars';
import { ModelSettings } from './ModelSettings';
import { ProfileSettings } from './ProfileSettings';
import { ChatSettings } from './ChatSettings';
import { AIEmployee } from '../types';
import aiEmployees from '../../../collections/ai-employees';
import { useAIEmployeesContext } from '../AIEmployeesProvider';
const EmployeeContext = createContext(null);
const AIEmployeeForm: React.FC = () => {
const AIEmployeeForm: React.FC<{
edit?: boolean;
}> = ({ edit }) => {
return (
<Tabs
items={[
{
key: 'profile',
label: 'Profile',
children: <ProfileSettings />,
},
{
key: 'chat',
label: 'Chat settings',
children: <ChatSettings />,
children: <ProfileSettings edit={edit} />,
forceRender: true,
},
// {
// key: 'chat',
// label: 'Chat settings',
// children: <ChatSettings />,
// },
// {
// key: 'skills',
// label: 'Skills',
// // children: (
@ -130,6 +153,7 @@ const AIEmployeeForm: React.FC = () => {
key: 'modelSettings',
label: 'Model Settings',
children: <ModelSettings />,
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,59 +252,269 @@ const useEditActionProps = () => {
};
};
export const Employees: React.FC = () => {
const t = useT();
const { message, modal } = App.useApp();
const { token } = useToken();
const api = useAPIClient();
const { data, loading, refresh } = useRequest<AIEmployee[]>(() =>
api
.resource('aiEmployees')
.list()
.then((res) => res?.data?.data),
);
// export const Employees: React.FC = () => {
// const t = useT();
// const { message, modal } = App.useApp();
// const { token } = useToken();
// const api = useAPIClient();
// const { data, loading, refresh } = useRequest<AIEmployee[]>(() =>
// 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 (
// <EmployeeContext.Provider value={{ refresh }}>
// <div
// style={{ marginBottom: token.marginLG }}
// className={css`
// justify-content: space-between;
// display: flex;
// align-items: center;
// `}
// >
// <div>
// <Input allowClear placeholder={t('Search')} />
// </div>
// <div>
// <Space>
// <Button>{t('New from template')}</Button>
// <SchemaComponent
// scope={{ useCreateFormProps, useCancelActionProps, useCreateActionProps }}
// components={{ AIEmployeeForm }}
// schema={{
// type: 'void',
// name: uid(),
// 'x-component': 'Action',
// 'x-component-props': {
// type: 'primary',
// },
// title: 'New AI employee',
// properties: {
// drawer: {
// type: 'void',
// 'x-component': 'Action.Drawer',
// title: 'New AI employee',
// 'x-decorator': 'FormV2',
// 'x-use-decorator-props': 'useCreateFormProps',
// properties: {
// form: {
// type: 'void',
// 'x-component': 'AIEmployeeForm',
// },
// 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': 'useCreateActionProps',
// },
// },
// },
// },
// },
// },
// }}
// />
// </Space>
// </div>
// </div>
// {loading ? (
// <Spin />
// ) : data && data.length ? (
// <Row gutter={[16, 16]}>
// {data.map((employee) => (
// <CollectionRecordProvider key={employee.username} record={employee}>
// <Col span={6}>
// <Card
// variant="borderless"
// actions={[
// <SchemaComponent
// key="edit"
// scope={{ useCancelActionProps, useEditFormProps, useEditActionProps }}
// components={{ AIEmployeeForm }}
// schema={{
// type: 'void',
// name: uid(),
// 'x-component': 'Action',
// 'x-component-props': {
// component: (props) => <EditOutlined {...props} />,
// },
// 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',
// },
// },
// },
// },
// },
// },
// }}
// />,
// <DeleteOutlined key="delete" onClick={() => del(employee.username)} />,
// ]}
// >
// <Meta
// avatar={employee.avatar ? <Avatar size={40} src={avatars(employee.avatar)} /> : null}
// title={employee.nickname}
// description={
// <>
// {employee.position && (
// <Tag
// style={{
// marginBottom: token.marginXS,
// }}
// >
// {employee.position}
// </Tag>
// )}
// <Typography.Paragraph
// style={{ height: token.fontSize * token.lineHeight * 3 }}
// ellipsis={{ rows: 3 }}
// type="secondary"
// >
// {employee.bio}
// </Typography.Paragraph>
// </>
// }
// />
// </Card>
// </Col>
// </CollectionRecordProvider>
// ))}
// </Row>
// ) : (
// <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
// )}
// </EmployeeContext.Provider>
// );
// };
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();
},
});
const Avatar: React.FC = (props) => {
const field = useField<Field>();
if (!field.value) {
return null;
}
return <AntdAvatar {...props} src={avatars(field.value)} />;
};
export const Employees = () => {
const t = useT();
return (
<EmployeeContext.Provider value={{ refresh }}>
<div
style={{ marginBottom: token.marginLG }}
className={css`
justify-content: space-between;
display: flex;
align-items: center;
`}
>
<div>
<Input allowClear placeholder={t('Search')} />
</div>
<div>
<Space>
<Button>{t('New from template')}</Button>
<ExtendCollectionsProvider collections={[aiEmployees]}>
<SchemaComponent
scope={{ useCreateFormProps, useCancelActionProps, useCreateActionProps }}
components={{ AIEmployeeForm }}
components={{ AIEmployeeForm, Avatar }}
scope={{
useCreateFormProps,
useEditFormProps,
useCancelActionProps,
useCreateActionProps,
useEditActionProps,
}}
schema={{
type: 'void',
name: uid(),
name: 'root',
properties: {
block: {
type: 'void',
'x-component': 'CardItem',
'x-component-props': {
heightMode: 'fullHeight',
},
'x-decorator': 'TableBlockProvider',
'x-decorator-props': {
collection: 'aiEmployees',
action: 'list',
rowKey: 'username',
dragSort: true,
dragSortBy: 'sort',
},
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 20,
},
},
properties: {
refresh: {
title: "{{t('Refresh')}}",
'x-component': 'Action',
'x-use-component-props': 'useRefreshActionProps',
'x-component-props': {
icon: 'ReloadOutlined',
},
},
add: {
type: 'void',
title: "{{t('New AI employee')}}",
'x-align': 'right',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
icon: 'PlusOutlined',
},
title: 'New AI employee',
properties: {
drawer: {
type: 'void',
@ -316,62 +552,130 @@ export const Employees: React.FC = () => {
},
},
},
}}
/>
</Space>
</div>
</div>
{loading ? (
<Spin />
) : data && data.length ? (
<Row gutter={[16, 16]}>
{data.map((employee) => (
<CollectionRecordProvider key={employee.username} record={employee}>
<Col span={6}>
<Card
variant="borderless"
actions={[
<SchemaComponent
key="edit"
scope={{ useCancelActionProps, useEditFormProps, useEditActionProps }}
components={{ AIEmployeeForm }}
schema={{
type: 'void',
name: uid(),
'x-component': 'Action',
},
},
},
table: {
type: 'array',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
component: (props) => <EditOutlined {...props} />,
rowKey: 'username',
rowSelection: {
type: 'checkbox',
},
},
properties: {
column0: {
type: 'void',
title: t('Avatar'),
'x-component': 'TableV2.Column',
properties: {
avatar: {
type: 'string',
'x-component': 'Avatar',
},
},
},
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',
},
properties: {
drawer: {
type: 'void',
title: t('Edit AI employee'),
'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',
title: t('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',
},
},
@ -379,43 +683,31 @@ export const Employees: React.FC = () => {
},
},
},
},
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?')}}",
},
},
},
},
},
},
},
},
},
},
},
},
}}
/>,
<DeleteOutlined key="delete" onClick={() => del(employee.username)} />,
]}
>
<Meta
avatar={employee.avatar ? <Avatar size={40} src={avatars(employee.avatar)} /> : null}
title={employee.nickname}
description={
<>
{employee.position && (
<Tag
style={{
marginBottom: token.marginXS,
}}
>
{employee.position}
</Tag>
)}
<Typography.Paragraph
style={{ height: token.fontSize * token.lineHeight * 3 }}
ellipsis={{ rows: 3 }}
type="secondary"
>
{employee.bio}
</Typography.Paragraph>
</>
}
/>
</Card>
</Col>
</CollectionRecordProvider>
))}
</Row>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</EmployeeContext.Provider>
</ExtendCollectionsProvider>
);
};

View File

@ -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 (
<SchemaComponent
@ -26,6 +28,7 @@ export const ProfileSettings: React.FC = () => {
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
'x-disabled': edit,
},
nickname: {
type: 'string',

View File

@ -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<Selector>(null);

View File

@ -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 (
<div onClick={(e) => 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,

View File

@ -79,6 +79,5 @@ export type ResendOptions = {
export type ShortcutOptions = {
aiEmployee: AIEmployee;
message: { type: MessageType; content: string };
infoFormValues: any;
autoSend: boolean;
};

View File

@ -187,7 +187,6 @@ export const llmsSchema = {
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
icon: 'EditOutlined',
},
properties: {
drawer: {

View File

@ -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',
},
],
});

View File

@ -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',
},
],
});

View File

@ -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',
},
],
});

View File

@ -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) {

View File

@ -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();
};