mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: online model settings
This commit is contained in:
@@ -1,269 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { BasicResponse } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import {
|
||||
CoordinateExtent,
|
||||
Edge,
|
||||
EdgeTypes,
|
||||
Node,
|
||||
NodeTypes,
|
||||
PanOnScrollMode,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState
|
||||
} from '@xyflow/react'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import { Button, Space, Spin } from 'antd'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import CustomEdge from './components/CustomEdge'
|
||||
import { KeyStatusNode } from './components/KeyStatusNode'
|
||||
import { ModelCardNode } from './components/ModelCardNode'
|
||||
import { ServiceCardNode } from './components/NodeComponents'
|
||||
import { LAYOUT } from './constants'
|
||||
import './styles.css'
|
||||
import { ModelListData } from './types'
|
||||
|
||||
export type ApiResponse = BasicResponse<{
|
||||
backup: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
providers: ModelListData[]
|
||||
}>
|
||||
|
||||
const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => {
|
||||
return models.reduce(
|
||||
(acc, model, index) => {
|
||||
const y = startY + index * gap
|
||||
return {
|
||||
...acc,
|
||||
[model.id]: {
|
||||
x: LAYOUT.MODEL_NODE_X,
|
||||
y
|
||||
},
|
||||
[`${model.id}-keys`]: {
|
||||
x: LAYOUT.KEY_NODE_X,
|
||||
y: y + 16
|
||||
}
|
||||
}
|
||||
},
|
||||
{} as Record<string, { x: number; y: number }>
|
||||
)
|
||||
}
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
modelCard: ModelCardNode,
|
||||
keyCard: KeyStatusNode,
|
||||
serviceCard: ServiceCardNode
|
||||
} as const
|
||||
|
||||
const edgeTypes: EdgeTypes = {
|
||||
custom: CustomEdge
|
||||
}
|
||||
|
||||
const AIFlowChart = () => {
|
||||
const [modelData, setModelData] = useState<ModelListData[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
|
||||
const { fetchData } = useFetch()
|
||||
const { aiConfigFlushed } = useGlobalContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchData<ApiResponse>('ai/providers/configured', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_llm']
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
.then((response) => {
|
||||
const mockApiResponse: ApiResponse = response as ApiResponse
|
||||
setModelData(mockApiResponse.data.providers)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}, [aiConfigFlushed])
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelData.length) return
|
||||
|
||||
const positions = calculateNodePositions(modelData)
|
||||
const firstSuccessModel = modelData.find((model) => model.status === 'enabled')
|
||||
console.log(firstSuccessModel)
|
||||
|
||||
// subtract 5 to make sure the service node is aligned with the top model node
|
||||
const serviceY = positions[modelData[0].id].y - 5
|
||||
const newNodes = [
|
||||
{
|
||||
id: 'apiService',
|
||||
type: 'serviceCard',
|
||||
position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY },
|
||||
draggable: false,
|
||||
data: {
|
||||
title: 'API Services',
|
||||
count: modelData.length
|
||||
}
|
||||
},
|
||||
...modelData.map((model) => ({
|
||||
id: model.id,
|
||||
type: 'modelCard',
|
||||
position: positions[model.id],
|
||||
data: {
|
||||
name: model.name,
|
||||
status: model.status,
|
||||
defaultLlm: model.defaultLlm,
|
||||
logo: model.logo,
|
||||
id: model.id,
|
||||
alternativeModel: firstSuccessModel
|
||||
}
|
||||
})),
|
||||
...modelData.map((model) => ({
|
||||
id: `${model.id}-keys`,
|
||||
type: 'keyCard',
|
||||
position: positions[`${model.id}-keys`],
|
||||
data: {
|
||||
title: '',
|
||||
keys: (model.keys || []).map((key, index) => ({
|
||||
id: key.id,
|
||||
status: key.status,
|
||||
priority: index + 1
|
||||
}))
|
||||
}
|
||||
}))
|
||||
]
|
||||
|
||||
const newEdges: any = [
|
||||
...modelData.map((model) => ({
|
||||
id: `service-${model.id}`,
|
||||
source: 'apiService',
|
||||
target: model.id,
|
||||
label: `${model.api_count} apis`,
|
||||
data: {
|
||||
id: model.id,
|
||||
status: model.status
|
||||
},
|
||||
animated: true,
|
||||
style: { stroke: model.status === 'enabled' ? '#52c41a' : '#ff4d4f' }
|
||||
})),
|
||||
...modelData.map((model) => ({
|
||||
id: `${model.id}-keys-edge`,
|
||||
source: model.id,
|
||||
target: `${model.id}-keys`,
|
||||
label: `${model.key_count} keys`,
|
||||
data: { id: model.id },
|
||||
animated: true
|
||||
}))
|
||||
]
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
}, [modelData])
|
||||
|
||||
const calculateExtent = useCallback(() => {
|
||||
const left = LAYOUT.SERVICE_NODE_X
|
||||
const right = LAYOUT.KEY_NODE_X
|
||||
const top = 0 // Allow slight negative scroll to reduce top padding
|
||||
const bottom = LAYOUT.NODE_START_Y + modelData.length * LAYOUT.NODE_GAP
|
||||
return [
|
||||
[left, top],
|
||||
[right, bottom < 100 ? 5000 : bottom]
|
||||
] as CoordinateExtent
|
||||
}, [modelData.length])
|
||||
|
||||
const updateProviderOrder = async (sortedProviderIds: string[]) => {
|
||||
await fetchData('ai/provider/sort', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
providers: sortedProviderIds
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const onNodeDragStop: any = useCallback((_: any, node: Node<any>) => {
|
||||
if (node.type !== 'modelCard') return
|
||||
|
||||
setNodes((nds) => {
|
||||
const modelNodes = nds.filter((n) => n.type === 'modelCard')
|
||||
const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y)
|
||||
const sortedProviderIds = sortedNodes.map((node) => node.id)
|
||||
|
||||
// Update provider order outside of setNodes callback
|
||||
updateProviderOrder(sortedProviderIds)
|
||||
// Update all node positions in a single pass
|
||||
return nds.map((n) => {
|
||||
if (n.type === 'modelCard') {
|
||||
const index = sortedNodes.findIndex((sn) => sn.id === n.id)
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: LAYOUT.MODEL_NODE_X,
|
||||
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n.type === 'keyCard') {
|
||||
const modelId = n.id.replace('-keys', '')
|
||||
const modelNode = sortedNodes.find((mn) => mn.id === modelId)
|
||||
if (modelNode) {
|
||||
const index = sortedNodes.findIndex((sn) => sn.id === modelId)
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: LAYOUT.KEY_NODE_X,
|
||||
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return n
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : modelData.length === 0 ? (
|
||||
<Space className="flex flex-col justify-center items-center h-[200px]">
|
||||
<div>{$t('未配置 AI 模型')}</div>
|
||||
<Button type="primary" onClick={() => navigate('/aisetting?status=unconfigure')}>
|
||||
{$t('前往设置')}
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
draggable={false}
|
||||
nodeTypes={nodeTypes}
|
||||
elementsSelectable={false}
|
||||
edgeTypes={edgeTypes}
|
||||
zoomOnScroll={false}
|
||||
panOnDrag={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnScroll={true}
|
||||
panOnScrollMode={PanOnScrollMode.Vertical}
|
||||
defaultEdgeOptions={{
|
||||
type: 'custom'
|
||||
}}
|
||||
translateExtent={calculateExtent()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIFlowChart
|
||||
@@ -1,136 +0,0 @@
|
||||
import Icon, { LoadingOutlined } from '@ant-design/icons'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { App, Button, Card, Empty, Spin, Tag } from 'antd'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAiSetting } from './contexts/AiSettingContext'
|
||||
import { AiSettingListItem } from './types'
|
||||
|
||||
const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => {
|
||||
const { openConfigModal } = useAiSetting()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleOpenModal = async (provider: AiSettingListItem) => {
|
||||
await openConfigModal(provider)
|
||||
navigate('/aisetting?status=configure')
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<div className="flex w-full items-center justify-between gap-[4px]">
|
||||
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
|
||||
<span
|
||||
className=" flex items-center h-[22px] ai-setting-svg-container"
|
||||
dangerouslySetInnerHTML={{ __html: provider.logo }}
|
||||
></span>
|
||||
<span className="font-normal truncate">{provider.name}</span>
|
||||
</div>
|
||||
<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 justify-between h-full gap-btnbase">
|
||||
<div className="flex items-center w-full h-[32px] flex-1">
|
||||
{provider.configured && (
|
||||
<>
|
||||
<label className="text-nowrap">{$t('默认')}:</label>
|
||||
<span className="overflow-hidden flex-1 truncate">{provider.defaultLlm}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<WithPermission access="system.settings.ai_provider.view">
|
||||
<Button
|
||||
block
|
||||
icon={<Icon icon="ic:outline-settings" width={18} height={18} />}
|
||||
onClick={() => handleOpenModal(provider)}
|
||||
classNames={{ icon: 'h-[18px]' }}
|
||||
>
|
||||
{$t('设置')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => {
|
||||
return (
|
||||
<>
|
||||
{modelList.length > 0 ? (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '20px'
|
||||
}}
|
||||
>
|
||||
{modelList.map((provider: AiSettingListItem) => (
|
||||
<CardBox key={provider.id} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const AIUnConfigure = () => {
|
||||
const [modelData, setModelData] = useState<AiSettingListItem[]>([])
|
||||
const { fetchData } = useFetch()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const { aiConfigFlushed } = useGlobalContext()
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
fetchData<BasicResponse<{ providers: Omit<AiSettingListItem>[] }>>(`ai/providers/unconfigured`, {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_llm', 'default_llm_logo']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setModelData(data.providers)
|
||||
} else {
|
||||
const { message } = App.useApp()
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [aiConfigFlushed])
|
||||
|
||||
return (
|
||||
<Spin
|
||||
className="h-full"
|
||||
wrapperClassName="h-full pr-PAGE_INSIDE_X"
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
|
||||
spinning={loading}
|
||||
>
|
||||
{modelData && modelData.length > 0 ? (
|
||||
<div>
|
||||
{modelData.filter((item) => !item.configured).length > 0 && (
|
||||
<>
|
||||
<ModelCardArea modelList={modelData.filter((item) => !item.configured) || []} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
export default AIUnConfigure
|
||||
@@ -3,9 +3,8 @@ import { useI18n } from '@common/locales'
|
||||
import { Tabs } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import AIFlowChart from './AIFlowChart'
|
||||
import AIUnConfigure from './AIUnconfigure'
|
||||
import { AiSettingProvider } from './contexts/AiSettingContext'
|
||||
import OnlineModelList from './OnlineModelList'
|
||||
|
||||
const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const
|
||||
|
||||
@@ -38,21 +37,17 @@ const AiSettingContent = () => {
|
||||
items={[
|
||||
{
|
||||
key: 'flow',
|
||||
label: $t('已设置'),
|
||||
label: $t('在线模型'),
|
||||
children: (
|
||||
<div className="overflow-auto" style={CONTENT_STYLE}>
|
||||
<AIFlowChart />
|
||||
<OnlineModelList />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: $t('未设置'),
|
||||
children: (
|
||||
<div className="overflow-auto" style={CONTENT_STYLE}>
|
||||
<AIUnConfigure />
|
||||
</div>
|
||||
)
|
||||
label: $t('本地模型'),
|
||||
children: <div className="overflow-auto" style={CONTENT_STYLE}></div>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { AIProvider } from '@core/components/AIProviderSelect'
|
||||
import { App, Divider, Space, Typography } from 'antd'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useAiSetting } from './contexts/AiSettingContext'
|
||||
import { AiSettingListItem, ModelListData } from './types'
|
||||
|
||||
const OnlineModelList: React.FC = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { modal, message } = App.useApp()
|
||||
const [provider, setProvider] = useState<AIProvider | undefined>()
|
||||
const { fetchData } = useFetch()
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const [total, setTotal] = useState<number>(0)
|
||||
const modalRef = useRef<any>()
|
||||
const { accessData } = useGlobalContext()
|
||||
const { openConfigModal } = useAiSetting()
|
||||
|
||||
const handleEdit = (record: ModelListData) => {
|
||||
openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem)
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
// openConfigModal()
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<any>>('ai/resource/key', {
|
||||
method: 'DELETE',
|
||||
eoParams: {
|
||||
id: id,
|
||||
branchID: 0
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('删除成功'))
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || RESPONSE_TIPS.error)
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
}
|
||||
}
|
||||
|
||||
const requestList = async (params: any) => {
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<{ data: ModelListData[] }>>('ai/providers/configured', {
|
||||
method: 'GET',
|
||||
eoParams: {
|
||||
page_size: params.pageSize,
|
||||
keyword: searchWord,
|
||||
page: params.current
|
||||
}
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
console.log(response)
|
||||
setTotal(response.data.total)
|
||||
return {
|
||||
data: response.data.providers,
|
||||
success: true,
|
||||
total: response.data.total
|
||||
}
|
||||
} else {
|
||||
message.error(response.msg || $t(RESPONSE_TIPS.error))
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
total: response.data.total
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
const statusEnum = {
|
||||
enabled: { text: <Typography.Text type="success">{$t('正常')}</Typography.Text> },
|
||||
disabled: { text: <Typography.Text type="warning">{$t('停用')}</Typography.Text> },
|
||||
abnormal: { text: <Typography.Text type="danger">{$t('异常')}</Typography.Text> }
|
||||
}
|
||||
|
||||
const operation: PageProColumns<ModelListData>[] = [
|
||||
{
|
||||
title: '',
|
||||
key: 'option',
|
||||
btnNums: 4,
|
||||
fixed: 'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: ModelListData) => [
|
||||
<TableBtnWithPermission
|
||||
access="system.devops.ai_provider.edit"
|
||||
key="edit"
|
||||
btnType="edit"
|
||||
onClick={() => handleEdit(entity)}
|
||||
btnTitle={$t('设置')}
|
||||
/>,
|
||||
<Divider type="vertical" className="mx-0" />,
|
||||
<TableBtnWithPermission
|
||||
access="system.devops.ai_provider.edit"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => handleDelete(entity.id as string)}
|
||||
btnTitle={$t('删除')}
|
||||
/>
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const columns: PageProColumns<ModelListData>[] = [
|
||||
{
|
||||
title: $t('名称'),
|
||||
dataIndex: 'name',
|
||||
render: (dom: React.ReactNode, entity: ModelListData) => <Space>{entity.name}</Space>
|
||||
},
|
||||
{
|
||||
title: $t('状态'),
|
||||
dataIndex: 'status',
|
||||
ellipsis: true,
|
||||
valueType: 'select',
|
||||
filters: true,
|
||||
onFilter: true,
|
||||
valueEnum: statusEnum,
|
||||
render: (dom: React.ReactNode, entity: ModelListData) => statusEnum[entity.status]?.text || entity.status
|
||||
},
|
||||
{
|
||||
title: $t('默认模型'),
|
||||
dataIndex: 'defaultLlm'
|
||||
},
|
||||
{
|
||||
title: $t('Apis'),
|
||||
dataIndex: 'api_count'
|
||||
},
|
||||
{
|
||||
title: $t('Keys'),
|
||||
dataIndex: 'key_count'
|
||||
},
|
||||
...operation
|
||||
]
|
||||
|
||||
return (
|
||||
<PageList
|
||||
ref={pageListRef}
|
||||
rowKey="id"
|
||||
request={requestList}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
pageListRef.current?.reload()
|
||||
}}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入名称搜索')}
|
||||
columns={columns}
|
||||
dragSortKey="drag"
|
||||
addNewBtnTitle={$t('添加模型')}
|
||||
onAddNewBtnClick={handleAdd}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnlineModelList
|
||||
@@ -1,72 +0,0 @@
|
||||
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useStore } from '@xyflow/react'
|
||||
|
||||
export default function CustomEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
label,
|
||||
data,
|
||||
source,
|
||||
target
|
||||
}: EdgeProps) {
|
||||
// Get all edges to check for duplicates
|
||||
const edges = useStore((state) => state.edges)
|
||||
|
||||
// Find duplicate edges between the same source and target
|
||||
const duplicateEdges = edges.filter((edge) => edge.source === source && edge.target === target)
|
||||
const edgeIndex = duplicateEdges.findIndex((edge) => edge.id === id)
|
||||
|
||||
// Adjust the path if this is a duplicate edge
|
||||
const offset = edgeIndex * 20 // 20px offset for each duplicate edge
|
||||
|
||||
const [edgePath] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY: sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY: targetY + offset,
|
||||
targetPosition,
|
||||
borderRadius: 16
|
||||
})
|
||||
|
||||
const modelId = data?.id
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
...style,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
{label && (
|
||||
<EdgeLabelRenderer>
|
||||
<a
|
||||
href={`${label?.toString().includes('apis') ? '/aiApis' : '/keysetting'}?modelId=${modelId}`}
|
||||
target="_blank"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(${targetX - 80}px,${targetY - 20 + offset}px)`,
|
||||
borderRadius: '4px',
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'all',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import React from 'react'
|
||||
import { KeyData } from '../types'
|
||||
|
||||
interface KeyStatusNodeData {
|
||||
id: string
|
||||
title: string
|
||||
keys: KeyData[]
|
||||
}
|
||||
|
||||
const KEY_SIZE = '1.25rem' // 20px
|
||||
const KEY_GAP = '0.25rem' // 4px
|
||||
const MAX_KEYS = 10
|
||||
|
||||
export const KeyStatusNode: React.FC<{ data: KeyStatusNodeData }> = ({ data }) => {
|
||||
const { title, keys = [] } = data
|
||||
const totalKeys = keys.length
|
||||
const keyWidth = totalKeys > 5 ? `calc((100% - ${(totalKeys - 1) * 0.25}rem) / ${totalKeys})` : KEY_SIZE
|
||||
return (
|
||||
<div
|
||||
className="relative p-4 bg-white rounded-lg shadow-sm node-card nodrag"
|
||||
style={{ border: '1px solid var(--border-color)' }}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm text-gray-900">{title}</div>
|
||||
<div
|
||||
className="flex gap-1 w-full"
|
||||
style={{
|
||||
minWidth: keys.length > 5 ? '118px' : 'auto',
|
||||
maxWidth: `calc(${MAX_KEYS} * ${KEY_SIZE} + (${MAX_KEYS} - 1) * ${KEY_GAP})`,
|
||||
minHeight: KEY_SIZE
|
||||
}}
|
||||
>
|
||||
{keys.map((key) => (
|
||||
<div
|
||||
key={key.id}
|
||||
style={{
|
||||
width: keyWidth,
|
||||
height: KEY_SIZE
|
||||
}}
|
||||
className={`
|
||||
flex-shrink-0
|
||||
${key.status === 'normal' ? 'bg-green-500' : 'bg-red-500'}
|
||||
transition-all duration-200 hover:opacity-80
|
||||
`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { $t } from '@common/locales'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import React from 'react'
|
||||
import { useAiSetting } from '../contexts/AiSettingContext'
|
||||
import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types'
|
||||
|
||||
type ModelCardNodeData = ModelDetailData & {
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
alternativeModel?: ModelDetailData
|
||||
}
|
||||
|
||||
export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => {
|
||||
const { name, status, defaultLlm, logo, alternativeModel } = data
|
||||
const { openConfigModal } = useAiSetting()
|
||||
|
||||
const getStatusIcon = (status: ModelStatus) => {
|
||||
switch (status) {
|
||||
case 'enabled':
|
||||
return { icon: 'mdi:check-circle', color: 'text-green-500' }
|
||||
case 'disabled':
|
||||
return { icon: 'mdi:pause-circle', color: 'text-gray-400' }
|
||||
case 'abnormal':
|
||||
return { icon: 'mdi:alert-circle', color: 'text-red-500' }
|
||||
}
|
||||
}
|
||||
|
||||
const statusConfig = getStatusIcon(status)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] group"
|
||||
style={{ border: '1px solid var(--border-color)' }}
|
||||
>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<Handle type="source" position={Position.Right} />
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
|
||||
<span
|
||||
className="flex items-center h-[22px] ai-setting-svg-container"
|
||||
dangerouslySetInnerHTML={{ __html: logo }}
|
||||
></span>
|
||||
</div>
|
||||
<span className="text-base text-gray-900 max-w-[180px] truncate">{name}</span>
|
||||
<Icon icon={statusConfig?.icon} className={`text-xl ${statusConfig?.color}`} />
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2 transition-opacity duration-200">
|
||||
<Icon
|
||||
icon="mdi:cog"
|
||||
className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]"
|
||||
onClick={() => {
|
||||
openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{$t('默认:')}
|
||||
{defaultLlm}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{status !== 'enabled' && alternativeModel && (
|
||||
<div className="ml-4 mt-1 text-sm text-gray-500">
|
||||
{$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { KeyStatusNode } from './KeyStatusNode'
|
||||
export { ModelCardNode } from './ModelCardNode'
|
||||
export { ServiceCardNode } from './ServiceCardNode'
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react'
|
||||
import React from 'react'
|
||||
|
||||
export const ServiceCardNode: React.FC<NodeProps> = () => {
|
||||
return (
|
||||
<div
|
||||
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[150px] nodrag"
|
||||
style={{ border: '1px solid var(--border-color)' }}
|
||||
>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<Icon icon="mdi:robot" className="text-3xl text-[--primary-color]" />
|
||||
<span className="text-base text-gray-900">AI Services</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import AIFlowChart from '../aiSetting/AIFlowChart'
|
||||
|
||||
export default function Playground() {
|
||||
return <AIFlowChart />
|
||||
return <iframe src="/playground" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user