diff --git a/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx b/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx index 87221bb14ce..9d0de01765e 100644 --- a/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx +++ b/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx @@ -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; 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 b27376a9286..ed6f0ca4d7b 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,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 (
- {/* */} + {!selectable ? ( - } + { setOpen(!open); }} /> ) : ( - { + 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 { diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts index e7369cde461..2e40293e8e5 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/utils.ts @@ -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, - localVariables?: Record, ) => { 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__'] || '/'; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/context/data-modeling.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/context/data-modeling.tsx deleted file mode 100644 index 807e052cf5c..00000000000 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/context/data-modeling.tsx +++ /dev/null @@ -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: , - label: tval('Modern pages', { ns: pkg.name }), - }, - children: { - // form: { - // menu: { - // icon: , - // Component: ({ onAdd }) => { - // const t = useT(); - // const { t: collectionT } = useTranslation('lm-collections'); - // const { startSelect, ctx } = useAISelectionContext(); - // return ( - //
{ - // 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')} - //
- // ); - // }, - // }, - // tag: { - // Component: ({ item }) => { - // const t = useT(); - // return ( - // <> - // {t('Form')} ({item.title}) - // - // ); - // }, - // }, - // actions: [ - // { - // responseType: 'json', - // Component: ({ item, message, value }) => { - // const t = useT(); - // const { ctx } = useAISelectionContext(); - // - // return ( - // - // ); - // }, - // }, - // ], - // }, - users: { - menu: { - Component: ({ onAdd }) => { - const t = useT(); - const { t: collectionT } = useTranslation('lm-collections'); - const { startSelect } = useAISelectionContext(); - return {t('Users #f61f6cda073')}; - }, - }, - tag: { - Component: ({ item }) => { - return ( - <> - {item.title} - - ); - }, - }, - }, - roles: { - menu: { - Component: ({ onAdd }) => { - const t = useT(); - const { startSelect } = useAISelectionContext(); - - return {t('Roles #4cb2eb66a99')}; - }, - }, - }, - }, -}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/context/flow-models.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/context/flow-models.tsx new file mode 100644 index 00000000000..8bc89b579fb --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/context/flow-models.tsx @@ -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 ( + <> + {model.title} + + ); + }, + }, + getContent: (app, { uid }) => { + const model = app.flowEngine.getModel(uid); + if (!model) { + return ''; + } + return JSON.stringify(parseModel(model)); + }, +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutListModel.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutListModel.tsx index ed8da402772..cdb6e181043 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutListModel.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutListModel.tsx @@ -18,24 +18,13 @@ type AIEmployeeShortcutListModelStructure = { }; export class AIEmployeeShortcutListModel extends FlowModel { - // onInit() { - // this.addSubModel('shortcuts', { - // uid: 'data-modeling', - // use: 'AIEmployeeShortcutModel', - // props: { - // avatar: 'notion-3-male', - // }, - // }); - // } - // - // render() { - // return ( - //
- // {this.mapSubModels('shortcuts', (shortcut) => { - // console.log(shortcut.props); - // return ; - // })} - //
- // ); - // } + render() { + return ( +
+ {this.mapSubModels('shortcuts', (shortcut) => { + return ; + })} +
+ ); + } } diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx index 367330e12d2..9c9b775662e 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/flow/models/AIEmployeeShortcutModel.tsx @@ -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 = ({ 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 ( + + } placement="topLeft"> + setFocus(true)} + onMouseLeave={() => setFocus(false)} + onClick={() => { + triggerTask({ aiEmployee, tasks }); + }} + /> + + + ); +}; export class AIEmployeeShortcutModel extends FlowModel { + public declare props: TriggerTaskOptions; + render() { - // const { avatar } = this.props; - // console.log(this.props, this.stepParams, this.getProps()); - // return ( - // } - // style={{ - // insetInlineEnd: '94px', - // }} - // /> - // ); + return ; } } diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/form-filler/tools/index.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/form-filler/tools/index.ts new file mode 100644 index 00000000000..761eb59a6b2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/form-filler/tools/index.ts @@ -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); + }, + }, +]; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.png b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.png index d40cf8c92e5..1273b3ab8a1 100644 Binary files a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.png and b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.png differ 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 0ee6e55f6d4..56f618a0250 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 @@ -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(null); + const flowEngine = useFlowEngine(); const [ctx, setCtx] = useState<{ [key: string]: Record; - }>({}); + }>({ + flowEngine: flowEngine, + }); const startSelect = (selectType: string, selector?: Selector) => { if (selector) { diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx new file mode 100644 index 00000000000..5bf2c8016ca --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/shortcuts/ShortcutList.tsx @@ -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 && ( + <> +