mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-12 18:11:34 +08:00
feat: ai-service ui
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',<Icon icon="ic:baseline-space-dashboard" width="18" height="18"/>, [
|
||||
getNavItem($t('我的'), 'my','/guide',null,[
|
||||
getNavItem(<a>{$t('首页')}</a>, 'guide','/guide',<Icon icon="ic:baseline-home" width="18" height="18"/>,undefined,undefined,''),
|
||||
getNavItem(<a>{$t('应用')}</a>, 'tenantManagement','/tenantManagement',<Icon icon="ic:baseline-apps" width="18" height="18"/>,undefined,undefined,''),
|
||||
getNavItem(<a>{$t('服务')}</a>, 'service','/service',<Icon icon="ic:baseline-blinds-closed" width="18" height="18"/>,undefined,undefined,''),
|
||||
getNavItem(<a>{$t('团队')}</a>, 'team','/team',<Icon icon="ic:baseline-people-alt" width="18" height="18"/>,undefined,undefined,''),
|
||||
],undefined,''),
|
||||
getNavItem($t('工作空间'), 'workspace','/guide/page',<Icon icon="ic:baseline-space-dashboard" width="18" height="18"/>, [
|
||||
getNavItem(<a>{$t('首页')}</a>, 'guide','/guide/page',<Icon icon="ic:baseline-home" width="18" height="18"/>,undefined,undefined,'all'),
|
||||
getNavItem(<a>{$t('应用')}</a>, 'tenantManagement','/tenantManagement',<Icon icon="ic:baseline-apps" width="18" height="18"/>,undefined,undefined,'all'),
|
||||
getNavItem(<a>{$t('团队')}</a>, 'team','/team',<Icon icon="ic:baseline-people-alt" width="18" height="18"/>,undefined,undefined,'all'),
|
||||
getNavItem($t('服务'), 'my','/service',null,[
|
||||
getNavItem(<a>{$t('REST 服务')}</a>, 'service','/service',<Icon icon="ic:baseline-blinds-closed" width="18" height="18"/>,undefined,undefined,''),
|
||||
getNavItem(<a>{$t('AI 服务')}</a>, 'aiservice','/aiservice',<Icon icon="eos-icons:ai" width="18" height="18"/>,undefined,undefined,''),
|
||||
],undefined,''),
|
||||
]),
|
||||
getNavItem(<a>{$t('API 市场')}</a>, 'serviceHub','/serviceHub',<Icon icon="ic:baseline-hub" width="18" height="18"/>,undefined,undefined,'system.workspace.api_market.view'),
|
||||
|
||||
@@ -72,6 +73,7 @@ const themeToken = {
|
||||
|
||||
getNavItem($t('运维与集成'), 'maintenanceCenter','/cluster', null, [
|
||||
getNavItem(<a>{$t('集群')}</a>, 'cluster','/cluster',<Icon icon="ic:baseline-device-hub" width="18" height="18"/>,undefined,undefined,'system.devops.cluster.view'),
|
||||
getNavItem(<a>{$t('AI 配置')}</a>, 'aisetting','/aisetting',<Icon icon="hugeicons:ai-network" width="18" height="18"/>,undefined,undefined,'system.devops.cluster.view'),
|
||||
getNavItem(<a>{$t('数据源')}</a>, 'datasourcing','/datasourcing',<Icon icon="ic:baseline-monitor-heart" width="18" height="18"/>,undefined,undefined,'system.devops.data_source.view'),
|
||||
getNavItem(<a>{$t('证书')}</a>, 'cert','/cert',<Icon icon="ic:baseline-security" width="18" height="18"/>,undefined,undefined,'system.devops.ssl_certificate.view'),
|
||||
getNavItem(<a>{$t('日志')}</a>, 'logsettings','/logsettings',<Icon icon="ic:baseline-sticky-note-2" width="18" height="18"/>,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,则保留
|
||||
|
||||
@@ -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<T> {
|
||||
configFields: PageProColumns<T>[];
|
||||
value?: T[]; // 外部传入的值
|
||||
className?: string;
|
||||
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数
|
||||
// tableProps?: TableProps<T>;
|
||||
disabled?:boolean
|
||||
getFromRef?:(form:MutableRefObject<EditableFormInstance<T> | undefined>)=>void
|
||||
}
|
||||
|
||||
const EditableTableNotAutoGen = <T extends { _id: string }>({
|
||||
configFields,
|
||||
value, // value 现在是外部传入的配置项数组
|
||||
onChange, // onChange 现在是当配置项数组变化时的回调函数
|
||||
// tableProps,
|
||||
disabled,
|
||||
className,
|
||||
getFromRef
|
||||
}: EditableTableProps<T>) => {
|
||||
const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]);
|
||||
const {state} = useGlobalContext()
|
||||
const form =useRef<EditableFormInstance<T>>();
|
||||
const [tableForm] = Form.useForm();
|
||||
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() =>
|
||||
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<string, string>)=>{
|
||||
if(r.message){
|
||||
r.message = $t(r.message)
|
||||
}
|
||||
return r
|
||||
})],
|
||||
}})),[state.language,configFields])
|
||||
|
||||
const debouncedOnChange = useMemo(() => debounce((value) => {
|
||||
onChange?.(value);
|
||||
}, 500), [onChange]);
|
||||
|
||||
return (
|
||||
<EditableProTable<T>
|
||||
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 [
|
||||
<TableBtnWithPermission key="delete" btnType="delete" btnTitle="删除"
|
||||
onClick={() => {
|
||||
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;
|
||||
@@ -24,9 +24,10 @@ class InsidePageProps {
|
||||
headerClassName?:string=''
|
||||
/** 整个页面滚动 */
|
||||
scrollPage?:boolean = true
|
||||
customBtn?:ReactNode
|
||||
}
|
||||
|
||||
const InsidePage:FC<InsidePageProps> = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl,showBorder=true,className='',contentClassName='',headerClassName='',scrollPage=true})=>{
|
||||
const InsidePage:FC<InsidePageProps> = ({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<InsidePageProps> = ({showBanner=true,pageTitle,tagList,showB
|
||||
{showBtn && <WithPermission access={btnAccess}><Button type="primary" onClick={()=> {
|
||||
onBtnClick&&onBtnClick()
|
||||
}}>{btnTitle}</Button></WithPermission>}
|
||||
{customBtn}
|
||||
</div>
|
||||
<p >
|
||||
{description}
|
||||
|
||||
+20
-16
@@ -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<PublishApprovalModalHandle,PublishApprovalModalProps>((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 = forwardRef<PublishApprovalModalHandle
|
||||
})),[state.language])
|
||||
|
||||
|
||||
const translatedRouteColumns = useMemo(()=>ApprovalRouteColumns.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 = forwardRef<PublishApprovalModalHandle
|
||||
}:{}
|
||||
),
|
||||
title: typeof x.title === 'string' ? $t(x.title) : x.title,
|
||||
})),[state.language])
|
||||
})),[state.language, serviceType])
|
||||
|
||||
const translatedPublishColumns = useMemo(()=>SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{
|
||||
if(x.dataIndex === 'status'){
|
||||
@@ -142,7 +142,7 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
|
||||
|
||||
return (
|
||||
<>
|
||||
{!insideSystem && <>
|
||||
{!insidePage && <>
|
||||
<Row className="my-mbase">
|
||||
<Col className="text-left" span={4}><span >{$t('申请系统')}:</span></Col>
|
||||
<Col span={18}>{(data as PublishApprovalInfoType).project || '-'}</Col>
|
||||
@@ -177,7 +177,7 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
|
||||
>
|
||||
|
||||
{
|
||||
insideSystem &&
|
||||
insidePage &&
|
||||
<>
|
||||
<Form.Item
|
||||
label={$t("版本号")}
|
||||
@@ -205,16 +205,20 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
|
||||
dataSource={data.diffs?.routers || []}
|
||||
pagination={false}
|
||||
/></Row>
|
||||
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('上游列表')}:</span></Row>
|
||||
<Row className="mb-mbase ">
|
||||
<Table
|
||||
bordered={true}
|
||||
columns={translatedUpstreamColumns}
|
||||
size="small"
|
||||
rowKey="id"
|
||||
dataSource={data.diffs?.upstreams || []}
|
||||
pagination={false}
|
||||
/></Row>
|
||||
{
|
||||
serviceType === 'rest' && <>
|
||||
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('上游列表')}:</span></Row>
|
||||
<Row className="mb-mbase ">
|
||||
<Table
|
||||
bordered={true}
|
||||
columns={translatedUpstreamColumns}
|
||||
size="small"
|
||||
rowKey="id"
|
||||
dataSource={data.diffs?.upstreams || []}
|
||||
pagination={false}
|
||||
/></Row>
|
||||
</>
|
||||
}
|
||||
<Form.Item
|
||||
label={$t("备注")}
|
||||
name="remark"
|
||||
|
||||
@@ -8,6 +8,8 @@ export const TranslateWord = ()=>{
|
||||
{$t('替换文件')}
|
||||
{$t('是否放行')}
|
||||
{$t('监控')}
|
||||
{$t('必填')}
|
||||
{$t('字符非法,仅支持英文')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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<PromptEditorProps> = ({
|
||||
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 (
|
||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||
<div className='relative min-h-5'>
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
|
||||
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<ComponentPickerBlock
|
||||
triggerString='/'
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
/>
|
||||
<ComponentPickerBlock
|
||||
triggerString='{'
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
/>
|
||||
{
|
||||
contextBlock?.show && (
|
||||
<>
|
||||
<ContextBlock {...contextBlock} />
|
||||
<ContextBlockReplacementBlock {...contextBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
queryBlock?.show && (
|
||||
<>
|
||||
<QueryBlock {...queryBlock} />
|
||||
<QueryBlockReplacementBlock />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
historyBlock?.show && (
|
||||
<>
|
||||
<HistoryBlock {...historyBlock} />
|
||||
<HistoryBlockReplacementBlock {...historyBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(variableBlock?.show || externalToolBlock?.show) && (
|
||||
<>
|
||||
<VariableBlock />
|
||||
<VariableValueBlock />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<>
|
||||
<WorkflowVariableBlock {...workflowVariableBlock} />
|
||||
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<OnChangePlugin onChange={handleEditorChange} />
|
||||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||
<UpdateBlock instanceId={instanceId} />
|
||||
<HistoryPlugin />
|
||||
{/* <TreeView /> */}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromptEditor
|
||||
+82
@@ -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<string[]>([])
|
||||
const handleChange = (newTemplates: string, keys: string[]) => {
|
||||
onChange?.(newTemplates)
|
||||
}
|
||||
|
||||
return ( <PromptEditorHeightResizeWrap
|
||||
className='px-4 pt-2 min-h-[94px] bg-white rounded-t-xl text-sm text-gray-700'
|
||||
height={editorHeight}
|
||||
minHeight={minHeight}
|
||||
onHeightChange={setEditorHeight}
|
||||
hideResize={false}
|
||||
footer={(
|
||||
<div className='pl-4 pb-2 flex bg-white rounded-b-xl'>
|
||||
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value?.length || 0}</div>
|
||||
</div>
|
||||
)}
|
||||
><>
|
||||
{value !== undefined && <PromptEditor
|
||||
className='min-h-[68px]'
|
||||
compact
|
||||
value={value}
|
||||
contextBlock={{
|
||||
show: false,
|
||||
selectable: true,
|
||||
// datasets: dataSets.map(item => ({
|
||||
// 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}
|
||||
/>
|
||||
}</>
|
||||
</PromptEditorHeightResizeWrap>)
|
||||
}
|
||||
|
||||
export default PromptEditorResizable
|
||||
@@ -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 []
|
||||
}
|
||||
@@ -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<undefined>) => [RefObject<HTMLDivElement>, boolean]
|
||||
export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => {
|
||||
const ref = useRef<HTMLDivElement>(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<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
|
||||
export const useTrigger: UseTriggerHandler = () => {
|
||||
const triggerRef = useRef<HTMLDivElement>(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<T extends TextNode>(
|
||||
getMatch: (text: string) => null | EntityMatch,
|
||||
targetNode: Klass<T>,
|
||||
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],
|
||||
)
|
||||
}
|
||||
+294
@@ -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 <PromptMenuItem
|
||||
title={$t('上下文')}
|
||||
icon={<></>}
|
||||
// icon={<File05 className='w-4 h-4 text-[#6938EF]' />}
|
||||
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 (
|
||||
<PromptMenuItem
|
||||
title={$t('查询内容')}
|
||||
icon={<></>}
|
||||
// icon={<UserEdit02 className='w-4 h-4 text-[#FD853A]' />}
|
||||
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 (
|
||||
<PromptMenuItem
|
||||
title={$t('会话历史')}
|
||||
icon={<></>}
|
||||
// icon={<MessageClockCircle className='w-4 h-4 text-[#DD2590]' />}
|
||||
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 (
|
||||
<VariableMenuItem
|
||||
title={item.value}
|
||||
icon={<></>}
|
||||
// icon={<BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />}
|
||||
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 (
|
||||
<VariableMenuItem
|
||||
title={$t('添加新变量')}
|
||||
icon={<></>}
|
||||
// icon={<BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />}
|
||||
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 (
|
||||
<VariableMenuItem
|
||||
title={item.name}
|
||||
icon={<></>}
|
||||
// icon={
|
||||
// <AppIcon
|
||||
// className='!w-[14px] !h-[14px]'
|
||||
// icon={item.icon}
|
||||
// background={item.icon_background}
|
||||
// />
|
||||
// }
|
||||
extraElement={<div className='text-xs text-gray-400'>{item.variableName}</div>}
|
||||
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 (
|
||||
<VariableMenuItem
|
||||
title={$t('添加工具')}
|
||||
// icon={<Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />}
|
||||
// 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])
|
||||
}
|
||||
+212
@@ -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<string | null>(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<MenuRenderFn<PickerBlockMenuOption>>((
|
||||
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
|
||||
<div className='w-0 h-0'>
|
||||
<div
|
||||
className='p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto overflow-x-hidden'
|
||||
style={{
|
||||
...floatingStyles,
|
||||
visibility: isPositioned ? 'visible' : 'hidden',
|
||||
maxHeight: 'calc(1 / 3 * 100vh)',
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
// Divider
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<>
|
||||
{
|
||||
(!!options.length) && (
|
||||
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
{/* <VarReferenceVars
|
||||
hideSearch
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={allFlattenOptions}
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
// The `translate` class is used to workaround the issue that the `typeahead-menu` menu is not positioned as expected.
|
||||
// See also https://github.com/facebook/lexical/blob/772520509308e8ba7e4a82b6cd1996a78b3298d0/packages/lexical-react/src/shared/LexicalMenu.ts#L498
|
||||
//
|
||||
// We no need the position function of the `LexicalTypeaheadMenuPlugin`,
|
||||
// so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui.
|
||||
anchorClassName='z-[999999] translate-y-[calc(-100%-3px)]'
|
||||
menuRenderFn={renderMenu}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ComponentPicker)
|
||||
+31
@@ -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) => <Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment>
|
||||
}
|
||||
+45
@@ -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 (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
|
||||
${isSelected && !disabled && '!bg-gray-50'}
|
||||
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={setRefElement}
|
||||
onMouseEnter={() => {
|
||||
if (disabled)
|
||||
return
|
||||
onMouseEnter()
|
||||
}}
|
||||
onClick={() => {
|
||||
if (disabled)
|
||||
return
|
||||
onClick()
|
||||
}}>
|
||||
{icon}
|
||||
<div className='ml-1 text-[13px] text-gray-900'>{title}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
PromptMenuItem.displayName = 'PromptMenuItem'
|
||||
+60
@@ -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 (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-6 rounded-md hover:bg-[#EBEEF2] cursor-pointer
|
||||
${isSelected && 'bg-[#EBEEF2]'}
|
||||
`}
|
||||
tabIndex={-1}
|
||||
ref={setRefElement}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onClick={onClick}>
|
||||
<div className='mr-2'>
|
||||
{icon}
|
||||
</div>
|
||||
<div className='grow text-[13px] text-gray-900 truncate' title={title}>
|
||||
{before}
|
||||
<span className='text-[#2970FF]'>{middle}</span>
|
||||
{after}
|
||||
</div>
|
||||
{extraElement}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
VariableMenuItem.displayName = 'VariableMenuItem'
|
||||
+56
@@ -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<ContextBlockComponentProps> = ({
|
||||
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<Dataset[]>(datasets)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v?.type === UPDATE_DATASETS_EVENT_EMITTER)
|
||||
setLocalDatasets(v.payload)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent bg-[#F4F3FF] text-[#6938EF] rounded-[5px] hover:bg-[#EBE9FE]
|
||||
${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'}
|
||||
${isSelected && '!border-[#9B8AFB]'}
|
||||
`} ref={ref}>
|
||||
{/* <File05 className='mr-1 w-[14px] h-[14px]' /> */}
|
||||
<div className='mr-1 text-xs font-medium'>{$t('上下文')}</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContextBlockComponent
|
||||
+63
@@ -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)
|
||||
+74
@@ -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'
|
||||
+100
@@ -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<JSX.Element> {
|
||||
__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 (
|
||||
<ContextBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
datasets={this.getDatasets()}
|
||||
onAddContext={this.getOnAddContext()}
|
||||
canNotAddContext={this.getCanNotAddContext()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+52
@@ -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)
|
||||
}
|
||||
+53
@@ -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<HistoryBlockComponentProps> = ({
|
||||
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>(roleName)
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v?.type === UPDATE_HISTORY_EVENT_EMITTER)
|
||||
setLocalRoleName(v.payload)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent text-[#DD2590] rounded-[5px] hover:bg-[#FCE7F6]
|
||||
${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'}
|
||||
${isSelected && '!border-[#F670C7]'}
|
||||
`} ref={ref}>
|
||||
{/* <MessageClockCircle className='mr-1 w-[14px] h-[14px]' /> */}
|
||||
<div className='mr-1 text-xs font-medium'>{$t('会话历史')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoryBlockComponent
|
||||
+61
@@ -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
|
||||
+79
@@ -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'
|
||||
+90
@@ -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<JSX.Element> {
|
||||
__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 (
|
||||
<HistoryBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
roleName={this.getRoleName()}
|
||||
onEditRole={this.getOnEditRole()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+67
@@ -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<OnBlurBlockProps> = ({
|
||||
onBlur,
|
||||
onFocus,
|
||||
}) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const ref = useRef<any>(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
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { $t } from '@common/locales'
|
||||
import { memo } from 'react'
|
||||
|
||||
const Placeholder = ({
|
||||
compact,
|
||||
value
|
||||
}: {
|
||||
compact?: boolean
|
||||
value?: string
|
||||
className?: string
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-0 left-0 h-full w-full text-sm text-[#BBB] select-none pointer-events-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'}`}
|
||||
>
|
||||
{value || $t('AI 模型调用默认仅使用 Query 变量,可输入 “{” 增加新变量。')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Placeholder)
|
||||
+33
@@ -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<QueryBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
}) => {
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
inline-flex items-center pl-1 pr-0.5 h-6 bg-[#FFF6ED] border border-transparent rounded-[5px] hover:bg-[#FFEAD5]
|
||||
${isSelected && '!border-[#FD853A]'}
|
||||
`}
|
||||
ref={ref}
|
||||
>
|
||||
{/* <UserEdit02 className='mr-1 w-[14px] h-[14px] text-[#FD853A]' /> */}
|
||||
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'{{'}</div>
|
||||
<div className='text-xs font-medium text-[#EC4A0A]'>{$t('查询内容')}</div>
|
||||
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'}}'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryBlockComponent
|
||||
+68
@@ -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'
|
||||
+59
@@ -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<JSX.Element> {
|
||||
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 <QueryBlockComponent nodeKey={this.getKey()} />
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+60
@@ -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)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { TreeView } from '@lexical/react/LexicalTreeView'
|
||||
|
||||
const TreeViewPlugin = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
return (
|
||||
<TreeView
|
||||
viewClassName="tree-view-output"
|
||||
treeTypeButtonClassName="debug-treetype-button"
|
||||
timeTravelPanelClassName="debug-timetravel-panel"
|
||||
timeTravelButtonClassName="debug-timetravel-button"
|
||||
timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
|
||||
timeTravelPanelButtonClassName="debug-timetravel-panel-button"
|
||||
editor={editor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeViewPlugin
|
||||
+42
@@ -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
|
||||
+45
@@ -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
|
||||
+52
@@ -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<VariableValueBlockNode>(
|
||||
getVariableValueMatch,
|
||||
VariableValueBlockNode,
|
||||
createVariableValueBlockNode,
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default VariableValueBlock
|
||||
+65
@@ -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
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export function getHashtagRegexString(): string {
|
||||
const hashtag = '\\{\\{[a-zA-Z_][a-zA-Z0-9_]{0,29}\\}\\}'
|
||||
|
||||
return hashtag
|
||||
}
|
||||
+114
@@ -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>(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 = (
|
||||
<div
|
||||
className={`mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none ${isSelected ? 'border-[#84ADFF] bg-[#F5F8FF]' : 'border-black/5 bg-white'}` }
|
||||
ref={ref}
|
||||
>
|
||||
{/* {!isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
node?.type && (
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-500'
|
||||
type={node?.type}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='shrink-0 mx-0.5 max-w-[60px] text-xs font-medium text-gray-500 truncate' title={node?.title} style={{
|
||||
}}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5 text-gray-300'></Line3>
|
||||
</div>
|
||||
)} */}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{/* {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
|
||||
{
|
||||
!node && !isEnv && !isChatVar && (
|
||||
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
|
||||
)
|
||||
} */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
return Item
|
||||
}
|
||||
|
||||
export default memo(WorkflowVariableBlockComponent)
|
||||
+80
@@ -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'
|
||||
+92
@@ -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<JSX.Element> {
|
||||
__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 (
|
||||
<WorkflowVariableBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
variables={this.__variables}
|
||||
workflowNodesMap={this.__workflowNodesMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+66
@@ -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)
|
||||
+94
@@ -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<Props> = ({
|
||||
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<HTMLElement>) => {
|
||||
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 (
|
||||
<div
|
||||
className='relative rounded ant-input-outlined'
|
||||
>
|
||||
<div className={`${className} overflow-y-auto`}
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{/* resize handler */}
|
||||
{footer}
|
||||
{!hideResize && (
|
||||
<div
|
||||
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize'
|
||||
onMouseDown={handleStartResize}>
|
||||
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PromptEditorHeightResizeWrap)
|
||||
@@ -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<string, unknown>
|
||||
onInsert?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export type MenuTextMatch = {
|
||||
leadOffset: number
|
||||
matchingString: string
|
||||
replaceableString: string
|
||||
}
|
||||
@@ -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<T extends TextNode>(
|
||||
editor: LexicalEditor,
|
||||
getMatch: (text: string) => null | EntityMatch,
|
||||
targetNode: Klass<T>,
|
||||
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<string, boolean> = {}
|
||||
// remove duplicate keys
|
||||
const res: string[] = []
|
||||
keys.forEach((key) => {
|
||||
if (keyObj[key])
|
||||
return
|
||||
|
||||
keyObj[key] = true
|
||||
res.push(key)
|
||||
})
|
||||
return res
|
||||
}
|
||||
@@ -219,6 +219,11 @@ export const ApprovalRouteColumns = [
|
||||
ellipsis:true,
|
||||
render:(value)=>value?.join(', ')
|
||||
},
|
||||
{
|
||||
title:('名称'),
|
||||
dataIndex:'name',
|
||||
ellipsis:true,
|
||||
},
|
||||
{
|
||||
title:('路径'),
|
||||
dataIndex:'path',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const routerKeyMap = new Map<string, string[]|string>([
|
||||
['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<string, string[]|string>([
|
||||
input:('请输入'),
|
||||
select:('请选择'),
|
||||
startWithAlphabet:('英文数字下划线任意一种,首字母必须为英文'),
|
||||
specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合')
|
||||
specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合'),
|
||||
onlyAlphabet:('字符非法,仅支持英文'),
|
||||
}
|
||||
|
||||
export const FORM_ERROR_TIPS = {
|
||||
|
||||
@@ -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"] }]
|
||||
|
||||
@@ -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<keyof typeof PERMISSION_DEFINITION[0]>)=>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<Map<string,string[]>>(new Map())
|
||||
const [pluginAccessDictionary, setPluginAccessDictionary] = useState<{[k:string]:string}>({})
|
||||
const [teamDataFlushed, setTeamDataFlushed] = useState<boolean>(false)
|
||||
const [accessInit, setAccessInit] = useState<boolean>(false)
|
||||
const [aiConfigFlushed, setAiConfigFlushed] = useState<boolean>(false)
|
||||
let getGlobalAccessPromise: Promise<BasicResponse<{ access:string[] }>> | 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 (
|
||||
<GlobalContext.Provider value={
|
||||
{ state, dispatch,accessData,pluginAccessDictionary,
|
||||
getGlobalAccessData,getPluginAccessDictionary,
|
||||
getGlobalAccessData,
|
||||
getPluginAccessDictionary,
|
||||
getTeamAccessData,teamDataFlushed,
|
||||
cleanTeamAccessData,
|
||||
resetAccess ,checkPermission,accessInit}}>
|
||||
resetAccess ,checkPermission,accessInit,
|
||||
aiConfigFlushed, setAiConfigFlushed}}>
|
||||
{children}
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
|
||||
@@ -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<string> | null }>({
|
||||
eventEmitter: null,
|
||||
})
|
||||
|
||||
export const useEventEmitterContextContext = () => useContext(EventEmitterContext)
|
||||
|
||||
type EventEmitterContextProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
export const EventEmitterContextProvider = ({
|
||||
children,
|
||||
}: EventEmitterContextProviderProps) => {
|
||||
const eventEmitter = useEventEmitter<string>()
|
||||
|
||||
return (
|
||||
<EventEmitterContext.Provider value={{ eventEmitter }}>
|
||||
{children}
|
||||
</EventEmitterContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventEmitterContext
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,7 +1,2 @@
|
||||
{
|
||||
"K7c97c5df": "移出当前部门",
|
||||
"K1362a512": "禁用成员",
|
||||
"K6e1289b1": "启用成员",
|
||||
"K1f4b5385": "删除成员",
|
||||
"Kf85b83a0": "输入 URL 查找路由"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"K7c97c5df": "移出当前部门",
|
||||
"K1362a512": "禁用成员",
|
||||
"K6e1289b1": "启用成员",
|
||||
"K1f4b5385": "删除成员",
|
||||
"Kf85b83a0": "输入 URL 查找路由"
|
||||
"K28b68036": "字符非法,仅支持英文"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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:<AiServiceOutlet />,
|
||||
key: uuidv4(),
|
||||
provider: AiServiceProvider,
|
||||
children:[
|
||||
{
|
||||
path:'',
|
||||
key:uuidv4(),
|
||||
component:<Navigate to="list" />
|
||||
},
|
||||
{
|
||||
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:<Outlet/>,
|
||||
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(),
|
||||
|
||||
@@ -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<AiServiceTableListItem>[] = [
|
||||
{
|
||||
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<AiServiceRouterTableListItem>[] = [
|
||||
{
|
||||
title:('URL'),
|
||||
dataIndex: 'requestPath',
|
||||
ellipsis:true
|
||||
},
|
||||
{
|
||||
title:('名称'),
|
||||
dataIndex: 'name',
|
||||
ellipsis:true,
|
||||
},
|
||||
{
|
||||
title:('模型'),
|
||||
dataIndex: ['model','logo'],
|
||||
ellipsis:true,
|
||||
render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) =><div className="flex items-center gap-[2px]" > <div className="flex items-center" dangerouslySetInnerHTML={{ __html: entity.model.logo }}></div><span>{entity.model.id}</span></div>
|
||||
},
|
||||
{
|
||||
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<VariableItems & {_id:string}>[] = [
|
||||
{
|
||||
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: ('发布申请记录'),
|
||||
}
|
||||
];
|
||||
@@ -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<string>;
|
||||
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<boolean|string>
|
||||
}
|
||||
|
||||
export type AiServiceMemberTableListItem = {
|
||||
user: EntityItem;
|
||||
email:string;
|
||||
roles:Array<EntityItem>;
|
||||
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<boolean|string>;
|
||||
save:()=>Promise<boolean|string>;
|
||||
}
|
||||
|
||||
|
||||
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<boolean|string>|undefined
|
||||
}
|
||||
|
||||
export type AiServiceInsideApiDocumentProps = {
|
||||
serviceId:string
|
||||
teamId:string
|
||||
apiId:string
|
||||
}
|
||||
|
||||
|
||||
export type AiServiceInsideApiProxyHandle = {
|
||||
validate:()=>Promise<void>
|
||||
}
|
||||
|
||||
|
||||
export interface MyServiceInsideConfigHandle {
|
||||
save:()=>Promise<boolean|string>
|
||||
}
|
||||
|
||||
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<boolean|string>
|
||||
}
|
||||
|
||||
export type SubSubscribeApprovalModalFieldType = {
|
||||
reason?:string;
|
||||
opinion?:string;
|
||||
};
|
||||
|
||||
export type AiServiceInsideUpstreamConfigProps = {
|
||||
upstreamNameForm:FormInstance
|
||||
setLoading:(loading:boolean) => void
|
||||
}
|
||||
|
||||
export type AiServiceInsideUpstreamConfigHandle = {
|
||||
save:()=>Promise<boolean|string>|undefined
|
||||
}
|
||||
|
||||
export type AiServiceInsideUpstreamContentHandle = {
|
||||
save:()=>Promise<boolean|string>|undefined
|
||||
}
|
||||
|
||||
|
||||
export type AiServiceConfigHandle = {
|
||||
save:()=>Promise<string|boolean>|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
|
||||
}
|
||||
@@ -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<SystemTableListItem>[] = [
|
||||
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<SystemSubscriberTab
|
||||
];
|
||||
|
||||
|
||||
export const memberModalColumn:ColumnsType<MemberItem> = [
|
||||
{title:('成员'),
|
||||
render:(_,entity)=>{
|
||||
return <>
|
||||
<div>
|
||||
<p>
|
||||
<span>{entity.name}</span>
|
||||
{entity.email !== undefined && <span className="text-status_offline">{entity.email}</span>}
|
||||
</p>
|
||||
<p>{entity.department}</p>
|
||||
</div>
|
||||
</>
|
||||
}}
|
||||
]
|
||||
|
||||
export const SYSTEM_MEMBER_TABLE_COLUMN: PageProColumns<SystemMemberTableListItem>[] = [
|
||||
{
|
||||
title:('用户名'),
|
||||
@@ -319,65 +287,6 @@ export const SYSTEM_API_TABLE_COLUMNS: PageProColumns<SystemApiTableListItem>[]
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const SYSTEM_UPSTREAM_TABLE_COLUMNS: PageProColumns<SystemUpstreamTableListItem>[] = [
|
||||
{
|
||||
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<ProxyHeaderItem>[] = [
|
||||
}
|
||||
]
|
||||
|
||||
export const NODE_CONFIG:ConfigField<NodeItem>[] = [
|
||||
{
|
||||
title:('集群'),
|
||||
key: 'cluster',
|
||||
component: <Select className="w-INPUT_NORMAL" options={[]}/>,
|
||||
required: true
|
||||
}, {
|
||||
title:('地址'),
|
||||
key: 'address',
|
||||
component: <Input className="w-INPUT_NORMAL" />,
|
||||
renderText: (value: string) => {
|
||||
return value
|
||||
},
|
||||
required: true
|
||||
}, {
|
||||
title:('权重'),
|
||||
key: 'weight',
|
||||
component: <InputNumber className="w-INPUT_NORMAL"/>,
|
||||
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<ServiceApiTableListItem>[] = [
|
||||
{
|
||||
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<SimpleApiItem> = [
|
||||
{
|
||||
title:('所有 API'),
|
||||
dataIndex:'method',
|
||||
},
|
||||
{
|
||||
title:'',
|
||||
dataIndex:'name',
|
||||
ellipsis:true
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export const SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS: PageProColumns<GlobalNodeItem & {_id:string}>[] = [
|
||||
|
||||
@@ -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<React.SetStateAction<string>>;
|
||||
prefixForce:boolean;
|
||||
setPrefixForce:React.Dispatch<React.SetStateAction<boolean>>;
|
||||
aiServiceInfo:(AiServiceConfigFieldType & {provider:EntityItem })|undefined
|
||||
setAiServiceInfo:React.Dispatch<React.SetStateAction<AiServiceConfigFieldType|undefined>>;
|
||||
}
|
||||
|
||||
const AiServiceContext = createContext<AiServiceContextProps | undefined>(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<string>('');
|
||||
const [prefixForce, setPrefixForce] = useState<boolean>(false);
|
||||
const [aiServiceInfo, setAiServiceInfo] = useState<AiServiceConfigFieldType>()
|
||||
|
||||
return <AiServiceContext.Provider value={{apiPrefix,setApiPrefix,prefixForce,setPrefixForce,aiServiceInfo, setAiServiceInfo }}>{children}</AiServiceContext.Provider>;
|
||||
};
|
||||
@@ -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<AiServiceConfigHandle>((_,ref) => {
|
||||
const { message,modal } = App.useApp()
|
||||
const { teamId, serviceId } = useParams<RouterParams>();
|
||||
const [onEdit, setOnEdit] = useState<boolean>(!!teamId)
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const [teamOptionList, setTeamOptionList] = useState<DefaultOptionType[]>()
|
||||
const [providerOptionList, setProviderOptionList] = useState<DefaultOptionType[]>()
|
||||
const navigate = useNavigate();
|
||||
const {setBreadcrumb} = useBreadcrumb()
|
||||
const { setAiServiceInfo} = useAiServiceContext()
|
||||
const [showClassify, setShowClassify] = useState<boolean>()
|
||||
const [imageBase64, setImageBase64] = useState<string | null>(null);
|
||||
const [tagOptionList, setTagOptionList] = useState<DefaultOptionType[]>([])
|
||||
const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState<DefaultOptionType[]>()
|
||||
const [uploadLoading, setUploadLoading] = useState<boolean>(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<FileReader>) => {
|
||||
setImageBase64(e.target?.result as string);
|
||||
form.setFieldValue('logo', e.target?.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
// }
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
const handleChange: UploadProps['onChange'] = (info: UploadChangeParam<UploadFile>) => {
|
||||
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 = (
|
||||
<div>
|
||||
{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24"/>}
|
||||
</div>
|
||||
);
|
||||
|
||||
const getTagAndServiceClassifyList = ()=>{
|
||||
setTagOptionList([])
|
||||
setServiceClassifyOptionList([])
|
||||
fetchData<BasicResponse<{ catalogues:CategorizesType[],tags:EntityItem[]}>>('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<BasicResponse<{ service: AiServiceConfigFieldType }>>('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<boolean|string> = () => {
|
||||
return form.validateFields().then((value)=>{
|
||||
return fetchData<BasicResponse<{service:{id:string}}>>(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<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('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: <div className="flex items-center" dangerouslySetInnerHTML={{ __html: x.logo }} />, value:x.id
|
||||
}}))
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getTeamOptionList = ()=>{
|
||||
setTeamOptionList([])
|
||||
|
||||
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(!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<BasicResponse<null>>('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: <Link to={`/aiservice/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<WithPermission access={onEdit ? 'team.service.service.edit' :''}>
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="w-full pr-PAGE_INSIDE_X "
|
||||
name="systemConfig"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div>
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("服务名称")}
|
||||
name="name"
|
||||
rules={[{ required: true ,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("服务ID")}
|
||||
name="id"
|
||||
rules={[{ required: true ,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("AI 模型供应商")}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
>{
|
||||
(providerOptionList && providerOptionList.length >0 ) ? <Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={providerOptionList} >
|
||||
</Select> : <p>未配置任何 AI 模型供应商,<a href="/aisetting" target="_blank" onClick={()=>setAiConfigFlushed(false)}>立即配置</a></p>
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("API 调用前缀")}
|
||||
name="prefix"
|
||||
extra={$t("选填,作为服务内所有API的前缀,比如host/{service_name}/{api_path},一旦保存无法修改")}
|
||||
rules={[
|
||||
{
|
||||
validator: validateUrlSlash,
|
||||
}]}
|
||||
>
|
||||
<Input prefix={onEdit ? '' : '/'} className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("图标")}
|
||||
name="logoFile"
|
||||
extra={$t("仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩")}
|
||||
valuePropName="fileList" getValueFromEvent={normFile}
|
||||
>
|
||||
<Upload
|
||||
listType="picture"
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleChange}
|
||||
showUploadList={false}
|
||||
maxCount={1}
|
||||
accept=".png, .jpg, .jpeg, .svg"
|
||||
>
|
||||
<div className="h-[68px] w-[68px] border-[1px] border-dashed border-BORDER flex items-center justify-center rounded bg-bar-theme cursor-pointer" style={{ marginTop: 8 }}>
|
||||
{imageBase64 ? <img src={imageBase64} alt="Logo" style={{ maxWidth: '200px', width:'68px',height:'68px'}} /> : uploadButton}
|
||||
</div>
|
||||
</Upload>
|
||||
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("描述")}
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("Logo")}
|
||||
name="logo"
|
||||
hidden
|
||||
>
|
||||
</Form.Item>
|
||||
|
||||
{!onEdit && <Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("所属团队")}
|
||||
name="team"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)} options={teamOptionList} >
|
||||
</Select>
|
||||
</Form.Item>}
|
||||
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("标签")}
|
||||
name="tags"
|
||||
>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
mode="tags"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={tagOptionList}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("服务类型")}
|
||||
name="serviceType"
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<Radio.Group className="flex flex-col" options={visualizationOptions} onChange={(e)=>{setShowClassify(e.target.value === 'public')}} />
|
||||
</Form.Item>
|
||||
|
||||
{showClassify &&
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t("所属服务分类")}
|
||||
name="catalogue"
|
||||
extra={$t("设置服务展示在服务市场中的哪个分类下")}
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<TreeSelect
|
||||
className="w-INPUT_NORMAL"
|
||||
fieldNames={{label:'name',value:'id',children:'children'}}
|
||||
showSearch
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
allowClear
|
||||
treeDefaultExpandAll
|
||||
treeData={serviceClassifyOptionList}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{onEdit && <>
|
||||
<Row className="mb-[10px]"
|
||||
// wrapperCol={{ offset: 5, span: 19 }}
|
||||
>
|
||||
<WithPermission access={onEdit ? 'team.service.service.edit' :''}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</Row></>}
|
||||
</div>
|
||||
{onEdit && <>
|
||||
<WithPermission access="team.service.service.delete" showDisabled={false}>
|
||||
<div className="bg-[rgb(255_120_117_/_5%)] rounded-[10px] mt-[50px] p-btnrbase pb-0">
|
||||
<p className="text-left"><span className="font-bold">{$t('删除服务')}:</span>{$t('删除操作不可恢复,请谨慎操作!')}</p>
|
||||
<div className="text-left">
|
||||
<WithPermission access="team.service.service.delete">
|
||||
<Button className="m-auto mt-[16px] mb-[20px]" type="default" danger={true} onClick={deleteAiServiceModal}>{$t('删除服务')}</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
</WithPermission>
|
||||
</>}
|
||||
</Form>
|
||||
</WithPermission>
|
||||
</>
|
||||
)
|
||||
})
|
||||
export default AiServiceConfig
|
||||
@@ -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<string>()
|
||||
const [updateTime,setUpdateTime]=useState<string>()
|
||||
const [initDoc, setInitDoc] = useState<string>()
|
||||
const [doc, setDoc] = useState<string>()
|
||||
const {fetchData} = useFetch()
|
||||
const { serviceId, teamId} = useParams<RouterParams>();
|
||||
|
||||
const save = ()=>{
|
||||
fetchData<BasicResponse<{service:{ id:string,name:string,updater:string,updateTime:string, doc:string} }>>('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<BasicResponse<{doc:{ id:string,name:string,updater:EntityItem,updateTime:string,creater:EntityItem, doc:string} }>>('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 (
|
||||
<div className="flex flex-col h-full border-[1px] rounded-[10px] border-BORDER border-solid mr-PAGE_INSIDE_X">
|
||||
<Editor
|
||||
tinymceScriptSrc={'/tinymce/tinymce.min.js'}
|
||||
initialValue={initDoc}
|
||||
init={{
|
||||
height: '100%',
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
|
||||
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'codesample', 'fullscreen', 'insertdatetime',
|
||||
'media', 'table', 'emoticons', 'help'
|
||||
], toolbar: 'undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify | codesample |table|' +
|
||||
'bullist numlist outdent indent | link image | print preview media fullscreen | ' +
|
||||
'forecolor backcolor emoticons | help',
|
||||
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
|
||||
setup: setupEditor,
|
||||
codesample_languages:[
|
||||
{
|
||||
text: 'HTML/XML',
|
||||
value: 'markup'
|
||||
},
|
||||
{
|
||||
text: 'JavaScript',
|
||||
value: 'javascript'
|
||||
},
|
||||
{
|
||||
text: 'CSS',
|
||||
value: 'css'
|
||||
},
|
||||
{
|
||||
text: 'PHP',
|
||||
value: 'php'
|
||||
},
|
||||
{
|
||||
text: 'Ruby',
|
||||
value: 'ruby'
|
||||
},
|
||||
{
|
||||
text:'GO',
|
||||
value:'go'
|
||||
},
|
||||
{
|
||||
text: 'Python',
|
||||
value: 'python'
|
||||
},
|
||||
{
|
||||
text: 'Java',
|
||||
value: 'java'
|
||||
},
|
||||
{
|
||||
text: 'C',
|
||||
value: 'c'
|
||||
},
|
||||
{
|
||||
text: 'C#',
|
||||
value: 'csharp'
|
||||
},
|
||||
{
|
||||
text: 'C++',
|
||||
value: 'cpp'
|
||||
},
|
||||
{ text: 'Bash/Shell', value: 'bash' },
|
||||
{ text: 'SQL', value: 'sql' }
|
||||
]
|
||||
}}
|
||||
onEditorChange={handleEditorChange}
|
||||
/>
|
||||
|
||||
<div className=" pl-[8px] py-btnbase ">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[14px] leading-[20px] text-[#999999]"><span className="mr-[20px]">{$t('最近一次更新者')}:{updater || '-'}</span><span>{$t('最近一次更新时间')}:{updateTime || '-'}</span></p>
|
||||
<WithPermission access="team.service.service.edit"><Button type="primary" className="mr-btnbase" onClick={save}>{$t('保存')}</Button></WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
export default ServiceInsideDocument
|
||||
@@ -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<RouterParams>();
|
||||
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<string>()
|
||||
const navigateTo = useNavigate()
|
||||
const [showMenu, setShowMenu] = useState<boolean>(false)
|
||||
|
||||
const getAiServiceInfo = ()=>{
|
||||
fetchData<BasicResponse<{ service:AiServiceConfigFieldType }>>('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<BasicResponse<{ prefix:string, force:boolean }>>('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(<Link to="./api">{$t('API')}</Link>, 'api',undefined,undefined,undefined,'team.service.api_doc.view'),
|
||||
getItem(<Link to="./route">{$t('路由')}</Link>, 'route',undefined,undefined,undefined,'team.service.router.view'),
|
||||
getItem(<Link to="./document">{$t('使用说明')}</Link>, 'document',undefined,undefined,undefined,''),
|
||||
getItem(<Link to="./publish">{$t('发布')}</Link>, 'publish',undefined,undefined,undefined,'team.service.release.view'),
|
||||
],
|
||||
'group'),
|
||||
getItem($t('订阅管理'), 'provideSer', null,
|
||||
[
|
||||
getItem(<Link to="./approval">{$t('订阅审批')}</Link>, 'approval',undefined,undefined,undefined,'team.service.subscription.view'),
|
||||
getItem(<Link to="./subscriber">{$t('订阅方管理')}</Link>, 'subscriber',undefined,undefined,undefined,'team.service.subscription.view'),
|
||||
],
|
||||
'group'),
|
||||
getItem($t('管理'), 'mng', null,
|
||||
[
|
||||
APP_MODE === 'pro' ? getItem(<Link to="./topology">{$t('调用拓扑图')}</Link>, 'topology',undefined,undefined,undefined,'project.myAiService.topology.view'):null,
|
||||
getItem(<Link to="./setting">{$t('设置')}</Link>, 'setting',undefined,undefined,undefined,'')],
|
||||
'group'),
|
||||
],[state.language])
|
||||
|
||||
|
||||
const menuData = useMemo(()=>{
|
||||
const filterMenu = (menu:MenuItemGroupType<MenuItemType>[])=>{
|
||||
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<MenuItemType>[])
|
||||
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 ?
|
||||
<InsidePage pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[{label:
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>{$t('服务 ID')}:{serviceId || '-'}</Paragraph>
|
||||
}]}
|
||||
backUrl="/aiservice/list">
|
||||
<div className="flex flex-1 h-full">
|
||||
<Menu
|
||||
onClick={onMenuClick}
|
||||
className="h-full overflow-y-auto"
|
||||
style={{ width: 220 }}
|
||||
selectedKeys={[activeMenu!]}
|
||||
mode="inline"
|
||||
items={menuData as unknown as ItemType<MenuItemType>[] }
|
||||
/>
|
||||
<div className={` ${['setting', 'upstream'].indexOf(activeMenu!) !== -1 ? '' :''} w-full h-full flex flex-1 flex-col overflow-auto bg-MAIN_BG pt-[20px] pl-[20px] pb-PAGE_INSIDE_B ` }>
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
</InsidePage>: <Outlet/> }
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsidePage
|
||||
@@ -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<RouterParams>()
|
||||
const addRef = useRef<AiServiceSubscriberConfigHandle>(null)
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const getAiServiceSubscriber = ()=>{
|
||||
return fetchData<BasicResponse<{subscribers:AiServiceSubscriberTableListItem[]}>>('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<BasicResponse<{ members: SimpleMemberItem[] }>>('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<BasicResponse<null>>('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=<AiServiceSubscriberConfig ref={addRef} serviceId={serviceId!} teamId={teamId!}/>
|
||||
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<AiServiceSubscriberTableListItem>[] =[
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums:1,
|
||||
fixed:'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: AiServiceSubscriberTableListItem) => [
|
||||
<TableBtnWithPermission access="team.service.subscription.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/>,
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/aiservice/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
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,<span>{$t('手动添加')}</span>],
|
||||
[1,<span>{$t('订阅申请')}</span>],
|
||||
])
|
||||
}
|
||||
return {
|
||||
...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}
|
||||
}
|
||||
)
|
||||
},[memberValueEnum,state.language])
|
||||
|
||||
return (
|
||||
<PageList
|
||||
id="global_system_subscriber"
|
||||
ref={pageListRef}
|
||||
columns = {[...columns,...operation]}
|
||||
request={()=>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<AiServiceSubscriberConfigHandle,AiServiceSubscriberConfigProps>((props, ref) => {
|
||||
const { message } = App.useApp()
|
||||
const { serviceId, teamId} = props
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const [systemOptionList, setAiServiceOptionList] = useState<DefaultOptionType[]>()
|
||||
const save:()=>Promise<boolean | string> = ()=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
form.validateFields().then((value)=>{
|
||||
fetchData<BasicResponse<null>>('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<BasicResponse<{ apps: SimpleAiServiceItem[] }>>('simple/apps/mine',{method:'GET'}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
const teamMap = new Map<string, unknown>();
|
||||
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 (<WithPermission access="team.service.subscription.add">
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto "
|
||||
name="systemInsideSubscriber"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<AiServiceSubscriberConfigFieldType>
|
||||
label={$t("订阅方")}
|
||||
name="application"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<TreeSelect
|
||||
className="w-INPUT_NORMAL"
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeData={systemOptionList}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
treeDefaultExpandAll
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
</Form>
|
||||
</WithPermission>)
|
||||
})
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<string>('')
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const [teamList, setTeamList] = useState<{ [k: string]: { text: string; }; }>()
|
||||
const {fetchData} = useFetch()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<AiServiceTableListItem[]>([]);
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true);
|
||||
const { message } = App.useApp()
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({})
|
||||
const [open, setOpen] = useState(false);
|
||||
const drawerFormRef = useRef<AiServiceConfigHandle>(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<BasicResponse<{services:AiServiceTableListItem[]}>>(!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<BasicResponse<{ teams: SimpleTeamItem[] }>>(!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<BasicResponse<{ members: SimpleMemberItem[] }>>('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 (
|
||||
<div className="h-full w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B">
|
||||
{/* <Joyride steps={steps} run={true} /> */}
|
||||
<PageList
|
||||
id="global_ai_system"
|
||||
ref={pageListRef}
|
||||
columns={[...columns]}
|
||||
request={()=>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}`)}
|
||||
/>
|
||||
<DrawerWithFooter title={$t("添加 AI 服务")} open={open} onClose={onClose} onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} >
|
||||
<AiServiceConfig ref={drawerFormRef} />
|
||||
</DrawerWithFooter>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
export default AiServiceList
|
||||
@@ -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<RouterParams>()
|
||||
const {getTeamAccessData,cleanTeamAccessData} = useGlobalContext()
|
||||
|
||||
useEffect(()=>{
|
||||
teamId ? getTeamAccessData(teamId) : cleanTeamAccessData()
|
||||
return ()=>{
|
||||
cleanTeamAccessData()
|
||||
}
|
||||
},[teamId])
|
||||
|
||||
|
||||
return (<Outlet />)
|
||||
}
|
||||
@@ -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<AiServiceInsideApiDocumentHandle,AiServiceInsideApiDocumentProps>(() => {
|
||||
const {serviceId, teamId} = useParams<RouterParams>()
|
||||
const {fetchData} = useFetch()
|
||||
const [apiDetail, setApiDetail] = useState<AiServiceApiDetail>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
useEffect(() => {
|
||||
getApiDetail()
|
||||
}, []);
|
||||
|
||||
const getApiDetail = ()=>{
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{doc:AiServiceApiDetail}>>('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 (
|
||||
<div className="h-full overflow-hidden">
|
||||
<div className="flex-1 overflow-auto pr-PAGE_INSIDE_X">
|
||||
<ApiDocument spec={spec}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (<>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' h-full overflow-hidden '>
|
||||
<div className=" h-full">
|
||||
{ apiDetail ? <ApiPreview spec={apiDetail} />
|
||||
: <Empty image={EmptySVG} >
|
||||
</Empty>}
|
||||
</div>
|
||||
</Spin>
|
||||
</>)
|
||||
})
|
||||
|
||||
export default AiServiceInsideApiDocument
|
||||
@@ -0,0 +1,7 @@
|
||||
import {FC} from "react";
|
||||
|
||||
const AiServiceInsideApiPlugin:FC = ()=>{
|
||||
|
||||
return (<></>)
|
||||
}
|
||||
export default AiServiceInsideApiPlugin
|
||||
@@ -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<RouterParams>()
|
||||
const [form] = Form.useForm();
|
||||
const {fetchData} = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const {apiPrefix,prefixForce ,aiServiceInfo} = useAiServiceContext()
|
||||
const [variablesTable,setVariablesTable] = useState<VariableItems[]>([])
|
||||
const [drawerType,setDrawerType]= useState<'edit'|undefined>()
|
||||
const [open, setOpen] = useState(false);
|
||||
const drawerAddFormRef = useRef<AiServiceRouterModelConfigHandle>(null)
|
||||
const [defaultLlm, setDefaultLlm] = useState<AiProviderDefaultConfig & {config:string}>()
|
||||
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>([])
|
||||
const [variablesTableRef, setVariablesTableRef] = useState<MutableRefObject<EditableFormInstance<T> | 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<BasicResponse<null>>('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<BasicResponse<{api:AiServiceRouterConfig}>>('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<BasicResponse<{llms:AiProviderLlmsItems[],provider:AiProviderDefaultConfig}>>('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<string,unknown>) => {
|
||||
if(changedValues.variables){
|
||||
setVariablesTable(changedValues.variables as VariableItems[])
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handlerSubmit:() => Promise<boolean>|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 (
|
||||
|
||||
<InsidePage pageTitle={ 'AI 路由设置'|| '-'}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
className="overflow-y-auto"
|
||||
backUrl={`/aiservice/${teamId}/inside/${serviceId}/route`}
|
||||
customBtn={
|
||||
<div className="flex gap-btnbase items-center">
|
||||
<Button icon={<Icon icon='ic:baseline-tune' height={18} width={18} />} iconPosition='end' onClick={()=>openDrawer('edit')}>
|
||||
<div className="flex items-center gap-[4px]">
|
||||
<span className="flex items-center " dangerouslySetInnerHTML={{__html: defaultLlm?.logo || ''}}></span>
|
||||
<span>{defaultLlm?.id || defaultLlm?.defaultLlm}</span>
|
||||
{defaultLlm?.scopes?.map(x=><Tag >{x?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button type="primary" onClick={onFinish}>
|
||||
{$t('保存')}
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'>
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="AiServiceInsideRouterCreate"
|
||||
onValuesChange={handleValuesChange}
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="">
|
||||
<Row className="flex items-center gap-btnbase w-full justify-between">
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("路由名称")}
|
||||
name="name"
|
||||
rules={[{ required: true,whitespace:true }]}
|
||||
>
|
||||
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("请求路径")}
|
||||
name="path"
|
||||
rules={[{ required: true,whitespace:true }]}
|
||||
>
|
||||
<Input prefix={prefixForce ? `${apiPrefix}/` :"/"} className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={$t("提示词")}
|
||||
name="prompt"
|
||||
>
|
||||
<PromptEditorResizable variablesChange={handleVariablesChange} promptVariables={variablesTable}/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={<div className="w-full flex justify-between items-center"><span>{$t("变量")}</span><a className="flex items-center gap-[4px]" onClick={addVariable}><Icon icon="ic:baseline-add" width={16} height={16} />New</a></div>}
|
||||
name="variables"
|
||||
className="[&>.ant-row>.ant-col>label]:w-full"
|
||||
>
|
||||
<EditableTableNotAutoGen<VariableItems & {_id:string}>
|
||||
getFromRef={setVariablesTableRef}
|
||||
configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterField>
|
||||
label={$t("描述")}
|
||||
name="description"
|
||||
>
|
||||
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t('输入这个接口的描述')}/>
|
||||
</Form.Item>
|
||||
|
||||
<Row className="flex items-center gap-btnbase w-full justify-between">
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("请求超时时间")}
|
||||
name={'timeout'}
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" suffix="ms" min={1} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterField>
|
||||
className="flex-1"
|
||||
label={$t("重试次数")}
|
||||
name={'retry'}
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<InputNumber className="w-INPUT_NORMAL" min={0} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
</Row>
|
||||
|
||||
|
||||
</div>
|
||||
</Form>
|
||||
</Spin>
|
||||
<DrawerWithFooter
|
||||
title={ $t("模型配置")}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onSubmit={()=>handlerSubmit()}
|
||||
>
|
||||
<AiServiceRouterModelConfig ref={drawerAddFormRef} llmList={llmList} entity={defaultLlm!} />
|
||||
</DrawerWithFooter>
|
||||
</InsidePage>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsideRouterCreate
|
||||
|
||||
|
||||
|
||||
@@ -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<string>('')
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { modal,message } = App.useApp()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<AiServiceRouterTableListItem[]>([]);
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true);
|
||||
const {fetchData} = useFetch()
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const {serviceId, teamId} = useParams<RouterParams>()
|
||||
const navigator = useNavigate()
|
||||
|
||||
const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[], success: boolean }>=> {
|
||||
if(!tableHttpReload){
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({
|
||||
data: tableListDataSource,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<{apis:AiServiceRouterTableListItem}>>('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<BasicResponse<null>>('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<AiServiceRouterTableListItem>[] =[
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums:2,
|
||||
fixed:'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [
|
||||
<TableBtnWithPermission access="team.service.router.edit" key="edit" btnType="edit" onClick={()=>{navigator(`/aiservice/${teamId}/inside/${serviceId}/route/${entity.id}`)}} btnTitle="编辑"/>,
|
||||
<Divider type="vertical" className="mx-0" key="div3"/>,
|
||||
<TableBtnWithPermission access="team.service.router.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/>,
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
const manualReloadTable = () => {
|
||||
setTableHttpReload(true); // 表格数据需要从后端接口获取
|
||||
pageListRef.current?.reload()
|
||||
};
|
||||
|
||||
const getMemberList = async ()=>{
|
||||
setMemberValueEnum([])
|
||||
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('simple/member',{method:'GET'})
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setMemberValueEnum(data.members)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumb([
|
||||
{
|
||||
title:<Link to={`/aiservice/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<PageList
|
||||
id="global_system_api"
|
||||
ref={pageListRef}
|
||||
columns = {[...columns,...operation]}
|
||||
request={()=>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
|
||||
@@ -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<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>((props, ref)=>{
|
||||
const [form] = Form.useForm();
|
||||
const {llmList,entity} = props
|
||||
|
||||
useImperativeHandle(ref, ()=>({
|
||||
save:form.validateFields
|
||||
})
|
||||
)
|
||||
|
||||
useEffect(()=>{
|
||||
form.setFieldsValue(entity)
|
||||
},[])
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("模型")}
|
||||
name="id"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={llmList?.map(x=>({
|
||||
value:x.id,
|
||||
label:<div className="flex items-center gap-[4px]">
|
||||
<div className="flex items-center" dangerouslySetInnerHTML={{ __html: x.logo }}></div>
|
||||
<span>{x.id}</span>
|
||||
{x?.scopes?.map(s=><Tag >{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>}))}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("参数")}
|
||||
name="config"
|
||||
>
|
||||
<Codebox editorTheme="vs-dark"
|
||||
width="100%" height="300px" language='json' enableToolbar={false} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default AiServiceRouterModelConfig
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Tabs defaultActiveKey={pageStatus.toString()} size="small" className="h-auto bg-MAIN_BG" tabBarStyle={{paddingLeft:'10px'}} tabBarGutter={20} items={tabItems} onChange={onChange} destroyInactiveTabPane={true}/>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AiServiceInsideApproval
|
||||
@@ -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<RouterParams>();
|
||||
const [init, setInit] = useState<boolean>(true)
|
||||
const {fetchData} = useFetch()
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true);
|
||||
const [tableListDataSource, setTableListDataSource] = useState<SubscribeApprovalTableListItem[]>([]);
|
||||
const pageListRef = useRef<ActionType>(null);
|
||||
const query =new URLSearchParams(useLocation().search)
|
||||
const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1)
|
||||
const subscribeRef = useRef<SubscribeApprovalModalHandle>(null)
|
||||
const [approvalBtnLoading,setApprovalBtnLoading] = useState<boolean>(false)
|
||||
const [memberValueEnum, setMemberValueEnum] = useState<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
|
||||
const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{
|
||||
message.loading($t(RESPONSE_TIPS.loading))
|
||||
const {code,data,msg} = await fetchData<BasicResponse<{approval:SubscribeApprovalInfoType}>>('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:<SubscribeApprovalModalContent ref={subscribeRef} data={{...data.approval} as SubscribeApprovalInfoType} type={type} serviceId={serviceId!} teamId={teamId!} inAiService/>,
|
||||
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' ? <>
|
||||
<CancelBtn/>
|
||||
<WithPermission access="team.service.release.approval"><Button type="primary" danger loading={approvalBtnLoading} onClick={()=>{setApprovalBtnLoading(true);subscribeRef.current?.save('refuse').then((res)=>{if(res === true ){manualReloadTable();modalIns?.destroy()}}).finally(()=>{setApprovalBtnLoading(false)})}}>{$t('拒绝')}</Button></WithPermission>
|
||||
<OkBtn/>
|
||||
</> :
|
||||
<>
|
||||
<CancelBtn/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const operation:PageProColumns<SubscribeApprovalTableListItem>[] =[
|
||||
{
|
||||
title: COLUMNS_TITLE.operate,
|
||||
key: 'option',
|
||||
btnNums:1,
|
||||
fixed:'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: SubscribeApprovalTableListItem) => [
|
||||
pageStatus === 0 ?
|
||||
<TableBtnWithPermission access="team.service.subscription.approval" key="approval" btnType="approval" onClick={()=>{openModal('approval',entity)}} btnTitle="审批"/>
|
||||
:<TableBtnWithPermission access="team.service.subscription.view" key="view" btnType="view" onClick={()=>{openModal('view',entity)}} btnTitle="查看"/>,
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
const getApprovalList = ()=>{
|
||||
if(!tableHttpReload){
|
||||
setTableHttpReload(true)
|
||||
return Promise.resolve({
|
||||
data: tableListDataSource,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
return fetchData<BasicResponse<{approvals:SubscribeApprovalTableListItem[]}>>('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<BasicResponse<{ members: SimpleMemberItem[] }>>('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:<Link to={`/aiservice/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
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<string,{text:string}>= {}
|
||||
memberValueEnum?.forEach((x:SimpleMemberItem)=>{
|
||||
tmpValueEnum[x.name] = {text:$t(x.name)}
|
||||
})
|
||||
x.valueEnum = tmpValueEnum
|
||||
}
|
||||
if(x.dataIndex === 'status'){
|
||||
x.valueEnum = new Map([
|
||||
[0, <span className="text-status_fail">{$t('拒绝')}</span>],
|
||||
[2,<span className="text-status_success">{$t('通过')}</span>],
|
||||
])
|
||||
}
|
||||
|
||||
return {...x,title: typeof x.title === 'string' ? $t(x.title as string) : x.title}})
|
||||
},[pageStatus,memberValueEnum,state.language])
|
||||
|
||||
return (
|
||||
<div className="h-full not-top-padding-table">
|
||||
<PageList
|
||||
id="global_system_approval"
|
||||
ref={pageListRef}
|
||||
columns = {[...columns,...operation]}
|
||||
request={()=>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"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsideApprovalList
|
||||
@@ -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:<Link to={`/aiservice/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
title:$t('发布')
|
||||
}
|
||||
])
|
||||
}, []);
|
||||
|
||||
const tabItems = useMemo(()=>SYSTEM_PUBLISH_TAB_ITEMS?.map((x)=>({...x, label:$t(x.label as string) })),[state.language])
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultActiveKey={pageStatus.toString()} size="small" className="h-auto bg-MAIN_BG" tabBarStyle={{paddingLeft:'10px'}} tabBarGutter={20} items={tabItems} onChange={onChange} destroyInactiveTabPane={true}/>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
export default AiServiceInsidePublic
|
||||
@@ -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<ActionType>(null);
|
||||
const [tableHttpReload, setTableHttpReload] = useState(true);
|
||||
const [init, setInit] = useState<boolean>(true)
|
||||
const {fetchData} = useFetch()
|
||||
const [tableListDataSource, setTableListDataSource] = useState<MemberTableListItem[]>([]);
|
||||
const {serviceId, teamId} = useParams<RouterParams>();
|
||||
const drawerRef = useRef<PublishApprovalModalHandle>(null)
|
||||
const [extraModalBtnLoading,setExtraModalBtnLoading] = useState<boolean>(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<SimpleMemberItem[]>([])
|
||||
const {accessData,state} = useGlobalContext()
|
||||
const [drawerTitle, setDrawerTitle] = useState<string>('')
|
||||
const [drawerType, setDrawerType] = useState<'approval'|'view'|'add'|'publish'|'online'>('view')
|
||||
const [drawerVisible, setDrawerVisible] = useState<boolean>(false)
|
||||
const [drawerData, setDrawerData] = useState<PublishTableListItem|PublishVersionTableListItem >({} as PublishTableListItem)
|
||||
const [drawerOkTitle, setDrawerOkTitle] = useState<string>('确认')
|
||||
const [isOkToPublish, setIsOkToPublish] = useState<boolean>(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<BasicResponse<{releases?:PublishVersionTableListItem[],publishs?:PublishTableListItem[]}>>(
|
||||
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<BasicResponse<null>>(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:<AiServiceInsidePublishOnline serviceId={serviceId!} teamId={teamId!} id={(entity as PublishVersionTableListItem).id}/>,
|
||||
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<BasicResponse<{ publish: PublishApprovalInfoType } | { release:AiServicePublishReleaseItem}>>(
|
||||
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<BasicResponse<{ publish: PublishApprovalInfoType }>>(
|
||||
'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<BasicResponse<{ publish: PublishApprovalInfoType }>>(
|
||||
'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<BasicResponse<{ diffs: PublishApprovalInfoType }>>(
|
||||
'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 }) => (
|
||||
<>
|
||||
<CancelBtn />
|
||||
<WithPermission>
|
||||
<OkBtn />
|
||||
</WithPermission>
|
||||
</>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const tableOperation = (entity:PublishTableListItem | PublishVersionTableListItem)=>{
|
||||
const viewBtn = <TableBtnWithPermission access="team.service.release.view" key="view" btnType="view" onClick={()=>{openDrawer('view',entity)}} btnTitle="查看详情"/>
|
||||
let btnArr:React.ReactNode[] = []
|
||||
if(pageType !== 'insidePage' && pageStatus !== 0){
|
||||
btnArr = [
|
||||
viewBtn
|
||||
]
|
||||
return btnArr
|
||||
}
|
||||
|
||||
if((entity as PublishVersionTableListItem).status === 'accept'){
|
||||
btnArr = [
|
||||
<TableBtnWithPermission access="team.service.release.online" key="online" btnType="online" onClick={()=>{openDrawer('online',entity)}} btnTitle="上线"/>,
|
||||
<Divider type="vertical" className="mx-0" key="div1"/>,
|
||||
viewBtn,
|
||||
<Divider type="vertical" className="mx-0" key="div2"/>,
|
||||
<TableBtnWithPermission access="team.service.release.stop" key="stop" btnType="stop" onClick={()=>{openModal('stop',entity)}} btnTitle="终止发布"/>
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
if((entity as PublishVersionTableListItem).status === 'publishing'){
|
||||
btnArr = [
|
||||
viewBtn,
|
||||
<Divider type="vertical" className="mx-0" key="div2"/>,
|
||||
<TableBtnWithPermission access="team.service.release.stop" key="stop" btnType="stop" onClick={()=>{openModal('stop',entity)}} btnTitle="终止发布"/>
|
||||
]
|
||||
}
|
||||
|
||||
if((entity as PublishVersionTableListItem).status === 'apply'){
|
||||
btnArr = [
|
||||
<TableBtnWithPermission access="team.service.release.approval" key="approval" btnType="approval" onClick={()=>{openDrawer('approval',entity)}} btnTitle="审批"/>,
|
||||
<Divider type="vertical" className="mx-0" key="div1"/>,
|
||||
viewBtn,
|
||||
<Divider type="vertical" className="mx-0" key="div2"/>,
|
||||
<TableBtnWithPermission access="team.service.release.cancel" key="cancel" btnType="cancel" onClick={()=>{openModal('cancel',entity)}} btnTitle="撤回申请"/>
|
||||
]
|
||||
}
|
||||
|
||||
// 第一期不做回滚
|
||||
// if( (entity as PublishVersionTableListItem).status === 'online' && (entity as PublishVersionTableListItem).canRollback){
|
||||
// btnArr = [...btnArr,
|
||||
// ...(btnArr.length > 0 ? [<Divider type="vertical" className="mx-0" />]:
|
||||
// [viewBtn,
|
||||
// <Divider type="vertical" className="mx-0" />]),
|
||||
// <WithPermission access="team.service.release.rollback"><Button className="h-[22px] border-none p-0 flex items-center bg-transparent " key="rollback" onClick={()=>openModal('rollback',entity)}>回滚版本</Button></WithPermission>
|
||||
// ]
|
||||
// }
|
||||
|
||||
if( ['close','refuse','none'].indexOf((entity as PublishVersionTableListItem).status as string) !== -1 || !(entity as PublishVersionTableListItem).flowId){
|
||||
btnArr = [...btnArr,
|
||||
...(btnArr.length > 0 ? [<Divider type="vertical" className="mx-0" key="div1" />]:
|
||||
[viewBtn,
|
||||
// <Divider type="vertical" className="mx-0" key="div1"/>
|
||||
]),
|
||||
// <TableBtnWithPermission access="team.service.release.add" key="publish" onClick={()=>{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 && <Divider type="vertical" className="mx-0" key="div5"/>,<TableBtnWithPermission access="team.service.release.delete" key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle="删除"/> ]
|
||||
}
|
||||
|
||||
return btnArr
|
||||
|
||||
}
|
||||
|
||||
const operation:PageProColumns<PublishTableListItem>[] =[
|
||||
{
|
||||
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:<Link to={`/aiservice/list`}>{$t('服务')}</Link>
|
||||
},
|
||||
{
|
||||
title:$t('发布')
|
||||
}
|
||||
])
|
||||
getMemberList()
|
||||
manualReloadTable()
|
||||
}, [serviceId]);
|
||||
|
||||
|
||||
const getMemberList = async ()=>{
|
||||
setMemberValueEnum([])
|
||||
const {code,data,msg} = await fetchData<BasicResponse<{ members: SimpleMemberItem[] }>>('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',<span className={PublishTableStatusColorClass.apply}>{$t(PublishApplyStatusEnum.apply || '-')}</span>],
|
||||
['running',<span className={PublishTableStatusColorClass.running}>{$t(PublishApplyStatusEnum.running || '-')}</span>],
|
||||
['none',<span className={PublishTableStatusColorClass.none}>{$t(PublishApplyStatusEnum.none || '-')}</span>],
|
||||
['refuse',<span className={PublishTableStatusColorClass.refuse}>{$t(PublishApplyStatusEnum.refuse || '-')}</span>],
|
||||
['publishing',<span className={PublishTableStatusColorClass.publishing}>{$t(PublishApplyStatusEnum.publishing || '-')}</span>],
|
||||
['error',<span className={PublishTableStatusColorClass.error}>{$t(PublishApplyStatusEnum.error || '-')}</span>],
|
||||
]) : new Map([
|
||||
['apply',<span className={PublishTableStatusColorClass.apply}>{$t(PublishStatusEnum.apply || '-')}</span>],
|
||||
['accept',<span className={PublishTableStatusColorClass.accept}>{$t(PublishStatusEnum.accept || '-')}</span>],
|
||||
['done',<span className={PublishTableStatusColorClass.done}>{$t(PublishStatusEnum.done || '-')}</span>],
|
||||
['stop',<span className={PublishTableStatusColorClass.stop}>{$t(PublishStatusEnum.stop || '-')}</span>],
|
||||
['close',<span className={PublishTableStatusColorClass.close}>{$t(PublishStatusEnum.close || '-')}</span>],
|
||||
['refuse',<span className={PublishTableStatusColorClass.refuse}>{$t(PublishStatusEnum.refuse || '-')}</span>],
|
||||
['publishing',<span className={PublishTableStatusColorClass.publishing}>{$t(PublishStatusEnum.publishing || '-')}</span>],
|
||||
['error',<span className={PublishTableStatusColorClass.error}>{$t(PublishStatusEnum.error || '-')}</span>],
|
||||
])
|
||||
}
|
||||
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<string, unknown>)?.data?.publish)
|
||||
}
|
||||
if (res === true && (drawerType === 'online' || drawerType === 'add')) {
|
||||
handleOnline(drawerData)
|
||||
}else if(res === true){
|
||||
manualReloadTable();
|
||||
}
|
||||
return res;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<PageList
|
||||
id="global_system_publish"
|
||||
ref={pageListRef}
|
||||
columns = {[...columns,...operation]}
|
||||
request = {(params: ParamsType & {
|
||||
pageSize?: number | undefined;
|
||||
current?: number | undefined;
|
||||
keyword?: string | undefined;
|
||||
})=>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"
|
||||
/>
|
||||
<DrawerWithFooter
|
||||
destroyOnClose={true}
|
||||
title={drawerTitle}
|
||||
width={'60%'}
|
||||
onClose={()=>{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') ? <WithPermission access={`team.service.release.${drawerType === 'approval'? 'approval' : 'stop'}`}>
|
||||
<Button
|
||||
type={drawerType === 'approval'? "primary" : 'default'}
|
||||
danger={drawerType === 'approval'}
|
||||
loading={extraModalBtnLoading}
|
||||
className={`${drawerType === 'online'? 'text-theme border-theme':''}`}
|
||||
onClick={() => {
|
||||
setExtraModalBtnLoading(true);
|
||||
if(drawerType === 'approval'){
|
||||
drawerRef.current?.save('refuse').then((res) => {
|
||||
if (res === true) {
|
||||
setDrawerVisible(false);manualReloadTable();
|
||||
}
|
||||
}).finally(() => {
|
||||
setExtraModalBtnLoading(false);
|
||||
});
|
||||
}else{
|
||||
handlePublishAction('stop', drawerData!).then((res) => {
|
||||
if (res === true) {
|
||||
setDrawerVisible(false);manualReloadTable();
|
||||
}
|
||||
}).finally(() => {
|
||||
setExtraModalBtnLoading(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{drawerType === 'approval'? $t("拒绝") : $t('终止发布')}
|
||||
</Button>
|
||||
</WithPermission> :undefined}
|
||||
>
|
||||
<PublishApprovalModalContent insidePage={true} serviceType='ai' ref={drawerRef}
|
||||
data={drawerData as PublishVersionTableListItem } type={drawerType} serviceId={serviceId!} teamId={teamId!} />
|
||||
</DrawerWithFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default AiServiceInsidePublicList
|
||||
@@ -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<BasicResponse<{publishStatusList:AiServiceInsidePublishOnlineItems[]}>>('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 <span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{$t('成功')}</span>
|
||||
case 'error':
|
||||
return <Tooltip title={entity.error || $t('上线失败')}><span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>{$t('失败')} {entity.error}</span></Tooltip>
|
||||
default:
|
||||
return <LoadingOutlined className="text-theme" spin />
|
||||
}
|
||||
}}
|
||||
}
|
||||
}),[state.language])
|
||||
|
||||
return (
|
||||
<Table
|
||||
className="min-h-[100px] h-full"
|
||||
bordered={true}
|
||||
columns={translatedPublishColumns}
|
||||
size="small"
|
||||
rowKey="id"
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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<AiSettingListItem[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
// const [updateLoading, setUpdateLoading] = useState<boolean>(false)
|
||||
const [loadingDefaultModel, setLoadingDefaultModel] = useState<string>('')
|
||||
const modalRef = useRef<AiSettingModalContentHandle>()
|
||||
const {setAiConfigFlushed,accessData} = useGlobalContext()
|
||||
const [llmMap, setLlmMap] = useState<Map<string, {loading:boolean, list:DefaultOptionType[]}>>(new Map<string, {loading:boolean, list:DefaultOptionType[]}>)
|
||||
const [currentProvider, setCurrentProvider] = useState<string>()
|
||||
|
||||
const getAiSettingList = ()=>{
|
||||
setLoading(true)
|
||||
return fetchData<BasicResponse<{providers:Omit<AiSettingListItem,'availableLlms'|'llmListStatus'>[]}>>(`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<BasicResponse<{llms:AiProviderLlmsItems[]}>>(`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:<div className="flex w-full items-center gap-[4px]">
|
||||
<div className="flex items-center" dangerouslySetInnerHTML={{ __html: x.logo }} />
|
||||
<span>{x.id}</span></div>,
|
||||
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<BasicResponse<{roles:AiSettingListItem[]}>>(`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<BasicResponse<{provider:AiProviderConfig}>>('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:<AiSettingModalContent ref={modalRef} entity={data.provider} readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}/>,
|
||||
onOk:()=>{
|
||||
return modalRef.current?.save().then((res)=>{if(res === true)
|
||||
setAiConfigFlushed(true)
|
||||
getAiSettingList()})
|
||||
},
|
||||
width:600,
|
||||
okText:$t('确认'),
|
||||
footer:(_, { OkBtn, CancelBtn }) =>{
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<a target="_blank" rel="noopener noreferrer" href={data.provider.getApikeyUrl} className="flex items-center gap-[8px]">
|
||||
<span>{$t('从 (0) 获取 API KEY',[data.provider.name])}</span>
|
||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||
</a>
|
||||
<div>
|
||||
<CancelBtn/>
|
||||
<WithPermission access="system.devops.ai_provider.edit" showDisabled={false}>
|
||||
<OkBtn/>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cancelText:$t('取消'),
|
||||
closable:true,
|
||||
icon:<></>,
|
||||
})
|
||||
}
|
||||
|
||||
const changeDefaultModel = (value: string, entity:AiSettingListItem) => {
|
||||
setLoadingDefaultModel(entity.id)
|
||||
return fetchData<BasicResponse<null>>(`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 (<>
|
||||
<InsidePage
|
||||
className="pb-PAGE_INSIDE_B overflow-y-auto"
|
||||
pageTitle={$t('AI 模型供应商')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
// customBtn={
|
||||
// <WithPermission access="system.devops.ai_provider.edit">
|
||||
// <Button
|
||||
// icon={<Icon icon="ic:baseline-refresh" width={20} height={20} />}
|
||||
// type="text"
|
||||
// iconPosition={'start'}
|
||||
// classNames={{icon:'h-[20px]'}}
|
||||
// loading={updateLoading}
|
||||
// onClick={updateModalList}>
|
||||
// {$t('同步最新模型')}
|
||||
// </Button>
|
||||
// </WithPermission>
|
||||
// }
|
||||
>
|
||||
<Spin className="h-full" wrapperClassName="h-full pr-PAGE_INSIDE_X" indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading}>
|
||||
{aiSettingList && aiSettingList.length > 0 ? <div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
{aiSettingList.map((provider:AiSettingListItem)=>(
|
||||
<Card title={
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center" dangerouslySetInnerHTML={{ __html: provider.logo }} />
|
||||
<Tag bordered={false} color={provider.configured ? 'green' : undefined} className="h-[22px] px-[4px] text-center">
|
||||
{provider.configured ? $t('已配置') : $t('未配置')}
|
||||
</Tag>
|
||||
</div> }
|
||||
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"}}
|
||||
>
|
||||
<div className="flex flex-col gap-btnbase h-full justify-between">
|
||||
<div className="flex items-center w-full h-[32px]">
|
||||
<label className="text-nowrap">{$t('默认')}:</label>
|
||||
<WithPermission access="system.devops.ai_provider.edit">
|
||||
<Select
|
||||
value={provider.defaultLlm}
|
||||
variant="borderless"
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value)=>changeDefaultModel(value, provider)}
|
||||
labelRender={(props)=>{
|
||||
return !props.label && !llmMap.get(provider.id)?.list?.length ?
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center" dangerouslySetInnerHTML={{__html:provider.defaultLlmLogo}}></div>
|
||||
<span>{provider.defaultLlm}</span>
|
||||
</div>: props.label }}
|
||||
x options={modelOptions}
|
||||
onFocus={()=>{if(!llmMap.get(provider.id)?.loading && !llmMap.get(provider.id)?.list?.length ){
|
||||
getLlmList(provider)
|
||||
setCurrentProvider(provider.id)
|
||||
}}}
|
||||
loading={llmMap.get(provider.id)?.loading || !!(loadingDefaultModel && loadingDefaultModel === provider.id )}
|
||||
/>
|
||||
</WithPermission>
|
||||
</div>
|
||||
<WithPermission access="system.devops.ai_provider.view">
|
||||
<Button block icon={<Icon icon="ic:outline-settings" width={18} height={18}/>} onClick={()=>openModal(provider)} classNames={{icon:'h-[18px]'}}>{$t('设置')}</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>:<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
|
||||
</Spin>
|
||||
</InsidePage>
|
||||
</>)
|
||||
}
|
||||
export default AiSettingList;
|
||||
@@ -0,0 +1,58 @@
|
||||
import WithPermission from "@common/components/aoplatform/WithPermission";
|
||||
import { BasicResponse, STATUS_CODE, RESPONSE_TIPS } from "@common/const/const";
|
||||
import { useFetch } from "@common/hooks/http";
|
||||
import { $t } from "@common/locales";
|
||||
import { App } from "antd";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { AiProviderConfig } from "./AiSettingList";
|
||||
import { Codebox } from "@common/components/postcat/api/Codebox";
|
||||
|
||||
|
||||
export type AiSettingModalContentProps = {
|
||||
entity:AiProviderConfig
|
||||
readOnly:boolean
|
||||
}
|
||||
|
||||
export type AiSettingModalContentHandle = {
|
||||
save:()=>Promise<boolean|string>
|
||||
}
|
||||
|
||||
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle,AiSettingModalContentProps>((props,ref)=>{
|
||||
const { message } = App.useApp()
|
||||
const {entity,readOnly} = props
|
||||
const {fetchData} = useFetch()
|
||||
const [code, setCode] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
try{
|
||||
entity!.config && setCode(JSON.stringify(JSON.parse(entity!.config),null,2))
|
||||
}catch(e){
|
||||
setCode('')
|
||||
}
|
||||
}, []);
|
||||
|
||||
const save: ()=>Promise<boolean | string> = ()=>{
|
||||
return fetchData<BasicResponse<null>>('ai/provider/config',{method:'PUT',eoParams:{provider:entity?.id}, eoBody:({config:code})}).then(response=>{
|
||||
const {code,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
return Promise.resolve(true)
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> Promise.reject(errorInfo))
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, ()=>({
|
||||
save
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<Codebox editorTheme="vs-dark" readOnly={readOnly}
|
||||
value={code} onChange={setCode} width="100%" height="300px" language='json' enableToolbar={false} />
|
||||
)
|
||||
})
|
||||
|
||||
export default AiSettingModalContent
|
||||
@@ -4,15 +4,26 @@ import { $t } from "@common/locales"
|
||||
import { Icon } from "@iconify/react/dist/iconify.js"
|
||||
import { Button, Card, Collapse } from "antd"
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
|
||||
export default function Guide(){
|
||||
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false' )
|
||||
const [, forceUpdate] = useState<unknown>(null);
|
||||
const {state} = useGlobalContext()
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
const navigator = useNavigate()
|
||||
|
||||
useEffect(()=>{
|
||||
localStorage.setItem('showGuide', showGuide.toString())
|
||||
},[showGuide])
|
||||
useEffect(()=>{
|
||||
if(currentUrl === '/guide'){
|
||||
setTimeout(()=>{
|
||||
navigator('/guide/page')
|
||||
},0)
|
||||
}
|
||||
},[])
|
||||
useEffect(()=>{forceUpdate({})},[state.language])
|
||||
return (
|
||||
<InsidePage
|
||||
|
||||
@@ -67,7 +67,7 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle,Syste
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
const {disable, protocols, path, methods, description, match, proxy} = data.router
|
||||
form.setFieldsValue({disable, protocols, path, methods, description, match,proxy
|
||||
form.setFieldsValue({disable, protocols, path:prefixForce && path?.startsWith(apiPrefix + '/')? path.slice((apiPrefix?.length || 0) + 1) : path, methods, description, match,proxy
|
||||
})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -158,7 +158,7 @@ const SystemInsideRouterCreate = forwardRef<SystemInsideRouterCreateHandle,Syste
|
||||
}]}
|
||||
className={styles['form-input-group']}
|
||||
>
|
||||
<Input prefix={type === 'edit' ? null :(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
|
||||
<Input prefix={(prefixForce ? `${apiPrefix}/` :"/")} className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -34,7 +34,7 @@ const SystemInsidePublicList:FC = ()=>{
|
||||
const drawerRef = useRef<PublishApprovalModalHandle>(null)
|
||||
const [extraModalBtnLoading,setExtraModalBtnLoading] = useState<boolean>(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<SimpleMemberItem[]>([])
|
||||
@@ -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<BasicResponse<{releases?:PublishVersionTableListItem[],publishs?:PublishTableListItem[]}>>(
|
||||
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 = <TableBtnWithPermission access="team.service.release.view" key="view" btnType="view" onClick={()=>{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',<span className={PublishTableStatusColorClass.apply}>{$t(PublishApplyStatusEnum.apply || '-')}</span>],
|
||||
['running',<span className={PublishTableStatusColorClass.running}>{$t(PublishApplyStatusEnum.running || '-')}</span>],
|
||||
['none',<span className={PublishTableStatusColorClass.none}>{$t(PublishApplyStatusEnum.none || '-')}</span>],
|
||||
@@ -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 = ()=>{
|
||||
</Button>
|
||||
</WithPermission> :undefined}
|
||||
>
|
||||
<PublishApprovalModalContent insideSystem ref={drawerRef}
|
||||
<PublishApprovalModalContent insidePage ref={drawerRef}
|
||||
data={drawerData as PublishVersionTableListItem } type={drawerType} serviceId={serviceId!} teamId={teamId!} />
|
||||
</DrawerWithFooter>
|
||||
</>
|
||||
|
||||
@@ -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 ? <img src={serviceBasicInfo?.logo} alt="Logo" style={{ maxWidth: '200px', width:'45px',height:'45px',objectFit:'unset'}}
|
||||
/> : undefined}
|
||||
icon={serviceBasicInfo?.logo ? '' :<iconpark-icon name="auto-generate-api"></iconpark-icon>}> </Avatar>
|
||||
icon={serviceBasicInfo?.logo ? '' :<iconpark-icon name="auto-generate-api"></iconpark-icon>}> </Avatar>
|
||||
|
||||
<div className="pl-[20px] w-[calc(100%-50px)]">
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold">{serviceName}</p>
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate font-bold flex items-center gap-[4px]">{serviceName}
|
||||
</p>
|
||||
<div className="mt-[10px] flex flex-col gap-btnrbase font-normal">
|
||||
{serviceDesc || '-'}
|
||||
<div>
|
||||
|
||||
@@ -179,15 +179,15 @@ const CardTitle = (service:ServiceHubTableListItem)=>{
|
||||
<div className="flex">
|
||||
<Avatar shape="square" size={50} className=" border-none bg-[linear-gradient(135deg,white,#f0f0f0)] text-[#333] rounded-[12px]" src={service.logo ? <img src={service.logo} alt="Logo" style={{ maxWidth: '200px', width:'45px',height:'45px',objectFit:'unset'}} /> : undefined}> {service.logo ? '' : service.name.substring(0,1)}</Avatar>
|
||||
<div className="pl-[20px] w-[calc(100%-50px)]">
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate w-full">{service.name}</p>
|
||||
<p className="text-[14px] h-[20px] leading-[20px] truncate w-full flex items-center gap-[4px]">{service.name}</p>
|
||||
<div className="mt-[10px] h-[20px] flex items-center font-normal">
|
||||
<Tag color="#7371fc1b" className="text-theme font-normal border-0 mr-[12px] max-w-[150px] truncate" key={service.id} bordered={false} title={service.catalogue?.name || '-'}>{service.catalogue?.name || '-'}</Tag>
|
||||
|
||||
<Tooltip title={$t('API 数量')}>
|
||||
<span className="mr-[12px]"><ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]"/><span className="font-normal text-[14px]">{service.apiNum ?? '-'}</span></span>
|
||||
<span className="mr-[12px] flex items-center"><ApiOutlined className="mr-[1px] text-[14px] h-[14px] w-[14px]"/><span className="font-normal text-[14px]">{service.apiNum ?? '-'}</span></span>
|
||||
</Tooltip>
|
||||
<Tooltip title={$t('接入应用数量')}>
|
||||
<span className="mr-[12px] flex items-center"><span className="h-[14px] mr-[4px] flex items-center"><iconpark-icon size="14px" name="auto-generate-api"></iconpark-icon></span><span className="font-normal text-[14px]">{service.subscriberNum ?? '-'}</span></span>
|
||||
<span className="mr-[12px] flex items-center"><span className="h-[14px] mr-[4px] flex items-center "><iconpark-icon size="14px" name="auto-generate-api"></iconpark-icon></span><span className="font-normal text-[14px]">{service.subscriberNum ?? '-'}</span></span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user