feat: aiApi detail page

This commit is contained in:
ningyv
2025-01-03 17:33:06 +08:00
parent c16e0edaf0
commit ee19c214e7
8 changed files with 269 additions and 189 deletions
@@ -9,8 +9,9 @@ const PromptEditorResizable = (props: {
onChange?: (value: string) => void
variablesChange?: (keys: string[]) => void
promptVariables: VariableItems[]
disabled?: boolean
}) => {
const { value, onChange, variablesChange, promptVariables } = props
const { value, onChange, variablesChange, promptVariables, disabled } = props
const minHeight = 68
const [editorHeight, setEditorHeight] = useState(minHeight)
const [previousKeys, setPreviousKeys] = useState<string[]>([])
@@ -82,7 +83,7 @@ const PromptEditorResizable = (props: {
setPreviousKeys(keys)
}
}}
editable={true}
editable={disabled ? false : true}
/>
)}
</>
@@ -157,7 +157,7 @@ const mockData = [
{
name: 'AI API',
key: 'aiApiList',
path: '/aiapis',
path: '/aiApis',
icon: 'ic:baseline-api'
// access: 'system.settings.ai_api.view'
}
@@ -92,10 +92,10 @@ const mockData = {
},
{
driver: 'apipark.builtIn.component',
name: 'aiapis',
name: 'aiApis',
router: [
{
path: 'aiapis',
path: 'aiApis',
type: 'normal'
}
]
+21 -2
View File
@@ -468,10 +468,29 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
}
],
[
'aiapis',
'aiApis',
{
type: 'component',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/index.tsx'))
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/aiApisLayout.tsx')),
key: 'aiApis',
provider: AiServiceProvider,
children: [
{
path: 'list',
key: 'apiList',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/aiApis/index.tsx')),
},
{
path: 'service/:teamId/aiInside/:serviceId/route/:routeId/:type',
key: 'apiDetail',
lazy: lazy(
() =>
import(
/* webpackChunkName: "[request]" */ '@core/pages/aiService/api/AiServiceInsideRouterCreate.tsx'
)
)
}
]
}
],
[
@@ -0,0 +1,14 @@
import { useEffect } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
export default function GlobalPolicyLayout(){
const location = useLocation()
const pathName = location.pathname
const navigator = useNavigate()
useEffect(()=>{
if(pathName === '/aiApis'){
navigator('/aiApis/list')
}
},[pathName])
return (<Outlet></Outlet>)
}
+101 -65
View File
@@ -22,6 +22,7 @@ const ApiSettings: React.FC = () => {
const [provider, setProvider] = useState<AIProvider | undefined>()
const { fetchData } = useFetch()
const [searchWord, setSearchWord] = useState<string>('')
const [columns, setColumns] = useState<PageProColumns<APIs>[]>([])
const [total, setTotal] = useState<number>(0)
const [timeButton, setTimeButton] = useState<TimeRangeButton>('day')
const navigate = useNavigate()
@@ -36,9 +37,14 @@ const ApiSettings: React.FC = () => {
}, [selectedProvider])
const handlePreview = (record: APIs) => {
navigate(`service/${record.team.id}/aiInside/${record.service.id}/route/${record.id}`)
navigate(`../service/${record.team.id}/aiInside/${record.service.id}/route/${record.id}/apiDetail`)
}
const requestApis = async (params: any) => {
const requestApis = async (params: any & {
pageSize: number;
current: number;
},
sort: Record<string, string>,
filter: Record<string, string>) => {
if (!selectedProvider) return
setQueryBtnLoading(true)
try {
@@ -46,6 +52,10 @@ const ApiSettings: React.FC = () => {
provider: selectedProvider,
page_size: params.pageSize,
keyword: searchWord,
sort: Object.keys(sort)?.length > 0 ? 'use_token' : undefined,
asc: Object.keys(sort)?.length > 0 ? Object.values(sort)?.[0] === 'ascend' : undefined,
models: filter?.model && filter?.model?.length ? JSON.stringify(filter.model) : undefined,
services: filter?.name && filter?.name?.length ? JSON.stringify(filter.name) : undefined,
page: params.current,
start: timeRange.start,
end: timeRange.end
@@ -62,8 +72,21 @@ const ApiSettings: React.FC = () => {
setQueryBtnLoading(false)
if (response.code === STATUS_CODE.SUCCESS) {
setTotal(response.data.total)
const modalMap: {
[key: string]: string
} = response.data?.condition?.models.reduce((acc: { [key: string]: string }, item: { id: string; name: string }) => {
acc[item.id] = $t(item.name)
return acc
}, {})
const serviceMap: {
[key: string]: string
} = response.data?.condition?.services.reduce((acc: { [key: string]: string }, item: { id: string; name: string }) => {
acc[item.id] = $t(item.name)
return acc
}, {})
setTableColumns(modalMap, serviceMap)
return {
data: response.data.apis,
data: response.data.apis || [],
success: true,
total: response.data.total
}
@@ -83,7 +106,74 @@ const ApiSettings: React.FC = () => {
}
}
}
const setTableColumns = (modalMap: {
[key: string]: string
}, serviceMap: {
[key: string]: string
}) => {
setColumns([
{
title: $t('AI 服务'),
dataIndex: 'name',
key: 'name',
width: 180,
filters: true,
valueEnum: serviceMap || {}
},
{
title: 'API URL',
dataIndex: 'request_path',
key: 'request_path',
ellipsis: true,
render: (text: string, record: APIs) => (
<p>
<Typography.Text type="success">{record.method}</Typography.Text>
<span className="ml-1">{text}</span>
</p>
)
},
{
title: $t('模型'),
dataIndex: ['model', 'name'],
key: 'model',
width: 150,
filters: true,
onFilter: true,
valueType: 'select',
valueEnum: modalMap || {}
},
{
title: $t('已用 Token'),
dataIndex: 'use_token',
key: 'use_token',
width: 120,
sorter: (a: any, b: any) => {
return (a.priority as number) - (b.priority as number)
}
},
{
title: $t('是否放行'),
dataIndex: 'disable',
ellipsis: true,
width: 120,
filters: true,
onFilter: true,
valueType: 'select',
valueEnum: {
true: { text: <Typography.Text type="danger">{$t('拦截')}</Typography.Text> },
false: { text: <Typography.Text type="success">{$t('放行')}</Typography.Text> }
}
},
{
title: $t('编辑时间'),
dataIndex: 'update_time',
key: 'update_time',
width: 200,
render: (time: string) => <Typography.Text>{dayjs(time).format('YYYY-MM-DD HH:mm:ss')}</Typography.Text>
},
...operation
])
}
const operation: PageProColumns<APIs>[] = [
{
title: '',
@@ -95,7 +185,7 @@ const ApiSettings: React.FC = () => {
<TableBtnWithPermission
access="team.service.router.view"
key="preview"
btnType="view"
btnType="logs"
onClick={() => handlePreview(entity)}
btnTitle={$t('预览')}
/>
@@ -103,65 +193,6 @@ const ApiSettings: React.FC = () => {
}
]
const columns: PageProColumns<APIs>[] = [
{
title: $t('AI 服务'),
dataIndex: 'name',
key: 'name',
width: 180
},
{
title: 'API URL',
dataIndex: 'request_path',
key: 'request_path',
width: 200,
ellipsis: true,
render: (text: string, record: APIs) => (
<p>
<Typography.Text type="success">{record.method}</Typography.Text>
<span className="ml-1">{text}</span>
</p>
)
},
{
title: $t('模型'),
dataIndex: ['model', 'name'],
key: 'model',
width: 150,
filters: true,
onFilter: true,
valueType: 'select',
valueEnum: {}
},
{
title: $t('已用 Token'),
dataIndex: 'use_token',
key: 'use_token',
width: 120,
sorter: true
},
{
title: $t('是否放行'),
dataIndex: 'disabled',
ellipsis: true,
filters: true,
onFilter: true,
valueType: 'select',
valueEnum: {
true: { text: <Typography.Text type="danger">{$t('拦截')}</Typography.Text> },
false: { text: <Typography.Text type="success">{$t('放行')}</Typography.Text> }
}
},
{
title: $t('编辑时间'),
dataIndex: 'update_time',
key: 'update_time',
width: 200,
render: (time: string) => <Typography.Text>{dayjs(time).format('YYYY-MM-DD HH:mm:ss')}</Typography.Text>
},
...operation
]
const resetQuery = () => {
setTimeButton('day')
setTimeRange({ start: null, end: null })
@@ -255,7 +286,12 @@ const ApiSettings: React.FC = () => {
</div>
</div>
}
request={requestApis}
request={async (params: any & {
pageSize: number;
current: number;
},
sort: Record<string, string>,
filter: Record<string, string>) => requestApis(params, sort, filter)}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
}}
@@ -20,6 +20,7 @@ import { App, Button, Form, Input, InputNumber, Row, Select, Space, Spin, Switch
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from './AiServiceInsideRouterModelConfig'
import WithPermission from '@common/components/aoplatform/WithPermission'
type AiServiceRouterField = {
name: string
@@ -51,7 +52,7 @@ type AiServiceRouterConfig = {
const AiServiceInsideRouterCreate = () => {
const navigator = useNavigate()
const { message } = App.useApp()
const { serviceId, teamId, routeId } = useParams<RouterParams>()
const { serviceId, teamId, routeId, type } = useParams<RouterParams>()
const [form] = Form.useForm()
const { fetchData } = useFetch()
const [loading, setLoading] = useState<boolean>(false)
@@ -103,7 +104,8 @@ const AiServiceInsideRouterCreate = () => {
})
.catch((errInfo) => Promise.reject(errInfo))
}
const isDelete = type === 'apiDetail'
const backUrl = isDelete ? `/aiApis/list` : `/service/${teamId}/aiInside/${serviceId}/route`
const openDrawer = (type: 'edit') => {
setDrawerType(type)
}
@@ -208,6 +210,7 @@ const AiServiceInsideRouterCreate = () => {
}, [])
const addVariable = () => {
if (isDelete) return
form.setFieldsValue({
variables: [...form.getFieldValue('variables'), { key: '', value: '', require: true }]
})
@@ -264,12 +267,13 @@ const AiServiceInsideRouterCreate = () => {
showBorder={false}
scrollPage={false}
className="overflow-y-auto"
backUrl={`/service/${teamId}/aiInside/${serviceId}/route`}
backUrl={backUrl}
customBtn={
<div className="flex items-center gap-btnbase">
<Button
icon={<Icon icon="ic:baseline-tune" height={18} width={18} />}
iconPosition="end"
disabled={isDelete}
onClick={() => openDrawer('edit')}
>
<div className="flex items-center gap-[10px]">
@@ -281,10 +285,11 @@ const AiServiceInsideRouterCreate = () => {
{defaultLlm?.scopes?.map((x) => <Tag>{x?.toLocaleUpperCase()}</Tag>)}
</div>
</Button>
<Button type="primary" onClick={onFinish}>
{$t('保存')}
</Button>
{
type !== 'apiDetail' && (<Button type="primary" onClick={onFinish}>
{$t('保存')}
</Button>)
}
</div>
}
>
@@ -293,125 +298,130 @@ const AiServiceInsideRouterCreate = () => {
spinning={loading}
wrapperClassName=" pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X"
>
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="flex flex-col mx-auto h-full"
name="AiServiceInsideRouterCreate"
onValuesChange={handleValuesChange}
onFinish={onFinish}
autoComplete="off"
>
<div className="">
<Row className="flex justify-between items-center w-full gap-btnbase">
<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>
<WithPermission disabled={isDelete}>
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="flex flex-col mx-auto h-full"
name="AiServiceInsideRouterCreate"
onValuesChange={handleValuesChange}
onFinish={onFinish}
autoComplete="off"
>
<div className="">
<Row className="flex justify-between items-center w-full gap-btnbase">
<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 className="flex-1" label={$t('请求路径')}>
<Space.Compact block>
<Form.Item
name="pathMatch"
rules={[
{ required: true, whitespace: true },
{
validator: validateUrlSlash
}
]}
noStyle
>
<Select
placeholder={$t(PLACEHOLDER.select)}
options={apiPathMatchRulesOptions}
className="w-[30%] min-w-[100px]"
/>
</Form.Item>
<Form.Item<AiServiceRouterField>
name="path"
rules={[
{ required: true, whitespace: true },
{
validator: validateUrlSlash
}
]}
noStyle
>
<Input
prefix={prefixForce ? `${apiPrefix}/` : '/'}
placeholder={$t(PLACEHOLDER.input)}
onChange={(e) => {
if ((e.target.value as string).endsWith('/*')) {
form.setFieldValue('path', e.target.value.slice(0, -2))
form.setFieldValue('pathMatch', 'prefix')
<Form.Item className="flex-1" label={$t('请求路径')}>
<Space.Compact block>
<Form.Item
name="pathMatch"
rules={[
{ required: true, whitespace: true },
{
validator: validateUrlSlash
}
}}
/>
</Form.Item>
</Space.Compact>
]}
noStyle
>
<Select
placeholder={$t(PLACEHOLDER.select)}
options={apiPathMatchRulesOptions}
className="w-[30%] min-w-[100px]"
/>
</Form.Item>
<Form.Item<AiServiceRouterField>
name="path"
rules={[
{ required: true, whitespace: true },
{
validator: validateUrlSlash
}
]}
noStyle
>
<Input
prefix={prefixForce ? `${apiPrefix}/` : '/'}
placeholder={$t(PLACEHOLDER.input)}
onChange={(e) => {
if ((e.target.value as string).endsWith('/*')) {
form.setFieldValue('path', e.target.value.slice(0, -2))
form.setFieldValue('pathMatch', 'prefix')
}
}}
/>
</Form.Item>
</Space.Compact>
</Form.Item>
</Row>
<Form.Item<AiServiceRouterField> label={$t('提示词')} name="prompt">
<PromptEditorResizable disabled={isDelete} variablesChange={handleVariablesChange} promptVariables={variablesTable} />
</Form.Item>
</Row>
<Form.Item<AiServiceRouterField> label={$t('提示词')} name="prompt">
<PromptEditorResizable variablesChange={handleVariablesChange} promptVariables={variablesTable} />
</Form.Item>
<Form.Item<AiServiceRouterField>
label={
<div className="flex justify-between items-center w-full">
<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 justify-between items-center w-full gap-btnbase">
<Form.Item<AiServiceRouterField>
className="flex-1"
label={$t('请求超时时间')}
name={'timeout'}
rules={[{ required: true }]}
label={
<div className="flex justify-between items-center w-full">
<span>{$t('变量')}</span>
<a
className={`flex items-center gap-[4px] ${isDelete ? 'cursor-not-allowed' : ''}`}
onClick={addVariable}
>
<Icon icon="ic:baseline-add" width={16} height={16} />
New
</a>
</div>
}
name="variables"
className="[&>.ant-row>.ant-col>label]:w-full"
>
<InputNumber className="w-INPUT_NORMAL" suffix="ms" min={1} placeholder={$t(PLACEHOLDER.input)} />
<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 justify-between items-center w-full gap-btnbase">
<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>
<Form.Item<AiServiceRouterField>
className="flex-1"
label={$t('重试次数')}
name={'retry'}
rules={[{ required: true }]}
label={$t('拦截接口')}
name="disabled"
extra={$t('开启拦截后,网关会拦截所有该路径的请求。')}
>
<InputNumber className="w-INPUT_NORMAL" min={0} placeholder={$t(PLACEHOLDER.input)} />
<Switch />
</Form.Item>
</Row>
<Form.Item<AiServiceRouterField>
label={$t('拦截接口')}
name="disabled"
extra={$t('开启拦截后,网关会拦截所有该路径的请求。')}
>
<Switch />
</Form.Item>
</div>
</Form>
</div>
</Form>
</WithPermission>
</Spin>
<DrawerWithFooter title={$t('模型配置')} open={open} onClose={onClose} onSubmit={() => handlerSubmit()}>
<AiServiceRouterModelConfig ref={drawerAddFormRef} llmList={llmList} entity={defaultLlm!} />
@@ -31,7 +31,7 @@ export default function CustomEdge({
{label && (
<EdgeLabelRenderer>
<a
href={`${label?.toString().includes('apis') ? '/aiapis' : '/keysetting'}?modelId=${modelId}`}
href={`${label?.toString().includes('apis') ? '/aiApis' : '/keysetting'}?modelId=${modelId}`}
target="_blank"
style={{
position: 'absolute',