| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import React from 'react'; |
| | import { |
| | Modal, |
| | Button, |
| | Input, |
| | Table, |
| | Tag, |
| | Typography, |
| | Select, |
| | } from '@douyinfe/semi-ui'; |
| | import { IconSearch } from '@douyinfe/semi-icons'; |
| | import { copy, showError, showInfo, showSuccess } from '../../../../helpers'; |
| | import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants'; |
| |
|
| | const ModelTestModal = ({ |
| | showModelTestModal, |
| | currentTestChannel, |
| | handleCloseModal, |
| | isBatchTesting, |
| | batchTestModels, |
| | modelSearchKeyword, |
| | setModelSearchKeyword, |
| | selectedModelKeys, |
| | setSelectedModelKeys, |
| | modelTestResults, |
| | testingModels, |
| | testChannel, |
| | modelTablePage, |
| | setModelTablePage, |
| | selectedEndpointType, |
| | setSelectedEndpointType, |
| | allSelectingRef, |
| | isMobile, |
| | t, |
| | }) => { |
| | const hasChannel = Boolean(currentTestChannel); |
| |
|
| | const filteredModels = hasChannel |
| | ? currentTestChannel.models |
| | .split(',') |
| | .filter((model) => |
| | model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), |
| | ) |
| | : []; |
| |
|
| | const endpointTypeOptions = [ |
| | { value: '', label: t('自动检测') }, |
| | { value: 'openai', label: 'OpenAI (/v1/chat/completions)' }, |
| | { value: 'openai-response', label: 'OpenAI Response (/v1/responses)' }, |
| | { value: 'anthropic', label: 'Anthropic (/v1/messages)' }, |
| | { |
| | value: 'gemini', |
| | label: 'Gemini (/v1beta/models/{model}:generateContent)', |
| | }, |
| | { value: 'jina-rerank', label: 'Jina Rerank (/rerank)' }, |
| | { |
| | value: 'image-generation', |
| | label: t('图像生成') + ' (/v1/images/generations)', |
| | }, |
| | { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' }, |
| | ]; |
| |
|
| | const handleCopySelected = () => { |
| | if (selectedModelKeys.length === 0) { |
| | showError(t('请先选择模型!')); |
| | return; |
| | } |
| | copy(selectedModelKeys.join(',')).then((ok) => { |
| | if (ok) { |
| | showSuccess( |
| | t('已复制 ${count} 个模型').replace( |
| | '${count}', |
| | selectedModelKeys.length, |
| | ), |
| | ); |
| | } else { |
| | showError(t('复制失败,请手动复制')); |
| | } |
| | }); |
| | }; |
| |
|
| | const handleSelectSuccess = () => { |
| | if (!currentTestChannel) return; |
| | const successKeys = currentTestChannel.models |
| | .split(',') |
| | .filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase())) |
| | .filter((m) => { |
| | const result = modelTestResults[`${currentTestChannel.id}-${m}`]; |
| | return result && result.success; |
| | }); |
| | if (successKeys.length === 0) { |
| | showInfo(t('暂无成功模型')); |
| | } |
| | setSelectedModelKeys(successKeys); |
| | }; |
| |
|
| | const columns = [ |
| | { |
| | title: t('模型名称'), |
| | dataIndex: 'model', |
| | render: (text) => ( |
| | <div className='flex items-center'> |
| | <Typography.Text strong>{text}</Typography.Text> |
| | </div> |
| | ), |
| | }, |
| | { |
| | title: t('状态'), |
| | dataIndex: 'status', |
| | render: (text, record) => { |
| | const testResult = |
| | modelTestResults[`${currentTestChannel.id}-${record.model}`]; |
| | const isTesting = testingModels.has(record.model); |
| |
|
| | if (isTesting) { |
| | return ( |
| | <Tag color='blue' shape='circle'> |
| | {t('测试中')} |
| | </Tag> |
| | ); |
| | } |
| |
|
| | if (!testResult) { |
| | return ( |
| | <Tag color='grey' shape='circle'> |
| | {t('未开始')} |
| | </Tag> |
| | ); |
| | } |
| |
|
| | return ( |
| | <div className='flex items-center gap-2'> |
| | <Tag color={testResult.success ? 'green' : 'red'} shape='circle'> |
| | {testResult.success ? t('成功') : t('失败')} |
| | </Tag> |
| | {testResult.success && ( |
| | <Typography.Text type='tertiary'> |
| | {t('请求时长: ${time}s').replace( |
| | '${time}', |
| | testResult.time.toFixed(2), |
| | )} |
| | </Typography.Text> |
| | )} |
| | </div> |
| | ); |
| | }, |
| | }, |
| | { |
| | title: '', |
| | dataIndex: 'operate', |
| | render: (text, record) => { |
| | const isTesting = testingModels.has(record.model); |
| | return ( |
| | <Button |
| | type='tertiary' |
| | onClick={() => |
| | testChannel( |
| | currentTestChannel, |
| | record.model, |
| | selectedEndpointType, |
| | ) |
| | } |
| | loading={isTesting} |
| | size='small' |
| | > |
| | {t('测试')} |
| | </Button> |
| | ); |
| | }, |
| | }, |
| | ]; |
| |
|
| | const dataSource = (() => { |
| | if (!hasChannel) return []; |
| | const start = (modelTablePage - 1) * MODEL_TABLE_PAGE_SIZE; |
| | const end = start + MODEL_TABLE_PAGE_SIZE; |
| | return filteredModels.slice(start, end).map((model) => ({ |
| | model, |
| | key: model, |
| | })); |
| | })(); |
| |
|
| | return ( |
| | <Modal |
| | title={ |
| | hasChannel ? ( |
| | <div className='flex flex-col gap-2 w-full'> |
| | <div className='flex items-center gap-2'> |
| | <Typography.Text |
| | strong |
| | className='!text-[var(--semi-color-text-0)] !text-base' |
| | > |
| | {currentTestChannel.name} {t('渠道的模型测试')} |
| | </Typography.Text> |
| | <Typography.Text type='tertiary' size='small'> |
| | {t('共')} {currentTestChannel.models.split(',').length}{' '} |
| | {t('个模型')} |
| | </Typography.Text> |
| | </div> |
| | </div> |
| | ) : null |
| | } |
| | visible={showModelTestModal} |
| | onCancel={handleCloseModal} |
| | footer={ |
| | hasChannel ? ( |
| | <div className='flex justify-end'> |
| | {isBatchTesting ? ( |
| | <Button type='danger' onClick={handleCloseModal}> |
| | {t('停止测试')} |
| | </Button> |
| | ) : ( |
| | <Button type='tertiary' onClick={handleCloseModal}> |
| | {t('取消')} |
| | </Button> |
| | )} |
| | <Button |
| | onClick={batchTestModels} |
| | loading={isBatchTesting} |
| | disabled={isBatchTesting} |
| | > |
| | {isBatchTesting |
| | ? t('测试中...') |
| | : t('批量测试${count}个模型').replace( |
| | '${count}', |
| | filteredModels.length, |
| | )} |
| | </Button> |
| | </div> |
| | ) : null |
| | } |
| | maskClosable={!isBatchTesting} |
| | className='!rounded-lg' |
| | size={isMobile ? 'full-width' : 'large'} |
| | > |
| | {hasChannel && ( |
| | <div className='model-test-scroll'> |
| | {/* 端点类型选择器 */} |
| | <div className='flex items-center gap-2 w-full mb-2'> |
| | <Typography.Text strong>{t('端点类型')}:</Typography.Text> |
| | <Select |
| | value={selectedEndpointType} |
| | onChange={setSelectedEndpointType} |
| | optionList={endpointTypeOptions} |
| | className='!w-full' |
| | placeholder={t('选择端点类型')} |
| | /> |
| | </div> |
| | |
| | {/* 搜索与操作按钮 */} |
| | <div className='flex items-center justify-end gap-2 w-full mb-2'> |
| | <Input |
| | placeholder={t('搜索模型...')} |
| | value={modelSearchKeyword} |
| | onChange={(v) => { |
| | setModelSearchKeyword(v); |
| | setModelTablePage(1); |
| | }} |
| | className='!w-full' |
| | prefix={<IconSearch />} |
| | showClear |
| | /> |
| | |
| | <Button onClick={handleCopySelected}>{t('复制已选')}</Button> |
| | |
| | <Button type='tertiary' onClick={handleSelectSuccess}> |
| | {t('选择成功')} |
| | </Button> |
| | </div> |
| | |
| | <Table |
| | columns={columns} |
| | dataSource={dataSource} |
| | rowSelection={{ |
| | selectedRowKeys: selectedModelKeys, |
| | onChange: (keys) => { |
| | if (allSelectingRef.current) { |
| | allSelectingRef.current = false; |
| | return; |
| | } |
| | setSelectedModelKeys(keys); |
| | }, |
| | onSelectAll: (checked) => { |
| | allSelectingRef.current = true; |
| | setSelectedModelKeys(checked ? filteredModels : []); |
| | }, |
| | }} |
| | pagination={{ |
| | currentPage: modelTablePage, |
| | pageSize: MODEL_TABLE_PAGE_SIZE, |
| | total: filteredModels.length, |
| | showSizeChanger: false, |
| | onPageChange: (page) => setModelTablePage(page), |
| | }} |
| | /> |
| | </div> |
| | )} |
| | </Modal> |
| | ); |
| | }; |
| |
|
| | export default ModelTestModal; |
| |
|