diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/createDetailsWithPaginationUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/createDetailsWithPaginationUISchema.ts index d48f5dda67..479453bdbe 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/createDetailsWithPaginationUISchema.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/createDetailsWithPaginationUISchema.ts @@ -50,6 +50,17 @@ export function createDetailsWithPaginationUISchema(options: { 'x-read-pretty': true, 'x-use-component-props': 'useDetailsWithPaginationProps', properties: { + [uid()]: { + type: 'void', + 'x-initializer': hideActionInitializer ? undefined : 'aiEmployees:configure', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + properties: {}, + }, [uid()]: { type: 'void', 'x-initializer': hideActionInitializer ? undefined : 'details:configureActions', diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts index 914565ae77..c902caf3de 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-single/createDetailsUISchema.ts @@ -51,6 +51,17 @@ export function createDetailsUISchema(options: { 'x-read-pretty': true, 'x-use-component-props': 'useDetailsProps', properties: { + [uid()]: { + type: 'void', + 'x-initializer': 'aiEmployees:configure', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + properties: {}, + }, [uid()]: { type: 'void', 'x-initializer': 'details:configureActions', diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts b/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts index 2d3f37234c..e1e097316a 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/createCreateFormBlockUISchema.ts @@ -60,6 +60,18 @@ export function createCreateFormBlockUISchema(options: CreateFormBlockUISchemaOp 'x-initializer': 'form:configureFields', properties: {}, }, + [uid()]: { + type: 'void', + 'x-initializer': 'aiEmployees:configure', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginBottom: 8, + }, + }, + properties: {}, + }, [uid()]: { type: 'void', 'x-initializer': 'createForm:configureActions', diff --git a/packages/core/logger/src/request-logger.ts b/packages/core/logger/src/request-logger.ts index 81a07ec3ac..ad6bef9185 100644 --- a/packages/core/logger/src/request-logger.ts +++ b/packages/core/logger/src/request-logger.ts @@ -53,6 +53,8 @@ export const requestLogger = (appName: string, requestLogger: Logger, options?: app: appName, reqId, }); + ctx.res.setHeader('X-Request-Id', reqId); + let error: Error; try { await next(); @@ -82,8 +84,6 @@ export const requestLogger = (appName: string, requestLogger: Logger, options?: } } - ctx.res.setHeader('X-Request-Id', reqId); - if (error) { throw error; } diff --git a/packages/plugins/@nocobase/plugin-ai/package.json b/packages/plugins/@nocobase/plugin-ai/package.json index 32170b6e2f..20853a6328 100644 --- a/packages/plugins/@nocobase/plugin-ai/package.json +++ b/packages/plugins/@nocobase/plugin-ai/package.json @@ -13,8 +13,10 @@ "@nocobase/test": "1.x" }, "devDependencies": { + "@ant-design/x": "^1.0.5", "@langchain/core": "^0.3.39", "@langchain/deepseek": "^0.0.1", - "@langchain/openai": "^0.4.3" + "@langchain/openai": "^0.4.3", + "snowflake-id": "^1.1.0" } } 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 new file mode 100644 index 0000000000..073e5d4591 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/AIEmployeesProvider.tsx @@ -0,0 +1,55 @@ +/** + * 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, { useContext } from 'react'; +import { createContext } from 'react'; +import { ChatBoxProvider } from './chatbox/ChatBoxProvider'; +import { useAPIClient, useRequest } from '@nocobase/client'; + +export type AIEmployee = { + username: string; + nickname?: string; + avatar?: string; + bio?: string; + greeting?: string; +}; + +export const AIEmployeesContext = createContext<{ + aiEmployees: AIEmployee[]; + setAIEmployees: (aiEmployees: AIEmployee[]) => void; +}>({} as any); + +export const AIEmployeesProvider: React.FC<{ + children: React.ReactNode; +}> = (props) => { + const [aiEmployees, setAIEmployees] = React.useState(null); + + return ( + + {props.children} + + ); +}; + +export const useAIEmployeesContext = () => { + const { aiEmployees, setAIEmployees } = useContext(AIEmployeesContext); + const api = useAPIClient(); + const service = useRequest( + () => + api + .resource('aiEmployees') + .list() + .then((res) => res?.data?.data), + { + ready: !aiEmployees, + onSuccess: (aiEmployees) => setAIEmployees(aiEmployees), + }, + ); + return { aiEmployees, service }; +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars.ts new file mode 100644 index 0000000000..4d6b99937b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars.ts @@ -0,0 +1,14 @@ +/** + * 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. + */ + +// @ts-ignore +export const avatars = import.meta.webpackContext('./avatars', { + recursive: false, + regExp: /\.svg$/, +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/abmafbcptm.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/abmafbcptm.svg new file mode 100644 index 0000000000..e2e1ab7661 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/abmafbcptm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ajdizlnsiu.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ajdizlnsiu.svg new file mode 100644 index 0000000000..030e03a49d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ajdizlnsiu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/anucruwfgr.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/anucruwfgr.svg new file mode 100644 index 0000000000..180790fec3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/anucruwfgr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/argbpwjzdz.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/argbpwjzdz.svg new file mode 100644 index 0000000000..beaca8d35b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/argbpwjzdz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bhqqwhpqej.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bhqqwhpqej.svg new file mode 100644 index 0000000000..e99fc4f2cc --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bhqqwhpqej.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bnxoomloyy.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bnxoomloyy.svg new file mode 100644 index 0000000000..f9bcd1eeda --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bnxoomloyy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bwfxiaaovf.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bwfxiaaovf.svg new file mode 100644 index 0000000000..ce73889920 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/bwfxiaaovf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ccivdooxdv.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ccivdooxdv.svg new file mode 100644 index 0000000000..c349326580 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ccivdooxdv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/cfevvrxupu.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/cfevvrxupu.svg new file mode 100644 index 0000000000..8b779b353e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/cfevvrxupu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/coepfxaanh.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/coepfxaanh.svg new file mode 100644 index 0000000000..fa23100f0c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/coepfxaanh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/dualkwfnzv.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/dualkwfnzv.svg new file mode 100644 index 0000000000..59df868726 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/dualkwfnzv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/eedrwbyvin.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/eedrwbyvin.svg new file mode 100644 index 0000000000..4dbd7b0a47 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/eedrwbyvin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/emcaqzwvzh.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/emcaqzwvzh.svg new file mode 100644 index 0000000000..6987ed8281 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/emcaqzwvzh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/eponjcjakq.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/eponjcjakq.svg new file mode 100644 index 0000000000..fb3218b4ed --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/eponjcjakq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fcaezgnkth.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fcaezgnkth.svg new file mode 100644 index 0000000000..bfe1b89f5b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fcaezgnkth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fhqvzdtlca.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fhqvzdtlca.svg new file mode 100644 index 0000000000..579b5b14a2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fhqvzdtlca.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fsplpzoene.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fsplpzoene.svg new file mode 100644 index 0000000000..ac076e64e0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/fsplpzoene.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/hmahcmijfo.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/hmahcmijfo.svg new file mode 100644 index 0000000000..57a41cafce --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/hmahcmijfo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ileamskdhp.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ileamskdhp.svg new file mode 100644 index 0000000000..874e19868f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ileamskdhp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/immbuifjvl.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/immbuifjvl.svg new file mode 100644 index 0000000000..dfbcc0311e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/immbuifjvl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/imnixyeazo.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/imnixyeazo.svg new file mode 100644 index 0000000000..6271182f29 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/imnixyeazo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/jftlmuvxpf.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/jftlmuvxpf.svg new file mode 100644 index 0000000000..59be8c669e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/jftlmuvxpf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/jrxtmsruwo.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/jrxtmsruwo.svg new file mode 100644 index 0000000000..cf644661f4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/jrxtmsruwo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/koebxgxzvw.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/koebxgxzvw.svg new file mode 100644 index 0000000000..8bddee422f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/koebxgxzvw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/kyaiswqpba.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/kyaiswqpba.svg new file mode 100644 index 0000000000..3c35f3c795 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/kyaiswqpba.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/lcnwftfyoe.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/lcnwftfyoe.svg new file mode 100644 index 0000000000..ad4a26d09b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/lcnwftfyoe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/leyvuiomql.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/leyvuiomql.svg new file mode 100644 index 0000000000..2bec4c103e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/leyvuiomql.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/lkusmjvkgn.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/lkusmjvkgn.svg new file mode 100644 index 0000000000..5af136d808 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/lkusmjvkgn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/msjhxiropq.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/msjhxiropq.svg new file mode 100644 index 0000000000..77841509ae --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/msjhxiropq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/naqkifyjca.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/naqkifyjca.svg new file mode 100644 index 0000000000..dde382094b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/naqkifyjca.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/nroldyhane.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/nroldyhane.svg new file mode 100644 index 0000000000..195286c8e5 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/nroldyhane.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/nsduhnntle.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/nsduhnntle.svg new file mode 100644 index 0000000000..d62a60217b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/nsduhnntle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/omndypzywd.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/omndypzywd.svg new file mode 100644 index 0000000000..1d7a11e7b9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/omndypzywd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ptrxxaxdet.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ptrxxaxdet.svg new file mode 100644 index 0000000000..45ff2fbb1f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ptrxxaxdet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/qgcyebfhha.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/qgcyebfhha.svg new file mode 100644 index 0000000000..abc99f4980 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/qgcyebfhha.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/qgozfteauh.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/qgozfteauh.svg new file mode 100644 index 0000000000..dd0ab58d8c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/qgozfteauh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/rdhvhwsxvo.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/rdhvhwsxvo.svg new file mode 100644 index 0000000000..6a54285f6d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/rdhvhwsxvo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/rzjwqyjgwi.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/rzjwqyjgwi.svg new file mode 100644 index 0000000000..f75dbf1890 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/rzjwqyjgwi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/spgxivaufc.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/spgxivaufc.svg new file mode 100644 index 0000000000..99fe839a83 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/spgxivaufc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/sswtdnsekr.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/sswtdnsekr.svg new file mode 100644 index 0000000000..a5a9f33883 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/sswtdnsekr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/tljoxuhpea.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/tljoxuhpea.svg new file mode 100644 index 0000000000..17a9468fb9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/tljoxuhpea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/tpypquucse.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/tpypquucse.svg new file mode 100644 index 0000000000..c3d094a0f2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/tpypquucse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ttiwovderq.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ttiwovderq.svg new file mode 100644 index 0000000000..4bb4bbf2ca --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/ttiwovderq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/txakiuxyas.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/txakiuxyas.svg new file mode 100644 index 0000000000..787ad9030a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/txakiuxyas.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vmnqzbjkec.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vmnqzbjkec.svg new file mode 100644 index 0000000000..79fd33ff96 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vmnqzbjkec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vxkjfijmuv.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vxkjfijmuv.svg new file mode 100644 index 0000000000..505b7ac43b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vxkjfijmuv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vynriwqtvl.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vynriwqtvl.svg new file mode 100644 index 0000000000..015ab74d5a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/vynriwqtvl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/whjohjgdji.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/whjohjgdji.svg new file mode 100644 index 0000000000..bd32b79d02 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/whjohjgdji.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wpuvnvazwl.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wpuvnvazwl.svg new file mode 100644 index 0000000000..d9d2d7cf3a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wpuvnvazwl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wsmuunwmas.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wsmuunwmas.svg new file mode 100644 index 0000000000..cbb03a9d1a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wsmuunwmas.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wwqpeyvsck.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wwqpeyvsck.svg new file mode 100644 index 0000000000..d8419eafd5 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/wwqpeyvsck.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xadcrykkbn.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xadcrykkbn.svg new file mode 100644 index 0000000000..915fc04ae0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xadcrykkbn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xmigbxgynv.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xmigbxgynv.svg new file mode 100644 index 0000000000..d3b4f0e8e8 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xmigbxgynv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xnphievblj.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xnphievblj.svg new file mode 100644 index 0000000000..493f15c81d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xnphievblj.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xyrijcehru.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xyrijcehru.svg new file mode 100644 index 0000000000..51ffb13730 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/xyrijcehru.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/yydkouapab.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/yydkouapab.svg new file mode 100644 index 0000000000..ec57b6508e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/yydkouapab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/zbduiufjtb.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/zbduiufjtb.svg new file mode 100644 index 0000000000..5514376fcc --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/zbduiufjtb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/zvcdwhyylg.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/zvcdwhyylg.svg new file mode 100644 index 0000000000..d655208c1a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/avatars/zvcdwhyylg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Attachment.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Attachment.tsx new file mode 100644 index 0000000000..81d18a1ce0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/Attachment.tsx @@ -0,0 +1,42 @@ +/** + * 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 { Tag } from 'antd'; +import { BuildOutlined } from '@ant-design/icons'; + +export type AttachmentType = 'image' | 'uiSchema'; +export type AttachmentProps = { + type: AttachmentType; + content: string; +}; + +export const Attachment: React.FC< + AttachmentProps & { + closeable?: boolean; + onClose?: () => void; + } +> = ({ type, content, closeable, onClose }) => { + let prefix: React.ReactNode; + switch (type) { + case 'uiSchema': + prefix = ( + <> + UI Schema {'>'}{' '} + + ); + break; + } + return ( + + {prefix} + {content} + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBox.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBox.tsx new file mode 100644 index 0000000000..0642359bd7 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBox.tsx @@ -0,0 +1,389 @@ +/** + * 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, { useContext, useEffect, useState } from 'react'; +import { Layout, Card, Divider, Button, Avatar, List, Input, Popover, Empty, Spin, Modal, Tag } from 'antd'; +import { Conversations, Sender, Attachments, Bubble } from '@ant-design/x'; +import type { ConversationsProps } from '@ant-design/x'; +import { + CloseOutlined, + ExpandOutlined, + EditOutlined, + LayoutOutlined, + DeleteOutlined, + BuildOutlined, +} from '@ant-design/icons'; +import { useAPIClient, useRequest, useToken } from '@nocobase/client'; +import { useT } from '../../locale'; +import { ChatBoxContext } from './ChatBoxProvider'; +const { Header, Footer, Sider, Content } = Layout; +import { avatars } from '../avatars'; +import { AIEmployee, AIEmployeesContext, useAIEmployeesContext } from '../AIEmployeesProvider'; +import { css } from '@emotion/css'; +import { ReactComponent as EmptyIcon } from '../empty-icon.svg'; +import { Attachment } from './Attachment'; + +export const ChatBox: React.FC = () => { + const api = useAPIClient(); + const { + send, + setOpen, + filterEmployee, + setFilterEmployee, + conversations: conversationsService, + currentConversation, + setCurrentConversation, + messages, + setMessages, + roles, + attachments, + setAttachments, + responseLoading, + senderRef, + } = useContext(ChatBoxContext); + const { loading: ConversationsLoading, data: conversationsRes } = conversationsService; + const { + aiEmployees, + service: { loading }, + } = useAIEmployeesContext(); + const t = useT(); + const { token } = useToken(); + const [showConversations, setShowConversations] = useState(true); + const aiEmployeesList = [{ username: 'all' } as AIEmployee, ...(aiEmployees || [])]; + const conversations: ConversationsProps['items'] = (conversationsRes || []).map((conversation) => ({ + key: conversation.sessionId, + label: conversation.title, + timestamp: new Date(conversation.updatedAt).getTime(), + })); + + const deleteConversation = async (sessionId: string) => { + await api.resource('aiConversations').destroy({ + filterByTk: sessionId, + }); + conversationsService.refresh(); + setCurrentConversation(undefined); + setMessages([]); + }; + + const getMessages = async (sessionId: string) => { + const res = await api.resource('aiConversations').getMessages({ + sessionId, + }); + const messages = res?.data?.data; + if (!messages) { + return; + } + setMessages(messages.reverse()); + }; + + return ( +
+ + + + { + const highlight = + aiEmployee.username === filterEmployee + ? `color: ${token.colorPrimary}; + border-color: ${token.colorPrimary};` + : ''; + return aiEmployee.username === 'all' ? ( + + ) : ( + +
+ +
+ {aiEmployee.nickname} +
+
+ + {t('Bio')} + +

{aiEmployee.bio}

+
+ } + > + + + ); + }} + /> + + + +
+ +
+ + + {conversations && conversations.length ? ( + { + if (sessionId === currentConversation) { + return; + } + setCurrentConversation(sessionId); + getMessages(sessionId); + }} + items={conversations} + menu={(conversation) => ({ + items: [ + { + label: 'Delete', + key: 'delete', + icon: , + }, + ], + onClick: ({ key }) => { + switch (key) { + case 'delete': + Modal.confirm({ + title: t('Delete this conversation?'), + content: t('Are you sure to delete this conversation?'), + onOk: () => deleteConversation(conversation.key), + }); + break; + } + }, + })} + /> + ) : ( + + )} + + +
+
+ +
+
+
+
+
+
+ + {messages?.length ? ( + + ) : ( +
+ +
+ )} +
+
+ + send({ + sessionId: currentConversation, + aiEmployee: { username: filterEmployee }, + messages: [ + { + type: 'text', + content, + }, + ], + }) + } + header={ + attachments.length ? ( +
+ {attachments.map((attachment, index) => { + return ( + { + setAttachments(attachments.filter((_, i) => i !== index)); + }} + {...attachment} + /> + ); + })} +
+ ) : null + } + disabled={filterEmployee === 'all' && !currentConversation} + placeholder={filterEmployee === 'all' && !currentConversation ? t('Please choose an AI employee.') : ''} + loading={responseLoading} + /> +
+ {/*
*/} + {/* */} + {/* */} + {/* */} + {/* */} + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxProvider.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxProvider.tsx new file mode 100644 index 0000000000..09e2c40e4a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/ChatBoxProvider.tsx @@ -0,0 +1,352 @@ +/** + * 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, { createContext, useContext, useEffect, useState, useRef } from 'react'; +import { FloatButton, Avatar, Typography, GetProp, GetRef, Button } from 'antd'; +import type { BubbleProps, Sender } from '@ant-design/x'; +import { Bubble } from '@ant-design/x'; +import { CurrentUserContext, useAPIClient, useLocalVariables, useRequest, useVariables } from '@nocobase/client'; +import { ChatBox } from './ChatBox'; +import icon from '../icon.svg'; +import { css } from '@emotion/css'; +import { AIEmployee, AIEmployeesContext } from '../AIEmployeesProvider'; +import { avatars } from '../avatars'; +import { uid } from '@formily/shared'; +import { useT } from '../../locale'; +import { Attachment, AttachmentProps, AttachmentType } from './Attachment'; +const { Paragraph } = Typography; + +type Conversation = { + sessionId: string; + title: string; + updatedAt: string; +}; + +type MessageType = 'text' | AttachmentType; +type Message = BubbleProps & { key?: string | number; role?: string }; +type Action = { + content: string; + onClick: (content: string) => void; +}; + +type SendOptions = { + sessionId?: string; + greeting?: boolean; + aiEmployee?: AIEmployee; + messages: { + type: MessageType; + content: string; + }[]; +}; + +const aiEmployeeRole = (aiEmployee: AIEmployee) => ({ + placement: 'start', + avatar: aiEmployee.avatar ? : null, + typing: { step: 5, interval: 20 }, + style: { + maxWidth: 400, + marginInlineEnd: 48, + }, + styles: { + footer: { + width: '100%', + }, + }, + variant: 'borderless', + messageRender: (msg: any) => { + switch (msg.type) { + case 'text': + return ; + case 'action': + return ; + } + }, +}); + +export const ChatBoxContext = createContext<{ + setOpen: (open: boolean) => void; + open: boolean; + filterEmployee: string; + setFilterEmployee: React.Dispatch>; + conversations: { + loading: boolean; + data?: Conversation[]; + refresh: () => void; + }; + currentConversation: string; + setCurrentConversation: React.Dispatch>; + messages: Message[]; + setMessages: React.Dispatch>; + roles: { [role: string]: any }; + responseLoading: boolean; + attachments: AttachmentProps[]; + setAttachments: React.Dispatch>; + actions: Action[]; + setActions: React.Dispatch>; + senderRef: React.MutableRefObject>; + send(opts: SendOptions): void; +}>({} as any); + +export const ChatBoxProvider: React.FC<{ + children: React.ReactNode; +}> = (props) => { + const t = useT(); + const api = useAPIClient(); + const ctx = useContext(CurrentUserContext); + const { aiEmployees } = useContext(AIEmployeesContext); + const [openChatBox, setOpenChatBox] = useState(false); + const [messages, setMessages] = useState([]); + const [filterEmployee, setFilterEmployee] = useState('all'); + const [currentConversation, setCurrentConversation] = useState(); + const [responseLoading, setResponseLoading] = useState(false); + const [attachments, setAttachments] = useState([]); + const [actions, setActions] = useState([]); + const senderRef = useRef>(null); + const [roles, setRoles] = useState>({ + user: { + placement: 'end', + styles: { + content: { + maxWidth: '400px', + }, + }, + variant: 'borderless', + messageRender: (msg: any) => { + switch (msg.type) { + case 'text': + return ; + default: + return ; + } + }, + }, + }); + const conversations = useRequest( + () => + api + .resource('aiConversations') + .list({ + sort: ['-updatedAt'], + ...(filterEmployee !== 'all' + ? { + filter: { + 'aiEmployees.username': filterEmployee, + }, + } + : {}), + }) + .then((res) => res?.data?.data), + { + ready: openChatBox, + refreshDeps: [filterEmployee], + }, + ); + const send = async ({ sessionId, aiEmployee, messages: sendMsgs, greeting }: SendOptions) => { + setRoles((prev) => ({ + ...prev, + [aiEmployee.username]: aiEmployeeRole(aiEmployee), + })); + const msgs: Message[] = []; + if (greeting) { + msgs.push({ + key: uid(), + role: aiEmployee.username, + content: { + type: 'text', + content: aiEmployee.greeting || t('Default greeting message', { nickname: aiEmployee.nickname }), + }, + }); + setMessages(msgs); + } + if (!sendMsgs.length) { + senderRef.current?.focus(); + return; + } + if (attachments.length) { + msgs.push( + ...attachments.map((attachment) => ({ + key: uid(), + role: 'user', + content: attachment, + })), + ); + setMessages(msgs); + } + msgs.push(...sendMsgs.map((msg) => ({ key: uid(), role: 'user', content: msg }))); + setMessages(msgs); + if (!sessionId) { + const createRes = await api.resource('aiConversations').create({ + values: { + aiEmployees: [aiEmployee], + }, + }); + const conversation = createRes?.data?.data; + if (!conversation) { + return; + } + sessionId = conversation.sessionId; + setCurrentConversation(conversation.sessionId); + conversations.refresh(); + } + setAttachments([]); + setResponseLoading(true); + setMessages((prev) => [ + ...prev, + { + key: uid(), + role: aiEmployee.username, + content: { + type: 'text', + content: '', + }, + loading: true, + }, + ]); + const sendRes = await api.request({ + url: 'aiConversations:sendMessages', + method: 'POST', + headers: { + Accept: 'text/event-stream', + }, + data: { + aiEmployee: aiEmployee.username, + sessionId, + messages: msgs, + }, + responseType: 'stream', + adapter: 'fetch', + }); + if (!sendRes?.data) { + setResponseLoading(false); + return; + } + const reader = sendRes.data.getReader(); + const decoder = new TextDecoder(); + let result = ''; + // eslint-disable-next-line no-constant-condition + while (true) { + let content = ''; + const { done, value } = await reader.read(); + if (done) { + setResponseLoading(false); + break; + } + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n').filter(Boolean); + for (const line of lines) { + const data = JSON.parse(line.replace(/^data: /, '')); + if (data.type === 'content' && data.body) { + content += data.body; + } + } + result += content; + setMessages((prev) => { + const last = prev[prev.length - 1]; + // @ts-ignore + last.content.content = last.content.content + content; + last.loading = false; + return [...prev]; + }); + } + if (actions) { + setMessages((prev) => [ + ...prev, + ...actions.map((action) => ({ + key: uid(), + role: aiEmployee.username, + content: { + type: 'action', + content: action.content, + onClick: () => { + action.onClick(result); + }, + }, + })), + ]); + setActions([]); + } + }; + + useEffect(() => { + if (!aiEmployees) { + return; + } + const roles = aiEmployees.reduce((prev, aiEmployee) => { + return { + ...prev, + [aiEmployee.username]: aiEmployeeRole(aiEmployee), + }; + }, {}); + setRoles((prev) => ({ + ...prev, + ...roles, + })); + }, [aiEmployees]); + + if (!ctx?.data?.data) { + return <>{props.children}; + } + return ( + + {props.children} + {!openChatBox && ( +
+ + } + onClick={() => { + setOpenChatBox(true); + }} + shape="square" + /> +
+ )} + {openChatBox ? : null} +
+ ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/design-icon.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/design-icon.svg new file mode 100644 index 0000000000..d496912f01 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/design-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/empty-icon.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/empty-icon.svg new file mode 100644 index 0000000000..1c5f28fb78 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/empty-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.svg b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.svg new file mode 100644 index 0000000000..fb040ba43c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file 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 new file mode 100644 index 0000000000..d49e4924af --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/AIEmployeeButton.tsx @@ -0,0 +1,167 @@ +/** + * 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, { useContext } from 'react'; +import { Avatar, Tag, Popover, Divider, Button } from 'antd'; +import { avatars } from '../avatars'; +import { ChatBoxContext } from '../chatbox/ChatBoxProvider'; +import { AIEmployee } from '../AIEmployeesProvider'; +import { + SortableItem, + useBlockContext, + useLocalVariables, + useSchemaToolbarRender, + useToken, + useVariables, +} from '@nocobase/client'; +import { useFieldSchema } from '@formily/react'; +import { useT } from '../../locale'; +import { css } from '@emotion/css'; +import { useForm } from '@formily/react'; + +export const AIEmployeeButton: React.FC<{ + aiEmployee: AIEmployee; + extraInfo?: string; +}> = ({ aiEmployee, extraInfo }) => { + const t = useT(); + const { setOpen, send, setAttachments, setFilterEmployee, setCurrentConversation, setActions } = + useContext(ChatBoxContext); + const { token } = useToken(); + const fieldSchema = useFieldSchema(); + const { render } = useSchemaToolbarRender(fieldSchema); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const { name: blockType } = useBlockContext() || {}; + const form = useForm(); + + return ( + { + setOpen(true); + setCurrentConversation(undefined); + setFilterEmployee(aiEmployee.username); + setAttachments([]); + setActions([]); + const messages = []; + if (blockType === 'form') { + console.log(fieldSchema.parent.parent.toJSON()); + setAttachments((prev) => [ + ...prev, + { + type: 'uiSchema', + content: fieldSchema.parent.parent['x-uid'], + }, + ]); + setActions([ + { + content: 'Fill form', + onClick: (content) => { + try { + const values = content.replace('```json', '').replace('```', ''); + form.setValues(JSON.parse(values)); + } catch (error) { + console.log(error); + } + }, + }, + ]); + } + let message = fieldSchema['x-component-props']?.message; + if (message) { + message = await variables + ?.parseVariable(fieldSchema['x-component-props']?.message, localVariables) + .then(({ value }) => value); + messages.push({ + type: 'text', + content: message, + }); + } + send({ + aiEmployee, + messages, + greeting: true, + }); + }} + > + +
+ +
+ {aiEmployee.nickname} +
+
+ + {t('Bio')} + +

{aiEmployee.bio}

+ {extraInfo && ( + <> + + {t('Extra information')} + +

{extraInfo}

+ + )} + + } + > + +
+ {render()} +
+ ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/ConfigureAIEmployees.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/ConfigureAIEmployees.tsx new file mode 100644 index 0000000000..590565d311 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/initializer/ConfigureAIEmployees.tsx @@ -0,0 +1,78 @@ +/** + * 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, { useContext } from 'react'; +import { SchemaInitializer, useAPIClient, useRequest, useSchemaInitializer } from '@nocobase/client'; +import { ReactComponent as DesignIcon } from '../design-icon.svg'; +import { AIEmployee, AIEmployeesContext, useAIEmployeesContext } from '../AIEmployeesProvider'; +import { Spin, Avatar } from 'antd'; +import { avatars } from '../avatars'; + +export const configureAIEmployees = new SchemaInitializer({ + name: 'aiEmployees:configure', + title: '{{t("AI employees")}}', + icon: ( + + + + ), + style: { + marginLeft: 8, + }, + items: [ + { + name: 'ai-employees', + type: 'itemGroup', + useChildren() { + const { + aiEmployees, + service: { loading }, + } = useAIEmployeesContext(); + + return loading + ? [ + { + name: 'spin', + Component: () => , + }, + ] + : aiEmployees.map((aiEmployee) => ({ + name: aiEmployee.username, + title: aiEmployee.nickname, + icon: , + type: 'item', + useComponentProps() { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-component': 'AIEmployeeButton', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'aiEmployees:button', + 'x-component-props': { + aiEmployee, + }, + }); + }; + + return { + onClick: handleClick, + }; + }, + })); + }, + }, + ], +}); 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 new file mode 100644 index 0000000000..3d5c2bb63c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/AvatarSelect.tsx @@ -0,0 +1,110 @@ +/** + * 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, { 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'; + +export const Avatar: React.FC<{ + srcs: [string, string][]; + size?: 'small' | 'large'; + selectable?: boolean; + highlightItem?: string; + onClick?: (name: string) => void; +}> = ({ srcs, size, selectable, highlightItem, onClick }) => { + const { token } = useToken(); + const { wrapSSR, hashId, componentCls: prefixCls } = useUploadStyles(); + useUploadStyle(prefixCls); + + const list = + srcs?.map(([src, name], index) => ( +
+
onClick && onClick(name)} + className={cls( + `${prefixCls}-list-item`, + `${prefixCls}-list-item-done`, + `${prefixCls}-list-item-list-type-picture-card`, + highlightItem === name + ? css` + border-color: ${token.colorPrimary} !important; + ` + : '', + selectable + ? css` + cursor: pointer; + &:hover { + border-color: ${token.colorPrimary} !important; + } + ` + : '', + )} + > +
+ + + +
+
+
+ )) || []; + + return wrapSSR( +
+
{list}
+
, + ); +}; + +export const AvatarSelect: React.FC = () => { + const field = useField(); + const [current, setCurrent] = React.useState(avatars?.keys()[0]); + + useEffect(() => { + if (field.value) { + return; + } + field.setInitialValue(avatars?.keys()[0]); + }, [field]); + + useEffect(() => { + if (!field.value) { + return; + } + setCurrent(field.value); + }, [field.value]); + + return ( + <> +
+ +
+ [avatars(a), a])} + size="small" + selectable + highlightItem={current} + onClick={(name) => (field.value = name)} + /> + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/Employees.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/Employees.tsx new file mode 100644 index 0000000000..f8a25ac258 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/Employees.tsx @@ -0,0 +1,477 @@ +/** + * 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, { createContext, useContext, useMemo } from 'react'; +import { Card, Row, Col, Avatar, Input, Space, Button, Tabs, App, Spin, Empty } from 'antd'; +import { + CollectionRecordProvider, + SchemaComponent, + useAPIClient, + useActionContext, + useCollectionRecordData, + useRequest, + useToken, +} from '@nocobase/client'; +import { useT } from '../../locale'; +const { Meta } = Card; +import { css } from '@emotion/css'; +import { EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { AvatarSelect } from './AvatarSelect'; +import { useForm } from '@formily/react'; +import { createForm } from '@formily/core'; +import { uid } from '@formily/shared'; +import { avatars } from '../avatars'; +import { ModelSettings } from './ModelSettings'; + +const EmployeeContext = createContext(null); + +const AIEmployeeForm: React.FC = () => { + return ( + + ), + }, + // { + // key: 'skills', + // label: 'Skills', + // // children: ( + // // <> + // // ( + // // }> + // // } + // // title={item.title} + // // description={{item.type}} + // // /> + // // {item.description} + // // + // // )} + // // /> + // // + // // + // // + // // + // // ), + // }, + { + key: 'modelSettings', + label: 'Model Settings', + children: ( + + ), + }, + ]} + /> + ); +}; + +const useCreateFormProps = () => { + const form = useMemo( + () => + createForm({ + initialValues: { + username: `${uid()}`, + }, + }), + [], + ); + return { + form, + }; +}; + +const useEditFormProps = () => { + const record = useCollectionRecordData(); + const form = useMemo( + () => + createForm({ + initialValues: record, + }), + [record], + ); + return { + form, + }; +}; + +const useCancelActionProps = () => { + const { setVisible } = useActionContext(); + const form = useForm(); + return { + type: 'default', + onClick() { + setVisible(false); + form.reset(); + }, + }; +}; + +const useCreateActionProps = () => { + const { setVisible } = useActionContext(); + const { message } = App.useApp(); + const form = useForm(); + const api = useAPIClient(); + const { refresh } = useContext(EmployeeContext); + const t = useT(); + + return { + type: 'primary', + async onClick() { + await form.submit(); + const values = form.values; + await api.resource('aiEmployees').create({ + values, + }); + refresh(); + message.success(t('Saved successfully')); + setVisible(false); + form.reset(); + }, + }; +}; + +const useEditActionProps = () => { + const { setVisible } = useActionContext(); + const { message } = App.useApp(); + const form = useForm(); + 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({ + values, + filterByTk: values.username, + }); + refresh(); + message.success(t('Saved successfully')); + setVisible(false); + form.reset(); + }, + }; +}; + +export const Employees: React.FC = () => { + const t = useT(); + const { token } = useToken(); + const api = useAPIClient(); + const { data, loading, refresh } = useRequest< + { + username: string; + nickname: string; + bio: string; + avatar: string; + }[] + >(() => + api + .resource('aiEmployees') + .list() + .then((res) => res?.data?.data), + ); + + 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', + }, + 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', + }, + }, + }, + }, + }, + }, + }} + />, + , + ]} + > + : null} + title={employee.nickname} + description={employee.bio} + /> + + + + ))} + + ) : ( + + )} +
+ ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ModelSettings.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ModelSettings.tsx new file mode 100644 index 0000000000..b46eea3fd8 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/manager/ModelSettings.tsx @@ -0,0 +1,43 @@ +/** + * 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 { observer, useForm } from '@formily/react'; +import { useAPIClient, usePlugin, useRequest } from '@nocobase/client'; +import React from 'react'; +import PluginAIClient from '../../'; + +export const useModelSettingsForm = (provider: string) => { + const plugin = usePlugin(PluginAIClient); + const p = plugin.aiManager.llmProviders.get(provider); + return p?.components?.ModelSettingsForm; +}; + +export const ModelSettings = observer( + () => { + const form = useForm(); + const api = useAPIClient(); + const { data, loading } = useRequest<{ provider: string }>( + () => + api + .resource('llmServices') + .get({ filterByTk: form.values?.modelSettings?.llmService }) + .then((res) => res?.data?.data), + { + ready: !!form.values?.modelSettings?.llmService, + refreshDeps: [form.values?.modelSettings?.llmService], + }, + ); + const Component = useModelSettingsForm(data?.provider); + if (loading) { + return null; + } + return Component ? : null; + }, + { displayName: 'AIEmployeeModelSettingsForm' }, +); 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 new file mode 100644 index 0000000000..c66d802d1f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/settings/AIEmployeeButton.tsx @@ -0,0 +1,127 @@ +/** + * 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, { useMemo } from 'react'; +import { + SchemaSettings, + SchemaSettingsModalItem, + useBlockContext, + useCollection, + useCollectionFilterOptions, + useCollectionRecordData, + useSchemaSettings, +} from '@nocobase/client'; +import { useT } from '../../locale'; +import { avatars } from '../avatars'; +import { Card, Avatar } from 'antd'; +const { Meta } = Card; +import { Schema } from '@formily/react'; + +export const useAIEmployeeButtonVariableOptions = () => { + const collection = useCollection(); + const t = useT(); + const fieldsOptions = useCollectionFilterOptions(collection); + const recordData = useCollectionRecordData(); + const { name: blockType } = useBlockContext() || {}; + const fields = useMemo(() => { + return Schema.compile(fieldsOptions, { t }); + }, [fieldsOptions]); + return useMemo(() => { + return [ + recordData && { + name: 'currentRecord', + title: t('Current record'), + children: [...fields], + }, + blockType === 'form' && { + name: '$nForm', + title: t('Current form'), + children: [...fields], + }, + ].filter(Boolean); + }, [recordData, t, fields, blockType]); +}; + +export const aiEmployeeButtonSettings = new SchemaSettings({ + name: 'aiEmployees:button', + items: [ + { + name: 'edit', + Component: () => { + const t = useT(); + const { dn } = useSchemaSettings(); + const aiEmployee = dn.getSchemaAttribute('x-component-props.aiEmployee') || {}; + return ( + ( + + : null} + title={aiEmployee.nickname} + description={aiEmployee.bio} + /> + + ), + }, + message: { + type: 'string', + title: t('Message'), + 'x-decorator': 'FormItem', + 'x-component': 'Variable.RawTextArea', + 'x-component-props': { + scope: '{{ useAIEmployeeButtonVariableOptions }}', + fieldNames: { + value: 'name', + label: 'title', + }, + }, + default: dn.getSchemaAttribute('x-component-props.message'), + }, + extraInfo: { + type: 'string', + title: t('Extra Information'), + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + default: dn.getSchemaAttribute('x-component-props.extraInfo'), + }, + }, + }} + title={t('Edit')} + onSubmit={({ message, extraInfo }) => { + dn.deepMerge({ + 'x-uid': dn.getSchemaAttribute('x-uid'), + 'x-component-props': { + aiEmployee, + message, + extraInfo, + }, + }); + }} + /> + ); + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/chat-settings/Messages.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/chat-settings/Messages.tsx index b5930db52d..b1552f31a7 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/chat-settings/Messages.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/chat-settings/Messages.tsx @@ -14,12 +14,14 @@ import { tval } from '@nocobase/utils/client'; import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; import { useField, observer } from '@formily/react'; import { Field } from '@formily/core'; +import { WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client'; const UserMessage: React.FC = observer(() => { const t = useT(); return ( { } return ( import('./ai-employees/AIEmployeesProvider'), 'AIEmployeesProvider'); +const { Employees } = lazy(() => import('./ai-employees/manager/Employees'), 'Employees'); const { LLMServices } = lazy(() => import('./llm-services/LLMServices'), 'LLMServices'); const { MessagesSettings } = lazy(() => import('./chat-settings/Messages'), 'MessagesSettings'); -const { Chat } = lazy(() => import('./llm-providers/components/Chat'), 'Chat'); const { ModelSelect } = lazy(() => import('./llm-providers/components/ModelSelect'), 'ModelSelect'); +const { AIEmployeeButton } = lazy(() => import('./ai-employees/initializer/AIEmployeeButton'), 'AIEmployeeButton'); export class PluginAIClient extends Plugin { aiManager = new AIManager(); @@ -31,11 +36,21 @@ export class PluginAIClient extends Plugin { // You can get and modify the app instance here async load() { + this.app.use(AIEmployeesProvider); + this.app.addComponents({ + AIEmployeeButton, + }); this.app.pluginSettingsManager.add('ai', { - icon: 'RobotOutlined', - title: tval('AI integration', { ns: namespace }), + icon: 'TeamOutlined', + title: tval('AI employees', { ns: namespace }), aclSnippet: 'pm.ai', }); + this.app.pluginSettingsManager.add('ai.employees', { + icon: 'TeamOutlined', + title: tval('AI employees', { ns: namespace }), + aclSnippet: 'pm.ai.employees', + Component: Employees, + }); this.app.pluginSettingsManager.add('ai.llm-services', { icon: 'LinkOutlined', title: tval('LLM services', { ns: namespace }), @@ -43,6 +58,9 @@ export class PluginAIClient extends Plugin { Component: LLMServices, }); + this.app.schemaInitializerManager.add(configureAIEmployees); + this.app.schemaSettingsManager.add(aiEmployeeButtonSettings); + this.aiManager.registerLLMProvider('openai', openaiProviderOptions); this.aiManager.registerLLMProvider('deepseek', deepseekProviderOptions); this.aiManager.chatSettings.set('messages', { @@ -53,9 +71,10 @@ export class PluginAIClient extends Plugin { const workflow = this.app.pm.get('workflow') as PluginWorkflowClient; workflow.registerInstructionGroup('ai', { label: tval('AI', { ns: namespace }) }); workflow.registerInstruction('llm', LLMInstruction); + workflow.registerInstruction('ai-employee', AIEmployeeInstruction); } } export default PluginAIClient; -export { Chat, ModelSelect }; +export { ModelSelect }; export type { LLMProviderOptions } from './manager/ai-manager'; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/Messages.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/Messages.tsx deleted file mode 100644 index 1aa97d2151..0000000000 --- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/Messages.tsx +++ /dev/null @@ -1,250 +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 { SchemaComponent } from '@nocobase/client'; -import React from 'react'; -import { namespace, useT } from '../../locale'; -import { tval } from '@nocobase/utils/client'; -import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; -import { useField, observer } from '@formily/react'; -import { Field } from '@formily/core'; -import { WorkflowVariableInput } from '@nocobase/plugin-workflow/client'; - -const UserMessage: React.FC = observer(() => { - const t = useT(); - const field = useField(); - const type = field.query('.type').take() as Field; - - if (type.value === 'image_url') { - return ( - - ); - } - - return ( - - ); -}); - -const Content: React.FC = observer(() => { - const t = useT(); - const field = useField(); - const role = field.query('.role').take() as Field; - - if (role.value === 'user') { - return ( - - ); - } - return ( - - ); -}); - -export const Messages: React.FC = () => { - const t = useT(); - - return ( - - ); -}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/ModelSelect.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/ModelSelect.tsx index c511ebdafc..e2e3bc50b2 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/ModelSelect.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/ModelSelect.tsx @@ -15,7 +15,7 @@ import { Field } from '@formily/core'; export const ModelSelect: React.FC = () => { const field = useField(); - const form = useForm(); + const serviceField = field.query('.llmService').take() as Field; const api = useAPIClient(); const ctx = useActionContext(); const [options, setOptions] = useState([]); @@ -29,7 +29,7 @@ export const ModelSelect: React.FC = () => { api .resource('ai') .listModels({ - llmService: form.values?.llmService, + llmService: serviceField?.value, }) .then( (res) => @@ -42,8 +42,8 @@ export const ModelSelect: React.FC = () => { ), ), { - ready: !!form.values?.llmService && ctx.visible, - refreshDeps: [form.values?.llmService], + ready: !!serviceField?.value && ctx.visible, + refreshDeps: [serviceField?.value], onSuccess: (data) => setOptions(data), }, ); diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/StructuredOutput.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/StructuredOutput.tsx deleted file mode 100644 index 7b3d052f09..0000000000 --- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/components/StructuredOutput.tsx +++ /dev/null @@ -1,70 +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 { SchemaComponent } from '@nocobase/client'; -import React from 'react'; -import { useT } from '../../locale'; -import { WorkflowVariableJSON } from '@nocobase/plugin-workflow/client'; - -export const StructuredOutput: React.FC = () => { - const t = useT(); - return ( - - {t('Syntax references')}:{' '} - - JSON Schema - - - ), - 'x-decorator': 'FormItem', - 'x-component': 'WorkflowVariableJSON', - 'x-component-props': { - json5: true, - autoSize: { - minRows: 10, - }, - }, - }, - name: { - title: t('Name'), - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Input', - }, - description: { - title: t('Description'), - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Input.TextArea', - }, - strict: { - title: 'Strict', - type: 'boolean', - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - }, - }, - }, - }, - }} - /> - ); -}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/deepseek/ModelSettings.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/deepseek/ModelSettings.tsx index 6b913ca575..eca537ec29 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/deepseek/ModelSettings.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/deepseek/ModelSettings.tsx @@ -14,7 +14,6 @@ import { namespace, useT } from '../../locale'; import { Collapse } from 'antd'; import { WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client'; import { ModelSelect } from '../components/ModelSelect'; -import { Chat } from '../components/Chat'; const Options: React.FC = () => { const t = useT(); @@ -144,7 +143,7 @@ const Options: React.FC = () => { export const ModelSettingsForm: React.FC = () => { return ( { type: 'void', 'x-component': 'Options', }, - chat: { - type: 'void', - 'x-component': 'Chat', - }, }, }} /> diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/openai/ModelSettings.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/openai/ModelSettings.tsx index 7e54ec16a8..57af27e7cf 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/openai/ModelSettings.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/llm-providers/openai/ModelSettings.tsx @@ -14,7 +14,6 @@ import { namespace, useT } from '../../locale'; import { Collapse } from 'antd'; import { WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client'; import { ModelSelect } from '../components/ModelSelect'; -import { Chat } from '../components/Chat'; const Options: React.FC = () => { const t = useT(); @@ -148,7 +147,7 @@ const Options: React.FC = () => { export const ModelSettingsForm: React.FC = () => { return ( { type: 'void', 'x-component': 'Options', }, - chat: { - type: 'void', - 'x-component': 'Chat', - }, }, }} /> diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/locale.ts b/packages/plugins/@nocobase/plugin-ai/src/client/locale.ts index 9df687e97d..5a3ca3dfc3 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/locale.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/client/locale.ts @@ -15,7 +15,7 @@ export const namespace = pkg.name; export function useT() { const app = useApp(); - return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] }); + return (str: string, options?: any) => app.i18n.t(str, { ns: [pkg.name, 'client'], ...options }); } export function tStr(key: string) { diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/employee/index.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/employee/index.tsx new file mode 100644 index 0000000000..178bf85e1b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/employee/index.tsx @@ -0,0 +1,77 @@ +/** + * 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 { Instruction, WorkflowVariableRawTextArea } from '@nocobase/plugin-workflow/client'; +import { UserOutlined } from '@ant-design/icons'; +import { Card, Avatar } from 'antd'; +const { Meta } = Card; + +const AIEmployee = () => { + return ( + + } + title="LinguaBridge" + description="Translates customer messages, emails, and documents in real-time across multiple languages." + /> + + ); +}; + +export class AIEmployeeInstruction extends Instruction { + title = 'AI employee'; + type = 'ai-employee'; + group = 'ai'; + // @ts-ignore + icon = (); + fieldset = { + employee: { + type: 'string', + title: 'AI Employee', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'AIEmployee', + }, + message: { + type: 'string', + title: 'Message', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'WorkflowVariableRawTextArea', + 'x-component-props': { + autoSize: { + minRows: 5, + }, + }, + }, + colleague: { + type: 'string', + title: 'Collaborating human colleague', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + }, + manual: { + type: 'boolean', + title: 'Require human colleague confirmation to continue', + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, + }; + components = { + AIEmployee, + WorkflowVariableRawTextArea, + }; + + isAvailable({ engine, workflow }) { + return !engine.isWorkflowSync(workflow); + } +} diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/llm/index.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/llm/index.tsx index 5a45c921f3..39312c6ee9 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/llm/index.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/workflow/nodes/llm/index.tsx @@ -13,6 +13,7 @@ import { RobotOutlined } from '@ant-design/icons'; import { tval } from '@nocobase/utils/client'; import { namespace } from '../../../locale'; import { Settings } from './ModelSettings'; +import { Chat } from '../../../llm-providers/components/Chat'; export class LLMInstruction extends Instruction { title = 'LLM'; @@ -47,9 +48,14 @@ export class LLMInstruction extends Instruction { type: 'void', 'x-component': 'Settings', }, + chat: { + type: 'void', + 'x-component': 'Chat', + }, }; components = { Settings, + Chat, }; isAvailable({ engine, workflow }) { diff --git a/packages/plugins/@nocobase/plugin-ai/src/collections/ai-employees.ts b/packages/plugins/@nocobase/plugin-ai/src/collections/ai-employees.ts new file mode 100644 index 0000000000..2986179f04 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/collections/ai-employees.ts @@ -0,0 +1,52 @@ +/** + * 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. + */ + +export default { + name: 'aiEmployees', + fields: [ + { + name: 'username', + type: 'string', + primaryKey: true, + }, + { + name: 'nickname', + type: 'string', + interface: 'input', + }, + { + name: 'avatar', + type: 'string', + interface: 'image', + }, + { + name: 'bio', + type: 'string', + interface: 'textarea', + }, + { + name: 'about', + type: 'string', + interface: 'textarea', + }, + { + name: 'greeting', + type: 'string', + interface: 'textarea', + }, + { + name: 'skills', + type: 'jsonb', + }, + { + name: 'modelSettings', + type: 'jsonb', + }, + ], +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json index cea675e125..484d50de44 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-ai/src/locale/en-US.json @@ -21,5 +21,6 @@ "Response format description": "Important: when using JSON mode, you must also instruct the model to produce JSON yourself via a system or user message.", "Temperature description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.", "Top P description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.", - "Get models list failed, you can enter a model name manually.": "Get models list failed, you can enter a model name manually." + "Get models list failed, you can enter a model name manually.": "Get models list failed, you can enter a model name manually.", + "Default greeting message": "Hi, I am {{ nickname }}" } diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/__tests__/snowflake-id.test.ts b/packages/plugins/@nocobase/plugin-ai/src/server/__tests__/snowflake-id.test.ts new file mode 100644 index 0000000000..c53d1b40c2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/__tests__/snowflake-id.test.ts @@ -0,0 +1,19 @@ +/** + * 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 snowflake from '../snowflake'; + +it('generate snowflake id', async () => { + const ids = []; + for (let i = 0; i < 10; i++) { + ids.push(snowflake.generate()); + } + // check unique + expect(new Set(ids).size).toBe(10); +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/__tests__/ui-schema.test.ts b/packages/plugins/@nocobase/plugin-ai/src/server/__tests__/ui-schema.test.ts new file mode 100644 index 0000000000..3af94fd56a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/__tests__/ui-schema.test.ts @@ -0,0 +1,297 @@ +/** + * 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 { convertUiSchemaToJsonSchema } from '../utils'; + +it('should convert ui schema to json schema', async () => { + const result = convertUiSchemaToJsonSchema({ + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.7.0-beta.9', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.7.0-beta.9', + properties: { + '9lv2wl150pl': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.7.0-beta.9', + properties: { + jjfipnudrfs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.7.0-beta.9', + properties: { + nickname: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.7.0-beta.9', + 'x-uid': 'mzsm3gfcix6', + 'x-async': false, + 'x-index': 1, + name: 'nickname', + }, + }, + 'x-uid': 'wx3p0h2uj3a', + 'x-async': false, + 'x-index': 1, + name: 'jjfipnudrfs', + }, + }, + 'x-uid': '1cndl1bxewd', + 'x-async': false, + 'x-index': 1, + name: '9lv2wl150pl', + }, + '25mljjny4u4': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.7.0-beta.9', + properties: { + l3vxr4qesnr: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.7.0-beta.9', + properties: { + username: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.username', + 'x-component-props': {}, + 'x-app-version': '1.7.0-beta.9', + 'x-uid': 'hyqq9y93osu', + 'x-async': false, + 'x-index': 1, + name: 'username', + }, + }, + 'x-uid': 'vzyckvjmhpq', + 'x-async': false, + 'x-index': 1, + name: 'l3vxr4qesnr', + }, + }, + 'x-uid': 'q3uc0eoceyb', + 'x-async': false, + 'x-index': 2, + name: '25mljjny4u4', + }, + xhswvdtwuif: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.7.0-beta.9', + properties: { + ue5sdpbq6hr: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.7.0-beta.9', + properties: { + email: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.email', + 'x-component-props': {}, + 'x-app-version': '1.7.0-beta.9', + 'x-uid': 'j8ayrm5ffvn', + 'x-async': false, + 'x-index': 1, + name: 'email', + }, + }, + 'x-uid': 'z2y5tchettt', + 'x-async': false, + 'x-index': 1, + name: 'ue5sdpbq6hr', + }, + }, + 'x-uid': '4e9k3kyh7a8', + 'x-async': false, + 'x-index': 3, + name: 'xhswvdtwuif', + }, + jpk5232nuty: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.7.0-beta.9', + properties: { + a5odai87axd: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.7.0-beta.9', + properties: { + phone: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.phone', + 'x-component-props': {}, + 'x-app-version': '1.7.0-beta.9', + 'x-uid': 'vsg8y5ezr9d', + 'x-async': false, + 'x-index': 1, + name: 'phone', + }, + }, + 'x-uid': 'cljjsqsovht', + 'x-async': false, + 'x-index': 1, + name: 'a5odai87axd', + }, + }, + 'x-uid': 'ybzvngylbwq', + 'x-async': false, + 'x-index': 4, + name: 'jpk5232nuty', + }, + }, + 'x-uid': 'ybw3ukyt20q', + 'x-async': false, + 'x-index': 1, + name: 'grid', + }, + m5fj43b5tv3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'aiEmployees:configure', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginBottom: 8, + }, + }, + 'x-app-version': '1.7.0-beta.9', + properties: { + '7mx0wfba3jq': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'AIEmployeeButton', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'aiEmployees:button', + 'x-component-props': { + aiEmployee: { + createdAt: '2025-03-30T05:32:21.213Z', + updatedAt: '2025-03-30T05:32:21.213Z', + username: 'fromStruct', + nickname: 'Structy', + avatar: './wpuvnvazwl.svg', + bio: 'An AI assistant specialized in structuring form content into organized and actionable data.', + about: + 'You are FormStructAI, an expert in structuring form data based on provided UI schemas. Your task is to analyze a given UI Schema, extract relevant form content, and return a structured JSON object that aligns with the schema. Ensure all required fields are properly populated, maintain data consistency, and infer missing values when possible. Your response must be formatted as JSON, ready to be pre-filled back into the form.', + greeting: + 'Hello! I’m Structy, here to help you turn messy form data into structured insights. How can I assist you today?', + skills: null, + modelSettings: null, + }, + }, + 'x-app-version': '1.7.0-beta.9', + 'x-uid': 'qjaxx03isdx', + 'x-async': false, + 'x-index': 1, + name: '7mx0wfba3jq', + }, + }, + 'x-uid': 'ms75vnxwv2q', + 'x-async': false, + 'x-index': 2, + name: 'm5fj43b5tv3', + }, + '32rn0quhzze': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.7.0-beta.9', + properties: { + '840bun8241p': { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useCreateActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:createSubmit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.7.0-beta.9', + 'x-uid': 'r7ou2wp9kbo', + 'x-async': false, + 'x-index': 1, + name: '840bun8241p', + }, + }, + 'x-uid': 'i97z5bbg537', + 'x-async': false, + 'x-index': 3, + name: '32rn0quhzze', + }, + }, + 'x-uid': 'cnkrukqdrfn', + 'x-async': false, + 'x-index': 1, + name: 'peiwud7mwvg', + }); + console.log(result); +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/ai-employees/built-in/translator.ts b/packages/plugins/@nocobase/plugin-ai/src/server/ai-employees/built-in/translator.ts new file mode 100644 index 0000000000..f3f1abbf93 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/ai-employees/built-in/translator.ts @@ -0,0 +1,10 @@ +/** + * 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. + */ + +export default {}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-conversations.ts b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-conversations.ts new file mode 100644 index 0000000000..16f5e8dc0b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-conversations.ts @@ -0,0 +1,56 @@ +/** + * 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({ + migrationRules: ['schema-only'], + autoGenId: false, + name: 'aiConversations', + fields: [ + { + name: 'sessionId', + type: 'uuid', + primaryKey: true, + }, + { + name: 'topicId', + type: 'string', + }, + { + name: 'user', + type: 'belongsTo', + target: 'users', + targetKey: 'id', + foreignKey: 'userId', + }, + { + name: 'aiEmployees', + type: 'belongsToMany', + target: 'aiEmployees', + through: 'aiConversationsEmployees', + foreignKey: 'username', + otherKey: 'sessionId', + targetKey: 'username', + sourceKey: 'sessionId', + }, + { + name: 'title', + type: 'string', + }, + { + name: 'messages', + type: 'hasMany', + target: 'aiMessages', + sourceKey: 'sessionId', + foreignKey: 'sessionId', + onDelete: 'CASCADE', + }, + ], +}); 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 new file mode 100644 index 0000000000..1c654d249b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-employees.ts @@ -0,0 +1,17 @@ +/** + * 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'; +import aiEmployees from '../../collections/ai-employees'; + +export default defineCollection({ + migrationRules: ['overwrite', 'schema-only'], + autoGenId: false, + ...aiEmployees, +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-messages.ts b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-messages.ts new file mode 100644 index 0000000000..f9a2a65f70 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/collections/ai-messages.ts @@ -0,0 +1,36 @@ +/** + * 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({ + migrationRules: ['schema-only'], + autoGenId: false, + name: 'aiMessages', + fields: [ + { + name: 'messageId', + type: 'string', + primaryKey: true, + }, + { + name: 'content', + type: 'text', + }, + { + name: 'role', + type: 'string', + }, + { + name: 'type', + type: 'string', + defaultValue: 'text', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/provider.ts b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/provider.ts index 5b9df78701..fcc1b11f22 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/provider.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/llm-providers/provider.ts @@ -55,6 +55,13 @@ export abstract class LLMProvider { return this.chatModel.invoke(this.messages); } + async stream() { + // for (const handler of this.chatHandlers.values()) { + // await handler(); + // } + return this.chatModel.stream(this.messages); + } + async listModels(): Promise<{ models?: { id: string }[]; code?: number; diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts index e50f5b1f64..dccb69095c 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/plugin.ts @@ -14,6 +14,7 @@ import { deepseekProviderOptions } from './llm-providers/deepseek'; import aiResource from './resource/ai'; import PluginWorkflowServer from '@nocobase/plugin-workflow'; import { LLMInstruction } from './workflow/nodes/llm'; +import aiConversations from './resource/aiConversations'; export class PluginAIServer extends Plugin { aiManager = new AIManager(); @@ -27,10 +28,16 @@ export class PluginAIServer extends Plugin { this.aiManager.registerLLMProvider('deepseek', deepseekProviderOptions); this.app.resourceManager.define(aiResource); + this.app.resourceManager.define(aiConversations); this.app.acl.registerSnippet({ name: `pm.${this.name}.llm-services`, actions: ['ai:*', 'llmServices:*'], }); + this.app.acl.registerSnippet({ + name: `pm.${this.name}.ai-employees`, + actions: ['aiEmployees:*'], + }); + this.app.acl.allow('aiConversations', '*', 'loggedIn'); const workflowSnippet = this.app.acl.snippetManager.snippets.get('pm.workflow.workflows'); if (workflowSnippet) { workflowSnippet.actions.push('ai:listModels'); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiConversations.ts b/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiConversations.ts new file mode 100644 index 0000000000..1f25187b93 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/resource/aiConversations.ts @@ -0,0 +1,201 @@ +/** + * 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 actions, { Context, Next } from '@nocobase/actions'; +import snowflake from '../snowflake'; +import PluginAIServer from '../plugin'; +import { Model } from '@nocobase/database'; +import { convertUiSchemaToJsonSchema } from '../utils'; + +export default { + name: 'aiConversations', + actions: { + async list(ctx: Context, next: Next) { + const userId = ctx.auth?.user.id; + if (!userId) { + return ctx.throw(403); + } + ctx.action.mergeParams({ + filter: { + userId, + }, + }); + return actions.list(ctx, next); + }, + async create(ctx: Context, next: Next) { + const userId = ctx.auth?.user.id; + const { aiEmployees } = ctx.action.params.values || {}; + const repo = ctx.db.getRepository('aiConversations'); + ctx.body = await repo.create({ + values: { + title: 'New Conversation', + userId, + aiEmployees, + }, + }); + await next(); + }, + async destroy(ctx: Context, next: Next) { + const userId = ctx.auth?.user.id; + if (!userId) { + return ctx.throw(403); + } + ctx.action.mergeParams({ + filter: { + userId, + }, + }); + return actions.destroy(ctx, next); + }, + async getMessages(ctx: Context, next: Next) { + const userId = ctx.auth?.user.id; + if (!userId) { + return ctx.throw(403); + } + const { sessionId } = ctx.action.params || {}; + if (!sessionId) { + ctx.throw(400); + } + const conversation = await ctx.db.getRepository('aiConversations').findOne({ + filter: { + sessionId, + userId, + }, + }); + if (!conversation) { + ctx.throw(400); + } + const rows = await ctx.db.getRepository('aiConversations.messages', sessionId).find({ + sort: ['-messageId'], + limit: 10, + }); + ctx.body = rows.map((row: Model) => ({ + messageId: row.messageId, + role: row.role, + content: { + content: row.content, + type: row.type, + }, + })); + await next(); + }, + async sendMessages(ctx: Context, next: Next) { + const { sessionId, aiEmployee, messages } = ctx.action.params.values || {}; + if (!sessionId) { + ctx.throw(400); + } + const userMessage = messages.find((message: any) => message.role === 'user'); + if (!userMessage) { + ctx.throw(400); + } + const conversation = await ctx.db.getRepository('aiConversations').findOne({ + filterByTk: sessionId, + }); + if (!conversation) { + ctx.throw(400); + } + const employee = await ctx.db.getRepository('aiEmployees').findOne({ + filter: { + username: aiEmployee, + }, + }); + if (!employee) { + ctx.throw(400); + } + const modelSettings = employee.modelSettings; + if (!modelSettings?.llmService) { + ctx.throw(500); + } + const service = await ctx.db.getRepository('llmServices').findOne({ + filter: { + name: modelSettings.llmService, + }, + }); + if (!service) { + ctx.throw(500); + } + const plugin = ctx.app.pm.get('ai') as PluginAIServer; + const providerOptions = plugin.aiManager.llmProviders.get(service.provider); + if (!providerOptions) { + ctx.throw(500); + } + + try { + await ctx.db.getRepository('aiConversations.messages', sessionId).create({ + values: messages.map((message: any) => ({ + messageId: snowflake.generate(), + role: message.role, + content: message.content.content, + type: message.content.type, + })), + }); + } catch (err) { + ctx.log.error(err); + ctx.throw(500); + } + ctx.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + ctx.status = 200; + const userMessages = []; + for (const msg of messages) { + if (msg.role !== 'user') { + continue; + } + let content = msg.content.content; + if (msg.content.type === 'uiSchema') { + const uiSchemaRepo = ctx.db.getRepository('uiSchemas'); + const schema = await uiSchemaRepo.getJsonSchema(content); + content = JSON.stringify(convertUiSchemaToJsonSchema(schema)); + } + userMessages.push({ + role: 'user', + content, + }); + } + const msgs = [ + { + role: 'system', + content: employee.about, + }, + ...userMessages, + ]; + const Provider = providerOptions.provider; + const provider = new Provider({ + app: ctx.app, + serviceOptions: service.options, + chatOptions: { + ...modelSettings, + messages: msgs, + }, + }); + const stream = await provider.stream(); + let message = ''; + for await (const chunk of stream) { + if (!chunk.content) { + continue; + } + message += chunk.content; + ctx.res.write(`data: ${JSON.stringify({ type: 'content', body: chunk.content })}\n\n`); + } + await ctx.db.getRepository('aiConversations.messages', sessionId).create({ + values: { + messageId: snowflake.generate(), + role: aiEmployee, + content: message, + type: 'text', + }, + }); + ctx.res.end(); + // await next(); + }, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/snowflake.ts b/packages/plugins/@nocobase/plugin-ai/src/server/snowflake.ts new file mode 100644 index 0000000000..963e58e0c9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/snowflake.ts @@ -0,0 +1,57 @@ +/** + * 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. + */ + +const SnowflakeId = require('snowflake-id').default; +import os from 'os'; + +class Snowflake { + snowflake: typeof SnowflakeId; + + constructor() { + const interfaces = os.networkInterfaces(); + let instanceId: number; + let hasFound = false; + + for (const name of Object.keys(interfaces)) { + if (!/^(eth|en|wl)/.test(name)) { + continue; + } + const addresses = interfaces[name]; + for (const addr of addresses) { + if (addr.family === 'IPv4' && !addr.internal) { + try { + const parts = addr.address.split('.').map((part) => parseInt(part, 10)); + if (parts.length !== 4 || parts.some((p) => p < 0 || p > 255)) { + continue; + } + const ipNum = ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0; + instanceId = ipNum % 100; + hasFound = true; + } catch (err) { + continue; + } + } + + if (instanceId) { + break; + } + } + } + + this.snowflake = new SnowflakeId({ + mid: instanceId, + }); + } + + generate() { + return this.snowflake.generate(); + } +} + +export default new Snowflake(); diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/utils.ts b/packages/plugins/@nocobase/plugin-ai/src/server/utils.ts new file mode 100644 index 0000000000..7f9841280d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/server/utils.ts @@ -0,0 +1,182 @@ +/** + * 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. + */ + +/** + * 将Formily的uiSchema转换成标准的JSON Schema + * 支持嵌套结构和VoidField + * @param {Object} uiSchema - Formily的表单uiSchema + * @returns {Object} 标准的JSON Schema + */ +export function convertUiSchemaToJsonSchema(uiSchema) { + // 创建基础的JSON Schema结构 + const jsonSchema = { + type: 'object', + properties: {}, + required: [], + }; + + // 递归处理UI Schema,提取数据字段 + function extractDataFields(schema, targetSchema) { + // 如果当前节点是数据字段(非void类型) + if (schema.type && schema.type !== 'void' && schema.name) { + const fieldSchema = createFieldSchema(schema); + targetSchema.properties[schema.name] = fieldSchema; + + // 如果字段是必填的 + if (schema.required) { + targetSchema.required.push(schema.name); + } + + return; + } + + // 如果是嵌套的properties对象 + if (schema.properties) { + Object.keys(schema.properties).forEach((key) => { + const field = schema.properties[key]; + + // 如果字段有自己的name属性,使用它 + const fieldName = field.name || key; + + // 处理数据字段 + if (field.type && field.type !== 'void') { + const fieldSchema = createFieldSchema(field); + targetSchema.properties[fieldName] = fieldSchema; + + // 如果字段是必填的 + if (field.required) { + targetSchema.required.push(fieldName); + } + } + // 递归处理嵌套的void字段 + else if (field.properties) { + extractDataFields(field, targetSchema); + } + // 特殊处理CollectionField类型 + else if (field['x-component'] === 'CollectionField' && field['x-collection-field']) { + // 从x-collection-field中提取字段名 + const collectionParts = field['x-collection-field'].split('.'); + const fieldType = field.type || 'string'; + + const fieldSchema = { + type: fieldType, + title: field.title || collectionParts[collectionParts.length - 1], + }; + + targetSchema.properties[fieldName] = fieldSchema; + } + }); + } + } + + // 创建字段Schema的辅助函数 + function createFieldSchema(field) { + const fieldSchema = { + title: field.title || field.name, + }; + + // 设置字段类型 + if (field.type) { + fieldSchema.type = field.type; + } else { + // 根据组件类型推断数据类型 + fieldSchema.type = inferTypeFromComponent(field['x-component']); + } + + // 处理字段验证和约束 + if (field['x-validator']) { + processValidator(field['x-validator'], fieldSchema); + } + + // 处理字段描述 + if (field.description) { + fieldSchema.description = field.description; + } + + // 处理枚举值 + if (field.enum) { + fieldSchema.enum = field.enum.map((item) => (typeof item === 'object' ? item.value : item)); + } + + // 处理默认值 + if (field.default !== undefined) { + fieldSchema.default = field.default; + } + + // 处理特定格式 + if (field.format) { + fieldSchema.format = field.format; + } + + // 处理CollectionField + if (field['x-component'] === 'CollectionField' && field['x-collection-field']) { + // 从x-collection-field中提取字段信息 + const collectionParts = field['x-collection-field'].split('.'); + fieldSchema.title = fieldSchema.title || collectionParts[collectionParts.length - 1]; + } + + return fieldSchema; + } + + // 根据组件类型推断数据类型 + function inferTypeFromComponent(component) { + if (!component) return 'string'; + + switch (component) { + case 'Input': + case 'Input.TextArea': + case 'Select': + case 'Radio': + case 'DatePicker': + case 'TimePicker': + return 'string'; + case 'NumberPicker': + return 'number'; + case 'Switch': + case 'Checkbox': + return 'boolean'; + case 'Upload': + return 'array'; + case 'CollectionField': + return 'string'; // 默认为string,实际类型应取决于collection字段类型 + default: + return 'string'; + } + } + + // 处理字段验证器 + function processValidator(validator, fieldSchema) { + if (Array.isArray(validator)) { + validator.forEach((rule) => applyValidationRule(rule, fieldSchema)); + } else if (typeof validator === 'object') { + applyValidationRule(validator, fieldSchema); + } + } + + // 应用验证规则到字段schema + function applyValidationRule(rule, fieldSchema) { + if (rule.min !== undefined) fieldSchema.minimum = rule.min; + if (rule.max !== undefined) fieldSchema.maximum = rule.max; + if (rule.minLength !== undefined) fieldSchema.minLength = rule.minLength; + if (rule.maxLength !== undefined) fieldSchema.maxLength = rule.maxLength; + if (rule.pattern) fieldSchema.pattern = rule.pattern; + if (rule.format) fieldSchema.format = rule.format; + if (rule.required) fieldSchema.required = true; + } + + // 开始转换 + extractDataFields(uiSchema, jsonSchema); + + // 如果没有必填字段,删除required数组 + if (jsonSchema.required.length === 0) { + delete jsonSchema.required; + } + + return jsonSchema; +} diff --git a/packages/plugins/@nocobase/plugin-users/src/client/SignOut.tsx b/packages/plugins/@nocobase/plugin-users/src/client/SignOut.tsx index d8c2b4b148..4723edf296 100644 --- a/packages/plugins/@nocobase/plugin-users/src/client/SignOut.tsx +++ b/packages/plugins/@nocobase/plugin-users/src/client/SignOut.tsx @@ -8,13 +8,14 @@ */ import React from 'react'; -import { SchemaSettingsItem, useNavigateNoUpdate, useAPIClient } from '@nocobase/client'; +import { SchemaSettingsItem, useNavigateNoUpdate, useAPIClient, useCurrentUserContext } from '@nocobase/client'; import { useTranslation } from 'react-i18next'; export const SignOut = () => { const { t } = useTranslation(); const navigate = useNavigateNoUpdate(); const api = useAPIClient(); + const ctx = useCurrentUserContext(); return ( { } else { navigate(`/signin?redirect=${encodeURIComponent('')}`); } + ctx.mutate(null); }} > {t('Sign out')}