feat: Improve models UX and robustness: add JSONEditor extraFooter, fix endpoints rendering, and clean up deps

- 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.
This commit is contained in:
t0ng7u
2025-08-08 02:59:45 +08:00
parent 26f44b8d4b
commit 4e75a9b3b3
5 changed files with 90 additions and 35 deletions

View File

@@ -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 && (
<div className="text-gray-600 mt-3 pt-3">
<Divider margin='12px' align='center'>
{extraText}
</Divider>
)}
{extraFooter && (
<div className="mt-1">
{extraFooter}
</div>
)}
</Card>

View File

@@ -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) => (
<Tag key={idx} color="white" size="small" shape='circle'>
{ep}
</Tag>
),
});
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) => (
<Tag key={idx} size="small" shape='circle' color={stringToColor(key)}>
{key}
</Tag>
),
maxDisplay: 3,
});
}
if (Array.isArray(parsed)) {
if (parsed.length === 0) return '-';
return renderLimitedItems({
items: parsed,
renderItem: (ep, idx) => (
<Tag key={idx} color="white" size="small" shape='circle'>
{ep}
</Tag>
),
maxDisplay: 3,
});
}
return value || '-';
} catch (_) {
return value || '-';
}
};
// Render quota type

View File

@@ -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(() => {

View File

@@ -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={(<Text type="tertiary" size="small">{t('留空则使用默认端点;支持 {path, method}')}</Text>)}
extraFooter={endpointGroups.length > 0 && (
<Space wrap>
{endpointGroups.map(group => (
<Button
key={group.id}
size='small'
type='primary'
onClick={() => {
try {
const current = formApiRef.current?.getValue('endpoints') || '';
let base = {};
if (current && current.trim()) base = JSON.parse(current);
const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {});
const merged = { ...base, ...groupObj };
formApiRef.current?.setValue('endpoints', JSON.stringify(merged, null, 2));
} catch (e) {
try {
const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {});
formApiRef.current?.setValue('endpoints', JSON.stringify(groupObj, null, 2));
} catch { }
}
}}
>
{group.name}
</Button>
))}
</Space>
)}
/>
</Col>
<Col span={24}>

View File

@@ -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('键为端点类型,值为路径和方法对象')}
/>
) : (