diff --git a/packages/core/client/src/flow/Formily/ReactiveField.tsx b/packages/core/client/src/flow/Formily/ReactiveField.tsx deleted file mode 100644 index 7da01ce867..0000000000 --- a/packages/core/client/src/flow/Formily/ReactiveField.tsx +++ /dev/null @@ -1,106 +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 { Form, GeneralField, isVoidField } from '@formily/core'; -import { RenderPropsChildren, SchemaComponentsContext } from '@formily/react'; -import { toJS } from '@formily/reactive'; -import { observer } from '@formily/reactive-react'; -import { FormPath, isFn } from '@formily/shared'; -import React, { Fragment, useContext } from 'react'; -interface IReactiveFieldProps { - field: GeneralField; - children?: RenderPropsChildren; -} - -const mergeChildren = (children: RenderPropsChildren, content: React.ReactNode) => { - if (!children && !content) return; - if (isFn(children)) return; - return ( - - {children} - {content} - - ); -}; - -const isValidComponent = (target: any) => target && (typeof target === 'object' || typeof target === 'function'); - -const renderChildren = (children: RenderPropsChildren, field?: GeneralField, form?: Form) => - isFn(children) ? children(field, form) : children; - -const ReactiveInternal: React.FC = (props) => { - const components = useContext(SchemaComponentsContext); - if (!props.field) { - return {renderChildren(props.children)}; - } - const field = props.field; - const content = mergeChildren( - renderChildren(props.children, field, field.form), - field.content ?? field.componentProps.children, - ); - if (field.display !== 'visible') return null; - - const getComponent = (target: any) => { - return isValidComponent(target) ? target : FormPath.getIn(components, target) ?? target; - }; - - const renderDecorator = (children: React.ReactNode) => { - if (!field.decoratorType) { - return {children}; - } - - return React.createElement(getComponent(field.decoratorType), toJS(field.decoratorProps), children); - }; - - const renderComponent = () => { - if (!field.componentType) return content; - const value = !isVoidField(field) ? field.value : undefined; - const onChange = !isVoidField(field) - ? (...args: any[]) => { - field.onInput(...args); - field.componentProps?.onChange?.(...args); - } - : field.componentProps?.onChange; - const onFocus = !isVoidField(field) - ? (...args: any[]) => { - field.onFocus(...args); - field.componentProps?.onFocus?.(...args); - } - : field.componentProps?.onFocus; - const onBlur = !isVoidField(field) - ? (...args: any[]) => { - field.onBlur(...args); - field.componentProps?.onBlur?.(...args); - } - : field.componentProps?.onBlur; - const disabled = !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined; - const readOnly = !isVoidField(field) ? field.pattern === 'readOnly' : undefined; - return React.createElement( - getComponent(field.componentType), - { - disabled, - readOnly, - ...toJS(field.componentProps), - value, - onChange, - onFocus, - onBlur, - }, - content, - ); - }; - - return renderDecorator(renderComponent()); -}; - -ReactiveInternal.displayName = 'ReactiveField'; - -export const ReactiveField = observer(ReactiveInternal, { - forwardRef: true, -}); diff --git a/packages/core/client/src/flow/Formily/index.tsx b/packages/core/client/src/flow/Formily/index.tsx deleted file mode 100644 index f3eb30f912..0000000000 --- a/packages/core/client/src/flow/Formily/index.tsx +++ /dev/null @@ -1,8 +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. - */ diff --git a/packages/plugins/@nocobase/plugin-ai/package.json b/packages/plugins/@nocobase/plugin-ai/package.json index 8a3ea47bcb..07a1129aa1 100644 --- a/packages/plugins/@nocobase/plugin-ai/package.json +++ b/packages/plugins/@nocobase/plugin-ai/package.json @@ -27,6 +27,8 @@ "echarts-for-react": "3.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "@xyflow/react": "^12.7.0", + "elkjs": "^0.10.0" } } diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/markdown/ECharts.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/markdown/ECharts.tsx index 7013e548ed..7fde9ec22f 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/markdown/ECharts.tsx +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/chatbox/markdown/ECharts.tsx @@ -31,7 +31,6 @@ export function replaceTagBlockByIndex( indexToReplace: number, newInnerStr: string, ): string { - console.log(input, tagName, indexToReplace, newInnerStr); const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'gi'); let currentIndex = 0; return input.replace(regex, (match, inner) => { @@ -100,9 +99,7 @@ const EditModal: React.FC<{ form.reset(); }} onOk={async () => { - console.log(option); const content = replaceTagBlockByIndex(message.content, 'echarts', index, JSON.stringify(option)); - console.log(content); await updateMessage({ sessionId: currentConversation, messageId: message.messageId, @@ -128,7 +125,7 @@ const EditModal: React.FC<{ marginTop: '16px', }} > - + - + {option ? ( { return ; }, + collections: (props) => { + return ; + }, }} rehypePlugins={[ rehypeRaw, @@ -50,7 +54,7 @@ export const Markdown: React.FC<{ rehypeSanitize, { ...defaultSchema, - tagNames: [...defaultSchema.tagNames, 'echarts', 'form'], + tagNames: [...defaultSchema.tagNames, 'echarts', 'form', 'collections'], attributes: { ...defaultSchema.attributes, form: ['uid', 'datasource', 'collection'], diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/CollectionNode.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/CollectionNode.tsx new file mode 100644 index 0000000000..8b0e42754c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/CollectionNode.tsx @@ -0,0 +1,145 @@ +/** + * 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 { Handle, Position } from 'reactflow'; +import { List, Card, Flex } from 'antd'; +import { useApp, useToken } from '@nocobase/client'; +import { useT } from '../../locale'; +import { Schema } from '@formily/react'; +import { KeyOutlined } from '@ant-design/icons'; + +export const CollectionNode = ({ data }) => { + const { token } = useToken(); + const app = useApp(); + const t = useT(); + const fim = app.dataSourceManager.collectionFieldInterfaceManager; + const handleStyle = { + top: '50%', + transform: 'translateY(-50%)', + width: 8, + height: 8, + opacity: 0, + pointerEvents: 'none' as const, + }; + return ( + + {data.title}{' '} + + {data.name} + + + } + styles={{ + body: { + padding: 0, + minWidth: '200px', + }, + }} + > + { + const fieldInterface = fim.getFieldInterface(item.interface); + + return ( + + + + + + + + +
+ {item.title} + {item.primaryKey ? ( + + ) : ( + '' + )} +
+
+ {item.name} +
+
+ +
+ {fieldInterface?.title ? Schema.compile(fieldInterface.title, { t }) : ''} +
+
+ {item.type} +
+
+
+
+ ); + }} + /> +
+ ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Collections.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Collections.tsx new file mode 100644 index 0000000000..bc964d6557 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Collections.tsx @@ -0,0 +1,174 @@ +/** + * 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, useRef } from 'react'; +import { Generating } from '../chatbox/markdown/Generating'; +import { useChatMessages } from '../chatbox/ChatMessagesProvider'; +import mermaid from 'mermaid'; +import { Card, Modal, Tabs } from 'antd'; +import { DatabaseOutlined, FileTextOutlined, NodeIndexOutlined } from '@ant-design/icons'; +import { useT } from '../../locale'; +import { CodeInternal } from '../chatbox/markdown/Code'; +import { Diagram } from './Diagram'; +import { Table } from './Table'; +import { useAPIClient, useApp } from '@nocobase/client'; + +mermaid.initialize({ + startOnLoad: true, +}); + +const Mermaid: React.FC<{ diagram: string }> = ({ diagram }) => { + const ref = useRef(null); + useEffect(() => { + if (ref.current) { + try { + const id = 'mermaid-' + Math.random().toString(36).slice(2); + mermaid.render(id, diagram, (svgCode) => { + ref.current!.innerHTML = svgCode; + }); + } catch (err) { + console.error(err); + console.log(diagram); + } + } + }, [diagram]); + + return
; +}; + +const TabPane: React.FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export const DataModelingModal: React.FC<{ + open: boolean; + setOpen: (open: boolean) => void; + collections: any[]; +}> = ({ open, setOpen, collections }) => { + const t = useT(); + const api = useAPIClient(); + const app = useApp(); + const fim = app.dataSourceManager.collectionFieldInterfaceManager; + + const items = [ + { + key: 'collections', + label: t('Collections'), + icon: , + children: , + }, + { + key: 'graph', + icon: , + label: t('Diagram'), + children: ( + + + + ), + }, + { + key: 'definition', + icon: , + label: 'Definition', + children: ( + + + + ), + }, + ]; + return ( + { + setOpen(false); + }} + okText={t('Finish review and apply')} + onOk={async () => { + const result = []; + for (const collection of collections) { + const fields = collection.fields.map((field: any) => { + const fieldInterface = fim.getFieldInterface(field.interface); + if (fieldInterface) { + field.type = fieldInterface.default?.type || field.type; + field.uiSchema = fieldInterface.default?.uiSchema || field.uiSchema; + } + field.uiSchema = { + ...field.uiSchema, + title: field.title, + }; + if (field.enum) { + field.uiSchema = { + ...field.uiSchema, + enum: field.enum, + }; + } + return field; + }); + result.push({ + ...collection, + fields, + }); + } + await api.resource('ai').defineCollections({ + values: { + collections, + }, + }); + setOpen(false); + }} + > + + + ); +}; + +export const Collections = (props: any) => { + const t = useT(); + const [open, setOpen] = React.useState(false); + const { children, className, message, index, ...rest } = props; + const { responseLoading } = useChatMessages(); + + if (responseLoading && !message.messageId) { + return ; + } + + const collectionsStr = String(children).replace(/\n$/, ''); + const collections = JSON.parse(collectionsStr); + + return ( + <> + setOpen(true)} + > + } + title={t('Data modeling')} + description={t('Please review and finish the process')} + /> + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Diagram.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Diagram.tsx new file mode 100644 index 0000000000..aa09e302ed --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Diagram.tsx @@ -0,0 +1,263 @@ +/** + * 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 ReactFlow, { + Background, + Controls, + MiniMap, + useNodesState, + useEdgesState, + Handle, + Position, + Edge, + Node, +} from 'reactflow'; +import ELK from 'elkjs/lib/elk.bundled.js'; +import '@xyflow/react/dist/style.css'; +import { CollectionNode } from './CollectionNode'; +import { css } from '@emotion/css'; +import { useGlobalTheme } from '@nocobase/client'; + +const elk = new ELK(); + +const nodeTypes = { + collection: CollectionNode, +}; + +const layoutElements = async (nodes: Node[], edges: Edge[]) => { + const isHorizontal = !edges.length; + const graph = { + id: 'root', + layoutOptions: { + 'elk.algorithm': edges.length ? 'org.eclipse.elk.layered' : 'org.eclipse.elk.box', + // 'elk.spacing.nodeNode': '100', + // 'elk.direction': isHorizontal ? 'RIGHT' : 'DOWN', + // 'elk.expandNodes': true, + }, + children: nodes.map((node) => ({ + ...node, + // Adjust the target and source handle positions based on the layout + // direction. + targetPosition: isHorizontal ? 'left' : 'top', + sourcePosition: isHorizontal ? 'right' : 'bottom', + + // Hardcode a width and height for elk to use when layouting. + width: 320, + height: 50 * (node.data.fields.length + 1), + })), + edges: edges, + }; + + return elk + .layout(graph) + .then((layoutedGraph) => ({ + nodes: layoutedGraph.children.map((node) => ({ + ...node, + // React Flow expects a position property on the node instead of `x` + // and `y` fields. + position: { x: node.x, y: node.y }, + })), + + edges: layoutedGraph.edges, + })) + .catch(console.error); +}; + +const getDiagramData = (collections: any[]) => { + const collectionMap = new Map(collections.map((col) => [col.name, col])); + + for (const collection of collections) { + if (collection.autoGenId) { + collection.fields.unshift({ + name: 'id', + interface: 'id', + type: 'bigInt', + title: 'ID', + primaryKey: true, + autoIncrement: true, + }); + } + } + + const nodes = collections.map((collection) => ({ + id: collection.name, + type: 'collection', + position: { x: 0, y: 0 }, + data: { + title: collection.title, + name: collection.name, + fields: [...collection.fields], + }, + })); + + const edges: any[] = []; + + // for (const collection of collections) { + // for (const field of collection.fields) { + // const { type, target, name: fieldName, foreignKey, targetKey, through } = field; + // + // if (!target || !collectionMap.has(target)) continue; + // const targetCollection = collectionMap.get(target); + // const targetFields = targetCollection.fields || []; + // const targetFieldExists = (key: string) => targetFields.some((f: any) => f.name === key); + // + // const tk = targetKey || 'id'; + // const fk = foreignKey || 'id'; + // + // if (type === 'belongsTo' && target === collection.name) { + // if (!targetFieldExists(tk)) continue; + // + // edges.push({ + // id: `${collection.name}-${fieldName}-to-self`, + // source: collection.name, + // sourceHandle: `${collection.name}-${fieldName}-source-right`, + // target: collection.name, + // targetHandle: `${collection.name}-${tk}-target-right`, + // type: 'smoothstep', + // animated: true, + // }); + // continue; + // } + // + // if (type === 'belongsTo') { + // if (!targetFieldExists(tk)) continue; + // + // edges.push({ + // id: `${collection.name}-${fieldName}-to-${tk}`, + // source: collection.name, + // sourceHandle: `${collection.name}-${fieldName}-source-right`, + // target, + // targetHandle: `${target}-${tk}-target-left`, + // // type: 'smoothstep', + // animated: true, + // }); + // continue; + // } + // + // if (type === 'hasMany' && target === collection.name) { + // if (!targetFieldExists(tk)) continue; + // + // edges.push({ + // id: `${collection.name}-${tk}-to-${fieldName}`, + // source: target, + // sourceHandle: `${collection.name}-${tk}-source-left`, + // target: collection.name, + // targetHandle: `${collection.name}-${fieldName}-target-left`, + // type: 'smoothstep', + // animated: true, + // }); + // continue; + // } + // + // if (type === 'hasMany') { + // if (!targetFieldExists(tk)) continue; + // + // edges.push({ + // id: `${collection.name}-${tk}-to-${fieldName}`, + // source: target, + // sourceHandle: `${target}-${tk}-source-right`, + // target: collection.name, + // targetHandle: `${collection.name}-${fieldName}-target-left`, + // // type: 'smoothstep', + // animated: true, + // }); + // continue; + // } + // + // if (type === 'hasOne') { + // if (!targetFieldExists(fk)) continue; + // + // edges.push({ + // id: `${collection.name}-${fieldName}-to-${fk}`, + // source: collection.name, + // sourceHandle: `${collection.name}-${fieldName}-source-right`, + // target, + // targetHandle: `${target}-${fk}-target-left`, + // // type: 'smoothstep', + // animated: true, + // }); + // continue; + // } + // + // // if (type === 'belongsToMany') { + // // let label = ''; + // // if (through && collectionMap.has(through)) { + // // const throughCollection = collectionMap.get(through); + // // const throughFields = throughCollection.fields || []; + // // const hasCurrent = throughFields.some((f: any) => f.target === collection.name); + // // const hasTarget = throughFields.some((f: any) => f.target === target); + // // + // // if (!hasCurrent || !hasTarget) continue; + // // label = `via ${through}`; + // // } else { + // // label = through ? `via ${through}` : 'via ?'; + // // } + // // + // // edges.push({ + // // id: `${collection.name}-${fieldName}-to-${target}`, + // // source: collection.name, + // // sourceHandle: `${collection.name}-${fieldName}-source`, + // // target, + // // targetHandle: `${target}-${fieldName}-target`, + // // type: 'smoothstep', + // // animated: true, + // // label, + // // }); + // // } + // } + // } + + return layoutElements(nodes, edges); +}; + +export const Diagram: React.FC<{ + collections: any[]; +}> = ({ collections }) => { + const { isDarkTheme } = useGlobalTheme(); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + useEffect(() => { + getDiagramData(collections) + .then(({ nodes, edges }) => { + setNodes(nodes); + setEdges(edges); + }) + .catch((error) => console.error('Error laying out elements:', error)); + }, [collections]); + + return ( +
+ + + + +
+ ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Table.tsx b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Table.tsx new file mode 100644 index 0000000000..0573401436 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/Table.tsx @@ -0,0 +1,161 @@ +/** + * 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 { Table as AntdTable, Checkbox, Tag } from 'antd'; +import type { TableColumnsType } from 'antd'; +import { useT } from '../../locale'; +import { useApp } from '@nocobase/client'; +import { Schema } from '@formily/react'; +import lodash from 'lodash'; + +const useColumns = () => { + const t = useT(); + const columns: TableColumnsType = [ + { title: t('Collection display name'), dataIndex: 'title', key: 'title', width: 200 }, + { title: t('Collection name'), dataIndex: 'name', key: 'name', width: 150 }, + { + title: t('Collection template'), + dataIndex: 'template', + key: 'template', + width: 150, + render: (value) => { + const template = value.charAt(0).toUpperCase() + value.slice(1); + return {t(`${template} collection`)}; + }, + }, + { + title: t('Description'), + dataIndex: 'description', + key: 'description', + width: 350, + }, + { + title: t('Preset fields'), + key: 'preset', + width: 300, + render: (_, record) => { + const value = []; + if (record.autoGenId !== false) { + value.push('id'); + } + if (record.createdAt !== false) { + value.push('createdAt'); + } + if (record.updatedAt !== false) { + value.push('updatedAt'); + } + if (record.createdBy) { + value.push('createdBy'); + } + if (record.updatedBy) { + value.push('updatedBy'); + } + return ( + + ); + }, + }, + ]; + return columns; +}; + +const useExpandColumns = () => { + const t = useT(); + const app = useApp(); + const fim = app.dataSourceManager.collectionFieldInterfaceManager; + const columns = [ + { + title: t('Field display name'), + dataIndex: 'title', + key: 'title', + }, + { + title: t('Field name'), + dataIndex: 'name', + key: 'name', + }, + { + title: t('Field interface'), + dataIndex: 'interface', + key: 'interface', + render: (value) => { + const fieldInterface = fim.getFieldInterface(value); + return {fieldInterface ? Schema.compile(fieldInterface.title, { t }) : value}; + }, + }, + { + title: t('Description'), + dataIndex: 'description', + key: 'description', + }, + { + title: t('Options'), + key: 'options', + render: (_, record) => { + return JSON.stringify(lodash.omit(record, ['title', 'name', 'interface', 'description']), null, 2); + }, + }, + ]; + return columns; +}; + +const ExpandedRowRender = (record) => { + const expandColumns = useExpandColumns(); + return ; +}; + +export const Table: React.FC<{ + collections: any[]; +}> = ({ collections }) => { + const columns = useColumns(); + + return ( + record.fields && record.fields.length > 0, + }} + /> + ); +}; diff --git a/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/types.ts b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/types.ts new file mode 100644 index 0000000000..50c794df45 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-ai/src/client/ai-employees/data-modeling/types.ts @@ -0,0 +1,251 @@ +/** + * 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 interface CollectionOptions extends Omit { + /** The unique identifier of the collection, must be unique across the database */ + name: string; + /** The display title of the collection, used for UI presentation */ + title?: string; + /** The description of the collection */ + description?: string; + /** Whether this collection is a through table for many-to-many relationships */ + isThrough?: boolean; + /** The target key(s) used for filtering operations, can be a single key or array of keys */ + filterTargetKey?: string | string[]; + /** Array of field definitions for the collection */ + fields?: FieldOptions[]; + /** + * Whether to automatically generate an 'id' field + * @default true + */ + autoGenId?: boolean; + /** + * Whether to automatically generate a 'createdAt' timestamp field + * @default true + */ + createdAt?: boolean; + /** Whether to automatically generate an 'updatedAt' timestamp field + * @default true + */ + updatedAt?: boolean; + /** + * Whether to automatically generate a 'createdById' field for record ownership + * @default false + */ + createdBy?: boolean; + /** + * Whether to automatically generate an 'updatedById' field for tracking updates + * @default false + */ + updatedBy?: boolean; + /** The template identifier used to create this collection */ + template: 'general' | 'tree' | 'file' | 'calendar' | 'expression'; + /** The field name used for tree structure functionality */ + tree?: 'adjacencyList'; +} + +export type FieldOptions = + | BaseFieldOptions + | StringFieldOptions + | IntegerFieldOptions + | FloatFieldOptions + | DecimalFieldOptions + | DoubleFieldOptions + | JsonFieldOptions + | JsonbFieldOptions + | BooleanFieldOptions + | RadioFieldOptions + | TextFieldOptions + | TimeFieldOptions + | DateFieldOptions + | DatetimeTzFieldOptions + | DatetimeNoTzFieldOptions + | DateOnlyFieldOptions + | UnixTimestampFieldOptions + | UidFieldOptions + | UUIDFieldOptions + | NanoidFieldOptions + | PasswordFieldOptions + | BelongsToFieldOptions + | HasOneFieldOptions + | HasManyFieldOptions + | BelongsToManyFieldOptions; + +/** + * Base options for all field types + * Provides common properties that are available to all field configurations + */ +export interface BaseFieldOptions { + /** The name of the field, used as the column name in the database */ + name: string; + /** The title of the field, used for display in the UI */ + title: string; + /** The description of the field */ + description?: string; + /** Whether the field should be hidden from API responses and UI */ + hidden?: boolean; + /** Required. The user interface component type for this field */ + interface: + | 'id' + | 'input' + | 'integer' + | 'checkbox' + | 'checkboxGroup' + | 'color' + | 'createdAt' + | 'updatedAt' + | 'createdBy' + | 'updatedBy' + | 'date' + | 'datetime' + | 'datetimeNoTz' + | 'email' + | 'icon' + | 'json' + | 'markdown' + | 'multipleSelect' + | 'nanoid' + | 'number' + | 'password' + | 'percent' + | 'phone' + | 'radioGroup' + | 'richText' + | 'select' + | 'textarea' + | 'time' + | 'unixTimestamp' + | 'url' + | 'uuid' + | 'm2m' + | 'm2o' + | 'o2m' + | 'o2o'; + /** enumeration options for the field, used for select/radio/checkbox interfaces */ + enum?: { + label: string; + value: string | number | boolean; + }[]; + /** Additional properties for extensibility */ + [key: string]: any; +} +/** + * Base options for column-based field types + * Extends BaseFieldOptions and includes Sequelize column-specific options + * Excludes the 'type' property as it's handled by the specific field implementations + */ +export interface BaseColumnFieldOptions extends BaseFieldOptions, Omit { + /** The Sequelize data type for the column */ + dataType?: DataType; + + /** Index configuration for the column, can be boolean or detailed index options */ + index?: boolean | ModelIndexesOptions; +} + +export interface StringFieldOptions extends BaseColumnFieldOptions { + type: 'string'; + length?: number; + trim?: boolean; +} +export interface IntegerFieldOptions extends BaseColumnFieldOptions { + type: 'integer'; +} +export interface FloatFieldOptions extends BaseColumnFieldOptions { + type: 'float'; +} +export interface DecimalFieldOptions extends BaseColumnFieldOptions { + type: 'decimal'; + precision: number; + scale: number; +} +export interface DoubleFieldOptions extends BaseColumnFieldOptions { + type: 'double'; +} +export interface JsonFieldOptions extends BaseColumnFieldOptions { + type: 'json'; +} +export interface JsonbFieldOptions extends BaseColumnFieldOptions { + type: 'jsonb'; +} +export interface BooleanFieldOptions extends BaseColumnFieldOptions { + type: 'boolean'; +} +export interface RadioFieldOptions extends BaseColumnFieldOptions { + type: 'radio'; +} +export interface TextFieldOptions extends BaseColumnFieldOptions { + type: 'text'; + length?: 'tiny' | 'medium' | 'long'; + trim?: boolean; +} +export interface TimeFieldOptions extends BaseColumnFieldOptions { + type: 'time'; +} +export interface DateFieldOptions extends BaseColumnFieldOptions { + type: 'date'; +} +export interface DatetimeTzFieldOptions extends BaseColumnFieldOptions { + type: 'datetimeTz'; +} +export interface DatetimeNoTzFieldOptions extends BaseColumnFieldOptions { + type: 'datetimeNoTz'; +} +export interface DateOnlyFieldOptions extends BaseColumnFieldOptions { + type: 'dateOnly'; +} +export interface UnixTimestampFieldOptions extends BaseColumnFieldOptions { + type: 'unixTimestamp'; +} +export interface UidFieldOptions extends BaseColumnFieldOptions { + type: 'uid'; + prefix?: string; + pattern?: string; +} +export interface UUIDFieldOptions extends BaseColumnFieldOptions { + type: 'uuid'; + autoFill?: boolean; +} +export interface NanoidFieldOptions extends BaseColumnFieldOptions { + type: 'nanoid'; + size?: number; + customAlphabet?: string; + autoFill?: boolean; +} +export interface PasswordFieldOptions extends BaseColumnFieldOptions { + type: 'password'; + /** + * @default 64 + */ + length?: number; + /** + * @default 8 + */ + randomBytesSize?: number; +} +export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions { + type: 'belongsTo'; + target?: string; +} +export interface HasOneFieldOptions extends BaseRelationFieldOptions, SequelizeHasOneOptions { + type: 'hasOne'; +} +export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions { + type: 'hasMany'; + target?: string; +} +export interface BelongsToManyFieldOptions + extends MultipleRelationFieldOptions, + Omit { + type: 'belongsToMany'; + target?: string; + through?: string; + throughScope?: AssociationScope; + throughUnique?: boolean; + throughParanoid?: boolean; +} diff --git a/packages/plugins/@nocobase/plugin-ai/src/server/resource/ai.ts b/packages/plugins/@nocobase/plugin-ai/src/server/resource/ai.ts index bd3131a537..624a6a18ac 100644 --- a/packages/plugins/@nocobase/plugin-ai/src/server/resource/ai.ts +++ b/packages/plugins/@nocobase/plugin-ai/src/server/resource/ai.ts @@ -9,6 +9,7 @@ import { ResourceOptions } from '@nocobase/resourcer'; import { PluginAIServer } from '../plugin'; +import _ from 'lodash'; const aiResource: ResourceOptions = { name: 'ai', @@ -47,6 +48,84 @@ const aiResource: ResourceOptions = { ctx.body = res.models || []; return next(); }, + + defineCollections: async (ctx, next) => { + const { collections } = ctx.action.params.values || {}; + const id = { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { + type: 'number', + title: '{{t("ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + interface: 'integer', + }; + const createdAt = { + name: 'createdAt', + interface: 'createdAt', + type: 'date', + field: 'createdAt', + uiSchema: { + type: 'datetime', + title: '{{t("Created at")}}', + 'x-component': 'DatePicker', + 'x-component-props': {}, + 'x-read-pretty': true, + }, + }; + const updatedAt = { + type: 'date', + field: 'updatedAt', + name: 'updatedAt', + interface: 'updatedAt', + uiSchema: { + type: 'datetime', + title: '{{t("Last updated at")}}', + 'x-component': 'DatePicker', + 'x-component-props': {}, + 'x-read-pretty': true, + }, + }; + for (const options of collections) { + if (options.name === 'users') { + continue; + } + if (options.autoGenId !== false) { + options.autoGenId = false; + options.fields.unshift(id); + } + if (options.createdAt !== false) { + options.fields.push(createdAt); + } + if (options.updatedAt !== false) { + options.fields.push(updatedAt); + } + const primaryKey = options.fields.find((field: any) => field.primaryKey); + if (!options.filterTargetKey) { + options.filterTargetKey = primaryKey?.name || 'id'; + } + // omit defaultValue + options.fields = options.fields.map((field: any) => { + return _.omit(field, ['defaultValue']); + }); + ctx.db.collection(options); + } + for (const options of collections) { + const collection = ctx.db.getCollection(options.name); + await collection.sync(); + } + const repo = ctx.db.getRepository('collections'); + for (const options of collections) { + await repo.db2cm(options.name); + } + + await next(); + }, }, }; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/schemas/collections.ts b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/schemas/collections.ts index 6811412cb4..2dd1b4f838 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/schemas/collections.ts +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/schemas/collections.ts @@ -152,6 +152,36 @@ export const collectionTableSchema: ISchema = { }, 'x-align': 'left', }, + aiEmployee: { + type: 'void', + 'x-component': 'AIEmployeeButton', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'aiEmployees:button', + 'x-component-props': { + username: 'orin', + tasks: [ + { + message: { + workContext: [], + attachments: [], + user: 'Create a todo list application with support for multiple lists, due dates, priorities, and task categories. Include tables for users, lists, tasks, and categories.', + }, + title: 'Create a todo list application', + autoSend: true, + }, + { + message: { + workContext: [], + attachments: [], + user: 'Create a CRM application that includes the following modules: Leads, Customers, Orders, Products, Tickets', + }, + title: 'Create a CRM application', + autoSend: true, + }, + ], + }, + }, + delete: { type: 'void', title: '{{ t("Delete") }}', diff --git a/yarn.lock b/yarn.lock index d2630b2e0c..884a09f35d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7939,7 +7939,7 @@ resolved "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== -"@types/d3-drag@*", "@types/d3-drag@^3.0.1": +"@types/d3-drag@*", "@types/d3-drag@^3.0.1", "@types/d3-drag@^3.0.7": version "3.0.7" resolved "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== @@ -7985,7 +7985,7 @@ resolved "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz#8d3638df273ec90da34b3ac89d8784c59708cb0d" integrity sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw== -"@types/d3-interpolate@*": +"@types/d3-interpolate@*", "@types/d3-interpolate@^3.0.4": version "3.0.4" resolved "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== @@ -8029,6 +8029,11 @@ resolved "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe" integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg== +"@types/d3-selection@^3.0.10": + version "3.0.11" + resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== + "@types/d3-shape@*": version "3.1.6" resolved "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" @@ -8063,7 +8068,14 @@ dependencies: "@types/d3-selection" "*" -"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": +"@types/d3-transition@^3.0.8": + version "3.0.9" + resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1", "@types/d3-zoom@^3.0.8": version "3.0.8" resolved "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== @@ -9548,6 +9560,30 @@ loupe "^2.3.7" pretty-format "^29.7.0" +"@xyflow/react@^12.7.0": + version "12.7.0" + resolved "https://registry.npmjs.org/@xyflow/react/-/react-12.7.0.tgz#a963692cf38466ba88d54b8ad9099d5e58034460" + integrity sha512-U6VMEbYjiCg1byHrR7S+b5ZdHTjgCFX4KpBc634G/WtEBUvBLoMQdlCD6uJHqodnOAxpt3+G2wiDeTmXAFJzgQ== + dependencies: + "@xyflow/system" "0.0.62" + classcat "^5.0.3" + zustand "^4.4.0" + +"@xyflow/system@0.0.62": + version "0.0.62" + resolved "https://registry.npmjs.org/@xyflow/system/-/system-0.0.62.tgz#3ddb0f549f4ffa81f3924875dce4ddd9ccc9a31d" + integrity sha512-Z2ufbnvuYxIOCGyzE/8eX8TAEM8Lpzc/JafjD1Tzy6ZJs/E7KGVU17Q1F5WDHVW+dbztJAdyXMG0ejR9bwSUAA== + dependencies: + "@types/d3-drag" "^3.0.7" + "@types/d3-interpolate" "^3.0.4" + "@types/d3-selection" "^3.0.10" + "@types/d3-transition" "^3.0.8" + "@types/d3-zoom" "^3.0.8" + d3-drag "^3.0.0" + d3-interpolate "^3.0.1" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -14670,6 +14706,11 @@ electron-to-chromium@^1.5.73: resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz#db20295c5061b68f07c8ea4dfcbd701485d94a3d" integrity sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ== +elkjs@^0.10.0: + version "0.10.0" + resolved "https://registry.npmjs.org/elkjs/-/elkjs-0.10.0.tgz#abe2aa6cb25e7439b708fab873b2448d26ed33a1" + integrity sha512-v/3r+3Bl2NMrWmVoRTMBtHtWvRISTix/s9EfnsfEWApNrsmNjqgqJOispCGg46BPwIFdkag3N/HYSxJczvCm6w== + elkjs@^0.8.2: version "0.8.2" resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" @@ -30827,6 +30868,11 @@ use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: resolved "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +use-sync-external-store@^1.2.2: + version "1.5.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" + integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + use@^3.1.0: version "3.1.1" resolved "https://registry.npmmirror.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -31967,6 +32013,13 @@ zrender@5.6.1: dependencies: tslib "2.3.0" +zustand@^4.4.0: + version "4.5.7" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" + integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== + dependencies: + use-sync-external-store "^1.2.2" + zustand@^4.4.1: version "4.4.7" resolved "https://registry.npmmirror.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c"