diff --git a/frontend/packages/core/src/pages/playground/components/KeyStatusNode.tsx b/frontend/packages/core/src/pages/playground/components/KeyStatusNode.tsx index 1b125274..8a7bc6b6 100644 --- a/frontend/packages/core/src/pages/playground/components/KeyStatusNode.tsx +++ b/frontend/packages/core/src/pages/playground/components/KeyStatusNode.tsx @@ -1,16 +1,10 @@ import { Handle, Position } from '@xyflow/react' import React from 'react' -import { ModelCardStatus } from './types' - -interface KeyStatus { - status: ModelCardStatus - keyID: number | string - priority?: number -} +import { KeyData } from './types' interface KeyStatusNodeData { title: string - keys: KeyStatus[] + keys: KeyData[] } const KEY_SIZE = '1.25rem' // 20px @@ -37,22 +31,20 @@ export const KeyStatusNode: React.FC<{ data: KeyStatusNodeData }> = ({ data }) = minHeight: KEY_SIZE }} > - {keys - .sort((a, b) => (a.priority || 0) - (b.priority || 0)) - .map((key) => ( -
( +
- ))} + /> + ))}
diff --git a/frontend/packages/core/src/pages/playground/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/playground/components/ModelCardNode.tsx index 87a7aa42..01571c8b 100644 --- a/frontend/packages/core/src/pages/playground/components/ModelCardNode.tsx +++ b/frontend/packages/core/src/pages/playground/components/ModelCardNode.tsx @@ -1,13 +1,14 @@ import { Icon } from '@iconify/react' import { Handle, Position } from '@xyflow/react' -import React, { useCallback, useState } from 'react' -import { ModelCardStatus } from './types' +import { Avatar } from 'antd' +import React from 'react' +import { ModelStatus } from './types' interface ModelCardData { title: string - status: ModelCardStatus + status: ModelStatus + logo: string defaultModel: string - onDragStart?: () => void } type ModelCardNodeData = ModelCardData & { @@ -16,44 +17,39 @@ type ModelCardNodeData = ModelCardData & { } export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => { - const [isHovered, setIsHovered] = useState(false) - const { title, status, defaultModel } = data - - const onDragHandleMouseDown = useCallback((event: React.MouseEvent) => { - // Prevent event propagation to allow dragging - event.stopPropagation() - - // Create a new drag event - const dragEvent = new MouseEvent('mousedown', { - clientX: event.clientX, - clientY: event.clientY, - bubbles: true - }) - // Find the node element and dispatch the event - const nodeElement = event.currentTarget.closest('.react-flow__node') - if (nodeElement) { - // Use the global `document` object if it exists - nodeElement.dispatchEvent(dragEvent) - } - }, []) + const { title, status, defaultModel, logo } = data return (
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} >
- + + ) : undefined + } + icon={logo ? '' : } + > + {' '} + {title}
diff --git a/frontend/packages/core/src/pages/playground/components/types.ts b/frontend/packages/core/src/pages/playground/components/types.ts index 73eff4fc..e360509d 100644 --- a/frontend/packages/core/src/pages/playground/components/types.ts +++ b/frontend/packages/core/src/pages/playground/components/types.ts @@ -1,9 +1,20 @@ -export type ModelCardStatus = 'success' | 'failure' +export type ModelStatus = 'enable' | 'abnormal'|'disable' +export type KeyStatus ='normal' | 'abnormal'|'disable' + +export interface KeyData { + id: string + name: string + status: KeyStatus, +} export interface ModelData { id: string - type: string - title: string - status: ModelCardStatus - defaultModel: string + name: string + logo: string + default_llm: string + status: ModelStatus + api_count: number + key_count: number + keys: KeyData[] + priority?: number } diff --git a/frontend/packages/core/src/pages/playground/index.tsx b/frontend/packages/core/src/pages/playground/index.tsx index 53dd27b2..1561ca59 100644 --- a/frontend/packages/core/src/pages/playground/index.tsx +++ b/frontend/packages/core/src/pages/playground/index.tsx @@ -2,20 +2,23 @@ import { addEdge, NodeTypes, ReactFlow, useEdgesState, useNodesState } from '@xyflow/react' import '@xyflow/react/dist/style.css' -import { useCallback } from 'react' +import { useCallback, useEffect, useState } from 'react' import { KeyStatusNode, ModelCardNode, ServiceCardNode } from './components/NodeComponents' import { ModelData } from './components/types' import { LAYOUT } from './constants' import './styles.css' -const modelData: ModelData[] = [ - { id: 'openai', type: 'openai', title: 'OpenAI', status: 'success', defaultModel: 'gpt-4' }, - { id: 'anthropic', type: 'anthropic', title: 'Anthropic', status: 'success', defaultModel: 'claude-2' }, - { id: 'gemini', type: 'gemini', title: 'Google Gemini', status: 'failure', defaultModel: 'gemini-pro' }, - { id: 'mistral', type: 'mistral', title: 'Mistral AI', status: 'success', defaultModel: 'mistral-medium' }, - { id: 'cohere', type: 'cohere', title: 'Cohere', status: 'success', defaultModel: 'command' }, - { id: 'azure', type: 'azure', title: 'Azure OpenAI', status: 'success', defaultModel: 'gpt-4-turbo' } -] +interface ApiResponse { + data: { + backup: { + id: string + name: string + } + providers: ModelData[] + } + code: number + success: string +} const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { return models.reduce( @@ -40,209 +43,339 @@ const nodeTypes: NodeTypes = { serviceCard: ServiceCardNode } as const -const initialNodes = [ - { - id: 'service', - type: 'serviceCard', - position: { x: LAYOUT.SERVICE_NODE_X, y: LAYOUT.NODE_START_Y }, - data: {} - }, - ...modelData.map((model) => ({ - id: model.id, - type: 'modelCard', - position: calculateNodePositions(modelData)[model.id], - data: { - title: model.title, - status: model.status, - defaultModel: model.defaultModel - } - })), - { - id: 'openai-keys', - type: 'keyCard', - position: calculateNodePositions(modelData)['openai-keys'], - data: { - title: 'API Keys', - keys: [ - { keyID: 1, status: 'success', priority: 1 }, - { keyID: 2, status: 'success', priority: 2 }, - { keyID: 3, status: 'failure', priority: 3 }, - { keyID: 4, status: 'success', priority: 4 }, - { keyID: 5, status: 'success', priority: 5 }, - { keyID: 6, status: 'failure', priority: 6 }, - { keyID: 7, status: 'success', priority: 7 }, - { keyID: 8, status: 'success', priority: 8 }, - { keyID: 9, status: 'failure', priority: 9 }, - { keyID: 10, status: 'success', priority: 10 }, - { keyID: 11, status: 'success', priority: 11 }, - { keyID: 12, status: 'failure', priority: 12 }, - { keyID: 13, status: 'success', priority: 13 }, - { keyID: 14, status: 'success', priority: 14 }, - { keyID: 15, status: 'failure', priority: 15 }, - { keyID: 16, status: 'success', priority: 16 }, - { keyID: 17, status: 'success', priority: 17 }, - { keyID: 18, status: 'failure', priority: 18 }, - { keyID: 19, status: 'success', priority: 19 }, - { keyID: 20, status: 'success', priority: 20 } - ] - } - }, - { - id: 'anthropic-keys', - type: 'keyCard', - position: calculateNodePositions(modelData)['anthropic-keys'], - data: { - title: 'API Keys', - keys: [ - { keyID: 1, status: 'success', priority: 1 }, - { keyID: 2, status: 'success', priority: 2 } - ] - } - }, - { - id: 'gemini-keys', - type: 'keyCard', - position: calculateNodePositions(modelData)['gemini-keys'], - data: { - title: 'API Keys', - keys: [ - { keyID: 1, status: 'failure', priority: 1 }, - { keyID: 2, status: 'failure', priority: 2 }, - { keyID: 3, status: 'failure', priority: 3 }, - { keyID: 4, status: 'failure', priority: 4 } - ] - } - }, - { - id: 'mistral-keys', - type: 'keyCard', - position: calculateNodePositions(modelData)['mistral-keys'], - data: { - title: 'API Keys', - keys: [] - } - }, - { - id: 'cohere-keys', - type: 'keyCard', - position: calculateNodePositions(modelData)['cohere-keys'], - data: { - title: 'API Keys', - keys: [ - { keyID: 1, status: 'failure', priority: 1 }, - { keyID: 2, status: 'success', priority: 2 }, - { keyID: 3, status: 'success', priority: 3 } - ] - } - }, - { - id: 'azure-keys', - type: 'keyCard', - position: calculateNodePositions(modelData)['azure-keys'], - data: { - title: 'API Keys', - keys: [ - { keyID: 1, status: 'success', priority: 1 }, - { keyID: 2, status: 'success', priority: 2 }, - { keyID: 3, status: 'success', priority: 3 } - ] - } - } -] - -const initialEdges = [ - { - id: 'service-openai', - source: 'service', - target: 'openai', - label: 'apis(12)', - style: { stroke: '#ddd', cursor: 'pointer' }, - data: { - endLabel: 'apis(12)' - // endLabelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, - // endLabelBgStyle: { fill: 'white', fillOpacity: 0.8 }, - }, - type: 'smoothstep', - markerEnd: { type: 'arrow' } - }, - { - id: 'service-anthropic', - source: 'service', - target: 'anthropic', - label: 'apis(8)', - style: { stroke: '#ddd', cursor: 'pointer' }, - labelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, - labelBgStyle: { fill: 'white', fillOpacity: 0.8 }, - labelBgPadding: [4, 2], - labelShowBg: true, - type: 'smoothstep', - markerEnd: { type: 'arrow' } - }, - { - id: 'service-gemini', - source: 'service', - target: 'gemini', - label: 'apis(5)', - style: { stroke: '#ddd', cursor: 'pointer' }, - labelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, - labelBgStyle: { fill: 'white', fillOpacity: 0.8 }, - labelBgPadding: [4, 2], - labelShowBg: true, - type: 'smoothstep', - markerEnd: { type: 'arrow' } - }, - { - id: 'service-mistral', - source: 'service', - target: 'mistral', - label: 'apis(4)', - style: { stroke: '#ddd', cursor: 'pointer' }, - labelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, - labelBgStyle: { fill: 'white', fillOpacity: 0.8 }, - labelBgPadding: [4, 2], - labelShowBg: true, - type: 'smoothstep', - markerEnd: { type: 'arrow' } - }, - { - id: 'service-cohere', - source: 'service', - target: 'cohere', - label: 'apis(6)', - style: { stroke: '#ddd', cursor: 'pointer' }, - labelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, - labelBgStyle: { fill: 'white', fillOpacity: 0.8 }, - labelBgPadding: [4, 2], - labelShowBg: true, - type: 'smoothstep', - markerEnd: { type: 'arrow' } - }, - { - id: 'service-azure', - source: 'service', - target: 'azure', - label: 'apis(10)', - style: { stroke: '#ddd', cursor: 'pointer' }, - labelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, - labelBgStyle: { fill: 'white', fillOpacity: 0.8 }, - labelBgPadding: [4, 2], - labelShowBg: true, - type: 'smoothstep', - markerEnd: { type: 'arrow' } - }, - ...modelData.map((model) => ({ - id: `${model.id}-keys`, - source: model.id, - type: 'smoothstep', - target: `${model.id}-keys`, - animated: true, - style: { stroke: '#ddd' } - })) -] - const Playground = () => { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) + const [modelData, setModelData] = useState([]) + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [edges, setEdges, onEdgesChange] = useEdgesState([]) + + useEffect(() => { + // Mock API call - replace with actual API call + const mockApiResponse: ApiResponse = { + code: 0, + msg: 'default_value', + data: { + providers: [ + { + id: '320000199612026216', + name: 'iFLYTEK SPARK', + logo: '\n\n \n\n', + status: 'enable', + default_llm: 'fakegpt-1.0', + api_count: 15, + key_count: 4, + keys: [ + { + id: '140000200804035469', + name: 'Margaret Perez', + status: 'normal' + }, + { + id: '460000198105142251', + name: 'Paul Moore', + status: 'disable' + }, + { + id: '640000198609159464', + name: 'Timothy Clark', + status: 'disable' + }, + { + id: '370000199901139021', + name: 'Mark Williams', + status: 'abnormal' + } + ] + }, + { + id: '640000200408193359', + name: 'OpenRouter', + logo: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', + status: 'disable', + default_llm: 'fakegpt-1.0', + api_count: 2, + key_count: 7, + keys: [ + { + id: '540000200004175839', + name: 'Maria Lewis', + status: 'abnormal' + }, + { + id: '510000199111048228', + name: 'Melissa Wilson', + status: 'disable' + } + ] + }, + { + id: '43000019760318266X', + name: '360 AI', + logo: '\n\n \n\n', + status: 'disable', + default_llm: 'fakegpt-1.0', + api_count: 3, + key_count: 5, + keys: [ + { + id: '150000201012143900', + name: 'Donald Johnson', + status: 'abnormal' + }, + { + id: '520000197104044707', + name: 'Daniel Perez', + status: 'normal' + }, + { + id: '230000200610198413', + name: 'Kimberly Lee', + status: 'abnormal' + } + ] + }, + { + id: '440000202405123330', + name: 'SiliconFlow', + logo: '\n\n\n\n\n\n\n\n\n\n\n', + status: 'abnormal', + default_llm: 'gpt-3.5-turbo-0125', + api_count: 6, + key_count: 6, + keys: [ + { + id: '500000199805052887', + name: 'Sharon Robinson', + status: 'abnormal' + }, + { + id: '110000198212026854', + name: 'Carol Thompson', + status: 'abnormal' + }, + { + id: '340000201911295145', + name: 'Mark Davis', + status: 'disable' + }, + { + id: '320000198302065610', + name: 'Donna Garcia', + status: 'disable' + } + ] + }, + { + id: '620000199406027251', + name: 'FakeGPT', + logo: '\n\n \n\n', + status: 'disable', + default_llm: 'gpt-3.5-turbo-0125', + api_count: 15, + key_count: 2, + keys: [ + { + id: '650000200812274267', + name: 'Jeffrey Taylor', + status: 'normal' + }, + { + id: '420000197301023899', + name: 'Ruth Jones', + status: 'abnormal' + }, + { + id: '820000201312084850', + name: 'Steven White', + status: 'disable' + } + ] + }, + { + id: '520000200005228695', + name: '360 AI', + logo: '\n\n \n\n', + status: 'enable', + default_llm: 'fakegpt-1.0', + api_count: 19, + key_count: 9, + keys: [ + { + id: '330000197006296104', + name: 'Karen Martinez', + status: 'abnormal' + }, + { + id: '420000199601313686', + name: 'Scott Davis', + status: 'disable' + } + ] + }, + { + id: '500000202103204115', + name: 'Google Gemini', + logo: '\n\n \n\n', + status: 'abnormal', + default_llm: 'gpt-3.5-turbo-0125', + api_count: 2, + key_count: 7, + keys: [ + { + id: '140000202101093813', + name: 'Jose Harris', + status: 'normal' + }, + { + id: '410000201802115211', + name: 'Richard Johnson', + status: 'abnormal' + } + ] + }, + { + id: '130000202402241558', + name: 'Google Gemini', + logo: '\n\n \n\n', + status: 'enable', + default_llm: 'gpt-3.5-turbo-0125', + api_count: 2, + key_count: 9, + keys: [ + { + id: '370000198910185957', + name: 'Christopher Thomas', + status: 'abnormal' + }, + { + id: '450000201505255688', + name: 'Mark Williams', + status: 'normal' + }, + { + id: '150000198812074566', + name: 'David Young', + status: 'disable' + } + ] + }, + { + id: '120000200209263834', + name: 'AWS Bedrock', + logo: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', + status: 'enable', + default_llm: 'fakegpt-1.0', + api_count: 6, + key_count: 1, + keys: [ + { + id: '310000200804263420', + name: 'Daniel Lewis', + status: 'normal' + }, + { + id: '370000201306032780', + name: 'Richard Walker', + status: 'abnormal' + }, + { + id: '500000200211133087', + name: 'Frank Jones', + status: 'disable' + }, + { + id: '350000199105019471', + name: 'Carol Wilson', + status: 'abnormal' + } + ] + }, + { + id: '110000198411203825', + name: 'deepseek', + logo: '\n\n\n\n\n\n\n\n\n\n\n', + status: 'abnormal', + default_llm: 'fakegpt-1.0', + api_count: 1, + key_count: 10, + keys: [ + { + id: '210000200509114330', + name: 'Jason Martin', + status: 'abnormal' + } + ] + } + ], + backup: '' + }, + msg_zh: 'default_value' + } + setModelData(mockApiResponse.data.providers) + }, []) + + useEffect(() => { + if (!modelData.length) return + + const newNodes = [ + { + id: 'service', + type: 'serviceCard', + position: { x: LAYOUT.SERVICE_NODE_X, y: LAYOUT.NODE_START_Y }, + data: {} + }, + ...modelData.map((model) => ({ + id: model.id, + type: 'modelCard', + position: calculateNodePositions(modelData)[model.id], + data: { + title: model.name, + status: model.status, + defaultModel: model.default_llm, + logo: model.logo + } + })), + ...modelData.map((model) => ({ + id: `${model.id}-keys`, + type: 'keyCard', + position: calculateNodePositions(modelData)[`${model.id}-keys`], + data: { + title: 'API Keys', + keys: model.keys.map((key, index) => ({ + keyID: key.id, + status: key.status === 'normal' ? 'enabled' : 'disable', + priority: index + 1 + })) + } + })) + ] + + const newEdges = [ + ...modelData.map((model) => ({ + id: `service-${model.id}`, + source: 'service', + target: model.id, + label: `apis(${model.api_count})`, + style: { stroke: '#ddd', cursor: 'pointer' }, + labelStyle: { fill: '#3d46f2', fontSize: 12, cursor: 'pointer' }, + labelBgStyle: { fill: 'white', fillOpacity: 0.8 }, + labelBgPadding: [4, 2], + labelShowBg: true, + type: 'smoothstep', + markerEnd: { type: 'arrow' } + })), + ...modelData.map((model) => ({ + id: `${model.id}-keys`, + source: model.id, + type: 'smoothstep', + target: `${model.id}-keys`, + animated: true, + style: { stroke: '#ddd' } + })) + ] + + setNodes(newNodes) + setEdges(newEdges) + }, [modelData]) const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]) @@ -250,7 +383,6 @@ const Playground = () => { (_: any, node: any) => { if (node.type !== 'modelCard') return - // Update positions of connected nodes during drag setNodes((nds) => { return nds.map((n) => { if (n.type === 'keyCard' && n.id === `${node.id}-keys`) { @@ -273,7 +405,6 @@ const Playground = () => { (_: any, node: any) => { if (node.type !== 'modelCard') return - // Reorder nodes based on vertical position setNodes((nds) => { const modelNodes = nds.filter((n) => n.type === 'modelCard') const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) @@ -307,18 +438,8 @@ const Playground = () => { onNodeDrag={onNodeDrag} onNodeDragStop={onNodeDragStop} nodeTypes={nodeTypes} - defaultEdgeOptions={{ - type: 'step', - style: { stroke: '#000', strokeWidth: 2 }, - animated: true - }} fitView - nodesDraggable={true} - elementsSelectable={false} - nodesConnectable={false} - zoomOnScroll={false} - zoomOnPinch={false} - zoomOnDoubleClick={false} + attributionPosition="bottom-left" />
)