mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-24 16:02:20 +08:00
feat: form filler 2.0
This commit is contained in:
@@ -23,7 +23,7 @@ import { buildSubModelGroups, buildSubModelItems } from './utils';
|
||||
|
||||
export interface SubModelItem {
|
||||
key?: string;
|
||||
label?: string;
|
||||
label?: string | React.ReactNode;
|
||||
type?: 'group' | 'divider';
|
||||
disabled?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
|
||||
+27
-31
@@ -7,18 +7,18 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { FloatButton, Avatar, Dropdown } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { FloatButton, Avatar, Dropdown, Button, Divider } from 'antd';
|
||||
import icon from '../icon.png';
|
||||
import { css } from '@emotion/css';
|
||||
import { useAIEmployeesContext } from '../AIEmployeesProvider';
|
||||
import { AIEmployeeListItem } from '../AIEmployeeListItem';
|
||||
import { useAISelectionContext } from '../selector/AISelectorProvider';
|
||||
import { PauseCircleFilled } from '@ant-design/icons';
|
||||
import { PauseCircleFilled, RightOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import { useToken } from '@nocobase/client';
|
||||
import { FlowModelRenderer, useFlowEngine } from '@nocobase/flow-engine';
|
||||
import { useChatBoxStore } from './stores/chat-box';
|
||||
import { useChatBoxActions } from './hooks/useChatBoxActions';
|
||||
import { ShortcutList } from '../shortcuts/ShortcutList';
|
||||
|
||||
export const ChatButton: React.FC = () => {
|
||||
const { aiEmployees } = useAIEmployeesContext();
|
||||
@@ -30,13 +30,6 @@ export const ChatButton: React.FC = () => {
|
||||
|
||||
const { stopSelect, selectable } = useAISelectionContext();
|
||||
const { token } = useToken();
|
||||
// const flowEngine = useFlowEngine();
|
||||
//
|
||||
// const model = useMemo(() => {
|
||||
// return flowEngine.createModel({
|
||||
// use: 'AIEmployeeShortcutListModel',
|
||||
// });
|
||||
// }, [flowEngine]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return aiEmployees?.map((employee) => ({
|
||||
@@ -58,36 +51,39 @@ export const ChatButton: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
.ant-float-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.ant-float-btn .ant-float-btn-body .ant-float-btn-content {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-float-btn .ant-float-btn-body .ant-float-btn-content .ant-float-btn-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
.ant-float-btn .ant-float-btn-body .ant-float-btn-content .ant-float-btn-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
gap: 8px;
|
||||
position: fixed;
|
||||
bottom: 48px;
|
||||
align-items: center;
|
||||
inset-inline-end: 8px;
|
||||
width: fit-content;
|
||||
height: 60px;
|
||||
padding: 4px;
|
||||
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||
`}
|
||||
>
|
||||
{/* <FlowModelRenderer model={model} /> */}
|
||||
<ShortcutList />
|
||||
{!selectable ? (
|
||||
<Dropdown menu={{ items }} placement="topRight">
|
||||
<FloatButton
|
||||
icon={<Avatar src={icon} size={56} />}
|
||||
<Avatar
|
||||
src={icon}
|
||||
size={52}
|
||||
shape="square"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<FloatButton
|
||||
<Button
|
||||
icon={
|
||||
<PauseCircleFilled
|
||||
style={{
|
||||
|
||||
+5
-6
@@ -19,10 +19,13 @@ import { parseTask } from '../utils';
|
||||
import { uid } from '@formily/shared';
|
||||
import { aiEmployeeRole } from '../roles';
|
||||
import { useChatToolsStore } from '../stores/chat-tools';
|
||||
import { useApp } from '@nocobase/client';
|
||||
|
||||
export const useChatBoxActions = () => {
|
||||
const app = useApp();
|
||||
const t = useT();
|
||||
|
||||
const open = useChatBoxStore.use.open();
|
||||
const setOpen = useChatBoxStore.use.setOpen();
|
||||
const setSenderValue = useChatBoxStore.use.setSenderValue();
|
||||
const setTaskVariables = useChatBoxStore.use.setTaskVariables();
|
||||
@@ -119,7 +122,7 @@ export const useChatBoxActions = () => {
|
||||
|
||||
const triggerTask = useCallback(
|
||||
async (options: TriggerTaskOptions) => {
|
||||
const { aiEmployee, tasks, variables, localVariables } = options;
|
||||
const { aiEmployee, tasks } = options;
|
||||
updateRole(aiEmployee);
|
||||
if (!open) {
|
||||
setOpen(true);
|
||||
@@ -147,11 +150,7 @@ export const useChatBoxActions = () => {
|
||||
if (tasks.length === 1) {
|
||||
setMessages(msgs);
|
||||
const task = tasks[0];
|
||||
const { userMessage, systemMessage, attachments, workContext } = await parseTask(
|
||||
task,
|
||||
variables,
|
||||
localVariables,
|
||||
);
|
||||
const { userMessage, systemMessage, attachments, workContext } = await parseTask(app, task);
|
||||
if (userMessage && userMessage.type === 'text') {
|
||||
setSenderValue(userMessage.content);
|
||||
} else {
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useLocalVariables, useVariables } from '@nocobase/client';
|
||||
import { Application, useLocalVariables, useVariables } from '@nocobase/client';
|
||||
import { ContextItem } from '../types';
|
||||
import PluginAIClient from '../..';
|
||||
|
||||
async function replaceVariables(template, variables, localVariables = {}) {
|
||||
const regex = /\{\{\s*(.*?)\s*\}\}/g;
|
||||
@@ -53,6 +54,7 @@ async function replaceVariables(template, variables, localVariables = {}) {
|
||||
}
|
||||
|
||||
export const parseTask = async (
|
||||
app: Application,
|
||||
task: {
|
||||
message: {
|
||||
user?: string;
|
||||
@@ -61,26 +63,23 @@ export const parseTask = async (
|
||||
workContext?: ContextItem[];
|
||||
};
|
||||
},
|
||||
variables: Record<string, any>,
|
||||
localVariables?: Record<string, any>,
|
||||
) => {
|
||||
let userMessage: any;
|
||||
const { message } = task;
|
||||
if (message?.user) {
|
||||
const content = await replaceVariables(message.user, variables, localVariables);
|
||||
userMessage = {
|
||||
type: 'text',
|
||||
content,
|
||||
content: message.user,
|
||||
};
|
||||
}
|
||||
let systemMessage: string;
|
||||
if (message?.system) {
|
||||
systemMessage = await replaceVariables(message.system, variables, localVariables);
|
||||
systemMessage = message.system;
|
||||
}
|
||||
const attachments = [];
|
||||
if (message?.attachments?.length) {
|
||||
for (const attachment of message.attachments) {
|
||||
const obj = await variables?.parseVariable(attachment, localVariables).then(({ value }) => value);
|
||||
const obj = attachment;
|
||||
if (!obj) {
|
||||
continue;
|
||||
}
|
||||
@@ -97,7 +96,27 @@ export const parseTask = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
return { userMessage, systemMessage, attachments, workContext: message.workContext };
|
||||
const workContext = [];
|
||||
if (message.workContext) {
|
||||
const plugin = app.pm.get('ai') as PluginAIClient;
|
||||
for (const context of message.workContext) {
|
||||
if (context.content) {
|
||||
workContext.push(context);
|
||||
continue;
|
||||
}
|
||||
const contextOptions = plugin.aiManager.workContext.get(context.type);
|
||||
if (!(contextOptions && contextOptions.getContent)) {
|
||||
workContext.push(context);
|
||||
continue;
|
||||
}
|
||||
const content = contextOptions.getContent(app, context);
|
||||
workContext.push({
|
||||
...context,
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { userMessage, systemMessage, attachments, workContext };
|
||||
};
|
||||
|
||||
const publicPath = window['__nocobase_dev_public_path__'] || window['__nocobase_public_path__'] || '/';
|
||||
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import { WorkContextOptions } from '../types';
|
||||
import { BuildOutlined, StarOutlined, SelectOutlined, DatabaseOutlined } from '@ant-design/icons';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
// @ts-ignore
|
||||
import pkg from '../../../../package.json';
|
||||
import { useT } from '../../locale';
|
||||
import { useAISelectionContext } from '../selector/AISelectorProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Schema } from '@formily/react';
|
||||
import { Checkbox } from 'antd';
|
||||
|
||||
export const CollectionDefinitionsContext: WorkContextOptions = {
|
||||
menu: {
|
||||
icon: <StarOutlined />,
|
||||
label: tval('Modern pages', { ns: pkg.name }),
|
||||
},
|
||||
children: {
|
||||
// form: {
|
||||
// menu: {
|
||||
// icon: <FormOutlined />,
|
||||
// Component: ({ onAdd }) => {
|
||||
// const t = useT();
|
||||
// const { t: collectionT } = useTranslation('lm-collections');
|
||||
// const { startSelect, ctx } = useAISelectionContext();
|
||||
// return (
|
||||
// <div
|
||||
// onClick={() => {
|
||||
// startSelect('form', {
|
||||
// onSelect: ({ uid, fieldSchema, collection }) => {
|
||||
// if (!uid) {
|
||||
// return;
|
||||
// }
|
||||
// let title = '';
|
||||
// if (collection) {
|
||||
// title = Schema.compile(collection.title, { t: collectionT });
|
||||
// }
|
||||
// onAdd({
|
||||
// uid,
|
||||
// title,
|
||||
// content: JSON.stringify(fieldSchema.toJSON()),
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// }}
|
||||
// >
|
||||
// {t('Select form')}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// tag: {
|
||||
// Component: ({ item }) => {
|
||||
// const t = useT();
|
||||
// return (
|
||||
// <>
|
||||
// <FormOutlined /> {t('Form')} ({item.title})
|
||||
// </>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// actions: [
|
||||
// {
|
||||
// responseType: 'json',
|
||||
// Component: ({ item, message, value }) => {
|
||||
// const t = useT();
|
||||
// const { ctx } = useAISelectionContext();
|
||||
//
|
||||
// return (
|
||||
// <Button
|
||||
// size="small"
|
||||
// icon={<FormOutlined />}
|
||||
// variant="link"
|
||||
// color="primary"
|
||||
// onClick={() => {
|
||||
// const content = value || (message.content as unknown as string);
|
||||
// const data = content.replace('```json', '').replace('```', '');
|
||||
// const json = JSON.parse(data);
|
||||
// const form = ctx[item.uid]?.form;
|
||||
// if (form) {
|
||||
// form.setValues(json);
|
||||
// }
|
||||
// }}
|
||||
// >
|
||||
// {t('Fill out form')} ({item.title})
|
||||
// </Button>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
users: {
|
||||
menu: {
|
||||
Component: ({ onAdd }) => {
|
||||
const t = useT();
|
||||
const { t: collectionT } = useTranslation('lm-collections');
|
||||
const { startSelect } = useAISelectionContext();
|
||||
return <Checkbox>{t('Users #f61f6cda073')}</Checkbox>;
|
||||
},
|
||||
},
|
||||
tag: {
|
||||
Component: ({ item }) => {
|
||||
return (
|
||||
<>
|
||||
<BuildOutlined /> {item.title}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
menu: {
|
||||
Component: ({ onAdd }) => {
|
||||
const t = useT();
|
||||
const { startSelect } = useAISelectionContext();
|
||||
|
||||
return <Checkbox>{t('Roles #4cb2eb66a99')}</Checkbox>;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
import { WorkContextOptions } from '../types';
|
||||
import { BuildOutlined, StarOutlined } from '@ant-design/icons';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
// @ts-ignore
|
||||
import pkg from '../../../../package.json';
|
||||
import { useFlowEngine } from '@nocobase/flow-engine';
|
||||
import _ from 'lodash';
|
||||
|
||||
function parseModel(node: any) {
|
||||
if (!node || typeof node !== 'object' || !node.uid || !node.use) return null;
|
||||
|
||||
const parsed = {
|
||||
uid: node.uid,
|
||||
use: node.use,
|
||||
props: node.props || {},
|
||||
decoratorProps: node.decoratorProps || {},
|
||||
stepParams: node.stepParams || {},
|
||||
collectionField: node.collectionField
|
||||
? _.pick(node.collectionField, ['dataSourceKey', 'enum', 'name', 'type', 'title', 'uiSchema'])
|
||||
: {},
|
||||
children: {},
|
||||
};
|
||||
|
||||
if (node.subModels && typeof node.subModels === 'object') {
|
||||
for (const key in node.subModels) {
|
||||
const group = node.subModels[key];
|
||||
if (Array.isArray(group)) {
|
||||
parsed.children[key] = group.map((sub) => parseModel(sub)).filter(Boolean);
|
||||
} else if (typeof group === 'object') {
|
||||
const child = parseModel(group);
|
||||
if (child) {
|
||||
parsed.children[key] = [child];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export const FlowModelsContext: WorkContextOptions = {
|
||||
name: 'flow-model',
|
||||
tag: {
|
||||
Component: ({ item }) => {
|
||||
const flowEngine = useFlowEngine();
|
||||
const model = flowEngine.getModel(item.uid);
|
||||
return (
|
||||
<>
|
||||
<BuildOutlined /> {model.title}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
getContent: (app, { uid }) => {
|
||||
const model = app.flowEngine.getModel(uid);
|
||||
if (!model) {
|
||||
return '';
|
||||
}
|
||||
return JSON.stringify(parseModel(model));
|
||||
},
|
||||
};
|
||||
+9
-20
@@ -18,24 +18,13 @@ type AIEmployeeShortcutListModelStructure = {
|
||||
};
|
||||
|
||||
export class AIEmployeeShortcutListModel extends FlowModel<AIEmployeeShortcutListModelStructure> {
|
||||
// onInit() {
|
||||
// this.addSubModel('shortcuts', {
|
||||
// uid: 'data-modeling',
|
||||
// use: 'AIEmployeeShortcutModel',
|
||||
// props: {
|
||||
// avatar: 'notion-3-male',
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// render() {
|
||||
// return (
|
||||
// <div>
|
||||
// {this.mapSubModels('shortcuts', (shortcut) => {
|
||||
// console.log(shortcut.props);
|
||||
// return <FlowModelRenderer key={shortcut.uid} model={shortcut} />;
|
||||
// })}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.mapSubModels('shortcuts', (shortcut) => {
|
||||
return <FlowModelRenderer key={shortcut.uid} model={shortcut} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+57
-12
@@ -7,22 +7,67 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar, FloatButton } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Avatar, Button, Spin, Popover } from 'antd';
|
||||
import { FlowModel } from '@nocobase/flow-engine';
|
||||
import { avatars } from '../../avatars';
|
||||
import { AIEmployee, TriggerTaskOptions } from '../../types';
|
||||
import { useAIEmployeesContext } from '../../AIEmployeesProvider';
|
||||
import { useChatBoxActions } from '../../chatbox/hooks/useChatBoxActions';
|
||||
import { ProfileCard } from '../../ProfileCard';
|
||||
|
||||
const Shortcut: React.FC<TriggerTaskOptions> = ({ aiEmployee: { username }, tasks }) => {
|
||||
const [focus, setFocus] = useState(false);
|
||||
|
||||
const {
|
||||
aiEmployeesMap,
|
||||
service: { loading },
|
||||
} = useAIEmployeesContext();
|
||||
const aiEmployee = aiEmployeesMap[username];
|
||||
|
||||
const { triggerTask } = useChatBoxActions();
|
||||
|
||||
const currentAvatar = useMemo(() => {
|
||||
const avatar = aiEmployee?.avatar;
|
||||
if (!avatar) {
|
||||
return null;
|
||||
}
|
||||
if (focus) {
|
||||
return avatars(avatar, {
|
||||
gesture: ['waveLongArm'],
|
||||
gestureProbability: 100,
|
||||
translateX: -15,
|
||||
});
|
||||
}
|
||||
return avatars(avatar);
|
||||
}, [aiEmployee, focus]);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Popover content={<ProfileCard aiEmployee={aiEmployee} />} placement="topLeft">
|
||||
<Avatar
|
||||
src={currentAvatar}
|
||||
size={52}
|
||||
shape="square"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
// @ts-ignore
|
||||
onMouseEnter={() => setFocus(true)}
|
||||
onMouseLeave={() => setFocus(false)}
|
||||
onClick={() => {
|
||||
triggerTask({ aiEmployee, tasks });
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export class AIEmployeeShortcutModel extends FlowModel {
|
||||
public declare props: TriggerTaskOptions;
|
||||
|
||||
render() {
|
||||
// const { avatar } = this.props;
|
||||
// console.log(this.props, this.stepParams, this.getProps());
|
||||
// return (
|
||||
// <FloatButton
|
||||
// icon={<Avatar src={avatars(avatar)} size={56} />}
|
||||
// style={{
|
||||
// insetInlineEnd: '94px',
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
return <Shortcut {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 { FlowEngine } from '@nocobase/flow-engine';
|
||||
import { ToolOptions } from '../../../manager/ai-manager';
|
||||
import { FormModel } from '@nocobase/client';
|
||||
|
||||
export const formFillerTool: [string, string, ToolOptions] = [
|
||||
'frontend',
|
||||
'formFiller',
|
||||
{
|
||||
invoke: (ctx, params) => {
|
||||
const { form: uid, data } = params;
|
||||
if (!uid || !data) {
|
||||
return;
|
||||
}
|
||||
const flowEngine = ctx.flowEngine as FlowEngine;
|
||||
const model = flowEngine.getModel(uid) as FormModel;
|
||||
model?.form?.setValues(data);
|
||||
},
|
||||
},
|
||||
];
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
+5
-1
@@ -9,6 +9,7 @@
|
||||
|
||||
import React, { createContext, useCallback, useContext, useState } from 'react';
|
||||
import { Selector } from '../types';
|
||||
import { useFlowEngine } from '@nocobase/flow-engine';
|
||||
|
||||
export const AISelectionContext = createContext<{
|
||||
selectable: string;
|
||||
@@ -32,9 +33,12 @@ export const AISelectionProvider: React.FC<{
|
||||
}> = (props) => {
|
||||
const [selectable, setSelectable] = useState('');
|
||||
const [selector, setSelector] = useState<Selector>(null);
|
||||
const flowEngine = useFlowEngine();
|
||||
const [ctx, setCtx] = useState<{
|
||||
[key: string]: Record<string, any>;
|
||||
}>({});
|
||||
}>({
|
||||
flowEngine: flowEngine,
|
||||
});
|
||||
|
||||
const startSelect = (selectType: string, selector?: Selector) => {
|
||||
if (selector) {
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 { Button, Divider } from 'antd';
|
||||
import { LeftOutlined, RightOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { AddSubModelButton, FlowModelRenderer, useFlowEngine } from '@nocobase/flow-engine';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useShortcuts } from './useShortcuts';
|
||||
import { useDesignable, useToken } from '@nocobase/client';
|
||||
import { useAIEmployeesContext } from '../AIEmployeesProvider';
|
||||
import { AIEmployeeListItem } from '../AIEmployeeListItem';
|
||||
|
||||
export const ShortcutList: React.FC = () => {
|
||||
const flowEngine = useFlowEngine();
|
||||
const { designable } = useDesignable();
|
||||
const { shortcuts, uid, builtIn } = useShortcuts();
|
||||
const { token } = useToken();
|
||||
const designMode = designable && !builtIn;
|
||||
|
||||
const {
|
||||
aiEmployees,
|
||||
service: { loading },
|
||||
} = useAIEmployeesContext();
|
||||
|
||||
const [folded, setFolded] = useState(false);
|
||||
|
||||
const model = useMemo(() => {
|
||||
return flowEngine.createModel({
|
||||
uid: `ai-shortcuts-${uid}`,
|
||||
use: 'AIEmployeeShortcutListModel',
|
||||
subModels: {
|
||||
shortcuts,
|
||||
},
|
||||
});
|
||||
}, [flowEngine, shortcuts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{shortcuts.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="text"
|
||||
color="default"
|
||||
icon={!folded ? <RightOutlined /> : <LeftOutlined />}
|
||||
style={{
|
||||
height: '52px',
|
||||
width: '12px',
|
||||
fontSize: token.fontSizeSM,
|
||||
}}
|
||||
onClick={() => setFolded(!folded)}
|
||||
/>
|
||||
{!folded && <FlowModelRenderer model={model} />}
|
||||
</>
|
||||
)}
|
||||
{!builtIn && (
|
||||
<AddSubModelButton
|
||||
model={model}
|
||||
subModelKey={'shortcuts'}
|
||||
items={async () => {
|
||||
return loading
|
||||
? []
|
||||
: aiEmployees.map((aiEmployee) => ({
|
||||
key: aiEmployee.username,
|
||||
label: <AIEmployeeListItem aiEmployee={aiEmployee} />,
|
||||
createModelOptions: {
|
||||
use: 'AIEmployeeShortcutModel',
|
||||
props: {
|
||||
aiEmployee: {
|
||||
username: 'form_assistant',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
variant="dashed"
|
||||
color="default"
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
color: 'var(--colorSettings)',
|
||||
borderColor: 'var(--colorSettings)',
|
||||
background: 'transparent',
|
||||
}}
|
||||
/>
|
||||
</AddSubModelButton>
|
||||
)}
|
||||
{(shortcuts.length || designMode) && (
|
||||
<Divider
|
||||
type="vertical"
|
||||
style={{
|
||||
height: '50px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 { useParams, useLocation } from 'react-router-dom';
|
||||
|
||||
export const useShortcuts = () => {
|
||||
const { name } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
if (pathname === '/admin/settings/data-source-manager/main/collections') {
|
||||
return {
|
||||
uid: 'data-modeling',
|
||||
builtIn: true,
|
||||
shortcuts: [
|
||||
{
|
||||
uid: 'orin',
|
||||
use: 'AIEmployeeShortcutModel',
|
||||
props: {
|
||||
aiEmployee: {
|
||||
username: 'orin',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (name === '0krytmcef9g') {
|
||||
return {
|
||||
uid: name,
|
||||
shortcuts: [
|
||||
{
|
||||
uid: 'form_assistant',
|
||||
use: 'AIEmployeeShortcutModel',
|
||||
props: {
|
||||
aiEmployee: {
|
||||
username: 'form_assistant',
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
message: {
|
||||
workContext: [
|
||||
{
|
||||
type: 'flow-model',
|
||||
uid: 'e3b85ce6e70',
|
||||
title: 'Form(Add new): Users',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
uid: name,
|
||||
builtIn: true,
|
||||
shortcuts: [],
|
||||
};
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { BubbleProps } from '@ant-design/x';
|
||||
import { Application } from '@nocobase/client';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type Selector = {
|
||||
@@ -37,7 +38,7 @@ export type ContextItem = {
|
||||
type: string;
|
||||
uid: string;
|
||||
title: string;
|
||||
content: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
export type ToolCall<T> = {
|
||||
@@ -112,9 +113,7 @@ export type Task = {
|
||||
|
||||
export type TriggerTaskOptions = {
|
||||
aiEmployee?: AIEmployee;
|
||||
tasks: Task[];
|
||||
variables?: Record<string, any>;
|
||||
localVariables?: Record<string, any>;
|
||||
tasks?: Task[];
|
||||
};
|
||||
|
||||
export type Tool = {
|
||||
@@ -163,4 +162,5 @@ export type WorkContextOptions = {
|
||||
};
|
||||
actions?: ActionOptions[];
|
||||
children?: Record<string, Omit<WorkContextOptions, 'children'>>;
|
||||
getContent?: (app: Application, item: ContextItem) => string;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ import { PermissionsTab } from './ai-employees/permissions/PermissionsTab';
|
||||
import { anthropicProviderOptions } from './llm-providers/anthropic';
|
||||
import { AIEmployeeShortcutListModel, AIEmployeeShortcutModel } from './ai-employees/flow/models';
|
||||
import { defineCollectionsTool } from './ai-employees/data-modeling/tools';
|
||||
import { FlowModelsContext } from './ai-employees/context/flow-models';
|
||||
import { formFillerTool } from './ai-employees/form-filler/tools';
|
||||
const { AIEmployeesProvider } = lazy(() => import('./ai-employees/AIEmployeesProvider'), 'AIEmployeesProvider');
|
||||
const { Employees } = lazy(() => import('./ai-employees/manager/Employees'), 'Employees');
|
||||
const { LLMServices } = lazy(() => import('./llm-services/LLMServices'), 'LLMServices');
|
||||
@@ -108,22 +110,9 @@ export class PluginAIClient extends Plugin {
|
||||
Component: StructuredOutputSettings,
|
||||
});
|
||||
|
||||
// this.aiManager.registerWorkContext('classic-pages', ClassicPagesContext);
|
||||
// this.aiManager.registerWorkContext('collection-definitions', CollectionDefinitionsContext);
|
||||
this.aiManager.registerWorkContext('flow-model', FlowModelsContext);
|
||||
this.aiManager.registerTool(...defineCollectionsTool);
|
||||
this.aiManager.registerTool('frontEnd', 'formFiller', {
|
||||
invoke: (ctx, params) => {
|
||||
const { form: uid, data } = params;
|
||||
if (!uid || !data) {
|
||||
return;
|
||||
}
|
||||
const form = ctx[uid]?.form;
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
form.setValues(data);
|
||||
},
|
||||
});
|
||||
this.aiManager.registerTool(...formFillerTool);
|
||||
}
|
||||
|
||||
setupWorkflow() {
|
||||
|
||||
-76
@@ -176,82 +176,6 @@ export const collectionTableSchema: ISchema = {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
aiEmployee: {
|
||||
type: 'void',
|
||||
'x-component': 'AIEmployeeButton',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'aiEmployees:button',
|
||||
'x-component-props': {
|
||||
username: 'orin',
|
||||
tasks: [
|
||||
{
|
||||
message: {
|
||||
workContext: [],
|
||||
attachments: [],
|
||||
system: `## Task: Create new schema from scenario
|
||||
|
||||
The user will describe a business scenario. Your task is to define the full schema from scratch.
|
||||
|
||||
- Confirm all entities and required fields
|
||||
- Clarify the relationships between entities
|
||||
- Normalize the structure
|
||||
- Output a complete <collections> block
|
||||
|
||||
Avoid defining system fields already provided by templates.`,
|
||||
user: 'Start from a business scenario and generate normalized collections',
|
||||
},
|
||||
title: 'Start from a business scenario and generate normalized collections',
|
||||
autoSend: true,
|
||||
},
|
||||
{
|
||||
message: {
|
||||
workContext: [],
|
||||
attachments: [],
|
||||
user: 'Add or adjust fields and relationships in your current database',
|
||||
system: `## Task: Modify or expand existing collections
|
||||
|
||||
The user provides part of the data model and wants to modify it.
|
||||
|
||||
- Ask what changes they want: add tables, add fields, update relations, rename, etc.
|
||||
- Only return the changed parts – do not repeat existing schema
|
||||
- Wrap new/updated collections in <collections> tags
|
||||
- Do not remove or rename unless explicitly instructed
|
||||
|
||||
Example: Add a new relation to "projects" and a new table:
|
||||
|
||||
<collections>
|
||||
[
|
||||
{
|
||||
name: "projects",
|
||||
fields: [
|
||||
{
|
||||
name: "ownerId",
|
||||
title: "Owner",
|
||||
type: "belongsTo",
|
||||
interface: "m2o",
|
||||
target: "users",
|
||||
foreignKey: "ownerId",
|
||||
targetKey: "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "project_tags",
|
||||
title: "Project Tags",
|
||||
template: "general",
|
||||
fields: [
|
||||
{ name: "name", title: "Name", type: "string", interface: "input" }
|
||||
]
|
||||
}
|
||||
]
|
||||
</collections>`,
|
||||
},
|
||||
title: 'Add or adjust fields and relationships in your current database',
|
||||
autoSend: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[uid()]: {
|
||||
|
||||
Reference in New Issue
Block a user