diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx deleted file mode 100644 index dfd90d5c..00000000 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ /dev/null @@ -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 - ) -} - -const nodeTypes: NodeTypes = { - modelCard: ModelCardNode, - keyCard: KeyStatusNode, - serviceCard: ServiceCardNode -} as const - -const edgeTypes: EdgeTypes = { - custom: CustomEdge -} - -const AIFlowChart = () => { - const [modelData, setModelData] = useState([]) - const [loading, setLoading] = useState(false) - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - const { fetchData } = useFetch() - const { aiConfigFlushed } = useGlobalContext() - const navigate = useNavigate() - - useEffect(() => { - setLoading(true) - fetchData('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) => { - 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 ( -
- {loading ? ( -
- -
- ) : modelData.length === 0 ? ( - -
{$t('未配置 AI 模型')}
- -
- ) : ( - - )} -
- ) -} - -export default AIFlowChart diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx deleted file mode 100644 index ba1ae334..00000000 --- a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx +++ /dev/null @@ -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 ( - -
- - {provider.name} -
- - {provider.configured ? $t('已配置') : $t('未配置')} - - - } - className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " - classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} - > -
-
- {provider.configured && ( - <> - - {provider.defaultLlm} - - )} -
- - - -
-
- ) -}) -const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { - return ( - <> - {modelList.length > 0 ? ( -
- {modelList.map((provider: AiSettingListItem) => ( - - ))} -
- ) : ( - - )} - - ) -} - -const AIUnConfigure = () => { - const [modelData, setModelData] = useState([]) - const { fetchData } = useFetch() - const [loading, setLoading] = useState(false) - const { aiConfigFlushed } = useGlobalContext() - - useEffect(() => { - setLoading(true) - fetchData[] }>>(`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 ( - } - spinning={loading} - > - {modelData && modelData.length > 0 ? ( -
- {modelData.filter((item) => !item.configured).length > 0 && ( - <> - !item.configured) || []} /> - - )} -
- ) : ( - - )} -
- ) -} -export default AIUnConfigure diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index aa7034d2..3a7151ab 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -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: (
- +
) }, { key: 'config', - label: $t('未设置'), - children: ( -
- -
- ) + label: $t('本地模型'), + children:
} ]} /> diff --git a/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx new file mode 100644 index 00000000..3170911b --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx @@ -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(null) + const { modal, message } = App.useApp() + const [provider, setProvider] = useState() + const { fetchData } = useFetch() + const [searchWord, setSearchWord] = useState('') + const [total, setTotal] = useState(0) + const modalRef = useRef() + 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>('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>('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: {$t('正常')} }, + disabled: { text: {$t('停用')} }, + abnormal: { text: {$t('异常')} } + } + + const operation: PageProColumns[] = [ + { + title: '', + key: 'option', + btnNums: 4, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: ModelListData) => [ + handleEdit(entity)} + btnTitle={$t('设置')} + />, + , + handleDelete(entity.id as string)} + btnTitle={$t('删除')} + /> + ] + } + ] + + const columns: PageProColumns[] = [ + { + title: $t('名称'), + dataIndex: 'name', + render: (dom: React.ReactNode, entity: ModelListData) => {entity.name} + }, + { + 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 ( + { + setSearchWord(e.target.value) + pageListRef.current?.reload() + }} + showPagination={true} + searchPlaceholder={$t('请输入名称搜索')} + columns={columns} + dragSortKey="drag" + addNewBtnTitle={$t('添加模型')} + onAddNewBtnClick={handleAdd} + /> + ) +} + +export default OnlineModelList diff --git a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx deleted file mode 100644 index 35d585f5..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx +++ /dev/null @@ -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 ( - <> - - {label && ( - - - {label} - - - )} - - ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx deleted file mode 100644 index ef24aee6..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx +++ /dev/null @@ -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 ( -
- -
-
{title}
-
5 ? '118px' : 'auto', - maxWidth: `calc(${MAX_KEYS} * ${KEY_SIZE} + (${MAX_KEYS} - 1) * ${KEY_GAP})`, - minHeight: KEY_SIZE - }} - > - {keys.map((key) => ( -
- ))} -
-
-
- ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx deleted file mode 100644 index ede8936b..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ /dev/null @@ -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 ( - <> -
- - -
-
-
-
- -
- {name} - -
- - {/* Action buttons */} -
- { - openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) - }} - /> -
-
-
- {$t('默认:')} - {defaultLlm} -
-
-
- {status !== 'enabled' && alternativeModel && ( -
- {$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm} -
- )} - - ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx b/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx deleted file mode 100644 index e2626cc2..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { KeyStatusNode } from './KeyStatusNode' -export { ModelCardNode } from './ModelCardNode' -export { ServiceCardNode } from './ServiceCardNode' diff --git a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx deleted file mode 100644 index 176d80da..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx +++ /dev/null @@ -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 = () => { - return ( -
- -
- - AI Services -
-
- ) -} diff --git a/frontend/packages/core/src/pages/playground/index.tsx b/frontend/packages/core/src/pages/playground/index.tsx index 12f5ec43..12a1b689 100644 --- a/frontend/packages/core/src/pages/playground/index.tsx +++ b/frontend/packages/core/src/pages/playground/index.tsx @@ -1,7 +1,5 @@ 'use client' -import AIFlowChart from '../aiSetting/AIFlowChart' - export default function Playground() { - return + return