From 4e75a9b3b364052a6f82472d410ee120f558650e Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Fri, 8 Aug 2025 02:59:45 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Improve=20models=20UX=20and?= =?UTF-8?q?=20robustness:=20add=20JSONEditor=20extraFooter,=20fix=20endpoi?= =?UTF-8?q?nts=20rendering,=20and=20clean=20up=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Why - Needed to separate help text from action buttons in JSONEditor for better layout and UX. - Models table should robustly render both new object-based endpoint mappings and legacy arrays. - Columns should re-render when vendor map changes. - Minor import cleanups for consistency. - What - JSONEditor.js - Added optional prop extraFooter to render content below the extraText divider. - Kept extraText rendered via Divider; extraFooter appears on the next line for clear separation. - EditModelModal.jsx - Moved endpoint group buttons from extraText into extraFooter to display under the helper text. - Kept merge-logic: group items are merged into current endpoints JSON with key override semantics. - Consolidated lucide-react imports into a single line. - ModelsColumnDefs.js - Made endpoint renderer resilient: - Supports object-based JSON (keys as endpoint types) and legacy array format. - Displays keys/items as tags and limits the number shown; uses stringToColor for visual consistency. - Consolidated Semi UI imports into a single line. - ModelsTable.jsx - Fixed columns memoization dependency to include vendorMap, ensuring re-render when vendor data changes. - Notes - Backward-compatible: extraFooter is additive; existing JSONEditor usage remains unchanged. - No API changes to backend. - No linter errors introduced. - Files touched - web/src/components/common/ui/JSONEditor.js - web/src/components/table/models/modals/EditModelModal.jsx - web/src/components/table/models/ModelsColumnDefs.js - web/src/components/table/models/ModelsTable.jsx - Impact - Clearer UI for endpoint editing (buttons now below helper text). - Correct endpoints display for object-based mappings in models list. - More reliable reactivity when vendor data updates. --- web/src/components/common/ui/JSONEditor.js | 9 +++- .../table/models/ModelsColumnDefs.js | 54 +++++++++++-------- .../components/table/models/ModelsTable.jsx | 8 +-- .../table/models/modals/EditModelModal.jsx | 45 +++++++++++++--- .../models/modals/EditPrefillGroupModal.jsx | 9 ++++ 5 files changed, 90 insertions(+), 35 deletions(-) diff --git a/web/src/components/common/ui/JSONEditor.js b/web/src/components/common/ui/JSONEditor.js index fd4064dd..bc256970 100644 --- a/web/src/components/common/ui/JSONEditor.js +++ b/web/src/components/common/ui/JSONEditor.js @@ -14,6 +14,7 @@ import { TextArea, Row, Col, + Divider, } from '@douyinfe/semi-ui'; import { IconCode, @@ -31,6 +32,7 @@ const JSONEditor = ({ label, placeholder, extraText, + extraFooter, showClear = true, template, templateLabel, @@ -634,8 +636,13 @@ const JSONEditor = ({ {/* 额外文本显示在卡片底部 */} {extraText && ( -
+ {extraText} + + )} + {extraFooter && ( +
+ {extraFooter}
)} diff --git a/web/src/components/table/models/ModelsColumnDefs.js b/web/src/components/table/models/ModelsColumnDefs.js index 48841e60..ef433553 100644 --- a/web/src/components/table/models/ModelsColumnDefs.js +++ b/web/src/components/table/models/ModelsColumnDefs.js @@ -18,13 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { - Button, - Space, - Tag, - Typography, - Modal -} from '@douyinfe/semi-ui'; +import { Button, Space, Tag, Typography, Modal } from '@douyinfe/semi-ui'; import { timestamp2string, getLobeHubIcon, @@ -81,21 +75,39 @@ const renderTags = (text) => { }); }; -// Render endpoints -const renderEndpoints = (text) => { - let arr; +// Render endpoints (supports object map or legacy array) +const renderEndpoints = (value) => { try { - arr = JSON.parse(text); - } catch (_) { } - if (!Array.isArray(arr)) return text || '-'; - return renderLimitedItems({ - items: arr, - renderItem: (ep, idx) => ( - - {ep} - - ), - }); + const parsed = typeof value === 'string' ? JSON.parse(value) : value; + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const keys = Object.keys(parsed || {}); + if (keys.length === 0) return '-'; + return renderLimitedItems({ + items: keys, + renderItem: (key, idx) => ( + + {key} + + ), + maxDisplay: 3, + }); + } + if (Array.isArray(parsed)) { + if (parsed.length === 0) return '-'; + return renderLimitedItems({ + items: parsed, + renderItem: (ep, idx) => ( + + {ep} + + ), + maxDisplay: 3, + }); + } + return value || '-'; + } catch (_) { + return value || '-'; + } }; // Render quota type diff --git a/web/src/components/table/models/ModelsTable.jsx b/web/src/components/table/models/ModelsTable.jsx index 7ced70c5..7c37079a 100644 --- a/web/src/components/table/models/ModelsTable.jsx +++ b/web/src/components/table/models/ModelsTable.jsx @@ -56,13 +56,7 @@ const ModelsTable = (modelsData) => { refresh, vendorMap, }); - }, [ - t, - manageModel, - setEditingModel, - setShowEdit, - refresh, - ]); + }, [t, manageModel, setEditingModel, setShowEdit, refresh, vendorMap]); // Handle compact mode by removing fixed positioning const tableColumns = useMemo(() => { diff --git a/web/src/components/table/models/modals/EditModelModal.jsx b/web/src/components/table/models/modals/EditModelModal.jsx index 8cfc1306..47173c77 100644 --- a/web/src/components/table/models/modals/EditModelModal.jsx +++ b/web/src/components/table/models/modals/EditModelModal.jsx @@ -32,17 +32,20 @@ import { Col, Row, } from '@douyinfe/semi-ui'; -import { - Save, - X, - FileText, -} from 'lucide-react'; +import { Save, X, FileText } from 'lucide-react'; import { API, showError, showSuccess } from '../../../../helpers'; import { useTranslation } from 'react-i18next'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const { Text, Title } = Typography; +// Example endpoint template for quick fill +const ENDPOINT_TEMPLATE = { + openai: { path: '/v1/chat/completions', method: 'POST' }, + anthropic: { path: '/v1/messages', method: 'POST' }, + 'image-generation': { path: '/v1/images/generations', method: 'POST' }, +}; + const nameRuleOptions = [ { label: '精确名称匹配', value: 0 }, { label: '前缀名称匹配', value: 1 }, @@ -385,7 +388,37 @@ const EditModelModal = (props) => { onChange={(val) => formApiRef.current?.setValue('endpoints', val)} formApi={formApiRef.current} editorType='object' - extraText={t('留空则使用默认端点;支持 {path, method}')} + template={ENDPOINT_TEMPLATE} + templateLabel={t('填入模板')} + extraText={({t('留空则使用默认端点;支持 {path, method}')})} + extraFooter={endpointGroups.length > 0 && ( + + {endpointGroups.map(group => ( + + ))} + + )} /> diff --git a/web/src/components/table/models/modals/EditPrefillGroupModal.jsx b/web/src/components/table/models/modals/EditPrefillGroupModal.jsx index 6e3a6f20..72791555 100644 --- a/web/src/components/table/models/modals/EditPrefillGroupModal.jsx +++ b/web/src/components/table/models/modals/EditPrefillGroupModal.jsx @@ -43,6 +43,13 @@ import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const { Text, Title } = Typography; +// Example endpoint template for quick fill +const ENDPOINT_TEMPLATE = { + openai: { path: '/v1/chat/completions', method: 'POST' }, + anthropic: { path: '/v1/messages', method: 'POST' }, + 'image-generation': { path: '/v1/images/generations', method: 'POST' }, +}; + const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => { const { t } = useTranslation(); const isMobile = useIsMobile(); @@ -240,6 +247,8 @@ const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => onChange={(val) => formRef.current?.setValue('items', val)} editorType='object' placeholder={'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'} + template={ENDPOINT_TEMPLATE} + templateLabel={t('填入模板')} extraText={t('键为端点类型,值为路径和方法对象')} /> ) : (