diff --git a/frontend/package.json b/frontend/package.json index 4ed98404..41e27f69 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "license": "ISC", "dependencies": { "@ant-design/icons": "^5.2.6", - "@ant-design/pro-components": "2.7.9", + "@ant-design/pro-components": "2.7.19", "@originjs/vite-plugin-federation": "^1.3.3", "@rollup/plugin-dynamic-import-vars": "^2.1.2", "@types/lodash-es": "^4.17.12", diff --git a/frontend/packages/common/package.json b/frontend/packages/common/package.json index 619c752a..10dd7d44 100644 --- a/frontend/packages/common/package.json +++ b/frontend/packages/common/package.json @@ -8,17 +8,26 @@ "test": "node ./__tests__/common.test.js" }, "dependencies": { + "@floating-ui/react": "^0.26.24", "@formkit/auto-animate": "^0.8.1", + "@lexical/code": "^0.17.1", + "@lexical/react": "^0.17.1", + "@lexical/selection": "^0.17.1", + "@lexical/text": "^0.17.1", + "@lexical/utils": "^0.17.1", "@mui/icons-material": "^5.15.6", "@mui/lab": "5.0.0-alpha.150", "@mui/material": "5.14.14", "@mui/x-data-grid-pro": "6.18.1", + "ahooks": "^3.8.1", "allotment": "^1.20.0", "echarts": "^5.5.0", + "lexical": "^0.17.1", "mockjs": "^1.1.0", "rc-picker": "^4.1.1", "react-dropzone": "^14.2.3", - "react-hook-form": "^7.49.3" + "react-hook-form": "^7.49.3", + "use-context-selector": "^2.0.0" }, "devDependencies": { "@formily/antd-v5": "^1.2.1", diff --git a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx index a9ad6a1c..86e851d0 100644 --- a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx +++ b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx @@ -46,13 +46,14 @@ const themeToken = { const mainPage = project === 'core' ?'/service/list':'/serviceHub/list' const TOTAL_MENU_ITEMS:MenuProps['items'] = useMemo(() => [ - getNavItem($t('工作空间'), 'workspace','/guide',, [ - getNavItem($t('我的'), 'my','/guide',null,[ - getNavItem({$t('首页')}, 'guide','/guide',,undefined,undefined,''), - getNavItem({$t('应用')}, 'tenantManagement','/tenantManagement',,undefined,undefined,''), - getNavItem({$t('服务')}, 'service','/service',,undefined,undefined,''), - getNavItem({$t('团队')}, 'team','/team',,undefined,undefined,''), - ],undefined,''), + getNavItem($t('工作空间'), 'workspace','/guide/page',, [ + getNavItem({$t('首页')}, 'guide','/guide/page',,undefined,undefined,'all'), + getNavItem({$t('应用')}, 'tenantManagement','/tenantManagement',,undefined,undefined,'all'), + getNavItem({$t('团队')}, 'team','/team',,undefined,undefined,'all'), + getNavItem($t('服务'), 'my','/service',null,[ + getNavItem({$t('REST 服务')}, 'service','/service',,undefined,undefined,''), + getNavItem({$t('AI 服务')}, 'aiservice','/aiservice',,undefined,undefined,''), + ],undefined,''), ]), getNavItem({$t('API 市场')}, 'serviceHub','/serviceHub',,undefined,undefined,'system.workspace.api_market.view'), @@ -72,6 +73,7 @@ const themeToken = { getNavItem($t('运维与集成'), 'maintenanceCenter','/cluster', null, [ getNavItem({$t('集群')}, 'cluster','/cluster',,undefined,undefined,'system.devops.cluster.view'), + getNavItem({$t('AI 配置')}, 'aisetting','/aisetting',,undefined,undefined,'system.devops.cluster.view'), getNavItem({$t('数据源')}, 'datasourcing','/datasourcing',,undefined,undefined,'system.devops.data_source.view'), getNavItem({$t('证书')}, 'cert','/cert',,undefined,undefined,'system.devops.ssl_certificate.view'), getNavItem({$t('日志')}, 'logsettings','/logsettings',,undefined,undefined,'system.devops.log_configuration.view'), @@ -109,7 +111,7 @@ const themeToken = { } // 处理没有 routes 的菜单项 if (item.access) { - return hasAccess(item.access) ? item : null; + return (item.access === 'all' || hasAccess(item.access)) ? item : null; } // 如果没有 access 和 routes,则保留 diff --git a/frontend/packages/common/src/components/aoplatform/EditableTableNotAutoGen.tsx b/frontend/packages/common/src/components/aoplatform/EditableTableNotAutoGen.tsx new file mode 100644 index 00000000..9ccac47c --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/EditableTableNotAutoGen.tsx @@ -0,0 +1,105 @@ +import { EditableFormInstance, EditableProTable } from "@ant-design/pro-components"; +import { useState, useEffect, useMemo, useRef, MutableRefObject } from "react"; +import { v4 as uuidv4} from 'uuid'; +import { PageProColumns } from "./PageList"; +import TableBtnWithPermission from "./TableBtnWithPermission"; +import { $t } from "@common/locales"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { Form } from "antd"; +import { debounce } from "lodash-es"; + +interface EditableTableProps { + configFields: PageProColumns[]; + value?: T[]; // 外部传入的值 + className?: string; + onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数 + // tableProps?: TableProps; + disabled?:boolean + getFromRef?:(form:MutableRefObject | undefined>)=>void +} + +const EditableTableNotAutoGen = ({ + configFields, + value, // value 现在是外部传入的配置项数组 + onChange, // onChange 现在是当配置项数组变化时的回调函数 + // tableProps, + disabled, + className, + getFromRef + }: EditableTableProps) => { + const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]); + const {state} = useGlobalContext() + const form =useRef>(); + const [tableForm] = Form.useForm(); + const [editableKeys, setEditableRowKeys] = useState(() => + value?.map((item) => item._id) || ['1234'] + ); + + useEffect(()=>{ + getFromRef?.(form) + },[form]) + + useEffect(() => { + const newValue = value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || [{_id:uuidv4()}] + setConfigurations(newValue); + setTimeout(()=>validateForm(),1000) + }, [value]); + + const validateForm = async ()=>{ + await tableForm.validateFields(); + } + + const translatedColumns = useMemo(()=>configFields.map((x)=>( + {...x, + title:$t(x.title as string), + formItemProps:{ + ...(x. formItemProps || {}), + rules:[...(x.formItemProps?.rules || []).map((r:Record)=>{ + if(r.message){ + r.message = $t(r.message) + } + return r + })], + }})),[state.language,configFields]) + + const debouncedOnChange = useMemo(() => debounce((value) => { + onChange?.(value); + }, 500), [onChange]); + + return ( + + className={className} + columns={translatedColumns} + onChange={debouncedOnChange} + controlled={true} + rowKey="_id" + value={configurations as T[]} + size="small" + editableFormRef={form} + bordered={true} + recordCreatorProps={false} + editable={ { + type: 'multiple', + form: tableForm, + // errorType:'default', + editableKeys:disabled ? [] : configurations?.map(x=>x._id), + actionRender: (row, config) => { + return [ + { + setConfigurations((prev)=>{ + const tmpPreData = [...prev]; + tmpPreData.splice(Number(config.index), 1); + onChange?.(tmpPreData); + return tmpPreData}); + setEditableRowKeys((prev)=>(prev.filter(x=>x !== config._id))) + }}/>, + ]; + }, + onChange: setEditableRowKeys + }} + /> + ) + } + +export default EditableTableNotAutoGen; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx index d394f6d6..dc97b7fc 100644 --- a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx +++ b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx @@ -24,9 +24,10 @@ class InsidePageProps { headerClassName?:string='' /** 整个页面滚动 */ scrollPage?:boolean = true + customBtn?:ReactNode } -const InsidePage:FC = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl,showBorder=true,className='',contentClassName='',headerClassName='',scrollPage=true})=>{ +const InsidePage:FC = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl,showBorder=true,className='',contentClassName='',headerClassName='',scrollPage=true,customBtn})=>{ const navigate = useNavigate(); const goBack = () => { @@ -50,6 +51,7 @@ const InsidePage:FC = ({showBanner=true,pageTitle,tagList,showB {showBtn && } + {customBtn}

{description} diff --git a/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx b/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx index 66ef75ae..45455e89 100644 --- a/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx +++ b/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx @@ -2,7 +2,7 @@ import {App, Col, Form, Input, Row, Table, Tooltip} from "antd"; import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react"; import {PublishApprovalInfoType, PublishApprovalModalHandle, PublishApprovalModalProps, PublishVersionTableListItem} from "@common/const/approval/type.tsx"; import {useFetch} from "@common/hooks/http.ts"; -import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR, VALIDATE_MESSAGE} from "@common/const/const.tsx"; +import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR} from "@common/const/const.tsx"; import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "@core/const/system/const.tsx"; import { $t } from "@common/locales"; @@ -14,7 +14,7 @@ import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/Syste export const PublishApprovalModalContent = forwardRef((props, ref) => { const { message } = App.useApp() - const { type,data,insideSystem = false,serviceId, teamId} = props + const { type,data,insidePage = false, serviceType = 'rest', serviceId, teamId} = props const [form] = Form.useForm(); const {fetchData} = useFetch() const {state} = useGlobalContext() @@ -109,7 +109,7 @@ export const PublishApprovalModalContent = forwardRefApprovalRouteColumns.map((x)=>({ + const translatedRouteColumns = useMemo(()=>ApprovalRouteColumns.filter(x=> serviceType === 'rest' ? x.dataIndex !== 'name' : x.dataIndex !== 'methods').map((x)=>({ ...x, ...(x.dataIndex === 'change' ? { render:(_,entity)=>( @@ -122,7 +122,7 @@ export const PublishApprovalModalContent = forwardRefSYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{ if(x.dataIndex === 'status'){ @@ -142,7 +142,7 @@ export const PublishApprovalModalContent = forwardRef - {!insideSystem && <> + {!insidePage && <> {$t('申请系统')}: {(data as PublishApprovalInfoType).project || '-'} @@ -177,7 +177,7 @@ export const PublishApprovalModalContent = forwardRef { - insideSystem && + insidePage && <> - {$t('上游列表')}: - - + { + serviceType === 'rest' && <> + {$t('上游列表')}: + +
+ + } { {$t('替换文件')} {$t('是否放行')} {$t('监控')} + {$t('必填')} + {$t('字符非法,仅支持英文')} ) } \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditor.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditor.tsx new file mode 100644 index 00000000..20320bb3 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditor.tsx @@ -0,0 +1,221 @@ +'use client' + +import type { FC } from 'react' +import { useEffect, useMemo } from 'react' +import type { + EditorState, +} from 'lexical' +import { + $getRoot, + TextNode, +} from 'lexical' +import { CodeNode } from '@lexical/code' +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary' +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +// import TreeView from './plugins/tree-view' +import Placeholder from './plugins/placeholder' +import ComponentPickerBlock from './plugins/component-picker-block/index' +import { + ContextBlock, + ContextBlockNode, + ContextBlockReplacementBlock, +} from './plugins/context-block/index' +import { + QueryBlock, + QueryBlockNode, + QueryBlockReplacementBlock, +} from './plugins/query-block/index' +import { + HistoryBlock, + HistoryBlockNode, + HistoryBlockReplacementBlock, +} from './plugins/history-block/index' +import { + WorkflowVariableBlock, + WorkflowVariableBlockNode, + WorkflowVariableBlockReplacementBlock, +} from './plugins/workflow-variable-block/index' +import VariableBlock from './plugins/variable-block/index' +import VariableValueBlock from './plugins/variable-value-block/index' +import { VariableValueBlockNode } from './plugins/variable-value-block/node' +import { CustomTextNode } from './plugins/custom-text/node' +import OnBlurBlock from './plugins/on-blur-or-focus-block' +import UpdateBlock from './plugins/update-block' +import { textToEditorState } from './utils' +import type { + ContextBlockType, + ExternalToolBlockType, + HistoryBlockType, + QueryBlockType, + VariableBlockType, + WorkflowVariableBlockType, +} from './types' +import { + UPDATE_DATASETS_EVENT_EMITTER, + UPDATE_HISTORY_EVENT_EMITTER, +} from './constants' +import { useEventEmitterContextContext } from '@common/contexts/event-emitter' + +export type PromptEditorProps = { + instanceId?: string + compact?: boolean + className?: string + placeholder?: string + placeholderClassName?: string + style?: React.CSSProperties + value?: string + editable?: boolean + onChange?: (text: string) => void + onBlur?: () => void + onFocus?: () => void + contextBlock?: ContextBlockType + queryBlock?: QueryBlockType + historyBlock?: HistoryBlockType + variableBlock?: VariableBlockType + externalToolBlock?: ExternalToolBlockType + workflowVariableBlock?: WorkflowVariableBlockType +} + +const PromptEditor: FC = ({ + instanceId, + compact, + className, + placeholder, + placeholderClassName, + style, + value, + editable = true, + onChange, + onBlur, + onFocus, + contextBlock, + queryBlock, + historyBlock, + variableBlock, + externalToolBlock, + workflowVariableBlock, +}) => { + const { eventEmitter } = useEventEmitterContextContext() + const initialConfig = { + namespace: 'prompt-editor', + nodes: [ + CodeNode, + CustomTextNode, + { + replace: TextNode, + with: (node: TextNode) => new CustomTextNode(node.__text), + }, + ContextBlockNode, + HistoryBlockNode, + QueryBlockNode, + WorkflowVariableBlockNode, + VariableValueBlockNode, + ], + editorState: textToEditorState(value || ''), + onError: (error: Error) => { + throw error + }, + } + + const handleEditorChange = (editorState: EditorState) => { + const text = editorState.read(() => { + return $getRoot().getChildren().map(p => p.getTextContent()).join('\n') + }) + if (onChange) + onChange(text) + } + + useEffect(() => { + eventEmitter?.emit({ + type: UPDATE_DATASETS_EVENT_EMITTER, + payload: contextBlock?.datasets, + } as any) + }, [eventEmitter, contextBlock?.datasets]) + useEffect(() => { + eventEmitter?.emit({ + type: UPDATE_HISTORY_EVENT_EMITTER, + payload: historyBlock?.history, + } as any) + }, [eventEmitter, historyBlock?.history]) + + return ( + +
+ } + placeholder={} + ErrorBoundary={LexicalErrorBoundary} + /> + + + { + contextBlock?.show && ( + <> + + + + ) + } + { + queryBlock?.show && ( + <> + + + + ) + } + { + historyBlock?.show && ( + <> + + + + ) + } + { + (variableBlock?.show || externalToolBlock?.show) && ( + <> + + + + ) + } + { + workflowVariableBlock?.show && ( + <> + + + + ) + } + + + + + {/* */} +
+
+ ) +} + +export default PromptEditor diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx new file mode 100644 index 00000000..24e37ebf --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx @@ -0,0 +1,82 @@ + +import PromptEditor from '@common/components/aoplatform/prompt-editor/PromptEditor.tsx'; +import PromptEditorHeightResizeWrap from '@common/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx'; +import { useEffect, useState } from 'react'; +import { getVars } from './utils'; +import { VariableItems } from '@core/const/ai-service/type'; + +const PromptEditorResizable = (props:{value?:string, onChange?:(value:string)=>void, variablesChange?:(keys:string[])=>void,promptVariables:VariableItems[]}) =>{ + const {value , onChange,variablesChange,promptVariables} = props + const minHeight = 68 + const [editorHeight, setEditorHeight] = useState(minHeight) + const [previousKeys, setPreviousKeys] = useState([]) + const handleChange = (newTemplates: string, keys: string[]) => { + onChange?.(newTemplates) + } + + return ( +
{value?.length || 0}
+ + )} + ><> + {value !== undefined && ({ + // id: item.id, + // name: item.name, + // type: item.data_source_type, + // })), + // onAddContext: ()=>{console.log('?onAddContext')}, + }} + variableBlock={{ + show: true, + variables:promptVariables?.map(x=>({name:x.key, value:x.key})) || [], + }} + externalToolBlock={{ + show: false, + externalTools: [], + // onAddExternalTool: handleOpenExternalDataToolModal, + }} + historyBlock={{ + show: false, + selectable: false, + history: { + user: '', + assistant: '', + }, + onEditRole: () => { }, + }} + queryBlock={{ + show: false, + selectable: true, + }} + onChange={(value) => { + handleChange?.(value, []) + }} + onBlur={() => { + const keys = getVars(value) + handleChange(value, keys) + if(keys.filter(key => !previousKeys.includes(key)).length > 0){ + variablesChange?.(keys) + setPreviousKeys(keys) + } + }} + editable={true} + /> + } +
) +} + +export default PromptEditorResizable \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/constants.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/constants.tsx new file mode 100644 index 00000000..f7fd5204 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/constants.tsx @@ -0,0 +1,52 @@ +// import type { ValueSelector } from '../../workflow/types' + +export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}' +export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' +export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}' +export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}' +export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' +export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role' +export const MAX_VAR_KEY_LENGTH = 30 + +export const checkHasContextBlock = (text: string) => { + if (!text) + return false + return text.includes(CONTEXT_PLACEHOLDER_TEXT) +} + +export const checkHasHistoryBlock = (text: string) => { + if (!text) + return false + return text.includes(HISTORY_PLACEHOLDER_TEXT) +} + +export const checkHasQueryBlock = (text: string) => { + if (!text) + return false + return text.includes(QUERY_PLACEHOLDER_TEXT) +} + +/* +* {{#1711617514996.name#}} => [1711617514996, name] +* {{#1711617514996.sys.query#}} => [sys, query] +*/ +export const getInputVars = (text: string) => { + if (!text) + return [] + + const allVars = text.match(/{{#([^#]*)#}}/g) + if (allVars && allVars?.length > 0) { + // {{#context#}}, {{#query#}} is not input vars + const inputVars = allVars + .filter(item => item.includes('.')) + .map((item) => { + const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.') + if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0])) + return valueSelector.slice(1) + + return valueSelector + }) + return inputVars + } + return [] +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/hooks.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/hooks.ts new file mode 100644 index 00000000..c9e4cc12 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/hooks.ts @@ -0,0 +1,185 @@ +import { + useCallback, + useEffect, + useRef, + useState, +} from 'react' +import type { Dispatch, RefObject, SetStateAction } from 'react' +import type { + Klass, + LexicalCommand, + LexicalEditor, + TextNode, +} from 'lexical' +import { + $getNodeByKey, + $getSelection, + $isDecoratorNode, + $isNodeSelection, + COMMAND_PRIORITY_LOW, + KEY_BACKSPACE_COMMAND, + KEY_DELETE_COMMAND, +} from 'lexical' +import type { EntityMatch } from '@lexical/text' +import { + mergeRegister, +} from '@lexical/utils' +import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $isContextBlockNode } from './plugins/context-block/node' +import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block' +import { $isHistoryBlockNode } from './plugins/history-block/node' +import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block' +import { $isQueryBlockNode } from './plugins/query-block/node' +import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block' +import type { CustomTextNode } from './plugins/custom-text/node' +import { registerLexicalTextEntity } from './utils' + +export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand) => [RefObject, boolean] +export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand) => { + const ref = useRef(null) + const [editor] = useLexicalComposerContext() + const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey) + + const handleDelete = useCallback( + (event: KeyboardEvent) => { + const selection = $getSelection() + const nodes = selection?.getNodes() + if ( + !isSelected + && nodes?.length === 1 + && ( + ($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND) + || ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND) + || ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND) + ) + ) + editor.dispatchCommand(command, undefined) + + if (isSelected && $isNodeSelection(selection)) { + event.preventDefault() + const node = $getNodeByKey(nodeKey) + if ($isDecoratorNode(node)) { + if (command) + editor.dispatchCommand(command, undefined) + + node.remove() + return true + } + } + + return false + }, + [isSelected, nodeKey, command, editor], + ) + + const handleSelect = useCallback((e: MouseEvent) => { + e.stopPropagation() + clearSelection() + setSelected(true) + }, [setSelected, clearSelection]) + + useEffect(() => { + const ele = ref.current + + if (ele) + ele.addEventListener('click', handleSelect) + + return () => { + if (ele) + ele.removeEventListener('click', handleSelect) + } + }, [handleSelect]) + useEffect(() => { + return mergeRegister( + editor.registerCommand( + KEY_DELETE_COMMAND, + handleDelete, + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + KEY_BACKSPACE_COMMAND, + handleDelete, + COMMAND_PRIORITY_LOW, + ), + ) + }, [editor, clearSelection, handleDelete]) + + return [ref, isSelected] +} + +export type UseTriggerHandler = () => [RefObject, boolean, Dispatch>] +export const useTrigger: UseTriggerHandler = () => { + const triggerRef = useRef(null) + const [open, setOpen] = useState(false) + const handleOpen = useCallback((e: MouseEvent) => { + e.stopPropagation() + setOpen(v => !v) + }, []) + + useEffect(() => { + const trigger = triggerRef.current + if (trigger) + trigger.addEventListener('click', handleOpen) + + return () => { + if (trigger) + trigger.removeEventListener('click', handleOpen) + } + }, [handleOpen]) + + return [triggerRef, open, setOpen] +} + +export function useLexicalTextEntity( + getMatch: (text: string) => null | EntityMatch, + targetNode: Klass, + createNode: (textNode: CustomTextNode) => T, +) { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode)) + }, [createNode, editor, getMatch, targetNode]) +} + +export type MenuTextMatch = { + leadOffset: number + matchingString: string + replaceableString: string +} +export type TriggerFn = ( + text: string, + editor: LexicalEditor, +) => MenuTextMatch | null +export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;' +export function useBasicTypeaheadTriggerMatch( + trigger: string, + { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }, +): TriggerFn { + return useCallback( + (text: string) => { + const validChars = `[${PUNCTUATION}\\s]` + const TypeaheadTriggerRegex = new RegExp( + '(.*)(' + + `[${trigger}]` + + `((?:${validChars}){0,${maxLength}})` + + ')$', + ) + const match = TypeaheadTriggerRegex.exec(text) + if (match !== null) { + const maybeLeadingWhitespace = match[1] + const matchingString = match[3] + if (matchingString.length >= minLength) { + return { + leadOffset: match.index + maybeLeadingWhitespace.length, + matchingString, + replaceableString: match[2], + } + } + } + return null + }, + [maxLength, minLength, trigger], + ) +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/hooks.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/hooks.tsx new file mode 100644 index 00000000..e09a83ed --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/hooks.tsx @@ -0,0 +1,294 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { $insertNodes } from 'lexical' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { + ContextBlockType, + ExternalToolBlockType, + HistoryBlockType, + QueryBlockType, + VariableBlockType, + WorkflowVariableBlockType, +} from '../../types' +import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block' +import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block' +import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block' +import { $createCustomTextNode } from '../custom-text/node' +import { PromptMenuItem } from './prompt-option' +import { VariableMenuItem } from './variable-option' +import { PickerBlockMenuOption } from './menu' +import { $t } from '@common/locales' +// import { File05 } from '@/app/components/base/icons/src/vender/solid/files' +// import { +// MessageClockCircle, +// Tool03, +// } from '@/app/components/base/icons/src/vender/solid/general' +// import { BracketsX } from '@/app/components/base/icons/src/vender/line/development' +// import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' +// import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' +// import AppIcon from '@/app/components/base/app-icon' + +export const usePromptOptions = ( + contextBlock?: ContextBlockType, + queryBlock?: QueryBlockType, + historyBlock?: HistoryBlockType, +) => { + const [editor] = useLexicalComposerContext() + + const promptOptions: PickerBlockMenuOption[] = [] + if (contextBlock?.show) { + promptOptions.push(new PickerBlockMenuOption({ + key: $t('上下文'), + group: 'prompt context', + render: ({ isSelected, onSelect, onSetHighlight }) => { + return } + // icon={} + disabled={!contextBlock.selectable} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + }, + onSelect: () => { + if (!contextBlock?.selectable) + return + editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined) + }, + })) + } + + if (queryBlock?.show) { + promptOptions.push( + new PickerBlockMenuOption({ + key: $t('查询内容'), + group: 'prompt query', + render: ({ isSelected, onSelect, onSetHighlight }) => { + return ( + } + // icon={} + disabled={!queryBlock.selectable} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + ) + }, + onSelect: () => { + if (!queryBlock?.selectable) + return + editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined) + }, + }), + ) + } + + if (historyBlock?.show) { + promptOptions.push( + new PickerBlockMenuOption({ + key: $t('会话历史'), + group: 'prompt history', + render: ({ isSelected, onSelect, onSetHighlight }) => { + return ( + } + // icon={} + disabled={!historyBlock.selectable + } + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + ) + }, + onSelect: () => { + if (!historyBlock?.selectable) + return + editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined) + }, + }), + ) + } + return promptOptions +} + +export const useVariableOptions = ( + variableBlock?: VariableBlockType, + queryString?: string, +): PickerBlockMenuOption[] => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + const options = useMemo(() => { + if (!variableBlock?.variables) + return [] + + const baseOptions = (variableBlock.variables).map((item) => { + return new PickerBlockMenuOption({ + key: item.value, + group: 'prompt variable', + render: ({ queryString, isSelected, onSelect, onSetHighlight }) => { + return ( + } + // icon={} + queryString={queryString} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + ) + }, + onSelect: () => { + editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`) + }, + }) + }) + if (!queryString) + return baseOptions + + const regex = new RegExp(queryString, 'i') + + return baseOptions.filter(option => regex.test(option.key)) + }, [editor, queryString, variableBlock]) + + const addOption = useMemo(() => { + return new PickerBlockMenuOption({ + key: $t('添加新变量'), + group: 'prompt variable', + render: ({ queryString, isSelected, onSelect, onSetHighlight }) => { + return ( + } + // icon={} + queryString={queryString} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + ) + }, + onSelect: () => { + editor.update(() => { + const prefixNode = $createCustomTextNode('{{') + const suffixNode = $createCustomTextNode('}}') + $insertNodes([prefixNode, suffixNode]) + prefixNode.select() + }) + }, + }) + }, [editor, t]) + + return useMemo(() => { + return variableBlock?.show ? [...options, addOption] : [] + }, [options, addOption, variableBlock?.show]) +} + +export const useExternalToolOptions = ( + externalToolBlockType?: ExternalToolBlockType, + queryString?: string, +) => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + const options = useMemo(() => { + if (!externalToolBlockType?.externalTools) + return [] + const baseToolOptions = (externalToolBlockType.externalTools).map((item) => { + return new PickerBlockMenuOption({ + key: item.name, + group: 'external tool', + render: ({ queryString, isSelected, onSelect, onSetHighlight }) => { + return ( + } + // icon={ + // + // } + extraElement={
{item.variableName}
} + queryString={queryString} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + ) + }, + onSelect: () => { + editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`) + }, + }) + }) + if (!queryString) + return baseToolOptions + + const regex = new RegExp(queryString, 'i') + + return baseToolOptions.filter(option => regex.test(option.key)) + }, [editor, queryString, externalToolBlockType]) + + const addOption = useMemo(() => { + return new PickerBlockMenuOption({ + key: $t('添加工具'), + group: 'external tool', + render: ({ queryString, isSelected, onSelect, onSetHighlight }) => { + return ( + } + // extraElement={< ArrowUpRight className='w-3 h-3 text-gray-400' />} + queryString={queryString} + isSelected={isSelected} + onClick={onSelect} + onMouseEnter={onSetHighlight} + /> + ) + }, + onSelect: () => { + externalToolBlockType?.onAddExternalTool?.() + }, + }) + }, [externalToolBlockType, t]) + + return useMemo(() => { + return externalToolBlockType?.show ? [...options, addOption] : [] + }, [options, addOption, externalToolBlockType?.show]) +} + +export const useOptions = ( + contextBlock?: ContextBlockType, + queryBlock?: QueryBlockType, + historyBlock?: HistoryBlockType, + variableBlock?: VariableBlockType, + externalToolBlockType?: ExternalToolBlockType, + workflowVariableBlockType?: WorkflowVariableBlockType, + queryString?: string, +) => { + const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) + const variableOptions = useVariableOptions(variableBlock, queryString) + const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) + const workflowVariableOptions = useMemo(() => { + if (!workflowVariableBlockType?.show) + return [] + + return workflowVariableBlockType.variables || [] + }, [workflowVariableBlockType]) + + return useMemo(() => { + return { + workflowVariableOptions, + allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions], + } + }, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions]) +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/index.tsx new file mode 100644 index 00000000..bfece5ba --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/index.tsx @@ -0,0 +1,212 @@ +import { + Fragment, + memo, + useCallback, + useState, +} from 'react' +import ReactDOM from 'react-dom' +import { + flip, + offset, + shift, + useFloating, +} from '@floating-ui/react' +import type { TextNode } from 'lexical' +import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin' +import type { + ContextBlockType, + ExternalToolBlockType, + HistoryBlockType, + QueryBlockType, + VariableBlockType, + WorkflowVariableBlockType, +} from '../../types' +import { useBasicTypeaheadTriggerMatch } from '../../hooks' +import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block' +import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block' +import { $splitNodeContainingQuery } from '../../utils' +import { useOptions } from './hooks' +import type { PickerBlockMenuOption } from './menu' +// import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' +import { useEventEmitterContextContext } from '@common/contexts/event-emitter' + +type ComponentPickerProps = { + triggerString: string + contextBlock?: ContextBlockType + queryBlock?: QueryBlockType + historyBlock?: HistoryBlockType + variableBlock?: VariableBlockType + externalToolBlock?: ExternalToolBlockType + workflowVariableBlock?: WorkflowVariableBlockType +} +const ComponentPicker = ({ + triggerString, + contextBlock, + queryBlock, + historyBlock, + variableBlock, + externalToolBlock, + workflowVariableBlock, +}: ComponentPickerProps) => { + const { eventEmitter } = useEventEmitterContextContext() + const { refs, floatingStyles, isPositioned } = useFloating({ + placement: 'bottom-start', + middleware: [ + offset(0), // fix hide cursor + shift({ + padding: 8, + }), + flip(), + ], + }) + const [editor] = useLexicalComposerContext() + const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, { + minLength: 0, + maxLength: 0, + }) + + const [queryString, setQueryString] = useState(null) + + eventEmitter?.useSubscription((v: any) => { + if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND) + editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`) + }) + + const { + allFlattenOptions, + workflowVariableOptions, + } = useOptions( + contextBlock, + queryBlock, + historyBlock, + variableBlock, + externalToolBlock, + workflowVariableBlock, + ) + + const onSelectOption = useCallback( + ( + selectedOption: PickerBlockMenuOption, + nodeToRemove: TextNode | null, + closeMenu: () => void, + ) => { + editor.update(() => { + if (nodeToRemove && selectedOption?.key) + nodeToRemove.remove() + + selectedOption.onSelectMenuOption() + closeMenu() + }) + }, + [editor], + ) + + const handleSelectWorkflowVariable = useCallback((variables: string[]) => { + editor.update(() => { + const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!) + if (needRemove) + needRemove.remove() + }) + + if (variables[1] === 'sys.query' || variables[1] === 'sys.files') + editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]]) + else + editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) + }, [editor, checkForTriggerMatch, triggerString]) + + const renderMenu = useCallback>(( + anchorElementRef, + { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, + ) => { + if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) + return null + refs.setReference(anchorElementRef.current) + + return ( + <> + { + ReactDOM.createPortal( + // The `LexicalMenu` will try to calculate the position of the floating menu based on the first child. + // Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected. + // See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493 +
+
+ { + options.map((option, index) => ( + + { + // Divider + index !== 0 && options.at(index - 1)?.group !== option.group && ( +
+ ) + } + {option.renderMenuOption({ + queryString, + isSelected: selectedIndex === index, + onSelect: () => { + selectOptionAndCleanUp(option) + }, + onSetHighlight: () => { + setHighlightedIndex(index) + }, + })} +
+ )) + } + { + workflowVariableBlock?.show && ( + <> + { + (!!options.length) && ( +
+ ) + } +
+ {/* { + handleSelectWorkflowVariable(variables) + }} + /> */} +
+ + ) + } +
+
, + anchorElementRef.current, + ) + } + + ) + }, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable]) + + return ( + + ) +} + +export default memo(ComponentPicker) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/menu.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/menu.tsx new file mode 100644 index 00000000..d8c71569 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/menu.tsx @@ -0,0 +1,31 @@ +import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin' +import { Fragment } from 'react' + +/** + * Corresponds to the `MenuRenderFn` type from `@lexical/react/LexicalTypeaheadMenuPlugin`. + */ +type MenuOptionRenderProps = { + isSelected: boolean + onSelect: () => void + onSetHighlight: () => void + queryString: string | null +} + +export class PickerBlockMenuOption extends MenuOption { + public group?: string + + constructor( + private data: { + key: string + group?: string + onSelect?: () => void + render: (menuRenderProps: MenuOptionRenderProps) => JSX.Element + }, + ) { + super(data.key) + this.group = data.group + } + + public onSelectMenuOption = () => this.data.onSelect?.() + public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => {this.data.render(menuRenderProps)} +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/prompt-option.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/prompt-option.tsx new file mode 100644 index 00000000..7aabbe4b --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/prompt-option.tsx @@ -0,0 +1,45 @@ +import { memo } from 'react' + +type PromptMenuItemMenuItemProps = { + icon: JSX.Element + title: string + disabled?: boolean + isSelected: boolean + onClick: () => void + onMouseEnter: () => void + setRefElement?: (element: HTMLDivElement) => void +} +export const PromptMenuItem = memo(({ + icon, + title, + disabled, + isSelected, + onClick, + onMouseEnter, + setRefElement, +}: PromptMenuItemMenuItemProps) => { + return ( +
{ + if (disabled) + return + onMouseEnter() + }} + onClick={() => { + if (disabled) + return + onClick() + }}> + {icon} +
{title}
+
+ ) +}) +PromptMenuItem.displayName = 'PromptMenuItem' diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/variable-option.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/variable-option.tsx new file mode 100644 index 00000000..bb5290c8 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/variable-option.tsx @@ -0,0 +1,60 @@ +import { memo } from 'react' + +type VariableMenuItemProps = { + title: string + icon?: JSX.Element + extraElement?: JSX.Element + isSelected: boolean + queryString: string | null + onClick: () => void + onMouseEnter: () => void + setRefElement?: (element: HTMLDivElement) => void +} +export const VariableMenuItem = memo(({ + title, + icon, + extraElement, + isSelected, + queryString, + onClick, + onMouseEnter, + setRefElement, +}: VariableMenuItemProps) => { + let before = title + let middle = '' + let after = '' + + if (queryString) { + const regex = new RegExp(queryString, 'i') + const match = regex.exec(title) + + if (match) { + before = title.substring(0, match.index) + middle = match[0] + after = title.substring(match.index + match[0].length) + } + } + + return ( +
+
+ {icon} +
+
+ {before} + {middle} + {after} +
+ {extraElement} +
+ ) +}) +VariableMenuItem.displayName = 'VariableMenuItem' diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/component.tsx new file mode 100644 index 00000000..60346493 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/component.tsx @@ -0,0 +1,56 @@ +import type { FC } from 'react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +// import { +// RiAddLine, +// } from '@remixicon/react' +import { useSelectOrDelete, useTrigger } from '../../hooks' +import { UPDATE_DATASETS_EVENT_EMITTER } from '../../constants' +import type { Dataset } from './index' +import { DELETE_CONTEXT_BLOCK_COMMAND } from './index' +// import { File05, Folder } from '@/app/components/base/icons/src/vender/solid/files' +// import { +// PortalToFollowElem, +// PortalToFollowElemContent, +// PortalToFollowElemTrigger, +// } from '@/app/components/base/portal-to-follow-elem' +import { useEventEmitterContextContext } from '@common/contexts/event-emitter' +import { $t } from '@common/locales' + +type ContextBlockComponentProps = { + nodeKey: string + datasets?: Dataset[] + onAddContext: () => void + canNotAddContext?: boolean +} + +const ContextBlockComponent: FC = ({ + nodeKey, + datasets = [], + onAddContext, + canNotAddContext, +}) => { + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND) + const [triggerRef, open, setOpen] = useTrigger() + const { eventEmitter } = useEventEmitterContextContext() + const [localDatasets, setLocalDatasets] = useState(datasets) + + eventEmitter?.useSubscription((v: any) => { + if (v?.type === UPDATE_DATASETS_EVENT_EMITTER) + setLocalDatasets(v.payload) + }) + + return ( +
+ {/* */} +
{$t('上下文')}
+ +
+ ) +} + +export default ContextBlockComponent diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/context-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/context-block-replacement-block.tsx new file mode 100644 index 00000000..7470edac --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/context-block-replacement-block.tsx @@ -0,0 +1,63 @@ +import { + memo, + useCallback, + useEffect, +} from 'react' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants' +import type { ContextBlockType } from '../../types' +import { + $createContextBlockNode, + ContextBlockNode, +} from './node' +import { CustomTextNode } from '../custom-text/node' + +const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT) + +const ContextBlockReplacementBlock = ({ + datasets = [], + onAddContext = () => {}, + onInsert, + canNotAddContext, +}: ContextBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([ContextBlockNode])) + throw new Error('ContextBlockNodePlugin: ContextBlockNode not registered on editor') + }, [editor]) + + const createContextBlockNode = useCallback((): ContextBlockNode => { + if (onInsert) + onInsert() + return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext)) + }, [datasets, onAddContext, onInsert, canNotAddContext]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)), + ) + }, []) + + return null +} + +export default memo(ContextBlockReplacementBlock) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/index.tsx new file mode 100644 index 00000000..5be4f1fa --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/index.tsx @@ -0,0 +1,74 @@ +import { + memo, + useEffect, +} from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { ContextBlockType } from '../../types' +import { + $createContextBlockNode, + ContextBlockNode, +} from './node' + +export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND') +export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND') + +export type Dataset = { + id: string + name: string + type: string +} + +const ContextBlock = memo(({ + datasets = [], + onAddContext = () => {}, + onInsert, + onDelete, + canNotAddContext, +}: ContextBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([ContextBlockNode])) + throw new Error('ContextBlockPlugin: ContextBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_CONTEXT_BLOCK_COMMAND, + () => { + const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext) + + $insertNodes([contextBlockNode]) + + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_CONTEXT_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext]) + + return null +}) +ContextBlock.displayName = 'ContextBlock' + +export { ContextBlock } +export { ContextBlockNode } from './node' +export { default as ContextBlockReplacementBlock } from './context-block-replacement-block' diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/node.tsx new file mode 100644 index 00000000..3800b9bb --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/node.tsx @@ -0,0 +1,100 @@ +import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import ContextBlockComponent from './component' +import type { Dataset } from './index' + +export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void; canNotAddContext: boolean } + +export class ContextBlockNode extends DecoratorNode { + __datasets: Dataset[] + __onAddContext: () => void + __canNotAddContext: boolean + + static getType(): string { + return 'context-block' + } + + static clone(node: ContextBlockNode): ContextBlockNode { + return new ContextBlockNode(node.__datasets, node.__onAddContext, node.getKey(), node.__canNotAddContext) + } + + isInline(): boolean { + return true + } + + constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey, canNotAddContext?: boolean) { + super(key) + + this.__datasets = datasets + this.__onAddContext = onAddContext + this.__canNotAddContext = canNotAddContext || false + } + + createDOM(): HTMLElement { + const div = document.createElement('div') + div.classList.add('inline-flex', 'items-center', 'align-middle') + return div + } + + updateDOM(): false { + return false + } + + decorate(): JSX.Element { + return ( + + ) + } + + getDatasets(): Dataset[] { + const self = this.getLatest() + + return self.__datasets + } + + getOnAddContext(): () => void { + const self = this.getLatest() + + return self.__onAddContext + } + + getCanNotAddContext(): boolean { + const self = this.getLatest() + + return self.__canNotAddContext + } + + static importJSON(serializedNode: SerializedNode): ContextBlockNode { + const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext, serializedNode.canNotAddContext) + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'context-block', + version: 1, + datasets: this.getDatasets(), + onAddContext: this.getOnAddContext(), + canNotAddContext: this.getCanNotAddContext(), + } + } + + getTextContent(): string { + return '{{#context#}}' + } +} +export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void, canNotAddContext?: boolean): ContextBlockNode { + return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext) +} + +export function $isContextBlockNode( + node: ContextBlockNode | LexicalNode | null | undefined, +): boolean { + return node instanceof ContextBlockNode +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/custom-text/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/custom-text/node.tsx new file mode 100644 index 00000000..5df4894c --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/custom-text/node.tsx @@ -0,0 +1,52 @@ +import type { EditorConfig, NodeKey, SerializedTextNode } from 'lexical' +import { $createTextNode, TextNode } from 'lexical' + +export class CustomTextNode extends TextNode { + static getType() { + return 'custom-text' + } + + static clone(node: CustomTextNode) { + return new CustomTextNode(node.__text, node.__key) + } + + constructor(text: string, key?: NodeKey) { + super(text, key) + } + + createDOM(config: EditorConfig) { + const dom = super.createDOM(config) + dom.classList.add('align-middle') + return dom + } + + static importJSON(serializedNode: SerializedTextNode): TextNode { + const node = $createTextNode(serializedNode.text) + node.setFormat(serializedNode.format) + node.setDetail(serializedNode.detail) + node.setMode(serializedNode.mode) + node.setStyle(serializedNode.style) + return node + } + + exportJSON(): SerializedTextNode { + return { + detail: this.getDetail(), + format: this.getFormat(), + mode: this.getMode(), + style: this.getStyle(), + text: this.getTextContent(), + type: 'custom-text', + version: 1, + } + } + + isSimpleText() { + return ( + (this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0) + } +} + +export function $createCustomTextNode(text: string): CustomTextNode { + return new CustomTextNode(text) +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/component.tsx new file mode 100644 index 00000000..54e0cc5a --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/component.tsx @@ -0,0 +1,53 @@ +import type { FC } from 'react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +// import { +// RiMoreFill, +// } from '@remixicon/react' +import { useSelectOrDelete, useTrigger } from '../../hooks' +import { UPDATE_HISTORY_EVENT_EMITTER } from '../../constants' +import type { RoleName } from './index' +import { DELETE_HISTORY_BLOCK_COMMAND } from './index' +// import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general' +// import { +// PortalToFollowElem, +// PortalToFollowElemContent, +// PortalToFollowElemTrigger, +// } from '@/app/components/base/portal-to-follow-elem' +import { useEventEmitterContextContext } from '@common/contexts/event-emitter' +import { $t } from '@common/locales' + +type HistoryBlockComponentProps = { + nodeKey: string + roleName?: RoleName + onEditRole: () => void +} + +const HistoryBlockComponent: FC = ({ + nodeKey, + roleName = { user: '', assistant: '' }, + onEditRole, +}) => { + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND) + const [triggerRef, open, setOpen] = useTrigger() + const { eventEmitter } = useEventEmitterContextContext() + const [localRoleName, setLocalRoleName] = useState(roleName) + + eventEmitter?.useSubscription((v: any) => { + if (v?.type === UPDATE_HISTORY_EVENT_EMITTER) + setLocalRoleName(v.payload) + }) + + return ( +
+ {/* */} +
{$t('会话历史')}
+
+ ) +} + +export default HistoryBlockComponent diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/history-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/history-block-replacement-block.tsx new file mode 100644 index 00000000..414d027c --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/history-block-replacement-block.tsx @@ -0,0 +1,61 @@ +import { + useCallback, + useEffect, +} from 'react' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import { HISTORY_PLACEHOLDER_TEXT } from '../../constants' +import type { HistoryBlockType } from '../../types' +import { + $createHistoryBlockNode, + HistoryBlockNode, +} from './node' +import { CustomTextNode } from '../custom-text/node' + +const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT) + +const HistoryBlockReplacementBlock = ({ + history = { user: '', assistant: '' }, + onEditRole = () => {}, + onInsert, +}: HistoryBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([HistoryBlockNode])) + throw new Error('HistoryBlockNodePlugin: HistoryBlockNode not registered on editor') + }, [editor]) + + const createHistoryBlockNode = useCallback((): HistoryBlockNode => { + if (onInsert) + onInsert() + return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole)) + }, [history, onEditRole, onInsert]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)), + ) + }, []) + + return null +} + +export default HistoryBlockReplacementBlock diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/index.tsx new file mode 100644 index 00000000..78c73495 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/index.tsx @@ -0,0 +1,79 @@ +import { + memo, + useEffect, +} from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { HistoryBlockType } from '../../types' +import { + $createHistoryBlockNode, + HistoryBlockNode, +} from './node' + +export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND') +export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND') + +export type RoleName = { + user: string + assistant: string +} + +export type HistoryBlockProps = { + roleName: RoleName + onEditRole: () => void + onInsert?: () => void + onDelete?: () => void +} + +const HistoryBlock = memo(({ + history = { user: '', assistant: '' }, + onEditRole = () => {}, + onInsert, + onDelete, +}: HistoryBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([HistoryBlockNode])) + throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_HISTORY_BLOCK_COMMAND, + () => { + const historyBlockNode = $createHistoryBlockNode(history, onEditRole) + + $insertNodes([historyBlockNode]) + + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_HISTORY_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, history, onEditRole, onInsert, onDelete]) + + return null +}) +HistoryBlock.displayName = 'HistoryBlock' + +export { HistoryBlock } +export { HistoryBlockNode } from './node' +export { default as HistoryBlockReplacementBlock } from './history-block-replacement-block' diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/node.tsx new file mode 100644 index 00000000..4112b736 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/node.tsx @@ -0,0 +1,90 @@ +import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import HistoryBlockComponent from './component' +import type { RoleName } from './index' + +export type SerializedNode = SerializedLexicalNode & { roleName: RoleName; onEditRole: () => void } + +export class HistoryBlockNode extends DecoratorNode { + __roleName: RoleName + __onEditRole: () => void + + static getType(): string { + return 'history-block' + } + + static clone(node: HistoryBlockNode): HistoryBlockNode { + return new HistoryBlockNode(node.__roleName, node.__onEditRole) + } + + constructor(roleName: RoleName, onEditRole: () => void, key?: NodeKey) { + super(key) + + this.__roleName = roleName + this.__onEditRole = onEditRole + } + + isInline(): boolean { + return true + } + + createDOM(): HTMLElement { + const div = document.createElement('div') + div.classList.add('inline-flex', 'items-center', 'align-middle') + return div + } + + updateDOM(): false { + return false + } + + decorate(): JSX.Element { + return ( + + ) + } + + getRoleName(): RoleName { + const self = this.getLatest() + + return self.__roleName + } + + getOnEditRole(): () => void { + const self = this.getLatest() + + return self.__onEditRole + } + + static importJSON(serializedNode: SerializedNode): HistoryBlockNode { + const node = $createHistoryBlockNode(serializedNode.roleName, serializedNode.onEditRole) + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'history-block', + version: 1, + roleName: this.getRoleName(), + onEditRole: this.getOnEditRole, + } + } + + getTextContent(): string { + return '{{#histories#}}' + } +} +export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => void): HistoryBlockNode { + return new HistoryBlockNode(roleName, onEditRole) +} + +export function $isHistoryBlockNode( + node: HistoryBlockNode | LexicalNode | null | undefined, +): node is HistoryBlockNode { + return node instanceof HistoryBlockNode +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/on-blur-or-focus-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/on-blur-or-focus-block.tsx new file mode 100644 index 00000000..2e3adc15 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/on-blur-or-focus-block.tsx @@ -0,0 +1,67 @@ +import type { FC } from 'react' +import { useEffect, useRef } from 'react' +import { + BLUR_COMMAND, + COMMAND_PRIORITY_EDITOR, + FOCUS_COMMAND, + KEY_ESCAPE_COMMAND, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block' + +type OnBlurBlockProps = { + onBlur?: () => void + onFocus?: () => void +} +const OnBlurBlock: FC = ({ + onBlur, + onFocus, +}) => { + const [editor] = useLexicalComposerContext() + + const ref = useRef(null) + + useEffect(() => { + return mergeRegister( + editor.registerCommand( + CLEAR_HIDE_MENU_TIMEOUT, + () => { + if (ref.current) { + clearTimeout(ref.current) + ref.current = null + } + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + BLUR_COMMAND, + () => { + ref.current = setTimeout(() => { + editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' })) + }, 200) + + if (onBlur) + onBlur() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + FOCUS_COMMAND, + () => { + if (onFocus) + onFocus() + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, onBlur, onFocus]) + + return null +} + +export default OnBlurBlock diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/placeholder.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/placeholder.tsx new file mode 100644 index 00000000..d0ed3c9a --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/placeholder.tsx @@ -0,0 +1,22 @@ +import { $t } from '@common/locales' +import { memo } from 'react' + +const Placeholder = ({ + compact, + value +}: { + compact?: boolean + value?: string + className?: string +}) => { + + return ( +
+ {value || $t('AI 模型调用默认仅使用 Query 变量,可输入 “{” 增加新变量。')} +
+ ) +} + +export default memo(Placeholder) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/component.tsx new file mode 100644 index 00000000..115e51f1 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/component.tsx @@ -0,0 +1,33 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelectOrDelete } from '../../hooks' +import { DELETE_QUERY_BLOCK_COMMAND } from './index' +import { $t } from '@common/locales' +// import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' + +type QueryBlockComponentProps = { + nodeKey: string +} + +const QueryBlockComponent: FC = ({ + nodeKey, +}) => { + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND) + + return ( +
+ {/* */} +
{'{{'}
+
{$t('查询内容')}
+
{'}}'}
+
+ ) +} + +export default QueryBlockComponent diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/index.tsx new file mode 100644 index 00000000..09461531 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/index.tsx @@ -0,0 +1,68 @@ +import { + memo, + useEffect, +} from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { QueryBlockType } from '../../types' +import { + $createQueryBlockNode, + QueryBlockNode, +} from './node' + +export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND') +export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND') + +export type QueryBlockProps = { + onInsert?: () => void + onDelete?: () => void +} +const QueryBlock = memo(({ + onInsert, + onDelete, +}: QueryBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([QueryBlockNode])) + throw new Error('QueryBlockPlugin: QueryBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_QUERY_BLOCK_COMMAND, + () => { + const contextBlockNode = $createQueryBlockNode() + + $insertNodes([contextBlockNode]) + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_QUERY_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, onInsert, onDelete]) + + return null +}) +QueryBlock.displayName = 'QueryBlock' + +export { QueryBlock } +export { QueryBlockNode } from './node' +export { default as QueryBlockReplacementBlock } from './query-block-replacement-block' diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/node.tsx new file mode 100644 index 00000000..3b5f6c0d --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/node.tsx @@ -0,0 +1,59 @@ +import type { LexicalNode, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import QueryBlockComponent from './component' + +export type SerializedNode = SerializedLexicalNode + +export class QueryBlockNode extends DecoratorNode { + static getType(): string { + return 'query-block' + } + + static clone(): QueryBlockNode { + return new QueryBlockNode() + } + + isInline(): boolean { + return true + } + + createDOM(): HTMLElement { + const div = document.createElement('div') + div.classList.add('inline-flex', 'items-center', 'align-middle') + return div + } + + updateDOM(): false { + return false + } + + decorate(): JSX.Element { + return + } + + static importJSON(): QueryBlockNode { + const node = $createQueryBlockNode() + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'query-block', + version: 1, + } + } + + getTextContent(): string { + return '{{#query#}}' + } +} +export function $createQueryBlockNode(): QueryBlockNode { + return new QueryBlockNode() +} + +export function $isQueryBlockNode( + node: QueryBlockNode | LexicalNode | null | undefined, +): node is QueryBlockNode { + return node instanceof QueryBlockNode +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/query-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/query-block-replacement-block.tsx new file mode 100644 index 00000000..0959f93c --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/query-block-replacement-block.tsx @@ -0,0 +1,60 @@ +import { + memo, + useCallback, + useEffect, +} from 'react' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import { QUERY_PLACEHOLDER_TEXT } from '../../constants' +import type { QueryBlockType } from '../../types' +import { + $createQueryBlockNode, + QueryBlockNode, +} from './node' +import { CustomTextNode } from '../custom-text/node' + +const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT) + +const QueryBlockReplacementBlock = ({ + onInsert, +}: QueryBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([QueryBlockNode])) + throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor') + }, [editor]) + + const createQueryBlockNode = useCallback((): QueryBlockNode => { + if (onInsert) + onInsert() + return $applyNodeReplacement($createQueryBlockNode()) + }, [onInsert]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)), + ) + }, []) + + return null +} + +export default memo(QueryBlockReplacementBlock) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/tree-view.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/tree-view.tsx new file mode 100644 index 00000000..29028cb2 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/tree-view.tsx @@ -0,0 +1,19 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { TreeView } from '@lexical/react/LexicalTreeView' + +const TreeViewPlugin = () => { + const [editor] = useLexicalComposerContext() + return ( + + ) +} + +export default TreeViewPlugin diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/update-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/update-block.tsx new file mode 100644 index 00000000..ad5c50dd --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/update-block.tsx @@ -0,0 +1,42 @@ +import { $insertNodes } from 'lexical' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { textToEditorState } from '../utils' +import { CustomTextNode } from './custom-text/node' +import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block' +import { useEventEmitterContextContext } from '@common/contexts/event-emitter' + +export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER' +export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY' + +type UpdateBlockProps = { + instanceId?: string +} +const UpdateBlock = ({ + instanceId, +}: UpdateBlockProps) => { + const { eventEmitter } = useEventEmitterContextContext() + const [editor] = useLexicalComposerContext() + + eventEmitter?.useSubscription((v: any) => { + if (v.type === PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER && v.instanceId === instanceId) { + const editorState = editor.parseEditorState(textToEditorState(v.payload)) + editor.setEditorState(editorState) + } + }) + + eventEmitter?.useSubscription((v: any) => { + if (v.type === PROMPT_EDITOR_INSERT_QUICKLY && v.instanceId === instanceId) { + editor.focus() + editor.update(() => { + const textNode = new CustomTextNode('/') + $insertNodes([textNode]) + + editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined) + }) + } + }) + + return null +} + +export default UpdateBlock diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-block/index.tsx new file mode 100644 index 00000000..3c995d59 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-block/index.tsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { CustomTextNode } from '../custom-text/node' + +export const INSERT_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_BLOCK_COMMAND') +export const INSERT_VARIABLE_VALUE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_VALUE_BLOCK_COMMAND') + +const VariableBlock = () => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + return mergeRegister( + editor.registerCommand( + INSERT_VARIABLE_BLOCK_COMMAND, + () => { + const textNode = new CustomTextNode('{') + $insertNodes([textNode]) + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + INSERT_VARIABLE_VALUE_BLOCK_COMMAND, + (value: string) => { + const textNode = new CustomTextNode(value) + $insertNodes([textNode]) + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor]) + + return null +} + +export default VariableBlock diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/index.tsx new file mode 100644 index 00000000..e93c0d7f --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/index.tsx @@ -0,0 +1,52 @@ +import { + useCallback, + useEffect, +} from 'react' +import type { TextNode } from 'lexical' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useLexicalTextEntity } from '../../hooks' +import { + $createVariableValueBlockNode, + VariableValueBlockNode, +} from './node' +import { getHashtagRegexString } from './utils' + +const REGEX = new RegExp(getHashtagRegexString(), 'i') + +const VariableValueBlock = () => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([VariableValueBlockNode])) + throw new Error('VariableValueBlockPlugin: VariableValueNode not registered on editor') + }, [editor]) + + const createVariableValueBlockNode = useCallback((textNode: TextNode): VariableValueBlockNode => { + return $createVariableValueBlockNode(textNode.getTextContent()) + }, []) + + const getVariableValueMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const hashtagLength = matchArr[0].length + const startOffset = matchArr.index + const endOffset = startOffset + hashtagLength + return { + end: endOffset, + start: startOffset, + } + }, []) + + useLexicalTextEntity( + getVariableValueMatch, + VariableValueBlockNode, + createVariableValueBlockNode, + ) + + return null +} + +export default VariableValueBlock diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/node.tsx new file mode 100644 index 00000000..163d4bfa --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/node.tsx @@ -0,0 +1,65 @@ +import type { + EditorConfig, + LexicalNode, + NodeKey, + SerializedTextNode, +} from 'lexical' +import { + $applyNodeReplacement, + TextNode, +} from 'lexical' + +export class VariableValueBlockNode extends TextNode { + static getType(): string { + return 'variable-value-block' + } + + static clone(node: VariableValueBlockNode): VariableValueBlockNode { + return new VariableValueBlockNode(node.__text, node.__key) + } + + constructor(text: string, key?: NodeKey) { + super(text, key) + } + + createDOM(config: EditorConfig): HTMLElement { + const element = super.createDOM(config) + element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-[#155EEF]', 'rounded-[5px]', 'align-middle') + return element + } + + static importJSON(serializedNode: SerializedTextNode): TextNode { + const node = $createVariableValueBlockNode(serializedNode.text) + node.setFormat(serializedNode.format) + node.setDetail(serializedNode.detail) + node.setMode(serializedNode.mode) + node.setStyle(serializedNode.style) + return node + } + + exportJSON(): SerializedTextNode { + return { + detail: this.getDetail(), + format: this.getFormat(), + mode: this.getMode(), + style: this.getStyle(), + text: this.getTextContent(), + type: 'variable-value-block', + version: 1, + } + } + + canInsertTextBefore(): boolean { + return false + } +} + +export function $createVariableValueBlockNode(text = ''): VariableValueBlockNode { + return $applyNodeReplacement(new VariableValueBlockNode(text)) +} + +export function $isVariableValueNodeBlock( + node: LexicalNode | null | undefined, +): node is VariableValueBlockNode { + return node instanceof VariableValueBlockNode +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/utils.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/utils.ts new file mode 100644 index 00000000..4d59d410 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/utils.ts @@ -0,0 +1,5 @@ +export function getHashtagRegexString(): string { + const hashtag = '\\{\\{[a-zA-Z_][a-zA-Z0-9_]{0,29}\\}\\}' + + return hashtag +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/component.tsx new file mode 100644 index 00000000..51017894 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -0,0 +1,114 @@ +import { + memo, + useEffect, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + COMMAND_PRIORITY_EDITOR, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +// import { +// RiErrorWarningFill, +// } from '@remixicon/react' +import { useSelectOrDelete } from '../../hooks' +import type { WorkflowNodesMap } from './node' +import { WorkflowVariableBlockNode } from './node' +import { + DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, + UPDATE_WORKFLOW_NODES_MAP, +} from './index' +// import cn from '@/utils/classnames' +// import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +// import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' +// import { VarBlockIcon } from '@/app/components/workflow/block-icon' +// import { Line3 } from '@/app/components/base/icons/src/public/common' +// import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +// import Tooltip from '@/app/components/base/tooltip' + +type WorkflowVariableBlockComponentProps = { + nodeKey: string + variables: string[] + workflowNodesMap: WorkflowNodesMap +} + +const WorkflowVariableBlockComponent = ({ + nodeKey, + variables, + workflowNodesMap = {}, +}: WorkflowVariableBlockComponentProps) => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND) + const variablesLength = variables.length + // const varName = ( + // () => { + // const isSystem = isSystemVar(variables) + // const varName = variablesLength >= 3 ? (variables).slice(-2).join('.') : variables[variablesLength - 1] + // return `${isSystem ? 'sys.' : ''}${varName}` + // } + // )() + const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState(workflowNodesMap) + const node = localWorkflowNodesMap![variables[0]] + // const isEnv = isENV(variables) + // const isChatVar = isConversationVar(variables) + + useEffect(() => { + if (!editor.hasNodes([WorkflowVariableBlockNode])) + throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + UPDATE_WORKFLOW_NODES_MAP, + (workflowNodesMap: WorkflowNodesMap) => { + setLocalWorkflowNodesMap(workflowNodesMap) + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor]) + + const Item = ( +
+ {/* {!isEnv && !isChatVar && ( +
+ { + node?.type && ( +
+ +
+ ) + } +
{node?.title}
+ +
+ )} */} +
+ {/* {!isEnv && !isChatVar && } + {isEnv && } + {isChatVar && } +
{varName}
+ { + !node && !isEnv && !isChatVar && ( + + ) + } */} +
+
+ ) + + + return Item +} + +export default memo(WorkflowVariableBlockComponent) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/index.tsx new file mode 100644 index 00000000..8f0e2f48 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/index.tsx @@ -0,0 +1,80 @@ +import { + memo, + useEffect, +} from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { WorkflowVariableBlockType } from '../../types' +import { + $createWorkflowVariableBlockNode, + WorkflowVariableBlockNode, +} from './node' +// import type { Node } from '@/app/components/workflow/types' + +export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND') +export const DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND') +export const CLEAR_HIDE_MENU_TIMEOUT = createCommand('CLEAR_HIDE_MENU_TIMEOUT') +export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP') + +export type WorkflowVariableBlockProps = { + getWorkflowNode: (nodeId: string) => Node + onInsert?: () => void + onDelete?: () => void +} +const WorkflowVariableBlock = memo(({ + workflowNodesMap, + onInsert, + onDelete, +}: WorkflowVariableBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + editor.update(() => { + editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap) + }) + }, [editor, workflowNodesMap]) + + useEffect(() => { + if (!editor.hasNodes([WorkflowVariableBlockNode])) + throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, + (variables: string[]) => { + editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined) + const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap) + + $insertNodes([workflowVariableBlockNode]) + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, onInsert, onDelete, workflowNodesMap]) + + return null +}) +WorkflowVariableBlock.displayName = 'WorkflowVariableBlock' + +export { WorkflowVariableBlock } +export { WorkflowVariableBlockNode } from './node' +export { default as WorkflowVariableBlockReplacementBlock } from './workflow-variable-block-replacement-block' diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/node.tsx new file mode 100644 index 00000000..e4154731 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -0,0 +1,92 @@ +import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import type { WorkflowVariableBlockType } from '../../types' +import WorkflowVariableBlockComponent from './component' + +export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap'] +export type SerializedNode = SerializedLexicalNode & { + variables: string[] + workflowNodesMap: WorkflowNodesMap +} + +export class WorkflowVariableBlockNode extends DecoratorNode { + __variables: string[] + __workflowNodesMap: WorkflowNodesMap + + static getType(): string { + return 'workflow-variable-block' + } + + static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode { + return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap) + } + + isInline(): boolean { + return true + } + + constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, key?: NodeKey) { + super(key) + + this.__variables = variables + this.__workflowNodesMap = workflowNodesMap + } + + createDOM(): HTMLElement { + const div = document.createElement('div') + div.classList.add('inline-flex', 'items-center', 'align-middle') + return div + } + + updateDOM(): false { + return false + } + + decorate(): JSX.Element { + return ( + + ) + } + + static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode { + const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap) + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'workflow-variable-block', + version: 1, + variables: this.getVariables(), + workflowNodesMap: this.getWorkflowNodesMap(), + } + } + + getVariables(): string[] { + const self = this.getLatest() + return self.__variables + } + + getWorkflowNodesMap(): WorkflowNodesMap { + const self = this.getLatest() + return self.__workflowNodesMap + } + + getTextContent(): string { + return `{{#${this.getVariables().join('.')}#}}` + } +} +export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode { + return new WorkflowVariableBlockNode(variables, workflowNodesMap) +} + +export function $isWorkflowVariableBlockNode( + node: WorkflowVariableBlockNode | LexicalNode | null | undefined, +): node is WorkflowVariableBlockNode { + return node instanceof WorkflowVariableBlockNode +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx new file mode 100644 index 00000000..4571c18b --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx @@ -0,0 +1,66 @@ +import { + memo, + useCallback, + useEffect, +} from 'react' +import type { TextNode } from 'lexical' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import type { WorkflowVariableBlockType } from '../../types' +import { CustomTextNode } from '../custom-text/node' +import { $createWorkflowVariableBlockNode } from './node' +import { WorkflowVariableBlockNode } from './index' +// import { VAR_REGEX as REGEX } from '@/config' + +export const REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi + +const WorkflowVariableBlockReplacementBlock = ({ + workflowNodesMap, + onInsert, +}: WorkflowVariableBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([WorkflowVariableBlockNode])) + throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor') + }, [editor]) + + const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => { + if (onInsert) + onInsert() + + const nodePathString = textNode.getTextContent().slice(3, -3) + return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap)) + }, [onInsert, workflowNodesMap]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + matchArr[0].length + return { + end: endOffset, + start: startOffset, + } + }, []) + + const transformListener = useCallback((textNode: any) => { + return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode) + }, [createWorkflowVariableBlockNode, getMatch]) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, transformListener), + ) + }, []) + + return null +} + +export default memo(WorkflowVariableBlockReplacementBlock) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx new file mode 100644 index 00000000..2376fb01 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx @@ -0,0 +1,94 @@ +'use client' +import React, { useCallback, useEffect, useState } from 'react' +import type { FC } from 'react' +import { useDebounceFn } from 'ahooks' + +type Props = { + className?: string + height: number + minHeight: number + onHeightChange: (height: number) => void + children: JSX.Element + footer?: JSX.Element + hideResize?: boolean +} + +const PromptEditorHeightResizeWrap: FC = ({ + className, + height, + minHeight, + onHeightChange, + children, + footer, + hideResize, +}) => { + const [clientY, setClientY] = useState(0) + const [isResizing, setIsResizing] = useState(false) + const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect) + + const handleStartResize = useCallback((e: React.MouseEvent) => { + setClientY(e.clientY) + setIsResizing(true) + setPrevUserSelectStyle(getComputedStyle(document.body).userSelect) + document.body.style.userSelect = 'none' + }, []) + + const handleStopResize = useCallback(() => { + setIsResizing(false) + document.body.style.userSelect = prevUserSelectStyle + }, [prevUserSelectStyle]) + + const { run: didHandleResize } = useDebounceFn((e) => { + if (!isResizing) + return + + const offset = e.clientY - clientY + let newHeight = height + offset + setClientY(e.clientY) + if (newHeight < minHeight) + newHeight = minHeight + onHeightChange(newHeight) + }, { + wait: 0, + }) + + const handleResize = useCallback(didHandleResize, [isResizing, height, minHeight, clientY]) + + useEffect(() => { + document.addEventListener('mousemove', handleResize) + return () => { + document.removeEventListener('mousemove', handleResize) + } + }, [handleResize]) + + useEffect(() => { + document.addEventListener('mouseup', handleStopResize) + return () => { + document.removeEventListener('mouseup', handleStopResize) + } + }, [handleStopResize]) + + return ( +
+
+ {children} +
+ {/* resize handler */} + {footer} + {!hideResize && ( +
+
+
+ )} +
+ ) +} +export default React.memo(PromptEditorHeightResizeWrap) diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/types.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/types.ts new file mode 100644 index 00000000..8b7df07f --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/types.ts @@ -0,0 +1,77 @@ +import type { Dataset } from './plugins/context-block/index' +import type { RoleName } from './plugins/history-block/index' +// import type { +// Node, +// } from '@/app/components/workflow/types' + + +export type NodeOutPutVar = { + nodeId: string + title: string + vars: Var[] + isStartNode?: boolean +} + + +export type Option = { + value: string + name: string +} + +export type ExternalToolOption = { + name: string + variableName: string + icon?: string + icon_background?: string +} + +export type ContextBlockType = { + show?: boolean + selectable?: boolean + datasets?: Dataset[] + canNotAddContext?: boolean + onAddContext?: () => void + onInsert?: () => void + onDelete?: () => void +} + +export type QueryBlockType = { + show?: boolean + selectable?: boolean + onInsert?: () => void + onDelete?: () => void +} + +export type HistoryBlockType = { + show?: boolean + selectable?: boolean + history?: RoleName + onInsert?: () => void + onDelete?: () => void + onEditRole?: () => void +} + +export type VariableBlockType = { + show?: boolean + variables?: Option[] +} + +export type ExternalToolBlockType = { + show?: boolean + externalTools?: ExternalToolOption[] + onAddExternalTool?: () => void +} +unknown +export type WorkflowVariableBlockType = { + show?: boolean + variables?: NodeOutPutVar[] + workflowNodesMap?: Record + onInsert?: () => void + onDelete?: () => void +} + +export type MenuTextMatch = { + leadOffset: number + matchingString: string + replaceableString: string +} diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/utils.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/utils.ts new file mode 100644 index 00000000..f1cca6dc --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/utils.ts @@ -0,0 +1,354 @@ +import { $isAtNodeEnd } from '@lexical/selection' +import type { + ElementNode, + Klass, + LexicalEditor, + LexicalNode, + RangeSelection, + TextNode, +} from 'lexical' +import { + $createTextNode, + $getSelection, + $isRangeSelection, + $isTextNode, +} from 'lexical' +import type { EntityMatch } from '@lexical/text' +import { CustomTextNode } from './plugins/custom-text/node' +import type { MenuTextMatch } from './types' +import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT, MAX_VAR_KEY_LENGTH } from './constants' + +export function getSelectedNode( + selection: RangeSelection, +): TextNode | ElementNode { + const anchor = selection.anchor + const focus = selection.focus + const anchorNode = selection.anchor.getNode() + const focusNode = selection.focus.getNode() + if (anchorNode === focusNode) + return anchorNode + + const isBackward = selection.isBackward() + if (isBackward) + return $isAtNodeEnd(focus) ? anchorNode : focusNode + else + return $isAtNodeEnd(anchor) ? anchorNode : focusNode +} + +export function registerLexicalTextEntity( + editor: LexicalEditor, + getMatch: (text: string) => null | EntityMatch, + targetNode: Klass, + createNode: (textNode: TextNode) => T, +) { + const isTargetNode = (node: LexicalNode | null | undefined): node is T => { + return node instanceof targetNode + } + + const replaceWithSimpleText = (node: TextNode): void => { + const textNode = $createTextNode(node.getTextContent()) + textNode.setFormat(node.getFormat()) + node.replace(textNode) + } + + const getMode = (node: TextNode): number => { + return node.getLatest().__mode + } + + const textNodeTransform = (node: TextNode) => { + if (!node.isSimpleText()) + return + + const prevSibling = node.getPreviousSibling() + let text = node.getTextContent() + let currentNode = node + let match + + if ($isTextNode(prevSibling)) { + const previousText = prevSibling.getTextContent() + const combinedText = previousText + text + const prevMatch = getMatch(combinedText) + + if (isTargetNode(prevSibling)) { + if (prevMatch === null || getMode(prevSibling) !== 0) { + replaceWithSimpleText(prevSibling) + return + } + else { + const diff = prevMatch.end - previousText.length + + if (diff > 0) { + const concatText = text.slice(0, diff) + const newTextContent = previousText + concatText + prevSibling.select() + prevSibling.setTextContent(newTextContent) + + if (diff === text.length) { + node.remove() + } + else { + const remainingText = text.slice(diff) + node.setTextContent(remainingText) + } + + return + } + } + } + else if (prevMatch === null || prevMatch.start < previousText.length) { + return + } + } + + while (true) { + match = getMatch(text) + let nextText = match === null ? '' : text.slice(match.end) + text = nextText + + if (nextText === '') { + const nextSibling = currentNode.getNextSibling() + + if ($isTextNode(nextSibling)) { + nextText = currentNode.getTextContent() + nextSibling.getTextContent() + const nextMatch = getMatch(nextText) + + if (nextMatch === null) { + if (isTargetNode(nextSibling)) + replaceWithSimpleText(nextSibling) + else + nextSibling.markDirty() + + return + } + else if (nextMatch.start !== 0) { + return + } + } + } + else { + const nextMatch = getMatch(nextText) + + if (nextMatch !== null && nextMatch.start === 0) + return + } + + if (match === null) + return + + if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) + continue + + let nodeToReplace + + if (match.start === 0) + [nodeToReplace, currentNode] = currentNode.splitText(match.end) + else + [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end) + + const replacementNode = createNode(nodeToReplace) + replacementNode.setFormat(nodeToReplace.getFormat()) + nodeToReplace.replace(replacementNode) + + if (currentNode == null) + return + } + } + + const reverseNodeTransform = (node: T) => { + const text = node.getTextContent() + const match = getMatch(text) + + if (match === null || match.start !== 0) { + replaceWithSimpleText(node) + return + } + + if (text.length > match.end) { + // This will split out the rest of the text as simple text + node.splitText(match.end) + return + } + + const prevSibling = node.getPreviousSibling() + + if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) { + replaceWithSimpleText(prevSibling) + replaceWithSimpleText(node) + } + + const nextSibling = node.getNextSibling() + + if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) { + replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block + + if (isTargetNode(node)) + replaceWithSimpleText(node) + } + } + + const removePlainTextTransform = editor.registerNodeTransform(CustomTextNode, textNodeTransform) + const removeReverseNodeTransform = editor.registerNodeTransform(targetNode, reverseNodeTransform) + return [removePlainTextTransform, removeReverseNodeTransform] +} + +export const decoratorTransform = ( + node: CustomTextNode, + getMatch: (text: string) => null | EntityMatch, + createNode: (textNode: TextNode) => LexicalNode, +) => { + if (!node.isSimpleText()) + return + + const prevSibling = node.getPreviousSibling() + let text = node.getTextContent() + let currentNode = node + let match + + while (true) { + match = getMatch(text) + let nextText = match === null ? '' : text.slice(match.end) + text = nextText + + if (nextText === '') { + const nextSibling = currentNode.getNextSibling() + + if ($isTextNode(nextSibling)) { + nextText = currentNode.getTextContent() + nextSibling.getTextContent() + const nextMatch = getMatch(nextText) + + if (nextMatch === null) { + nextSibling.markDirty() + return + } + else if (nextMatch.start !== 0) { + return + } + } + } + else { + const nextMatch = getMatch(nextText) + + if (nextMatch !== null && nextMatch.start === 0) + return + } + + if (match === null) + return + + if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) + continue + + let nodeToReplace + + if (match.start === 0) + [nodeToReplace, currentNode] = currentNode.splitText(match.end) + else + [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end) + + const replacementNode = createNode(nodeToReplace) + nodeToReplace.replace(replacementNode) + + if (currentNode == null) + return + } +} + +function getFullMatchOffset( + documentText: string, + entryText: string, + offset: number, +): number { + let triggerOffset = offset + for (let i = triggerOffset; i <= entryText.length; i++) { + if (documentText.substr(-i) === entryText.substr(0, i)) + triggerOffset = i + } + return triggerOffset +} + +export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null { + const selection = $getSelection() + if (!$isRangeSelection(selection) || !selection.isCollapsed()) + return null + const anchor = selection.anchor + if (anchor.type !== 'text') + return null + const anchorNode = anchor.getNode() + if (!anchorNode.isSimpleText()) + return null + const selectionOffset = anchor.offset + const textContent = anchorNode.getTextContent().slice(0, selectionOffset) + const characterOffset = match.replaceableString.length + const queryOffset = getFullMatchOffset( + textContent, + match.matchingString, + characterOffset, + ) + const startOffset = selectionOffset - queryOffset + if (startOffset < 0) + return null + let newNode + if (startOffset === 0) + [newNode] = anchorNode.splitText(selectionOffset) + else + [, newNode] = anchorNode.splitText(startOffset, selectionOffset) + + return newNode +} + +export function textToEditorState(text: string) { + const paragraph = text.split('\n') + + return JSON.stringify({ + root: { + children: paragraph.map((p) => { + return { + children: [{ + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: p, + type: 'custom-text', + version: 1, + }], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + } + }), + direction: 'ltr', + format: '', + indent: 0, + type: 'root', + version: 1, + }, + }) +} + + + +const varRegex = /\{\{(.+?)\}\}/g +export const getVars = (value: string) => { + if (!value) + return [] + + const keys = value.match(varRegex)?.filter((item) => { + return ![CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT].includes(item) + }).map((item) => { + return item.replace('{{', '').replace('}}', '') + }).filter(key => key.length <= MAX_VAR_KEY_LENGTH) || [] + const keyObj: Record = {} + // remove duplicate keys + const res: string[] = [] + keys.forEach((key) => { + if (keyObj[key]) + return + + keyObj[key] = true + res.push(key) + }) + return res +} diff --git a/frontend/packages/common/src/const/approval/const.tsx b/frontend/packages/common/src/const/approval/const.tsx index 46b82764..bf0753a7 100644 --- a/frontend/packages/common/src/const/approval/const.tsx +++ b/frontend/packages/common/src/const/approval/const.tsx @@ -219,6 +219,11 @@ export const ApprovalRouteColumns = [ ellipsis:true, render:(value)=>value?.join(', ') }, + { + title:('名称'), + dataIndex:'name', + ellipsis:true, + }, { title:('路径'), dataIndex:'path', diff --git a/frontend/packages/common/src/const/approval/type.tsx b/frontend/packages/common/src/const/approval/type.tsx index 4af11d83..35d86bf8 100644 --- a/frontend/packages/common/src/const/approval/type.tsx +++ b/frontend/packages/common/src/const/approval/type.tsx @@ -106,11 +106,11 @@ export type PublishTableListItem = { export type PublishApprovalModalProps = { type:'approval'|'view'|'add'|'publish'|'online' data:PublishApprovalInfoType | PublishApprovalInfoType &{id?:string} | PublishVersionTableListItem - insideSystem?:boolean + insidePage?:boolean serviceId:string teamId:string clusterPublishStatus?:SystemInsidePublishOnlineItems[] - + serviceType?:'rest'|'ai' } export type PublishApprovalModalHandle = { diff --git a/frontend/packages/common/src/const/const.tsx b/frontend/packages/common/src/const/const.tsx index 2f85209c..3476e58f 100644 --- a/frontend/packages/common/src/const/const.tsx +++ b/frontend/packages/common/src/const/const.tsx @@ -26,7 +26,7 @@ export const routerKeyMap = new Map([ ['operationCenter',['member','user','role','servicecategories']], ['organization',['member','user','role']], ['serviceHubSetting',['servicecategories']], - ['maintenanceCenter',['datasourcing','cluster','cert','logsettings','resourcesettings','openapi'] + ['maintenanceCenter',['aisetting','datasourcing','cluster','cert','logsettings','resourcesettings','openapi'] ]]) @@ -43,7 +43,8 @@ export const routerKeyMap = new Map([ input:('请输入'), select:('请选择'), startWithAlphabet:('英文数字下划线任意一种,首字母必须为英文'), - specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合') + specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合'), + onlyAlphabet:('字符非法,仅支持英文'), } export const FORM_ERROR_TIPS = { diff --git a/frontend/packages/common/src/const/permissions.ts b/frontend/packages/common/src/const/permissions.ts index d7739302..954be69c 100644 --- a/frontend/packages/common/src/const/permissions.ts +++ b/frontend/packages/common/src/const/permissions.ts @@ -174,6 +174,16 @@ export const PERMISSION_DEFINITION = [ "anyOf": [{ "backend": ["system.devops.cluster.manager"] }] } }, + "system.devops.ai_provider.view": { + "granted": { + "anyOf": [{ "backend": ["system.devops.ai_provider.view"] }] + } + }, + "system.devops.ai_provider.edit": { + "granted": { + "anyOf": [{ "backend": ["system.devops.ai_provider.manager"] }] + } + }, "system.devops.ssl_certificate.view": { "granted": { "anyOf": [{ "backend": ["system.devops.ssl_certificate.view"] }] diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index 5648d241..6cf49469 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -4,6 +4,7 @@ import { App } from "antd"; import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const"; import { checkAccess } from "@common/utils/permission"; import { PERMISSION_DEFINITION } from "@common/const/permissions"; +import { $t } from "@common/locales"; interface GlobalState { isAuthenticated: boolean; @@ -46,7 +47,8 @@ export const GlobalContext = createContext<{ checkPermission:(access:keyof typeof PERMISSION_DEFINITION[0] | Array)=>boolean teamDataFlushed:boolean accessInit:boolean - + aiConfigFlushed:boolean + setAiConfigFlushed:(flush:boolean)=>void } | undefined>(undefined); const globalReducer = (state: GlobalState, action: GlobalAction): GlobalState => { @@ -107,13 +109,14 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => { version: '1.0.0', updateDate: '2024-07-01', powered:'Powered by https://apipark.com', - mainPage:'/guide', + mainPage:'/guide/page', language:'en' }); const [accessData,setAccessData] = useState>(new Map()) const [pluginAccessDictionary, setPluginAccessDictionary] = useState<{[k:string]:string}>({}) const [teamDataFlushed, setTeamDataFlushed] = useState(false) const [accessInit, setAccessInit] = useState(false) + const [aiConfigFlushed, setAiConfigFlushed] = useState(false) let getGlobalAccessPromise: Promise> | null = null const getGlobalAccessData = ()=>{ @@ -153,7 +156,6 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => { setAccessData(prevData => prevData.set('team',[])) } - const getPluginAccessDictionary = (pluginData:{[k:string]:string})=>{ setPluginAccessDictionary(pluginData) } @@ -174,14 +176,15 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => { return revs } - return ( + resetAccess ,checkPermission,accessInit, + aiConfigFlushed, setAiConfigFlushed}}> {children} ); diff --git a/frontend/packages/common/src/contexts/event-emitter.tsx b/frontend/packages/common/src/contexts/event-emitter.tsx new file mode 100644 index 00000000..d31e32e8 --- /dev/null +++ b/frontend/packages/common/src/contexts/event-emitter.tsx @@ -0,0 +1,28 @@ +'use client' + +import { createContext, useContext } from 'use-context-selector' +import { useEventEmitter } from 'ahooks' +import type { EventEmitter } from 'ahooks/lib/useEventEmitter' + +const EventEmitterContext = createContext<{ eventEmitter: EventEmitter | null }>({ + eventEmitter: null, +}) + +export const useEventEmitterContextContext = () => useContext(EventEmitterContext) + +type EventEmitterContextProviderProps = { + children: React.ReactNode +} +export const EventEmitterContextProvider = ({ + children, +}: EventEmitterContextProviderProps) => { + const eventEmitter = useEventEmitter() + + return ( + + {children} + + ) +} + +export default EventEmitterContext diff --git a/frontend/packages/common/src/locales/scan/en-GB.json b/frontend/packages/common/src/locales/scan/en-GB.json index 0ad4828c..ecd7c84a 100644 --- a/frontend/packages/common/src/locales/scan/en-GB.json +++ b/frontend/packages/common/src/locales/scan/en-GB.json @@ -678,5 +678,29 @@ "K1362a512": "Disable Member", "K6e1289b1": "Enable Member", "K1f4b5385": "Delete Member", - "Kf85b83a0": "Enter URL to Search Route" + "Kf85b83a0": "Enter URL to Search Route", + "K62840d62": "REST Service", + "Kd2c34e2c": "AI Service", + "K4d5960c1": "AI Setting", + "Kc6340091": "Context", + "K74ecb1fa": "Query", + "K79f2e2f9": "Conversation History", + "K3a8912ee": "New variable", + "Kb291a19": "New tool", + "K27ece71d": "AI Model Invocation Defaults to Using Only Query Variables; Enter '{' to Add New Variables", + "K14700c7": "AI Model Provider", + "K1786a4c8": "Add AI Service", + "K66060758": "Name", + "K2bb86fb4": "Prompt", + "K13ffbe88": "Variable", + "K79c8cfaf": "Enter the Description for This Interface", + "K8a35059b": "Model Configuration", + "Kfede1c7c": "Model", + "Ke99513a0": "Parameters", + "K18dccc1a": "Synchronize Latest Model", + "Ke66a17dd": "Required", + "Kb3e34847": "Get API Key from (0)", + "Kd9a46c29": "Default", + "K66a7d24c": "Configured", + "K28b68036": "Invalid characters, only alphabetical characters are allowed" } \ No newline at end of file diff --git a/frontend/packages/common/src/locales/scan/newJson/en-GB.json b/frontend/packages/common/src/locales/scan/newJson/en-GB.json index 1aca1dde..2c63c085 100644 --- a/frontend/packages/common/src/locales/scan/newJson/en-GB.json +++ b/frontend/packages/common/src/locales/scan/newJson/en-GB.json @@ -1,7 +1,2 @@ { - "K7c97c5df": "移出当前部门", - "K1362a512": "禁用成员", - "K6e1289b1": "启用成员", - "K1f4b5385": "删除成员", - "Kf85b83a0": "输入 URL 查找路由" } diff --git a/frontend/packages/common/src/locales/scan/newJson/zh-CN.json b/frontend/packages/common/src/locales/scan/newJson/zh-CN.json index 1aca1dde..b2fbe168 100644 --- a/frontend/packages/common/src/locales/scan/newJson/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/newJson/zh-CN.json @@ -1,7 +1,3 @@ { - "K7c97c5df": "移出当前部门", - "K1362a512": "禁用成员", - "K6e1289b1": "启用成员", - "K1f4b5385": "删除成员", - "Kf85b83a0": "输入 URL 查找路由" + "K28b68036": "字符非法,仅支持英文" } diff --git a/frontend/packages/core/src/App.css b/frontend/packages/core/src/App.css index e0925d02..24f83f0b 100644 --- a/frontend/packages/core/src/App.css +++ b/frontend/packages/core/src/App.css @@ -94,6 +94,10 @@ color:#fff !important; } } + + li.ant-menu-submenu-horizontal.ant-menu-overflow-item-rest .ant-menu-submenu-title{ + color:#fff !important; + } } .ant-layout-sider.apipark-layout-sider{ height:calc(100vh - var(--layout-header-height)) !important; @@ -108,6 +112,9 @@ .ant-menu-item{ margin-block:0 !important; } + .ant-menu-light:not(.ant-menu-horizontal) .ant-menu-item:not(.ant-menu-item-selected):active{ + background-color: unset; + } } .apipark-layout-sider-collapsed-button{ @@ -138,9 +145,9 @@ > li:active{ background-color: transparent; } - /* > li.ant-menu-item-active { + > li.ant-menu-item-active { color:#fff !important; - } */ + } > li.ant-menu-item-selected { background-color: #fff !important; border: 1px solid #fff !important; @@ -221,4 +228,11 @@ a{ } } +} + +.ant-pro-table .ant-popover .ant-popover-inner-content{ + .ant-form-item{ + background-color: transparent; + border:none; + } } \ No newline at end of file diff --git a/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx b/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx index e7800d1d..b842f1d9 100644 --- a/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx +++ b/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx @@ -5,14 +5,15 @@ import BasicLayout from '@common/components/aoplatform/BasicLayout'; import {createElement, ReactElement,ReactNode,Suspense} from 'react'; import { v4 as uuidv4 } from 'uuid' import {App, Skeleton} from "antd"; -import ApprovalPage from "@core/pages/approval/ApprovalPage.tsx"; -import {SystemProvider} from "@core/contexts/SystemContext.tsx"; import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx"; import {FC,lazy} from 'react'; import { TeamProvider } from '@core/contexts/TeamContext.tsx'; -import SystemOutlet from '@core/pages/system/SystemOutlet.tsx'; import { TenantManagementProvider } from '@market/contexts/TenantManagementContext.tsx'; import Guide from '@core/pages/guide/Guide'; +import { AiServiceProvider } from '@core/contexts/AiServiceContext'; +import AiServiceOutlet from '@core/pages/aiService/AiServiceOutlet'; +import SystemOutlet from '@core/pages/system/SystemOutlet'; +import { SystemProvider } from '@core/contexts/SystemContext'; type RouteConfig = { path:string @@ -42,6 +43,7 @@ export type RouterParams = { appId:string roleType:string roleId:string + routeId:string } const PUBLIC_ROUTES:RouteConfig[] = [ @@ -219,6 +221,120 @@ const PUBLIC_ROUTES:RouteConfig[] = [ } ] }, + { + path:'aiservice', + component:, + key: uuidv4(), + provider: AiServiceProvider, + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:'list', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceList.tsx')), + }, + { + path:'list/:teamId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceList.tsx')), + }, + { + path:':teamId', + component:, + key: uuidv4(), + children:[ + { + path:'inside/:serviceId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsidePage.tsx')), + children:[ + { + path:'api', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideApiDocument')), + }, + { + + path:'route/create', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')), + }, + { + + path:'route/:routeId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate')), + }, + { + path:'route', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterList')), + }, + { + path:'document', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsideDocument.tsx')), + }, + { + path:'subscriber', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceInsideSubscriber.tsx')), + children:[ + + ] + }, + { + path:'approval', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApproval')), + children:[ + { + path:'', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApprovalList')), + }, + { + path:'*', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/approval/AiServiceInsideApprovalList')), + } + ] + }, + { + path:'publish', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublish')), + children:[ + { + path:'', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublishList')), + }, + { + path:'*', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/publish/AiServiceInsidePublishList')), + } + ] + }, + { + path:'setting', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiService/AiServiceConfig.tsx')), + children:[ + + ] + }, + ] + } + ] + } + ] + }, { path:'datasourcing', key: uuidv4(), @@ -229,6 +345,11 @@ const PUBLIC_ROUTES:RouteConfig[] = [ key: uuidv4(), lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCluster.tsx')), }, + { + path:'aisetting', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiSetting/AiSettingList.tsx')), + }, { path:'cert', key: uuidv4(), diff --git a/frontend/packages/core/src/const/ai-service/const.tsx b/frontend/packages/core/src/const/ai-service/const.tsx new file mode 100644 index 00000000..c3424ae5 --- /dev/null +++ b/frontend/packages/core/src/const/ai-service/const.tsx @@ -0,0 +1,170 @@ +import { AiServiceTableListItem, AiServiceRouterTableListItem, VariableItems } from "./type"; +import { TabsProps } from "antd"; +import { frontendTimeSorter } from "@common/utils/dataTransfer"; +import { COLUMNS_TITLE, PLACEHOLDER } from "@common/const/const"; + +import { PageProColumns } from "@common/components/aoplatform/PageList"; + +export const AI_SERVICE_TABLE_COLUMNS: PageProColumns[] = [ + { + title:('服务名称'), + dataIndex: 'name', + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title:('服务 ID'), + dataIndex: 'id', + width: 140, + ellipsis:true, + }, + { + title:('AI 模型供应商'), + dataIndex: ['provider','name'], + ellipsis:true, + }, + { + title:('所属团队'), + dataIndex: ['team','name'], + ellipsis:true, + // filters: true, + // onFilter: true, + // filterSearch: true, + }, + { + title:('API 数量'), + dataIndex: 'apiNum', + ellipsis:true, + sorter: (a,b)=> { + return a.apiNum - b.apiNum + }, + }, + { + title: ('描述'), + dataIndex: 'description', + ellipsis:true, + }, + { + title:('创建时间'), + dataIndex: 'createTime', + width:182, + ellipsis:true, + sorter: (a,b)=>frontendTimeSorter(a,b,'createTime') + } +]; + +export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns[] = [ + { + title:('URL'), + dataIndex: 'requestPath', + ellipsis:true + }, + { + title:('名称'), + dataIndex: 'name', + ellipsis:true, + }, + { + title:('模型'), + dataIndex: ['model','logo'], + ellipsis:true, + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) =>
{entity.model.id}
+ }, + { + title:('描述'), + dataIndex: 'description', + ellipsis:true + }, + { + title:('创建者'), + dataIndex: ['creator','name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title:('更新时间'), + dataIndex: 'updateTime', + ellipsis:true, + hideInSearch: true, + width:182, + sorter: (a,b)=>frontendTimeSorter(a,b,'updateTime') + }, +]; + + +export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ + { + title:('Key'), + dataIndex: 'key', + key:'key', + width: '30%', + formItemProps: { + className:'p-0 bg-transparent border-none', + rules: [ + { + required: true, + whitespace: true + }, + { + pattern:/^[a-zA-Z][a-zA-Z0-9-_]*$/, + message: PLACEHOLDER.onlyAlphabet + } + ], + }, + ellipsis:true + }, + { + title:('描述'), + dataIndex: 'description', + key:'description', + formItemProps: { + className:'p-0 bg-transparent border-none'} + }, + { + title:('必填'), + dataIndex: 'require', + key:'require', + valueType:'switch', + width:64, + formItemProps: { + className:'p-0 bg-transparent border-none'} + }, + { + title: COLUMNS_TITLE.operate, + valueType: 'option', + width:34, + render: ()=>null + }, + ]; + + +export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label:('待审批'), + }, + { + key: '1', + label: ('已审批'), + } +]; + + + +export const AiService_PUBLISH_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label: ('发布版本'), + }, + { + key: '1', + label: ('发布申请记录'), + } +]; diff --git a/frontend/packages/core/src/const/ai-service/type.ts b/frontend/packages/core/src/const/ai-service/type.ts new file mode 100644 index 00000000..01170345 --- /dev/null +++ b/frontend/packages/core/src/const/ai-service/type.ts @@ -0,0 +1,293 @@ + +import { FormInstance, UploadFile } from "antd"; +import { EntityItem } from "@common/const/type"; +import { SubscribeEnum, SubscribeFromEnum } from "./const"; + +export type AiServiceTableListItem = { + id:string; + name: string; + team: EntityItem; + apiNum: number; + description:string; + createTime:string; + updateTime:string; + canDelete:boolean; + provider:EntityItem +} + +export type AiServiceConfigFieldType = { + name?: string; + id?: string; + provider?:string + prefix?:string; + logo?:string; + logoFile?:UploadFile; + tags?:Array; + description?: string; + team?:string; + master?:string; + serviceType?:'public'|'inner'; + catalogue?:string | string[]; +}; + +export type AiServiceSubServiceTableListItem = { + id:string; + applyStatus:typeof SubscribeEnum; + project:EntityItem; + team:EntityItem + service:EntityItem + applier:EntityItem + from:SubscribeFromEnum + createTime:string +}; + + + +export type AiServiceSubscriberTableListItem = { + id:string + service:EntityItem + applyStatus:typeof SubscribeEnum + project:EntityItem + team:EntityItem; + applier:EntityItem + approver:EntityItem; + from:SubscribeFromEnum + applyTime:string +}; + +export type AiServiceSubscriberConfigFieldType = { + application:string + applier:string +}; + +export type AiServiceSubscriberConfigProps = { + serviceId:string + teamId:string +} + +export type AiServiceSubscriberConfigHandle = { + save:()=>Promise +} + +export type AiServiceMemberTableListItem = { + user: EntityItem; + email:string; + roles:Array; + canDelete:boolean +}; + +export type AiServiceApiDetail = { + content:string + updateTime:string + updater:string +} + + + +export type AiServiceInsideRouterCreateProps = { + type?:'add'|'edit'|'copy' + entity?:AiServiceRouterTableListItem + modalApiPrefix?:string + modalPrefixForce?:boolean + serviceId:string + teamId:string +} + +export type AiServiceInsideRouterCreateHandle = { + copy:()=>Promise; + save:()=>Promise; +} + + +export type AiServiceRouterTableListItem = { + id:string; + name:string; + requestPath:string; + description:string + creator:EntityItem; + createTime:string; + updater:EntityItem + updateTime:string + model:{ + id:string + logo:string + } +}; + +export type MyServiceFieldType = { + name?: string; + id?: string; + description?: string; + team?:string; + project?:string; + status?:'off'|'on' +}; + +export type SimpleAiServiceItem = { + id:string + name:string + team:EntityItem +} + +export type ServiceApiTableListItem = { + id:string; + name: string; + method:string; + path:string; + description:string; +}; + +export type SimpleApiItem = { + id:string + name:string + method:string + requestPath:string +} + +export type AiServiceAuthorityTableListItem = { + id:string + name: string; + driver:string; + hideCredential:boolean; + expireTime:number; + creator:EntityItem; + updater:EntityItem; + createTime:string; + updateTime:string +}; + +export type MyServiceTableListItem = { + id:string; + name: string; + serviceType:'public'|'inner'; + apiNum:number; + status:string; + createTime:string; + updateTime:string; +}; + + +export type AiServiceInsideApiDetailProps = { + serviceId:string; + teamId:string; + apiId:string; +} + + +export type AiServiceInsideApiDocumentHandle = { + save:()=>Promise|undefined +} + +export type AiServiceInsideApiDocumentProps = { + serviceId:string + teamId:string + apiId:string +} + + +export type AiServiceInsideApiProxyHandle = { + validate:()=>Promise +} + + +export interface MyServiceInsideConfigHandle { + save:()=>Promise +} + +export interface MyServiceInsideConfigProps { + + teamId:string + serviceId?:string + closeDrawer?:() => void +} + + +export type SubSubscribeApprovalModalProps = { + type:'reApply'|'view' + data?:AiServiceSubServiceTableListItem + teamId:string + serviceId?:string +} + +export type SubSubscribeApprovalModalHandle = { + reApply:() =>Promise +} + +export type SubSubscribeApprovalModalFieldType = { + reason?:string; + opinion?:string; +}; + +export type AiServiceInsideUpstreamConfigProps = { + upstreamNameForm:FormInstance + setLoading:(loading:boolean) => void +} + +export type AiServiceInsideUpstreamConfigHandle = { + save:()=>Promise|undefined +} + +export type AiServiceInsideUpstreamContentHandle = { + save:()=>Promise|undefined +} + + +export type AiServiceConfigHandle = { + save:()=>Promise|undefined +} + + +export type AiServiceTopologyServiceItem = EntityItem & { + project:string +} + +export interface AiServiceTopologySubscriber { + project: EntityItem; + services: EntityItem[]; + } + + export interface AiServiceTopologyInvoke { + project: EntityItem; + services: EntityItem[]; + } + + + // 接口返回的数据格式 + export interface AiServiceTopologyResponse { + services: AiServiceTopologyServiceItem[]; + subscribers: AiServiceTopologySubscriber[]; + invoke: AiServiceTopologyInvoke[]; + } + +export enum AiServiceReleaseStatus { + '正常' = 0, + '未设置' = 1, + '缺失' = 2 +} + + export type AiServicePublishReleaseItem = { + api: Array<{ + name: string, + method: string, + path: string, + upstream: string, + change: string, + status: { + upstreamStatus: AiServiceReleaseStatus, + docStatus: AiServiceReleaseStatus, + proxyStatus: AiServiceReleaseStatus + } + }> + upstream: Array<{ + name: "", + type: "", + addr: [], + status: "" + }> + } + + export type VariableItems = { + key:string, + description:string, + required:boolean + } \ No newline at end of file diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index c2d6c3c7..f760f2e8 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -1,18 +1,11 @@ -import { GlobalNodeItem, MyServiceTableListItem, NodeItem, ProxyHeaderItem, ServiceApiTableListItem, SimpleApiItem, SystemApiTableListItem, SystemAuthorityTableListItem, SystemMemberTableListItem, SystemSubServiceTableListItem, SystemSubscriberTableListItem, SystemTableListItem, SystemUpstreamTableListItem } from "./type"; -import { Input, InputNumber, MenuProps, Select, TabsProps, Tooltip } from "antd"; -import { ColumnsType } from "antd/es/table"; -import { getItem } from "@common/utils/navigation"; -import { MatchItem, MemberItem } from "@common/const/type"; +import { GlobalNodeItem, ProxyHeaderItem, SystemApiTableListItem, SystemMemberTableListItem, SystemSubscriberTableListItem, SystemTableListItem } from "./type"; +import { Input, TabsProps } from "antd"; +import { MatchItem } from "@common/const/type"; import { ConfigField } from "@common/components/aoplatform/EditableTableWithModal"; import { frontendTimeSorter } from "@common/utils/dataTransfer"; -import { COLUMNS_TITLE, STATUS_COLOR, VALIDATE_MESSAGE } from "@common/const/const"; -import { LoadingOutlined } from "@ant-design/icons"; -import { SystemInsidePublishOnlineItems } from "../../pages/system/publish/SystemInsidePublishOnline"; -import dayjs from 'dayjs'; -import { Link } from "react-router-dom"; +import { COLUMNS_TITLE } from "@common/const/const"; import { PageProColumns } from "@common/components/aoplatform/PageList"; -import { $t } from "@common/locales"; export enum SubscribeEnum{ Rejected = 0, @@ -127,16 +120,6 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns[] = [ dataIndex: 'description', ellipsis:true, }, - { - title:('负责人'), - dataIndex: ['master','name'], - ellipsis: true, - width:108, - filters: true, - onFilter: true, - valueType: 'select', - filterSearch: true, - }, { title:('创建时间'), dataIndex: 'createTime', @@ -195,21 +178,6 @@ export const SYSTEM_SUBSCRIBER_TABLE_COLUMNS: PageProColumns = [ - {title:('成员'), - render:(_,entity)=>{ - return <> -
-

- {entity.name} - {entity.email !== undefined && {entity.email}} -

-

{entity.department}

-
- - }} -] - export const SYSTEM_MEMBER_TABLE_COLUMN: PageProColumns[] = [ { title:('用户名'), @@ -319,65 +287,6 @@ export const SYSTEM_API_TABLE_COLUMNS: PageProColumns[] }, ]; - - -export const SYSTEM_UPSTREAM_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('名称'), - dataIndex: 'name', - ellipsis:true, - width:160, - fixed:'left', - sorter: (a,b)=> { - return a.name.localeCompare(b.name) - }, - }, - { - title:('上游 ID'), - dataIndex: 'id', - width: 140, - ellipsis:true - }, - { - title:('创建人'), - dataIndex: ['creator','name'], - ellipsis: true, - width:88, - filters: true, - onFilter: true, - valueType: 'select', - filterSearch: true, - }, - { - title:('更新人'), - dataIndex: ['updater','name'], - ellipsis: true, - width:88, - filters: true, - onFilter: true, - valueType: 'select', - filterSearch: true, - }, - { - title:('创建时间'), - dataIndex: 'createTime', - width:182, - ellipsis:true, - sorter: (a,b)=> { - return a.createTime.localeCompare(b.createTime) - } - }, - { - title:('更新时间'), - dataIndex: 'updateTime', - width:182, - ellipsis:true, - sorter: (a,b)=> { - return a.updateTime.localeCompare(b.updateTime) - }, - }, -]; - export const UpstreamDriverEnum = { 'static':('静态上游'), 'discoveries':('动态服务发现'), @@ -435,80 +344,11 @@ export const PROXY_HEADER_CONFIG:ConfigField[] = [ } ] -export const NODE_CONFIG:ConfigField[] = [ - { - title:('集群'), - key: 'cluster', - component: , - renderText: (value: string) => { - return value - }, - required: true - }, { - title:('权重'), - key: 'weight', - component: , - renderText: (value: string) => { - return value - }, - required: true - } -] export const SERVICE_VISUALIZATION_OPTIONS = [ {label:('内部服务:可通过网关访问,但不展示在服务广场'),value:'inner'}, {label:('公开服务:可通过网关访问,展示在服务广场,可被其他应用订阅'),value:'public'}]; - - -export const SYSTEM_MYSERVICE_API_TABLE_COLUMNS: PageProColumns[] = [ - { - title:(' '), - dataIndex: 'id', - width:'40px', - fixed:'left' - }, - { - title:('名称'), - dataIndex: 'name', - width:160, - fixed:'left', - ellipsis:true - }, - { - title:('请求方式'), - dataIndex: 'method', - ellipsis:true - }, - { - title:('请求路径'), - dataIndex: 'path', - ellipsis:true - }, - { - title: ('描述'), - dataIndex: 'description', - ellipsis:true - } -]; - - -export const apiModalColumn:ColumnsType = [ - { - title:('所有 API'), - dataIndex:'method', - }, - { - title:'', - dataIndex:'name', - ellipsis:true - } -] export const SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS: PageProColumns[] = [ diff --git a/frontend/packages/core/src/contexts/AiServiceContext.tsx b/frontend/packages/core/src/contexts/AiServiceContext.tsx new file mode 100644 index 00000000..fe024d11 --- /dev/null +++ b/frontend/packages/core/src/contexts/AiServiceContext.tsx @@ -0,0 +1,30 @@ +import { EntityItem } from '@common/const/type'; +import { AiServiceConfigFieldType } from '@core/const/ai-service/type'; +import {FC, createContext, useContext, useState, ReactNode } from 'react'; + +interface AiServiceContextProps { + apiPrefix:string; + setApiPrefix:React.Dispatch>; + prefixForce:boolean; + setPrefixForce:React.Dispatch>; + aiServiceInfo:(AiServiceConfigFieldType & {provider:EntityItem })|undefined + setAiServiceInfo:React.Dispatch>; +} + +const AiServiceContext = createContext(undefined); + +export const useAiServiceContext = () => { + const context = useContext(AiServiceContext); + if (!context) { + throw new Error('useArray must be used within a ArrayProvider'); + } + return context; +}; + +export const AiServiceProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [apiPrefix, setApiPrefix] = useState(''); + const [prefixForce, setPrefixForce] = useState(false); + const [aiServiceInfo, setAiServiceInfo] = useState() + + return {children}; +}; \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceConfig.tsx b/frontend/packages/core/src/pages/aiService/AiServiceConfig.tsx new file mode 100644 index 00000000..802a0085 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceConfig.tsx @@ -0,0 +1,416 @@ + +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { App, Button, Form, Input, Radio, Row, Select, TreeSelect, Upload } from "antd"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import { BasicResponse, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const.tsx"; +import { useFetch} from "@common/hooks/http.ts"; +import { DefaultOptionType } from "antd/es/cascader"; +import { EntityItem, MemberItem, SimpleTeamItem } from "@common/const/type.ts"; +import { v4 as uuidv4 } from 'uuid' +import { validateUrlSlash } from "@common/utils/validate.ts"; +import { normFile } from "@common/utils/uploadPic.ts"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx"; +import { SERVICE_VISUALIZATION_OPTIONS } from "@core/const/system/const.tsx"; +import { RcFile, UploadChangeParam, UploadFile, UploadProps } from "antd/es/upload/interface"; +import { LoadingOutlined } from "@ant-design/icons"; +import { getImgBase64 } from "@common/utils/dataTransfer.ts"; +import { CategorizesType } from "@market/const/serviceHub/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { $t } from "@common/locales/index.ts"; +import { AiServiceConfigHandle, AiServiceConfigFieldType } from "@core/const/ai-service/type"; +import { useAiServiceContext } from "@core/contexts/AiServiceContext"; + +type SimpleAiProviderItem = EntityItem & { + configured:boolean + logo:string +} + +const AiServiceConfig = forwardRef((_,ref) => { + const { message,modal } = App.useApp() + const { teamId, serviceId } = useParams(); + const [onEdit, setOnEdit] = useState(!!teamId) + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const [teamOptionList, setTeamOptionList] = useState() + const [providerOptionList, setProviderOptionList] = useState() + const navigate = useNavigate(); + const {setBreadcrumb} = useBreadcrumb() + const { setAiServiceInfo} = useAiServiceContext() + const [showClassify, setShowClassify] = useState() + const [imageBase64, setImageBase64] = useState(null); + const [tagOptionList, setTagOptionList] = useState([]) + const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState() + const [uploadLoading, setUploadLoading] = useState(false) + const {checkPermission,accessInit, getGlobalAccessData,state, aiConfigFlushed, setAiConfigFlushed} = useGlobalContext() + + useImperativeHandle(ref, () => ({ + save:onFinish + })); + + const beforeUpload = async (file: RcFile) => { + if (!['image/png', 'image/jpeg', 'image/svg+xml'].includes(file.type)) { + alert($t('只允许上传PNG、JPG或SVG格式的图片')); + return false; + } + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + setImageBase64(e.target?.result as string); + form.setFieldValue('logo', e.target?.result); + }; + reader.readAsDataURL(file); + // } + return false; + }; + + + const handleChange: UploadProps['onChange'] = (info: UploadChangeParam) => { + if (info.file.status === 'uploading') { + setUploadLoading(true); + return; + } + if (info.file.status === 'done') { + getImgBase64(info.file.originFileObj as RcFile, () => { + setUploadLoading(false); + }); + } + if (info.fileList.length === 0) { + form.setFieldValue( "logo", null ); + } + }; + + const uploadButton = ( +
+ {uploadLoading ? : } +
+ ); + + const getTagAndServiceClassifyList = ()=>{ + setTagOptionList([]) + setServiceClassifyOptionList([]) + fetchData>('catalogues',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTagOptionList(data.tags?.map((x:EntityItem)=>{return { + label:x.name, value:x.name + }})||[]) + setServiceClassifyOptionList(data.catalogues) + + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + + // 获取表单默认值 + const getAiServiceInfo = () => { + fetchData>('ai-service/info',{method:'GET',eoParams:{team:teamId, service:serviceId},eoTransformKeys:['team_id','service_type']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTimeout(()=>{ + form.setFieldsValue({ + ...data.service, + team:data.service.team.id, + catalogue:data.service.catalogue?.id, + tags:data.service.tags?.map((x:EntityItem)=>x.name), + provider:data.service.provider.id, + logoFile:[ + { + uid: '-1', // 文件唯一标识 + name: 'image.png', // 文件名 + status: 'done', // 状态有:uploading, done, error, removed + url: data.service?.logo || '', // 图片 Base64 数据 + } + ] + }) + setImageBase64(data.service.logo) + setShowClassify(data.service.serviceType === 'public') + },0) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + }; + + const onFinish:()=>Promise = () => { + return form.validateFields().then((value)=>{ + return fetchData>(serviceId === undefined? 'team/ai-service':'ai-service/info',{method:serviceId === undefined? 'POST' : 'PUT',eoParams: {...(serviceId === undefined ? {team:value.team} :{service:serviceId,team:teamId})},eoBody:({...value,prefix:value.prefix?.trim()}), eoTransformKeys:['serviceType']},).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + setAiServiceInfo(data.service) + return Promise.resolve(true) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return Promise.reject(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=>{ + return Promise.reject(errorInfo) + }) + }) + }; + + const getProviderOptionList = ()=>{ + setProviderOptionList([]) + fetchData>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setProviderOptionList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x, + label:
, value:x.id + }})) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + const getTeamOptionList = ()=>{ + setTeamOptionList([]) + + fetchData>(!checkPermission('system.workspace.team.view_all') ?'simple/teams/mine' :'simple/teams',{method:'GET',eoTransformKeys:[]}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTeamOptionList(data.teams?.map((x:MemberItem)=>{return {...x, + label:x.name, value:x.id + }})) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + const deleteAiService = ()=>{ + fetchData>('team/ai-service',{method:'DELETE',eoParams:{team:teamId,service:serviceId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + navigate(`/aiservice/list`) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + useEffect(()=>{ + aiConfigFlushed && getProviderOptionList() + },[aiConfigFlushed]) + + useEffect(() => { + getProviderOptionList() + getTagAndServiceClassifyList() + if(accessInit){ + getTeamOptionList() + }else{ + getGlobalAccessData()?.then(()=>{ + getTeamOptionList() + }) + } + if (serviceId !== undefined) { + setOnEdit(true); + getAiServiceInfo(); + setBreadcrumb([ + { + title: {$t('服务')} + }, + { + title: $t('设置') + }]) + + } else { + setOnEdit(false); + form.setFieldValue('id',uuidv4()); + form.setFieldValue('team',teamId); + form.setFieldValue('serviceType','inner'); + } + return (form.setFieldsValue({})) + }, [serviceId]); + + + const deleteAiServiceModal = async ()=>{ + modal.confirm({ + title:$t('删除'), + content:$t(DELETE_TIPS.default), + onOk:()=> { + return deleteAiService() + }, + width:600, + okText:$t('确认'), + okButtonProps:{ + danger:true + }, + cancelText:$t('取消'), + closable:true, + icon:<> + }) + } + + const visualizationOptions = useMemo(()=>SERVICE_VISUALIZATION_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language]) + + return ( + <> + +
+
+ + label={$t("服务名称")} + name="name" + rules={[{ required: true ,whitespace:true }]} + > + + + + + label={$t("服务ID")} + name="id" + rules={[{ required: true ,whitespace:true }]} + > + + + + + label={$t("AI 模型供应商")} + name="provider" + rules={[{ required: true }]} + >{ + (providerOptionList && providerOptionList.length >0 ) ? :

未配置任何 AI 模型供应商,setAiConfigFlushed(false)}>立即配置

+ } + + + + label={$t("API 调用前缀")} + name="prefix" + extra={$t("选填,作为服务内所有API的前缀,比如host/{service_name}/{api_path},一旦保存无法修改")} + rules={[ + { + validator: validateUrlSlash, + }]} + > + + + + + label={$t("图标")} + name="logoFile" + extra={$t("仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩")} + valuePropName="fileList" getValueFromEvent={normFile} + > + +
+ {imageBase64 ? Logo : uploadButton} +
+
+ + + + + label={$t("描述")} + name="description" + > + + + + + label={$t("Logo")} + name="logo" + hidden + > + + + {!onEdit && + label={$t("所属团队")} + name="team" + rules={[{ required: true }]} + > + + } + + + + label={$t("标签")} + name="tags" + > + + + + + label={$t("服务类型")} + name="serviceType" + rules={[{required: true}]} + > + {setShowClassify(e.target.value === 'public')}} /> + + + {showClassify && + + label={$t("所属服务分类")} + name="catalogue" + extra={$t("设置服务展示在服务市场中的哪个分类下")} + rules={[{required: true}]} + > + + + } + {onEdit && <> + + + + + } +
+ {onEdit && <> + +
+

{$t('删除服务')}:{$t('删除操作不可恢复,请谨慎操作!')}

+
+ + + +
+
+
+ } + +
+ + ) +}) +export default AiServiceConfig \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx new file mode 100644 index 00000000..411f367d --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsideDocument.tsx @@ -0,0 +1,145 @@ +import { Editor } from '@tinymce/tinymce-react'; +import hljs from 'highlight.js'; +import 'highlight.js/styles/default.css'; +import {useEffect, useState} from "react"; +import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import {App, Button} from "antd"; +import { EntityItem } from '@common/const/type.ts'; +import WithPermission from '@common/components/aoplatform/WithPermission.tsx'; +import { RouterParams } from '@core/components/aoplatform/RenderRoutes'; +import { useParams } from 'react-router-dom'; +import { $t } from '@common/locales'; +const ServiceInsideDocument = ()=>{ + const { message } = App.useApp() + const [updater,setUpdater] = useState() + const [updateTime,setUpdateTime]=useState() + const [initDoc, setInitDoc] = useState() + const [doc, setDoc] = useState() + const {fetchData} = useFetch() + const { serviceId, teamId} = useParams(); + + const save = ()=>{ + fetchData>('service/doc',{method:'PUT',eoBody:({doc:doc}) ,eoParams:{service:serviceId,team:teamId},eoTransformKeys:['update_time']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + getServiceDoc() + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + const handleEditorChange = (content:string, editor:unknown) => { + setDoc(content) + }; + const setupEditor = (editor:unknown) => { + editor.on('init', () => { + editor.contentDocument.querySelectorAll('pre code').forEach((block:HTMLElement) => { + hljs.highlightBlock(block); + }); + }); + + editor.on('SetContent', () => { + editor.contentDocument.querySelectorAll('pre code').forEach((block:HTMLElement) => { + hljs.highlightBlock(block); + }); + }); + }; + + const getServiceDoc = ()=>{ + fetchData>('service/doc',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['update_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setUpdater(data.doc.updater.id === '' ? '-' : data.doc.updater.name) + setUpdateTime(data.doc.updater.id === '' ? '-' : data.doc.updateTime) + setInitDoc(data.doc.doc) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + useEffect(() => { + getServiceDoc() + }, []); + + return ( +
+ + +
+
+

{$t('最近一次更新者')}:{updater || '-'}{$t('最近一次更新时间')}:{updateTime || '-'}

+ +
+
+
) +} +export default ServiceInsideDocument \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx new file mode 100644 index 00000000..1b2c70f5 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx @@ -0,0 +1,158 @@ + +import {FC, useEffect, useMemo, useState} from "react"; +import {Link, Outlet, useLocation, useNavigate, useParams} from "react-router-dom"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import {App, Menu, MenuProps} from "antd"; +import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import { useAiServiceContext} from "../../contexts/AiServiceContext.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { PERMISSION_DEFINITION } from "@common/const/permissions.ts"; +import InsidePage from "@common/components/aoplatform/InsidePage.tsx"; +import Paragraph from "antd/es/typography/Paragraph"; +import { cloneDeep } from "lodash-es"; +import { $t } from "@common/locales/index.ts"; +import { getItem } from "@common/utils/navigation.tsx"; +import { AiServiceConfigFieldType } from "@core/const/ai-service/type.ts"; +import { MenuItemGroupType, MenuItemType, ItemType } from "antd/es/menu/interface"; +const APP_MODE = import.meta.env.VITE_APP_MODE; + +const AiServiceInsidePage:FC = ()=> { + const { message } = App.useApp() + const { teamId,serviceId,apiId, routeId } = useParams(); + const location = useLocation() + const currentUrl = location.pathname + const {fetchData} = useFetch() + const { setPrefixForce,setApiPrefix ,aiServiceInfo ,setAiServiceInfo} = useAiServiceContext() + const { accessData,checkPermission,accessInit,state} = useGlobalContext() + const [activeMenu, setActiveMenu] = useState() + const navigateTo = useNavigate() + const [showMenu, setShowMenu] = useState(false) + + const getAiServiceInfo = ()=>{ + fetchData>('service/info',{method:'GET',eoParams:{team:teamId, service:serviceId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setAiServiceInfo(data.service) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + + const getApiDefine = ()=>{ + setApiPrefix('') + setPrefixForce(false) + fetchData>('service/router/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setApiPrefix(data.prefix) + setPrefixForce(data.force) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + + + const SYSTEM_PAGE_MENU_ITEMS = useMemo(()=>[ + getItem($t('服务'), 'assets', null, + [ + getItem({$t('API')}, 'api',undefined,undefined,undefined,'team.service.api_doc.view'), + getItem({$t('路由')}, 'route',undefined,undefined,undefined,'team.service.router.view'), + getItem({$t('使用说明')}, 'document',undefined,undefined,undefined,''), + getItem({$t('发布')}, 'publish',undefined,undefined,undefined,'team.service.release.view'), + ], + 'group'), + getItem($t('订阅管理'), 'provideSer', null, + [ + getItem({$t('订阅审批')}, 'approval',undefined,undefined,undefined,'team.service.subscription.view'), + getItem({$t('订阅方管理')}, 'subscriber',undefined,undefined,undefined,'team.service.subscription.view'), + ], + 'group'), + getItem($t('管理'), 'mng', null, + [ + APP_MODE === 'pro' ? getItem({$t('调用拓扑图')}, 'topology',undefined,undefined,undefined,'project.myAiService.topology.view'):null, + getItem({$t('设置')}, 'setting',undefined,undefined,undefined,'')], + 'group'), +],[state.language]) + + + const menuData = useMemo(()=>{ + const filterMenu = (menu:MenuItemGroupType[])=>{ + const newMenu = cloneDeep(menu) + return newMenu!.filter((m:MenuItemGroupType )=>{ + if(m.children && m.children.length > 0){ + m.children = m.children.filter( + (c)=>(c&&(c as MenuItemType&{access:string} ).access ? + checkPermission((c as MenuItemType&{access:string} ).access as keyof typeof PERMISSION_DEFINITION[0]): + true)) + } + return m.children && m.children.length > 0 + }) + } + const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType[]) + setActiveMenu((pre)=>{ + return pre ?? 'api' + }) + return filteredMenu || [] + },[accessData,accessInit, SYSTEM_PAGE_MENU_ITEMS]) + + const onMenuClick: MenuProps['onClick'] = ({key}) => { + setActiveMenu(key) + }; + + useEffect(() => { + setShowMenu(!routeId && !currentUrl.includes('route/create')) + if(apiId !== undefined){ + setActiveMenu('api') + }else if(serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]){ + setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1]) + }else{ + setActiveMenu('api') + } + }, [currentUrl]); + + useEffect(()=>{ + if(accessData && accessData.get('team') && accessData.get('team')?.indexOf('team.service.router.view') !== -1){ + getApiDefine() + } + },[accessData]) + + useEffect(()=>{ + if( activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]){ + navigateTo(`/aiservice/${teamId}/inside/${serviceId}/${activeMenu}`) + } + },[activeMenu]) + + useEffect(() => { + serviceId && getAiServiceInfo() + }, [serviceId]); + + return ( + <>{showMenu ? + {$t('服务 ID')}:{serviceId || '-'} + }]} + backUrl="/aiservice/list"> +
+ [] } + /> +
+ +
+
+
: } + + ) +} +export default AiServiceInsidePage \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsideSubscriber.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsideSubscriber.tsx new file mode 100644 index 00000000..c8e36e58 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsideSubscriber.tsx @@ -0,0 +1,268 @@ +import {ActionType} from "@ant-design/pro-components"; +import {FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react"; +import {Link, useParams} from "react-router-dom"; +import {App, Form,TreeSelect} from "antd"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, VALIDATE_MESSAGE} from "@common/const/const.tsx"; +import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"; +import {DefaultOptionType} from "antd/es/cascader"; +import { SYSTEM_SUBSCRIBER_TABLE_COLUMNS } from "../../const/system/const.tsx"; +import { AiServiceSubscriberTableListItem, AiServiceSubscriberConfigFieldType, AiServiceSubscriberConfigHandle, AiServiceSubscriberConfigProps, SimpleAiServiceItem } from "../../const/system/type.ts"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { $t } from "@common/locales/index.ts"; + +const AiServiceInsideSubscriber:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const {fetchData} = useFetch() + const {serviceId, teamId} = useParams() + const addRef = useRef(null) + const pageListRef = useRef(null); + const [memberValueEnum, setMemberValueEnum] = useState([]) + const {accessData,state} = useGlobalContext() + const getAiServiceSubscriber = ()=>{ + return fetchData>('service/subscribers',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['apply_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + return {data:data.subscribers, success: true} + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const getMemberList = async ()=>{ + setMemberValueEnum([]) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + setMemberValueEnum(data.members) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + const manualReloadTable = () => { + pageListRef.current?.reload() + }; + + const deleteSubscriber = (entity:AiServiceSubscriberTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('service/subscriber',{method:'DELETE',eoParams:{application:entity!.id,service:entity!.service.id,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal =async (type:'delete'|'add',entity?:AiServiceSubscriberTableListItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'add': + title=$t('新增订阅方') + content= + break; + case 'delete': + title=$t('删除') + content=$t(DELETE_TIPS.default) + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'add': + return addRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'delete': + return deleteSubscriber(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:$t('确认'), + okButtonProps:{ + disabled : !checkAccess( `team.service.subscription.${type}`, accessData) + }, + cancelText:$t('取消'), + closable:true, + icon:<>, + }) + } + + const operation:PageProColumns[] =[ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums:1, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: AiServiceSubscriberTableListItem) => [ + {openModal('delete',entity)}} btnTitle="删除"/>, + ], + } + ] + + useEffect(() => { + setBreadcrumb([ + { + title:{$t('服务')} + }, + { + title:$t('订阅方管理') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + const columns = useMemo(()=>{ + return [...SYSTEM_SUBSCRIBER_TABLE_COLUMNS].map(x=>{ + if(x.filters &&((x.dataIndex as string[])?.indexOf('applier') !== -1 || (x.dataIndex as string[])?.indexOf('approver') !== -1) ){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + memberValueEnum?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + x.valueEnum = tmpValueEnum + } + if(x.dataIndex === 'from'){ + x.valueEnum = new Map([ + [0,{$t('手动添加')}], + [1,{$t('订阅申请')}], + ]) + } + return { + ...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title} + } + ) + },[memberValueEnum,state.language]) + + return ( + getAiServiceSubscriber()} + // dataSource={tableListDataSource} + showPagination={false} + addNewBtnTitle={$t("新增订阅方")} + onAddNewBtnClick={()=>{openModal('add')}} + addNewBtnAccess="team.service.subscription.add" + tableClass="pr-PAGE_INSIDE_X" + /> + ) +} + +export default AiServiceInsideSubscriber + +export const AiServiceSubscriberConfig = forwardRef((props, ref) => { + const { message } = App.useApp() + const { serviceId, teamId} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const [systemOptionList, setAiServiceOptionList] = useState() + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('service/subscriber',{method:'POST',eoBody:({...value}), eoParams:{service:serviceId,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(msg || $t(RESPONSE_TIPS.error)) + } + }) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + + const getAiServiceList = ()=>{ + setAiServiceOptionList([]) + fetchData>('simple/apps/mine',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const teamMap = new Map(); + data.apps + .filter((x:SimpleAiServiceItem)=>x.id !== serviceId) + .forEach((item:SimpleAiServiceItem) => { + if (!teamMap.has(item.team.id)) { + teamMap.set(item.team.id, { + title: item.team.name, + value: item.team.id, + key: item.team.id, + children: [], + selectable: false, // 第一级不可选 + disabled:true + }); + } + + teamMap.get(item.team.id)!.children!.push({ + title: item.name, + value: item.id, + key: item.id, + selectable: true, // 子级可选 + // partition:item.partition?.map((x:EntityItem)=>x.id) || [] + }); + }); + setAiServiceOptionList(Array.from(teamMap.values())) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + + useEffect(() => { + getAiServiceList() + }, [serviceId]); + + return ( +
+ + label={$t("订阅方")} + name="application" + rules={[{ required: true }]} + > + + + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceList.module.css b/frontend/packages/core/src/pages/aiService/AiServiceList.module.css new file mode 100644 index 00000000..16b7677c --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceList.module.css @@ -0,0 +1,14 @@ +.system-tree { + :global(.ant-tree .ant-tree-switcher){ + width:8px !important; + } + + :global .ant-tree-node-content-wrapper{ + height:40px; + padding:0 10px; + } + :global .ant-tree-title{ + line-height:40px; + + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceList.tsx b/frontend/packages/core/src/pages/aiService/AiServiceList.tsx new file mode 100644 index 00000000..dbb3ec7b --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceList.tsx @@ -0,0 +1,159 @@ +import PageList from "@common/components/aoplatform/PageList.tsx" +import {ActionType} from "@ant-design/pro-components"; +import {FC, useEffect, useMemo, useRef, useState} from "react"; +import {useNavigate} from "react-router-dom"; +import { App} from "antd"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import { SimpleTeamItem ,SimpleMemberItem} from "@common/const/type.ts"; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx"; +import AiServiceConfig from "./AiServiceConfig.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { $t } from "@common/locales/index.ts"; +import { AiServiceTableListItem, AiServiceConfigHandle } from "@core/const/ai-service/type.ts"; +import { AI_SERVICE_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; + +const AiServiceList:FC = ()=>{ + const navigate = useNavigate(); + const [tableSearchWord, setTableSearchWord] = useState('') + const { setBreadcrumb } = useBreadcrumb() + const [teamList, setTeamList] = useState<{ [k: string]: { text: string; }; }>() + const {fetchData} = useFetch() + const [tableListDataSource, setTableListDataSource] = useState([]); + const [tableHttpReload, setTableHttpReload] = useState(true); + const { message } = App.useApp() + const pageListRef = useRef(null); + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + const [open, setOpen] = useState(false); + const drawerFormRef = useRef(null) + const {checkPermission,accessInit, getGlobalAccessData,state} = useGlobalContext() + + const getAiServiceList = ()=>{ + if(!accessInit){ + getGlobalAccessData()?.then(()=>{ + getAiServiceList() + }) + return + } + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>(!checkPermission('system.workspace.service.view_all') ? 'my_ai_services':'ai-services',{method:'GET',eoParams:{keyword:tableSearchWord},eoTransformKeys:['api_num','can_delete','create_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.services) + setTableHttpReload(false) + return {data:data.services, success: true} + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const getTeamsList = ()=>{ + if(!accessInit){ + getGlobalAccessData()?.then(()=>{ + getTeamsList() + }) + return + } + fetchData>(!checkPermission('system.workspace.team.view_all') ?'simple/teams/mine' :'simple/teams',{method:'GET',eoTransformKeys:[]}).then(response=>{ + const {code,data,msg} = response + setTeamList(data.teams) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.teams?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setTeamList(tmpValueEnum) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return {data:[], success:false} + } + }) + } + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + useEffect(() => { + getTeamsList(); + getMemberList() + setBreadcrumb([ + { + title: $t('服务') + }]) + }, []); + + const onClose = () => { + setOpen(false); + }; + + const columns = useMemo(()=>{ + const res = AI_SERVICE_TABLE_COLUMNS.map(x=>{ + if(x.filters &&((x.dataIndex as string[])?.indexOf('master') !== -1 ) ){ + x.valueEnum = memberValueEnum + } + if(x.filters &&((x.dataIndex as string[])?.indexOf('team') !== -1 ) ){ + x.valueEnum = teamList + } + + return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}}) + return res + },[memberValueEnum,teamList,state.language]); + + return ( +
+ {/* */} + getAiServiceList()} + addNewBtnTitle={$t("添加服务")} + addNewBtnWrapperClass={'my-first-step'} + searchPlaceholder={$t("输入名称、ID、所属团队、负责人查找服务")} + onAddNewBtnClick={() => { + setOpen(true) + }} + manualReloadTable={manualReloadTable} + onChange={() => { + setTableHttpReload(false) + }} + onSearchWordChange={(e) => { + setTableSearchWord(e.target.value) + }} + onRowClick={(row:AiServiceTableListItem)=>navigate(`/aiservice/${row.team.id}/inside/${row.id}`)} + /> + drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} > + + +
+ ) + +} +export default AiServiceList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/AiServiceOutlet.tsx b/frontend/packages/core/src/pages/aiService/AiServiceOutlet.tsx new file mode 100644 index 00000000..e696fe6b --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/AiServiceOutlet.tsx @@ -0,0 +1,20 @@ + +import { Outlet, useParams } from "react-router-dom" +import { RouterParams } from "@core/components/aoplatform/RenderRoutes" +import { useEffect } from "react" +import { useGlobalContext } from "@common/contexts/GlobalStateContext" + +export default function AiServiceOutlet(){ + const {teamId} = useParams() + const {getTeamAccessData,cleanTeamAccessData} = useGlobalContext() + + useEffect(()=>{ + teamId ? getTeamAccessData(teamId) : cleanTeamAccessData() + return ()=>{ + cleanTeamAccessData() + } + },[teamId]) + + + return () +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx new file mode 100644 index 00000000..2c5ebc5b --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiDocument.tsx @@ -0,0 +1,58 @@ + +import {forwardRef, useEffect, useState} from "react"; +import { Empty, Spin, message} from "antd"; +import {BasicResponse, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import { LoadingOutlined } from "@ant-design/icons"; +import EmptySVG from '@common/assets/empty.svg' +import { $t } from "@common/locales/index.ts"; +import ApiDocument from '@common/components/aoplatform/ApiDocument.tsx' +import { useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import { AiServiceInsideApiDocumentHandle, AiServiceInsideApiDocumentProps, AiServiceApiDetail } from "@core/const/ai-service/type.ts"; + +const AiServiceInsideApiDocument = forwardRef(() => { + const {serviceId, teamId} = useParams() + const {fetchData} = useFetch() + const [apiDetail, setApiDetail] = useState() + const [loading, setLoading] = useState(false) + useEffect(() => { + getApiDetail() + }, []); + + const getApiDetail = ()=>{ + setLoading(true) + fetchData>('service/api_doc',{method:'GET',eoParams:{service:serviceId,team:teamId },eoTransformKeys:['update_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setApiDetail(data.doc?.content) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).finally(()=>{setLoading(false)}) + } + + + const ApiPreview = ({spec}:{spec?:string | object})=>{ + return ( +
+
+ +
+
+ ) + } + + + return (<> + } spinning={loading} wrapperClassName=' h-full overflow-hidden '> +
+ { apiDetail ? + : + } +
+
+ ) +}) + +export default AiServiceInsideApiDocument \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiPlugin.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiPlugin.tsx new file mode 100644 index 00000000..4a8af2aa --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideApiPlugin.tsx @@ -0,0 +1,7 @@ +import {FC} from "react"; + +const AiServiceInsideApiPlugin:FC = ()=>{ + + return (<>) +} +export default AiServiceInsideApiPlugin \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx new file mode 100644 index 00000000..63b19785 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -0,0 +1,304 @@ +import {App, Button, Form, Input, InputNumber, Row, Spin, Tag} from "antd"; +import { MutableRefObject, useEffect, useRef, useState} from "react"; +import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import { $t } from "@common/locales/index.ts"; +import { LoadingOutlined } from "@ant-design/icons"; +import InsidePage from "@common/components/aoplatform/InsidePage.tsx"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import { useNavigate, useParams } from "react-router-dom"; +import { useAiServiceContext } from "@core/contexts/AiServiceContext.tsx"; +import EditableTableNotAutoGen from "@common/components/aoplatform/EditableTableNotAutoGen.tsx"; +import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; +import { VariableItems } from "@core/const/ai-service/type.ts"; +import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx'; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter"; +import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from "./AiServiceInsideRouterModelConfig"; +import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList"; +import { EditableFormInstance } from "@ant-design/pro-components"; + +type AiServiceRouterField = { + name:string + path:string + prompt:string + variables:Array<{key:string, description:string, require:true}> + description:string + timeout:number + retry:number +} + +type AiServiceRouterConfig = { + name:string + path:string + aiPrompt:{ + prompt:string + variables:Array<{key:string, description:string, require:true}> + } + aiModel:{ + id:string + config:string + } + description:string + timeout:number + retry:number +} + +const AiServiceInsideRouterCreate = () => { + const navigator = useNavigate() + const { message } = App.useApp() + const {serviceId, teamId,routeId} = useParams() + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const [loading, setLoading] = useState(false) + const {apiPrefix,prefixForce ,aiServiceInfo} = useAiServiceContext() + const [variablesTable,setVariablesTable] = useState([]) + const [drawerType,setDrawerType]= useState<'edit'|undefined>() + const [open, setOpen] = useState(false); + const drawerAddFormRef = useRef(null) + const [defaultLlm, setDefaultLlm] = useState() + const [llmList, setLlmList] = useState([]) + const [variablesTableRef, setVariablesTableRef] = useState | undefined>>() + const onFinish = ()=>{ + return variablesTableRef?.current?.validateFields().then(()=>{ + return form.validateFields().then((formValue)=>{ + const {name, path, description, variables, prompt, timeout, retry} = formValue + const body = {name, path: !routeId && prefixForce ? `${apiPrefix}/${path}`:path , description,timeout, retry,aiPrompt:{variables:variables, prompt:prompt},aiModel:{id:defaultLlm?.id, config:defaultLlm?.config}} + return fetchData>('service/ai-router',{method: routeId ? 'PUT' : 'POST',eoBody:(body), eoParams: {service:serviceId,team:teamId, ...(routeId ? {router:routeId}: {})},eoTransformKeys:['aiPrompt','aiModel']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + navigator(`/aiservice/${teamId}/inside/${serviceId}/route`) + return Promise.resolve(true) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return Promise.reject(msg || $t(RESPONSE_TIPS.error)) + } + }).catch(errInfo=>Promise.reject(errInfo)) + }) + }) + .catch(errInfo=>Promise.reject(errInfo)) + } + + const openDrawer = (type:'edit')=>{ + setDrawerType(type) + } + + useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType]) + + const getRouterConfig = ()=>{ + setLoading(true) + fetchData>('service/ai-router',{method:'GET',eoParams:{service:serviceId,team:teamId, router:routeId}, eoTransformKeys:['ai_model', 'ai_prompt']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const {path, aiPrompt,aiModel} = data.api + form.setFieldsValue({...data.api,...aiPrompt, path:prefixForce && path?.startsWith(apiPrefix + '/')? path.slice((apiPrefix?.length || 0) + 1) : path }) + setVariablesTable(aiPrompt.variables as VariableItems[]) + setDefaultLlm(prev => ({...prev, id:aiModel?.id, config:aiModel.config}) as (AiProviderDefaultConfig & { config: string; })) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=> console.error(errorInfo)) + .finally(()=>setLoading(false)) + } + + const getDefaultModelConfig = ()=>{ + fetchData>('ai/provider/llms',{method:'GET',eoParams:{provider:aiServiceInfo?.provider?.id}, eoTransformKeys:['default_llm']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setLlmList(data.llms) + setDefaultLlm(prev => { + const llmSetting = data.llms?.find((x:AiProviderLlmsItems)=>x.id ===( prev?.id ?? data.provider.defaultLlm)) + return {...prev, + defaultLlm:data.provider.defaultLlm, + name:data.provider.name, + config:llmSetting?.config || '', + ...(llmSetting ?? {}) + } as (AiProviderDefaultConfig & { config: string; }) + }) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=> console.error(errorInfo)) + } + + + useEffect(()=>{ + aiServiceInfo?.provider && getDefaultModelConfig() + },[ + aiServiceInfo + ]) + + + useEffect(() => { + if(routeId){ + getRouterConfig() + }else{ + form.setFieldsValue({ + prefix:apiPrefix, + variables:[{key:'Query',value:'',require:true}], + prompt:'{{Query}}', + retry:0, + timeout:300000 + }) + } + return (form.setFieldsValue({})) + }, []); + + const addVariable = ()=>{ + form.setFieldsValue({ + variables:[...form.getFieldValue('variables'),{key:'',value:'',require:true}] + }) + } + + const handleVariablesChange = (newKeys:string[])=>{ + const variables = form.getFieldValue('variables') || [] + const variablesKeys = variables?.map(({key}:{key:string})=>(key)) + for(const key of newKeys){ + if(!variablesKeys ||variablesKeys.indexOf(key) === -1){ + variables.push({key, value:'',require:true}) + } + } + form.setFieldsValue({ + variables:[...variables] + }) + setVariablesTable(variables as VariableItems[]) + } + + + const handleValuesChange = (changedValues:Record) => { + if(changedValues.variables){ + setVariablesTable(changedValues.variables as VariableItems[]) + } + }; + + + const handlerSubmit:() => Promise|undefined= ()=>{ + return drawerAddFormRef.current?.save()?.then((res:{id:string, config:string})=>{ + setDefaultLlm(prev => ({...prev, id:res.id, config:res.config, logo:llmList?.find((x:AiProviderLlmsItems)=>x.id === res.id)?.logo}) as (AiProviderDefaultConfig & { config: string; })) + return true}) + } + + const onClose = () => { + setDrawerType(undefined); + }; + + return ( + + + + + +
+ }> + } spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'> +
+
+ + + className="flex-1" + label={$t("路由名称")} + name="name" + rules={[{ required: true,whitespace:true }]} + > + + + + + className="flex-1" + label={$t("请求路径")} + name="path" + rules={[{ required: true,whitespace:true }]} + > + + + + + + label={$t("提示词")} + name="prompt" + > + + + + + label={
{$t("变量")}New
} + name="variables" + className="[&>.ant-row>.ant-col>label]:w-full" + > + + getFromRef={setVariablesTableRef} + configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} + /> + + + + label={$t("描述")} + name="description" + > + + + + + + className="flex-1" + label={$t("请求超时时间")} + name={'timeout'} + rules={[{required: true}]} + > + + + + className="flex-1" + label={$t("重试次数")} + name={'retry'} + rules={[{required: true}]} + > + + + + + +
+ +
+ handlerSubmit()} + > + + + + ) +} +export default AiServiceInsideRouterCreate + + + \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx new file mode 100644 index 00000000..8b9624b1 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx @@ -0,0 +1,182 @@ +import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx" +import {ActionType} from "@ant-design/pro-components"; +import {FC, useEffect, useMemo, useRef, useState} from "react"; +import {Link, useNavigate, useParams} from "react-router-dom"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Divider} from "antd"; +import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import { SimpleMemberItem} from '@common/const/type.ts' +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { $t } from "@common/locales/index.ts"; +import { AiServiceRouterTableListItem } from "@core/const/ai-service/type.ts"; +import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; + +const AiServiceInsideRouterList:FC = ()=>{ + const [searchWord, setSearchWord] = useState('') + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const [tableListDataSource, setTableListDataSource] = useState([]); + const [tableHttpReload, setTableHttpReload] = useState(true); + const {fetchData} = useFetch() + const pageListRef = useRef(null); + const [memberValueEnum, setMemberValueEnum] = useState([]) + const {accessData,state} = useGlobalContext() + const {serviceId, teamId} = useParams() + const navigator = useNavigate() + + const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[], success: boolean }>=> { + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + + return fetchData>('service/ai-routers',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','disable']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.apis) + setTableHttpReload(false) + return {data:data.apis, success: true} + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const deleteRoute = (entity:AiServiceRouterTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('service/ai-router',{method:'DELETE',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal = async (type: 'delete',entity:AiServiceRouterTableListItem) =>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'delete': + title=$t('删除') + content=$t(DELETE_TIPS.default) + break; + } + + modal.confirm({ + title, + content, + onOk:()=> { + switch (type){ + case 'delete': + return deleteRoute(entity).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:$t('确认'), + okButtonProps:{ + disabled : !checkAccess( `team.service.router.${type}`, accessData ) + }, + cancelText:$t('取消'), + closable:true, + icon:<>, + }) + } + + const operation:PageProColumns[] =[ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums:2, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ + {navigator(`/aiservice/${teamId}/inside/${serviceId}/route/${entity.id}`)}} btnTitle="编辑"/>, + , + {openModal('delete',entity)}} btnTitle="删除"/>, + ], + } + ] + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const getMemberList = async ()=>{ + setMemberValueEnum([]) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + setMemberValueEnum(data.members) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + useEffect(() => { + setBreadcrumb([ + { + title:{$t('服务')} + }, + { + title:$t('路由') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + const columns = useMemo(()=>{ + return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map(x=>{ + if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + memberValueEnum?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + x.valueEnum = tmpValueEnum + } + + return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}}) + },[memberValueEnum,state.language]) + + + return ( + <> + getRoutesList()} + dataSource={tableListDataSource} + addNewBtnTitle={$t('添加路由')} + searchPlaceholder={$t('输入 URL 查找路由')} + onAddNewBtnClick={()=>{navigator(`/aiservice/${teamId}/inside/${serviceId}/route/create`)}} + addNewBtnAccess="team.service.router.add" + tableClickAccess="team.service.router.view" + manualReloadTable={manualReloadTable} + onSearchWordChange={(e)=>{setSearchWord(e.target.value)}} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row:AiServiceRouterTableListItem)=>navigator(`/aiservice/${teamId}/inside/${serviceId}/route/${row.id}`)} + tableClass="mr-PAGE_INSIDE_X " + /> + + ) + +} +export default AiServiceInsideRouterList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx new file mode 100644 index 00000000..9f8ab6e1 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx @@ -0,0 +1,74 @@ +import { Codebox } from "@common/components/postcat/api/Codebox" +import { PLACEHOLDER } from "@common/const/const" +import { $t } from "@common/locales" +import { AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList" +import { Form, Select, Tag } from "antd" +import { forwardRef, useEffect, useImperativeHandle } from "react" + +export type AiServiceRouterModelConfigHandle = { + save:()=>Promise<{id:string, config:string}> +} + +export type AiServiceRouterModelConfigProps = { + entity:AiServiceRouterModelConfigField + llmList:AiProviderLlmsItems[] +} + +type AiServiceRouterModelConfigField = { + id:string + config:string +} + +const AiServiceRouterModelConfig = forwardRef((props, ref)=>{ + const [form] = Form.useForm(); + const {llmList,entity} = props + + useImperativeHandle(ref, ()=>({ + save:form.validateFields + }) + ) + + useEffect(()=>{ + form.setFieldsValue(entity) + },[]) + + return ( +
+ + + label={$t("模型")} + name="id" + rules={[{ required: true }]} + > + +
+ + + label={$t("参数")} + name="config" + > + + + + ) +}) + +export default AiServiceRouterModelConfig \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApproval.module.css b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApproval.module.css new file mode 100644 index 00000000..55cfe008 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApproval.module.css @@ -0,0 +1,9 @@ +:global .ant-tabs.ant-tabs-top{ + height:100%; + .ant-tabs-content.ant-tabs-content-top{ + height:100%; + .ant-tabs-tabpane.ant-tabs-tabpane-active{ + height:100%; + } + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApproval.tsx b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApproval.tsx new file mode 100644 index 00000000..1b1c10e7 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApproval.tsx @@ -0,0 +1,36 @@ + +import {Tabs} from "antd"; +import {Outlet, useLocation, useNavigate} from "react-router-dom"; +import './AiServiceInsideApproval.module.css' +import {FC, useEffect, useMemo, useState} from "react"; +import { SYSTEM_INSIDE_APPROVAL_TAB_ITEMS } from "../../../const/system/const"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { $t } from "@common/locales"; + + +const AiServiceInsideApproval:FC = ()=>{ + const navigateTo = useNavigate() + const location = useLocation() + const query =new URLSearchParams(useLocation().search) + const currentUrl = location.pathname + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + const {state} = useGlobalContext() + const onChange = (key: string) => { + setPageStatus(Number(key) as 0|1) + navigateTo(`${currentUrl}?status=${key}`); + }; + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [currentUrl]); + const tabItems = useMemo(()=>SYSTEM_INSIDE_APPROVAL_TAB_ITEMS?.map((x)=>({...x, label:$t(x.label as string) })),[state.language]) + + return ( + <> + + + + ) +} + +export default AiServiceInsideApproval \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx new file mode 100644 index 00000000..d6106a68 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/approval/AiServiceInsideApprovalList.tsx @@ -0,0 +1,200 @@ + +import {ActionType} from "@ant-design/pro-components"; +import {FC, useEffect, useMemo, useRef, useState} from "react"; +import {Link, useLocation, useParams} from "react-router-dom"; +import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Button} from "antd"; +import { + SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN, + SUBSCRIBE_APPROVAL_INNER_TODO_TABLE_COLUMN, + SubscribeApprovalTableListItem, TODO_LIST_COLUMN_NOT_INCLUDE_KEY +} from "@common/const/approval/const.tsx"; +import {BasicResponse, COLUMNS_TITLE, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import { + SubscribeApprovalModalContent, + SubscribeApprovalModalHandle +} from "@common/components/aoplatform/SubscribeApprovalModalContent.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { SubscribeApprovalInfoType } from "@common/const/approval/type.tsx"; +import { $t } from "@common/locales"; + +const AiServiceInsideApprovalList:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const {serviceId, teamId} = useParams(); + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const [tableHttpReload, setTableHttpReload] = useState(true); + const [tableListDataSource, setTableListDataSource] = useState([]); + const pageListRef = useRef(null); + const query =new URLSearchParams(useLocation().search) + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + const subscribeRef = useRef(null) + const [approvalBtnLoading,setApprovalBtnLoading] = useState(false) + const [memberValueEnum, setMemberValueEnum] = useState([]) + const {accessData,state} = useGlobalContext() + + const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{ + message.loading($t(RESPONSE_TIPS.loading)) + const {code,data,msg} = await fetchData>('service/approval/subscribe',{method:'GET',eoParams:{apply:entity!.id, service:serviceId,team:teamId},eoTransformKeys:['apply_project','apply_team','apply_time','approval_time']}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + const modalIns = modal.confirm({ + title:type === 'approval' ? $t('审批') : $t('查看'), + content:, + onOk:()=>{ + return subscribeRef.current?.save('pass').then((res)=>res === true && manualReloadTable()) + }, + width:600, + okText:type === 'approval' ? $t('通过') : $t('确认'), + cancelText:type === 'approval' ?$t('取消'):$t('关闭'), + okButtonProps:{ + disabled : type === 'approval' ? !checkAccess('team.service.release.approval', accessData): false + }, + closable:true, + onCancel:()=>{setApprovalBtnLoading(false)}, + icon:<>, + footer:(_, { OkBtn, CancelBtn }) =>{ + return ( + <> + {type === 'approval' ? <> + + + + : + <> + + + } + + ) + }, + }) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return + } + } + + const operation:PageProColumns[] =[ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums:1, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: SubscribeApprovalTableListItem) => [ + pageStatus === 0 ? + {openModal('approval',entity)}} btnTitle="审批"/> + :{openModal('view',entity)}} btnTitle="查看"/>, + ], + } + ] + + const getApprovalList = ()=>{ + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>('service/approval/subscribes',{method:'GET',eoParams:{service:serviceId,team:teamId, status:(query.get('status') || 0)},eoTransformKeys:['apply_time','apply_project','approval_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.approvals) + setInit((prev)=>prev ? false : prev) + return {data:data.approvals, success: true} + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const getMemberList = async ()=>{ + setMemberValueEnum([]) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + setMemberValueEnum(data.members) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + useEffect(() => { + !init && pageListRef.current?.reload() + }, [pageStatus]); + + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [query]); + + useEffect(() => { + setBreadcrumb([ + { + title:{$t('服务')} + }, + { + title:$t('订阅审批') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + + const columns = useMemo(()=>{ + const newColumns = [...(!(query.get('status'))? SUBSCRIBE_APPROVAL_INNER_TODO_TABLE_COLUMN:SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN)] + const filteredCol = pageStatus === 0 ? newColumns.filter((x)=>TODO_LIST_COLUMN_NOT_INCLUDE_KEY.indexOf(x.dataIndex as string) === -1): newColumns + return filteredCol.map(x=>{ + if(x.filters &&((x.dataIndex as string[])?.indexOf('applier') !== -1 || (x.dataIndex as string[])?.indexOf('approver') !== -1) ){ + const tmpValueEnum :Record= {} + memberValueEnum?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:$t(x.name)} + }) + x.valueEnum = tmpValueEnum + } + if(x.dataIndex === 'status'){ + x.valueEnum = new Map([ + [0, {$t('拒绝')}], + [2,{$t('通过')}], + ]) + } + + return {...x,title: typeof x.title === 'string' ? $t(x.title as string) : x.title}}) + },[pageStatus,memberValueEnum,state.language]) + + return ( +
+ getApprovalList()} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row:SubscribeApprovalTableListItem)=>openModal(pageStatus === 0 ? 'approval': 'view',row)} + tableClickAccess={pageStatus === 0 ?'team.service.subscription.approval':'team.service.subscription.view'} + tableClass="pr-PAGE_INSIDE_X" + /> +
+ ) +} +export default AiServiceInsideApprovalList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx new file mode 100644 index 00000000..be17b4e9 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublish.tsx @@ -0,0 +1,48 @@ + +import { Tabs } from "antd" +import { useState, useEffect, FC, useMemo } from "react" +import { Link, Outlet, useLocation, useNavigate } from "react-router-dom" +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext" +import { SYSTEM_PUBLISH_TAB_ITEMS } from "../../../const/system/const" +import { $t } from "@common/locales" +import { useGlobalContext } from "@common/contexts/GlobalStateContext" + +const AiServiceInsidePublic:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const query =new URLSearchParams(useLocation().search) + const location = useLocation() + const currentUrl = location.pathname + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + const navigateTo = useNavigate() + const { state } = useGlobalContext() + + const onChange = (key: string) => { + setPageStatus(Number(key) as 0|1) + navigateTo(`${currentUrl}?status=${key}`); + }; + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [currentUrl]); + + useEffect(() => { + setBreadcrumb([ + { + title:{$t('服务')} + }, + { + title:$t('发布') + } + ]) + }, []); + + const tabItems = useMemo(()=>SYSTEM_PUBLISH_TAB_ITEMS?.map((x)=>({...x, label:$t(x.label as string) })),[state.language]) + return ( + <> + + + + ) + +} +export default AiServiceInsidePublic \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx new file mode 100644 index 00000000..0df663fc --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishList.tsx @@ -0,0 +1,526 @@ +import { ActionType, ParamsType } from "@ant-design/pro-components"; +import { App, Button, Divider } from "antd"; +import { useState, useRef, useEffect, useMemo, FC } from "react"; +import { useParams, Link, useLocation } from "react-router-dom"; +import PageList, { PageProColumns } from "@common/components/aoplatform/PageList"; +import { PublishApprovalModalContent } from "@common/components/aoplatform/PublishApprovalModalContent"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import { PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN, PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN, PublishApplyStatusEnum, PublishStatusEnum, PublishTableStatusColorClass } from "@common/const/approval/const"; +import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import { MemberTableListItem } from "../../../const/member/type"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { useFetch } from "@common/hooks/http"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { AiServicePublishReleaseItem } from "../../../const/system/type"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { PERMISSION_DEFINITION } from "@common/const/permissions"; +import { checkAccess } from "@common/utils/permission"; +import AiServiceInsidePublishOnline from "./AiServiceInsidePublishOnline"; +import { PublishVersionTableListItem, PublishTableListItem, PublishApprovalInfoType, PublishApprovalModalHandle } from "@common/const/approval/type"; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter"; +import { $t } from "@common/locales"; + +const AiServiceInsidePublicList:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const pageListRef = useRef(null); + const [tableHttpReload, setTableHttpReload] = useState(true); + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const [tableListDataSource, setTableListDataSource] = useState([]); + const {serviceId, teamId} = useParams(); + const drawerRef = useRef(null) + const [extraModalBtnLoading,setExtraModalBtnLoading] = useState(false) + const [pageStatus,setPageStatus] = useState<0|1>(0 as 0|1) + const [pageType, setPageType] = useState<'insidePage'|'global'>('insidePage') + const query =new URLSearchParams(useLocation().search) + const currLocation = useLocation().pathname + const [memberValueEnum, setMemberValueEnum] = useState([]) + const {accessData,state} = useGlobalContext() + const [drawerTitle, setDrawerTitle] = useState('') + const [drawerType, setDrawerType] = useState<'approval'|'view'|'add'|'publish'|'online'>('view') + const [drawerVisible, setDrawerVisible] = useState(false) + const [drawerData, setDrawerData] = useState({} as PublishTableListItem) + const [drawerOkTitle, setDrawerOkTitle] = useState('确认') + const [isOkToPublish, setIsOkToPublish] = useState(false) + const getAiServicePublishList = (params?: ParamsType & { + pageSize?: number | undefined; + current?: number | undefined; + keyword?: string | undefined; + })=>{ + if(!(pageType !== 'insidePage' && pageStatus !== 0 ) && !tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>( + pageStatus === 0 ? 'service/releases':'service/publishs', + {method:'GET',eoParams:(pageType !== 'insidePage' && pageStatus !== 0 ) ? {service:serviceId,team:teamId,page:params?.current,page_size:params?.pageSize}:{service:serviceId,team:teamId},eoTransformKeys:['pageSize','apply_time','approve_time','release_status','is_valid','fail_msg','create_time','can_rollback','flow_id','can_delete']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const finalRes = pageStatus === 0 ? data.releases.map((x:PublishVersionTableListItem)=>{if(!x.status|| x.status === 'close'){x.status = 'none'} return x}):data.publishs + setTableListDataSource(finalRes) + setInit((prev)=>prev ? false : prev) + return {data:finalRes, success: true} + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + setInit((prev)=>prev ? false : prev) + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const handlePublishAction = (type:'rollback'|'delete'|'stop',entity:PublishTableListItem | PublishVersionTableListItem)=>{ + let url:string ='service/release' + let method:string + let params:{[k:string]:unknown} = {} + switch(type){ + case 'rollback': + method = 'POST' + params = {service:serviceId,team:teamId, id:entity.id} + break; + case 'delete': + method = 'DELETE' + params = {service:serviceId,team:teamId,id:entity.id} + break; + case 'stop': + url = 'service/publish/stop' + method = 'DELETE' + params = {service:serviceId,team:teamId,id:(entity as PublishVersionTableListItem).flowId} + break; + } + + return new Promise((resolve, reject)=>{ + fetchData>(url,{method,eoParams:params}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || $t(RESPONSE_TIPS.success)) + resolve(true) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=> reject(errorInfo)) + })} + + + const isActionAllowed = (type:'view' | 'delete' | 'add' |'stop'|'online'|'cancel'|'approval' | 'rollback'|'publish') => { + const permission :keyof typeof PERMISSION_DEFINITION[0]= `team.service.release.${type === 'publish'? 'add' : type}`; + return !checkAccess(permission, accessData); + }; + + const handleOnline = (entity:PublishTableListItem | PublishVersionTableListItem)=>{ + modal.confirm({ + title:$t('发布结果'), + content:, + width: 600, + closable: true, + wrapClassName:'ant-modal-without-footer', + icon: <>, + footer:null, + onCancel:()=>{ + manualReloadTable() + } + }); + } + + const openDrawer = async(type: 'view' | 'add'|'online'|'approval'|'publish', entity?: PublishTableListItem|PublishVersionTableListItem)=>{ + setIsOkToPublish(false) + switch (type) { + case 'view':{ + message.loading($t(RESPONSE_TIPS.loading)); + const viewPublish:boolean = pageStatus !== 0 || ((entity as PublishVersionTableListItem)?.status && (entity as PublishVersionTableListItem)?.status !== 'none') + const { code, data, msg } = await fetchData>( + viewPublish ? 'service/publish':'service/release', + { method: 'GET', eoParams:{id: (entity as PublishVersionTableListItem)?.[viewPublish && pageStatus === 0 ? 'flowId':'id'],service:serviceId,team:teamId },eoTransformKeys:['cluster_publish_status','upstream_status','doc_status','proxy_status','version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle($t('查看详情')) + setDrawerType(type) + setDrawerData(viewPublish ? data.publish : data.release)} else { + message.error(msg || $t(RESPONSE_TIPS.error)); + return + } + break; + } + case 'online':{ + message.loading($t(RESPONSE_TIPS.loading)); + const { code, data, msg } = await fetchData>( + 'service/publish', + { method: 'GET', eoParams:{ id: (entity as PublishVersionTableListItem)?.flowId,service:serviceId,team:teamId },eoTransformKeys:['version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle($t('上线')) + setDrawerType(type) + setDrawerOkTitle($t('上线')) + setDrawerData({...data.publish, flowId:(entity as PublishVersionTableListItem)?.flowId}) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)); + return + } + break; + } + case 'approval':{ + message.loading($t(RESPONSE_TIPS.loading)); + const { code, data, msg } = await fetchData>( + 'service/publish', + { method: 'GET', eoParams:{ id: (entity as PublishVersionTableListItem)?.flowId,service:serviceId,team:teamId },eoTransformKeys:['version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle($t('审批')) + setDrawerType(type) + setDrawerData(data.publish) + setDrawerOkTitle($t('通过')) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)); + return + } + break; + } + case 'publish': + case 'add':{ + message.loading($t(RESPONSE_TIPS.loading)); + const { code, data, msg } = await fetchData>( + 'service/publish/check', + { method: 'GET', eoParams:{service:serviceId,team:teamId, ...(type === 'publish' ?{ release:entity?.id }:{})},eoTransformKeys:['version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle($t('申请发布')) + setDrawerType(type) + setDrawerData({...data, ...(type === 'publish'&& {version:entity?.version, id:entity?.id})}) + setDrawerOkTitle($t('确认')) + setIsOkToPublish(data.isOk??true) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)); + return + } + break; + } + } + setDrawerVisible(true) + } + + + const openModal = async (type: 'delete' |'stop'|'cancel' | 'rollback', entity?: PublishTableListItem|PublishVersionTableListItem) => { + let title: string = ''; + let content: string | React.ReactNode = ''; + switch (type) { + case 'delete': + title = $t('删除'); + content = $t(DELETE_TIPS.default); + break; + case 'rollback': + title = $t('回滚'); + content = $t('请确认是否回滚?'); + break; + case 'cancel': + title = $t('撤销申请'); + content = $t('请确认是否撤销申请?'); + break; + case 'stop': + title = $t('终止发布'); + content = $t('请确认是否终止发布?'); + break; + } + + modal.confirm({ + title, + content, + onOk: () => { + switch (type){ + case 'rollback': + return handlePublishAction('rollback',entity!).then((res)=>{if(res === true)manualReloadTable()}) + case 'delete': + return handlePublishAction('delete',entity!).then((res)=>{if(res === true)manualReloadTable()}) + case 'cancel': + case 'stop': + return handlePublishAction('stop',entity!).then((res)=>{if(res === true)manualReloadTable()}) + } + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + onCancel:()=>{setExtraModalBtnLoading(false)}, + closable: true, + icon: <>, + okButtonProps:{ + disabled: isActionAllowed(type) || false + }, + footer: (_, { OkBtn, CancelBtn }) => ( + <> + + + + + + ), + }); + }; + + const tableOperation = (entity:PublishTableListItem | PublishVersionTableListItem)=>{ + const viewBtn = {openDrawer('view',entity)}} btnTitle="查看详情"/> + let btnArr:React.ReactNode[] = [] + if(pageType !== 'insidePage' && pageStatus !== 0){ + btnArr = [ + viewBtn + ] + return btnArr + } + + if((entity as PublishVersionTableListItem).status === 'accept'){ + btnArr = [ + {openDrawer('online',entity)}} btnTitle="上线"/>, + , + viewBtn, + , + {openModal('stop',entity)}} btnTitle="终止发布"/> + ] + } + + + if((entity as PublishVersionTableListItem).status === 'publishing'){ + btnArr = [ + viewBtn, + , + {openModal('stop',entity)}} btnTitle="终止发布"/> + ] + } + + if((entity as PublishVersionTableListItem).status === 'apply'){ + btnArr = [ + {openDrawer('approval',entity)}} btnTitle="审批"/>, + , + viewBtn, + , + {openModal('cancel',entity)}} btnTitle="撤回申请"/> + ] + } + + // 第一期不做回滚 + // if( (entity as PublishVersionTableListItem).status === 'online' && (entity as PublishVersionTableListItem).canRollback){ + // btnArr = [...btnArr, + // ...(btnArr.length > 0 ? []: + // [viewBtn, + // ]), + // + // ] + // } + + if( ['close','refuse','none'].indexOf((entity as PublishVersionTableListItem).status as string) !== -1 || !(entity as PublishVersionTableListItem).flowId){ + btnArr = [...btnArr, + ...(btnArr.length > 0 ? []: + [viewBtn, + // + ]), + // {openDrawer('publish',entity)}} btnTitle="申请发布"/> + ] + } + + if( ['running','error'].indexOf((entity as PublishVersionTableListItem).status as string) !== -1 && (entity as PublishVersionTableListItem).flowId){ + btnArr = [viewBtn] + } + + if((entity as PublishVersionTableListItem).canDelete){ + btnArr = [...btnArr, btnArr.length > 0 && ,{openModal('delete',entity)}} btnTitle="删除"/> ] + } + + return btnArr + + } + + const operation:PageProColumns[] =[ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums:pageStatus === 0 ? 2 : 1, + valueType: 'option', + fixed:'right', + render: (_: React.ReactNode, entity: PublishTableListItem|PublishVersionTableListItem) => tableOperation(entity) + } + ] + + useEffect(() => { + setBreadcrumb([ + { + title:{$t('服务')} + }, + { + title:$t('发布') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + + const getMemberList = async ()=>{ + setMemberValueEnum([]) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + setMemberValueEnum(data.members) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + const columns = useMemo(()=>{ + return ((pageType === 'insidePage' || pageStatus === 0 ) ? + PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN + :PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN) + .map(x=>{ + if(x.filters &&(x.dataIndex as string[])?.indexOf('creator') !== -1){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + memberValueEnum?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:$t(x.name)} + }) + x.valueEnum = tmpValueEnum + } + if(x.dataIndex === 'status'){ + x.valueEnum = (pageType === 'insidePage' || pageStatus === 0 ) ? new Map([ + ['apply',{$t(PublishApplyStatusEnum.apply || '-')}], + ['running',{$t(PublishApplyStatusEnum.running || '-')}], + ['none',{$t(PublishApplyStatusEnum.none || '-')}], + ['refuse',{$t(PublishApplyStatusEnum.refuse || '-')}], + ['publishing',{$t(PublishApplyStatusEnum.publishing || '-')}], + ['error',{$t(PublishApplyStatusEnum.error || '-')}], + ]) : new Map([ + ['apply',{$t(PublishStatusEnum.apply || '-')}], + ['accept',{$t(PublishStatusEnum.accept || '-')}], + ['done',{$t(PublishStatusEnum.done || '-')}], + ['stop',{$t(PublishStatusEnum.stop || '-')}], + ['close',{$t(PublishStatusEnum.close || '-')}], + ['refuse',{$t(PublishStatusEnum.refuse || '-')}], + ['publishing',{$t(PublishStatusEnum.publishing || '-')}], + ['error',{$t(PublishStatusEnum.error || '-')}], + ]) + } + return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title} + } + ) + },[pageType, pageStatus, memberValueEnum,state.language]) + + useEffect(() => { + !init && pageListRef.current?.reload() + }, [pageStatus]); + + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [query]); + + useEffect(()=>{ + setPageType(currLocation.split('/')[0] === 'service' ? 'insidePage' : 'global') + },[currLocation]) + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const drawerActions = { + approval: () => drawerRef.current?.save('pass'), + add: () => drawerRef.current?.publish(), + publish: () => drawerRef.current?.publish(true), + online: () => drawerRef.current?.online(), + }; + + + const onSubmit = () => { + const action = drawerActions[drawerType as keyof typeof drawerActions]; + if (action) { + return action()?.then((res) => { + if(drawerType === 'add' && res){ + handleOnline((res as unknown as Record)?.data?.publish) + } + if (res === true && (drawerType === 'online' || drawerType === 'add')) { + handleOnline(drawerData) + }else if(res === true){ + manualReloadTable(); + } + return res; + }); + } else { + return Promise.resolve(true); + } + }; + return ( + <> + getAiServicePublishList(params)} + addNewBtnTitle={pageStatus === 0 ? $t("新建版本"):''} + onAddNewBtnClick={()=>{openDrawer('add')}} + addNewBtnAccess="team.service.release.add" + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row:PublishTableListItem|PublishVersionTableListItem)=>openDrawer('view',row)} + tableClickAccess="team.service.release.view" + tableClass="pr-PAGE_INSIDE_X" + /> + {setDrawerVisible(false)}} + open={drawerVisible} + okBtnTitle={drawerOkTitle} + submitDisabled={drawerType === 'add' ? !isOkToPublish : false} + submitAccess={`team.service.release.${drawerType === 'publish'? 'add' : drawerType}`} + cancelBtnTitle={drawerType === 'online' ? $t('关闭') : undefined} + showOkBtn={drawerType !== 'view'} + onSubmit={onSubmit} + extraBtn={(drawerType === 'approval'||drawerType === 'online') ? + + :undefined} + > + + + + ) +} +export default AiServiceInsidePublicList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishOnline.tsx b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishOnline.tsx new file mode 100644 index 00000000..44b93bb7 --- /dev/null +++ b/frontend/packages/core/src/pages/aiService/publish/AiServiceInsidePublishOnline.tsx @@ -0,0 +1,90 @@ + +import { App, Table, Tooltip } from "antd"; +import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "../../../const/system/const"; +import { useEffect, useMemo, useState } from "react"; +import { useFetch } from "@common/hooks/http"; +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR } from "@common/const/const"; +import { EntityItem } from "@common/const/type"; +import { LoadingOutlined } from "@ant-design/icons"; +import { $t } from "@common/locales"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; + +type AiServiceInsidePublishOnlineProps = { + serviceId:string + teamId:string + id:string +} + +export type AiServiceInsidePublishOnlineItems = { + cluster:EntityItem + status:'done' | 'error' | 'publishing' + error:string +} +export default function AiServiceInsidePublishOnline(props:AiServiceInsidePublishOnlineProps ){ + const {serviceId, teamId, id} = props + const {message} = App.useApp() + const [dataSource, setDataSource] = useState<[]>() + const {fetchData} = useFetch() + const [isStopped, setIsStopped] = useState(false); + const { state } = useGlobalContext() + + const getOnlineStatus = ()=>{ + fetchData>('service/publish/status',{method:'GET',eoParams:{service:serviceId,team:teamId, id}, eoTransformKeys:['publish_status_list']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setDataSource(data.publishStatusList) + if(data.publishStatusList.filter((x:AiServiceInsidePublishOnlineItems)=>x.status === 'publishing').length === 0){ + setIsStopped(true) + } + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).catch((errorInfo)=> message.error(errorInfo)) + } + + useEffect(()=>{ + getOnlineStatus(); + },[]) + + useEffect(() => { + let intervalId: NodeJS.Timeout; + if (!isStopped) { + intervalId = setInterval(() => { + !isStopped && getOnlineStatus(); + }, 5000); + } + + return () => { + clearInterval(intervalId); + }; + }, [isStopped]); + + + const translatedPublishColumns = useMemo(()=>SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{ + if(x.dataIndex === 'status'){ + return {...x,title:$t(x.title), + render:(_:unknown,entity:AiServiceInsidePublishOnlineItems)=>{ + switch(entity.status){ + case 'done': + return {$t('成功')} + case 'error': + return {$t('失败')} {entity.error} + default: + return + } + }} + } + }),[state.language]) + + return ( +
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx new file mode 100644 index 00000000..95b669e5 --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -0,0 +1,258 @@ +import { LoadingOutlined } from "@ant-design/icons"; +import InsidePage from "@common/components/aoplatform/InsidePage"; +import { BasicResponse, STATUS_CODE, RESPONSE_TIPS } from "@common/const/const"; +import { EntityItem } from "@common/const/type"; +import { useFetch } from "@common/hooks/http"; +import { $t } from "@common/locales"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { App, Spin, Card, Tag, Select, Button, Empty } from "antd"; +import { useEffect, useMemo, useRef, useState } from "react"; +import AiSettingModalContent, { AiSettingModalContentHandle } from "./AiSettingModal"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { DefaultOptionType } from "antd/es/select"; +import { checkAccess } from "@common/utils/permission"; + +export type AiSettingListItem = { + name: string + id:string + logo:string + defaultLlm: string + defaultLlmLogo:string + enable:boolean + configured:boolean +} + +export type AiProviderLlmsItems = { + id:string + logo:string + scopes:('chat'|'completions')[] +} + +export type AiProviderDefaultConfig = { + id:string + name:string + logo:string + defaultLlm:string + scopes:string[] +} + +export type AiProviderConfig = { + id:string + name:string + config:string + getApikeyUrl:string +} +const AiSettingList = ()=>{ + const { modal,message } = App.useApp() + const {fetchData} = useFetch() + const [aiSettingList, setAiSettingList] = useState([]) + const [loading, setLoading] = useState(false) + // const [updateLoading, setUpdateLoading] = useState(false) + const [loadingDefaultModel, setLoadingDefaultModel] = useState('') + const modalRef = useRef() + const {setAiConfigFlushed,accessData} = useGlobalContext() + const [llmMap, setLlmMap] = useState>(new Map) + const [currentProvider, setCurrentProvider] = useState() + + const getAiSettingList = ()=>{ + setLoading(true) + return fetchData[]}>>(`ai/providers`,{method:'GET', eoTransformKeys:['default_llm','default_llm_logo']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setAiSettingList(data.providers?.map((x:AiSettingListItem)=>({...x,name:$t(x.name),llmListStatus:'unload', availableLlms:[]}) + )) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).finally(()=>setLoading(false)) + } + + const getLlmList = (provider:AiSettingListItem)=>{ + setLlmMap(prev=>{ + const newMap = new Map(prev); + if(newMap.get(provider.id)){ + newMap.get(provider.id)!.loading = true + }else{ + newMap.set(provider.id, {loading:true,list:[]}) + } + return newMap + }) + + fetchData>(`ai/provider/llms`,{method:'GET',eoParams:{provider:provider.id}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setLlmMap(prev=>{ + const newMap = new Map(prev); + const llmDetail = newMap.get(provider.id) + llmDetail!.list = data.llms?.map((x:AiProviderLlmsItems)=>({ + label:
+
+ {x.id}
, + value:x.id})) + return newMap + }) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).finally(()=>{ + setLlmMap(prev=>{ + const newMap = new Map(prev); + const llmDetail = newMap.get(provider.id) + llmDetail!.loading = false + return newMap + }) + }) + } + + // 第一期暂时隐藏 + // const updateModalList = ()=>{ + // setUpdateLoading(true) + // return fetchData>(`aisetting`,{method:'GET'}).then(response=>{ + // const {code,msg} = response + // if(code === STATUS_CODE.SUCCESS){ + // getAiSettingList() + // }else{ + // message.error(msg || $t(RESPONSE_TIPS.error)) + // } + // }).finally(()=>setUpdateLoading(false)) + // } + + const openModal = async (entity:AiSettingListItem)=>{ + message.loading($t(RESPONSE_TIPS.loading)) + const {code,data,msg} = await fetchData>('ai/provider/config',{method:'GET',eoParams:{provider:entity!.id}, eoTransformKeys:['get_apikey_url']}) + message.destroy() + if(code !== STATUS_CODE.SUCCESS){ + message.error(msg || $t(RESPONSE_TIPS.error)) + return + } + modal.confirm({ + title:$t('模型配置'), + content:, + onOk:()=>{ + return modalRef.current?.save().then((res)=>{if(res === true) + setAiConfigFlushed(true) + getAiSettingList()}) + }, + width:600, + okText:$t('确认'), + footer:(_, { OkBtn, CancelBtn }) =>{ + return ( + + ); + }, + cancelText:$t('取消'), + closable:true, + icon:<>, + }) + } + + const changeDefaultModel = (value: string, entity:AiSettingListItem) => { + setLoadingDefaultModel(entity.id) + return fetchData>(`ai/provider/default-llm`,{method:'PUT', eoBody:{llm:value}, eoParams:{provider:entity.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + getAiSettingList() + message.success(msg || $t(RESPONSE_TIPS.success)) + }else{ + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }).finally(()=>setLoadingDefaultModel('')) + }; + + const modelOptions = useMemo(()=>{ + + return currentProvider ? llmMap?.get(currentProvider)?.list : [] + },[currentProvider,llmMap]) + + useEffect(() => { + getAiSettingList() + }, []); + + + return (<> + + // + // + // } + > + } spinning={loading}> + {aiSettingList && aiSettingList.length > 0 ?
+ {aiSettingList.map((provider:AiSettingListItem)=>( + +
+ + {provider.configured ? $t('已配置') : $t('未配置')} + +
} + className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " + classNames={{header:'border-b-[0px] p-[20px] px-[24px]', body:"pt-0 flex-1"}} + > +
+
+ + + diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideRouterList.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideRouterList.tsx index 88fc6eea..93ff1824 100644 --- a/frontend/packages/core/src/pages/system/api/SystemInsideRouterList.tsx +++ b/frontend/packages/core/src/pages/system/api/SystemInsideRouterList.tsx @@ -11,7 +11,7 @@ import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; import SystemInsideRouterCreate from "./SystemInsideRouterCreate.tsx"; import {useSystemContext} from "../../../contexts/SystemContext.tsx"; import { SYSTEM_API_TABLE_COLUMNS } from "../../../const/system/const.tsx"; -import {SystemApiTableListItem, SystemInsideRouterCreateHandle, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts"; +import {SystemApiTableListItem, SystemInsideRouterCreateHandle } from "../../../const/system/type.ts"; import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; import { checkAccess } from "@common/utils/permission.ts"; diff --git a/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx b/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx index c8ef2a7b..1c8d095d 100644 --- a/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx +++ b/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx @@ -34,7 +34,7 @@ const SystemInsidePublicList:FC = ()=>{ const drawerRef = useRef(null) const [extraModalBtnLoading,setExtraModalBtnLoading] = useState(false) const [pageStatus,setPageStatus] = useState<0|1>(0 as 0|1) - const [pageType, setPageType] = useState<'insideSystem'|'global'>('insideSystem') + const [pageType, setPageType] = useState<'insidePage'|'global'>('insidePage') const query =new URLSearchParams(useLocation().search) const currLocation = useLocation().pathname const [memberValueEnum, setMemberValueEnum] = useState([]) @@ -50,7 +50,7 @@ const SystemInsidePublicList:FC = ()=>{ current?: number | undefined; keyword?: string | undefined; })=>{ - if(!(pageType !== 'insideSystem' && pageStatus !== 0 ) && !tableHttpReload){ + if(!(pageType !== 'insidePage' && pageStatus !== 0 ) && !tableHttpReload){ setTableHttpReload(true) return Promise.resolve({ data: tableListDataSource, @@ -59,7 +59,7 @@ const SystemInsidePublicList:FC = ()=>{ } return fetchData>( pageStatus === 0 ? 'service/releases':'service/publishs', - {method:'GET',eoParams:(pageType !== 'insideSystem' && pageStatus !== 0 ) ? {service:serviceId,team:teamId,page:params?.current,page_size:params?.pageSize}:{service:serviceId,team:teamId},eoTransformKeys:['pageSize','apply_time','approve_time','release_status','is_valid','fail_msg','create_time','can_rollback','flow_id','can_delete']}).then(response=>{ + {method:'GET',eoParams:(pageType !== 'insidePage' && pageStatus !== 0 ) ? {service:serviceId,team:teamId,page:params?.current,page_size:params?.pageSize}:{service:serviceId,team:teamId},eoTransformKeys:['pageSize','apply_time','approve_time','release_status','is_valid','fail_msg','create_time','can_rollback','flow_id','can_delete']}).then(response=>{ const {code,data,msg} = response if(code === STATUS_CODE.SUCCESS){ const finalRes = pageStatus === 0 ? data.releases.map((x:PublishVersionTableListItem)=>{if(!x.status|| x.status === 'close'){x.status = 'none'} return x}):data.publishs @@ -270,7 +270,7 @@ const SystemInsidePublicList:FC = ()=>{ const tableOperation = (entity:PublishTableListItem | PublishVersionTableListItem)=>{ const viewBtn = {openDrawer('view',entity)}} btnTitle="查看详情"/> let btnArr:React.ReactNode[] = [] - if(pageType !== 'insideSystem' && pageStatus !== 0){ + if(pageType !== 'insidePage' && pageStatus !== 0){ btnArr = [ viewBtn ] @@ -374,7 +374,7 @@ const SystemInsidePublicList:FC = ()=>{ } const columns = useMemo(()=>{ - return ((pageType === 'insideSystem' || pageStatus === 0 ) ? + return ((pageType === 'insidePage' || pageStatus === 0 ) ? PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN :PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN) .map(x=>{ @@ -386,7 +386,7 @@ const SystemInsidePublicList:FC = ()=>{ x.valueEnum = tmpValueEnum } if(x.dataIndex === 'status'){ - x.valueEnum = (pageType === 'insideSystem' || pageStatus === 0 ) ? new Map([ + x.valueEnum = (pageType === 'insidePage' || pageStatus === 0 ) ? new Map([ ['apply',{$t(PublishApplyStatusEnum.apply || '-')}], ['running',{$t(PublishApplyStatusEnum.running || '-')}], ['none',{$t(PublishApplyStatusEnum.none || '-')}], @@ -419,7 +419,7 @@ const SystemInsidePublicList:FC = ()=>{ }, [query]); useEffect(()=>{ - setPageType(currLocation.split('/')[0] === 'service' ? 'insideSystem' : 'global') + setPageType(currLocation.split('/')[0] === 'service' ? 'insidePage' : 'global') },[currLocation]) const manualReloadTable = () => { @@ -517,7 +517,7 @@ const SystemInsidePublicList:FC = ()=>{ :undefined} > - diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx index 06d78f14..3e28d41a 100644 --- a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx @@ -131,10 +131,11 @@ const ServiceHubDetail = ()=>{ className={ `rounded-[12px] border-none rounded-[12px] ${ serviceBasicInfo?.logo ? 'bg-[linear-gradient(135deg,white,#f0f0f0)]' : 'bg-theme'}`} src={ serviceBasicInfo?.logo ? Logo : undefined} - icon={serviceBasicInfo?.logo ? '' :}> + icon={serviceBasicInfo?.logo ? '' :}>
-

{serviceName}

+

{serviceName} +

{serviceDesc || '-'}
diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx index 29535d1b..fc3c449b 100644 --- a/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx @@ -179,15 +179,15 @@ const CardTitle = (service:ServiceHubTableListItem)=>{
: undefined}> {service.logo ? '' : service.name.substring(0,1)}
-

{service.name}

+

{service.name}

{service.catalogue?.name || '-'} - {service.apiNum ?? '-'} + {service.apiNum ?? '-'} - {service.subscriberNum ?? '-'} + {service.subscriberNum ?? '-'}