778 lines
28 KiB
JavaScript
778 lines
28 KiB
JavaScript
|
|
// LocalExportTab.js 组件
|
|||
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import {
|
|||
|
|
Button,
|
|||
|
|
FormControl,
|
|||
|
|
FormControlLabel,
|
|||
|
|
RadioGroup,
|
|||
|
|
Radio,
|
|||
|
|
TextField,
|
|||
|
|
Checkbox,
|
|||
|
|
Typography,
|
|||
|
|
Box,
|
|||
|
|
Paper,
|
|||
|
|
useTheme,
|
|||
|
|
Grid,
|
|||
|
|
Table,
|
|||
|
|
TableRow,
|
|||
|
|
TableHead,
|
|||
|
|
TableBody,
|
|||
|
|
TableCell,
|
|||
|
|
TableContainer,
|
|||
|
|
Dialog,
|
|||
|
|
DialogTitle,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogActions,
|
|||
|
|
Chip,
|
|||
|
|
Alert,
|
|||
|
|
CircularProgress
|
|||
|
|
} from '@mui/material';
|
|||
|
|
|
|||
|
|
const LocalExportTab = ({
|
|||
|
|
fileFormat,
|
|||
|
|
formatType,
|
|||
|
|
systemPrompt,
|
|||
|
|
confirmedOnly,
|
|||
|
|
includeCOT,
|
|||
|
|
customFields,
|
|||
|
|
alpacaFieldType,
|
|||
|
|
customInstruction,
|
|||
|
|
reasoningLanguage,
|
|||
|
|
handleFileFormatChange,
|
|||
|
|
handleFormatChange,
|
|||
|
|
handleSystemPromptChange,
|
|||
|
|
handleReasoningLanguageChange,
|
|||
|
|
handleConfirmedOnlyChange,
|
|||
|
|
handleIncludeCOTChange,
|
|||
|
|
handleCustomFieldChange,
|
|||
|
|
handleIncludeLabelsChange,
|
|||
|
|
handleIncludeChunkChange,
|
|||
|
|
handleQuestionOnlyChange,
|
|||
|
|
handleAlpacaFieldTypeChange,
|
|||
|
|
handleCustomInstructionChange,
|
|||
|
|
handleExport,
|
|||
|
|
projectId
|
|||
|
|
}) => {
|
|||
|
|
const theme = useTheme();
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
|
|||
|
|
// Balance export related state
|
|||
|
|
const [balanceDialogOpen, setBalanceDialogOpen] = useState(false);
|
|||
|
|
const [tagStats, setTagStats] = useState([]);
|
|||
|
|
const [balanceConfig, setBalanceConfig] = useState([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [error, setError] = useState('');
|
|||
|
|
const [totalCount, setTotalCount] = useState(0);
|
|||
|
|
|
|||
|
|
// Get label statistics (changed to GET + query parameters)
|
|||
|
|
const fetchTagStats = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoading(true);
|
|||
|
|
const url = `/api/projects/${projectId}/datasets/export?confirmed=${confirmedOnly ? 'true' : 'false'}`;
|
|||
|
|
const response = await fetch(url, { method: 'GET' });
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(t('errors.getTagStatsFailed'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const stats = await response.json();
|
|||
|
|
setTagStats(stats);
|
|||
|
|
|
|||
|
|
// 初始化平衡配置
|
|||
|
|
const initialConfig = stats.map(stat => ({
|
|||
|
|
tagLabel: stat.tagLabel,
|
|||
|
|
maxCount: Math.min(stat.datasetCount, 100), // 默认最多100条
|
|||
|
|
availableCount: stat.datasetCount
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
setBalanceConfig(initialConfig);
|
|||
|
|
|
|||
|
|
// 计算总数
|
|||
|
|
const total = initialConfig.reduce((sum, config) => sum + config.maxCount, 0);
|
|||
|
|
setTotalCount(total);
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(err.message);
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 打开平衡导出对话框
|
|||
|
|
const handleOpenBalanceDialog = () => {
|
|||
|
|
setBalanceDialogOpen(true);
|
|||
|
|
fetchTagStats();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 更新单个标签的数量配置
|
|||
|
|
const updateBalanceConfig = (tagLabel, newCount) => {
|
|||
|
|
const newConfig = balanceConfig.map(config => {
|
|||
|
|
if (config.tagLabel === tagLabel) {
|
|||
|
|
const count = Math.min(Math.max(0, parseInt(newCount) || 0), config.availableCount);
|
|||
|
|
return { ...config, maxCount: count };
|
|||
|
|
}
|
|||
|
|
return config;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
setBalanceConfig(newConfig);
|
|||
|
|
|
|||
|
|
// 重新计算总数
|
|||
|
|
const total = newConfig.reduce((sum, config) => sum + config.maxCount, 0);
|
|||
|
|
setTotalCount(total);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 一键设置所有标签为相同数量
|
|||
|
|
const setAllToSameCount = count => {
|
|||
|
|
const newConfig = balanceConfig.map(config => ({
|
|||
|
|
...config,
|
|||
|
|
maxCount: Math.min(Math.max(0, parseInt(count) || 0), config.availableCount)
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
setBalanceConfig(newConfig);
|
|||
|
|
|
|||
|
|
const total = newConfig.reduce((sum, config) => sum + config.maxCount, 0);
|
|||
|
|
setTotalCount(total);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理平衡导出
|
|||
|
|
const handleBalancedExport = () => {
|
|||
|
|
// 过滤出数量大于0的配置
|
|||
|
|
const validConfig = balanceConfig.filter(config => config.maxCount > 0);
|
|||
|
|
|
|||
|
|
if (validConfig.length === 0) {
|
|||
|
|
setError(t('export.balancedExport.atLeastOneTag', '请至少为一个标签设置大于0的数量'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用原有的导出函数,但传递平衡配置
|
|||
|
|
handleExport({
|
|||
|
|
balanceMode: true,
|
|||
|
|
balanceConfig: validConfig,
|
|||
|
|
formatType,
|
|||
|
|
systemPrompt,
|
|||
|
|
reasoningLanguage,
|
|||
|
|
confirmedOnly,
|
|||
|
|
fileFormat,
|
|||
|
|
includeCOT,
|
|||
|
|
alpacaFieldType,
|
|||
|
|
customInstruction,
|
|||
|
|
customFields: formatType === 'custom' ? customFields : undefined
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
setBalanceDialogOpen(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 自定义格式的示例
|
|||
|
|
const getCustomFormatExample = () => {
|
|||
|
|
const { questionField, answerField, cotField, includeLabels, includeChunk } = customFields;
|
|||
|
|
const example = {
|
|||
|
|
[questionField]: t('sampleData.questionContent'),
|
|||
|
|
[answerField]: t('sampleData.answerContent')
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 如果包含思维链字段,添加到示例中
|
|||
|
|
if (includeCOT) {
|
|||
|
|
example[cotField] = t('sampleData.cotContent');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (includeLabels) {
|
|||
|
|
example.labels = [t('sampleData.domainLabel')];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (includeChunk) {
|
|||
|
|
example.chunk = t('sampleData.textChunk');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return fileFormat === 'json' ? JSON.stringify([example], null, 2) : JSON.stringify(example);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// CSV 自定义格式化示例
|
|||
|
|
const getPreviewData = () => {
|
|||
|
|
if (formatType === 'alpaca') {
|
|||
|
|
// 根据选择的字段类型生成不同的示例
|
|||
|
|
if (alpacaFieldType === 'instruction') {
|
|||
|
|
return {
|
|||
|
|
headers: ['instruction', 'input', 'output', 'system'],
|
|||
|
|
rows: [
|
|||
|
|
{
|
|||
|
|
instruction: t('export.sampleInstruction', '人类指令(必填)'),
|
|||
|
|
input: '',
|
|||
|
|
output: t('export.sampleOutput', '模型回答(必填)'),
|
|||
|
|
system: t('export.sampleSystem', '系统提示词(选填)')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
instruction: t('export.sampleInstruction2', '第二个指令'),
|
|||
|
|
input: '',
|
|||
|
|
output: t('export.sampleOutput2', '第二个回答'),
|
|||
|
|
system: t('export.sampleSystemShort', '系统提示词')
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
// input
|
|||
|
|
return {
|
|||
|
|
headers: ['instruction', 'input', 'output', 'system'],
|
|||
|
|
rows: [
|
|||
|
|
{
|
|||
|
|
instruction: customInstruction || t('export.fixedInstruction', '固定的指令内容'),
|
|||
|
|
input: t('export.sampleInput', '人类问题(必填)'),
|
|||
|
|
output: t('export.sampleOutput', '模型回答(必填)'),
|
|||
|
|
system: t('export.sampleSystem', '系统提示词(选填)')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
instruction: customInstruction || t('export.fixedInstruction', '固定的指令内容'),
|
|||
|
|
input: t('export.sampleInput2', '第二个问题'),
|
|||
|
|
output: t('export.sampleOutput2', '第二个回答'),
|
|||
|
|
system: t('export.sampleSystemShort', '系统提示词')
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
} else if (formatType === 'sharegpt') {
|
|||
|
|
return {
|
|||
|
|
headers: ['messages'],
|
|||
|
|
rows: [
|
|||
|
|
{
|
|||
|
|
messages: JSON.stringify(
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
messages: [
|
|||
|
|
{
|
|||
|
|
role: 'system',
|
|||
|
|
content: t('export.sampleSystem', '系统提示词(选填)')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
role: 'user',
|
|||
|
|
content: t('export.sampleUserMessage', '人类指令') // 映射到 question 字段
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
role: 'assistant',
|
|||
|
|
content: t('export.sampleAssistantMessage', '模型回答') // 映射到 cot+answer 字段
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
null,
|
|||
|
|
2
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
} else if (formatType === 'multilingualthinking') {
|
|||
|
|
return {
|
|||
|
|
headers: 'messages',
|
|||
|
|
rows: {
|
|||
|
|
messages: JSON.stringify(
|
|||
|
|
{
|
|||
|
|
reasoning_language: 'English',
|
|||
|
|
developer: t('export.sampleSystem', '系统提示词(选填)'),
|
|||
|
|
user: t('export.sampleUserMessage', '人类指令'), // 映射到 question 字段
|
|||
|
|
analysis: t('export.sampleAnalysis', '模型的思维链内容'), // 映射到 cot 字段
|
|||
|
|
final: t('export.sampleFinal', '模型回答'), // 映射到 answer 字段
|
|||
|
|
messages: [
|
|||
|
|
{
|
|||
|
|
role: 'system',
|
|||
|
|
content: '系统提示词(选填)',
|
|||
|
|
thinking: 'null'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
role: 'user',
|
|||
|
|
content: '人类指令', // 映射到 question 字段
|
|||
|
|
thinking: 'null'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
role: 'assistant',
|
|||
|
|
content: '模型回答', // 映射到 answer 字段
|
|||
|
|
thinking: '模型的思维链内容' // 映射到 cot 字段
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
null,
|
|||
|
|
2
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
} else if (formatType === 'custom') {
|
|||
|
|
// 如果选择仅导出问题,只包含问题字段
|
|||
|
|
if (customFields.questionOnly) {
|
|||
|
|
const headers = [customFields.questionField];
|
|||
|
|
if (customFields.includeLabels) headers.push('labels');
|
|||
|
|
if (customFields.includeChunk) headers.push('chunk');
|
|||
|
|
|
|||
|
|
const row = {
|
|||
|
|
[customFields.questionField]: t('sampleData.questionContent')
|
|||
|
|
};
|
|||
|
|
if (customFields.includeLabels) row.labels = t('sampleData.domainLabel');
|
|||
|
|
if (customFields.includeChunk) row.chunk = t('sampleData.textChunk');
|
|||
|
|
return {
|
|||
|
|
headers,
|
|||
|
|
rows: [row]
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
// 正常的自定义格式
|
|||
|
|
const headers = [customFields.questionField, customFields.answerField];
|
|||
|
|
if (includeCOT) headers.push(customFields.cotField);
|
|||
|
|
if (customFields.includeLabels) headers.push('labels');
|
|||
|
|
if (customFields.includeChunk) headers.push('chunk');
|
|||
|
|
|
|||
|
|
const row = {
|
|||
|
|
[customFields.questionField]: t('sampleData.questionContent'),
|
|||
|
|
[customFields.answerField]: t('sampleData.answerContent')
|
|||
|
|
};
|
|||
|
|
if (includeCOT) row[customFields.cotField] = t('sampleData.cotContent');
|
|||
|
|
if (customFields.includeLabels) row.labels = t('sampleData.domainLabel');
|
|||
|
|
if (customFields.includeChunk) row.chunk = t('sampleData.textChunk');
|
|||
|
|
return {
|
|||
|
|
headers,
|
|||
|
|
rows: [row]
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<Box sx={{ mb: 3 }}>
|
|||
|
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|||
|
|
{t('export.fileFormat')}
|
|||
|
|
</Typography>
|
|||
|
|
<FormControl component="fieldset">
|
|||
|
|
<RadioGroup
|
|||
|
|
aria-label="fileFormat"
|
|||
|
|
name="fileFormat"
|
|||
|
|
value={fileFormat}
|
|||
|
|
onChange={handleFileFormatChange}
|
|||
|
|
row
|
|||
|
|
>
|
|||
|
|
<FormControlLabel value="json" control={<Radio />} label="JSON" />
|
|||
|
|
<FormControlLabel value="jsonl" control={<Radio />} label="JSONL" />
|
|||
|
|
{/* <FormControlLabel value="csv" control={<Radio />} label="CSV" /> */}
|
|||
|
|
<FormControlLabel
|
|||
|
|
value="csv"
|
|||
|
|
control={<Radio disabled={formatType === 'multilingualthinking'} />}
|
|||
|
|
label="CSV"
|
|||
|
|
/>
|
|||
|
|
</RadioGroup>
|
|||
|
|
</FormControl>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 数据集风格 */}
|
|||
|
|
<Box sx={{ mb: 3 }}>
|
|||
|
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|||
|
|
{t('export.format')}
|
|||
|
|
</Typography>
|
|||
|
|
<FormControl component="fieldset">
|
|||
|
|
<RadioGroup aria-label="format" name="format" value={formatType} onChange={handleFormatChange} row>
|
|||
|
|
<FormControlLabel value="alpaca" control={<Radio />} label="Alpaca" />
|
|||
|
|
<FormControlLabel value="sharegpt" control={<Radio />} label="ShareGPT" />
|
|||
|
|
{/* NEW: Multilingual‑Thinking format */}
|
|||
|
|
<FormControlLabel
|
|||
|
|
value="multilingualthinking"
|
|||
|
|
control={<Radio disabled={fileFormat === 'csv'} />}
|
|||
|
|
label={t('export.multilingualThinkingFormat') || 'Multilingual‑Thinking'}
|
|||
|
|
/>
|
|||
|
|
<FormControlLabel value="custom" control={<Radio />} label={t('export.customFormat')} />
|
|||
|
|
</RadioGroup>
|
|||
|
|
</FormControl>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* Alpaca 格式特有的设置 */}
|
|||
|
|
{formatType === 'alpaca' && (
|
|||
|
|
<Box sx={{ mb: 3, pl: 2, borderLeft: `1px solid ${theme.palette.divider}` }}>
|
|||
|
|
<Typography variant="subtitle2" gutterBottom>
|
|||
|
|
{t('export.alpacaSettings', 'Alpaca 格式设置')}
|
|||
|
|
</Typography>
|
|||
|
|
<FormControl component="fieldset">
|
|||
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|||
|
|
{t('export.questionFieldType', '问题字段类型')}
|
|||
|
|
</Typography>
|
|||
|
|
<RadioGroup
|
|||
|
|
aria-label="alpacaFieldType"
|
|||
|
|
name="alpacaFieldType"
|
|||
|
|
value={alpacaFieldType}
|
|||
|
|
onChange={handleAlpacaFieldTypeChange}
|
|||
|
|
row
|
|||
|
|
>
|
|||
|
|
<FormControlLabel
|
|||
|
|
value="instruction"
|
|||
|
|
control={<Radio />}
|
|||
|
|
label={t('export.useInstruction', '使用 instruction 字段')}
|
|||
|
|
/>
|
|||
|
|
<FormControlLabel value="input" control={<Radio />} label={t('export.useInput', '使用 input 字段')} />
|
|||
|
|
</RadioGroup>
|
|||
|
|
|
|||
|
|
{alpacaFieldType === 'input' && (
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
size="small"
|
|||
|
|
label={t('export.customInstruction', '自定义 instruction 字段内容')}
|
|||
|
|
value={customInstruction}
|
|||
|
|
onChange={handleCustomInstructionChange}
|
|||
|
|
margin="normal"
|
|||
|
|
placeholder={t('export.instructionPlaceholder', '请输入固定的指令内容')}
|
|||
|
|
helperText={t(
|
|||
|
|
'export.instructionHelperText',
|
|||
|
|
'当使用 input 字段时,可以在这里指定固定的 instruction 内容'
|
|||
|
|
)}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</FormControl>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 自定义格式选项 */}
|
|||
|
|
{formatType === 'custom' && (
|
|||
|
|
<Box sx={{ mb: 3, pl: 2, borderLeft: `1px solid ${theme.palette.divider}` }}>
|
|||
|
|
<Typography variant="subtitle2" gutterBottom>
|
|||
|
|
{t('export.customFormatSettings')}
|
|||
|
|
</Typography>
|
|||
|
|
<Grid container spacing={2}>
|
|||
|
|
<Grid item xs={12} sm={6}>
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
size="small"
|
|||
|
|
label={t('export.questionFieldName')}
|
|||
|
|
value={customFields.questionField}
|
|||
|
|
onChange={handleCustomFieldChange('questionField')}
|
|||
|
|
margin="normal"
|
|||
|
|
/>
|
|||
|
|
</Grid>
|
|||
|
|
<Grid item xs={12} sm={6}>
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
size="small"
|
|||
|
|
label={t('export.answerFieldName')}
|
|||
|
|
value={customFields.answerField}
|
|||
|
|
onChange={handleCustomFieldChange('answerField')}
|
|||
|
|
margin="normal"
|
|||
|
|
/>
|
|||
|
|
</Grid>
|
|||
|
|
{/* 添加思维链字段名输入框 */}
|
|||
|
|
<Grid item xs={12} sm={6}>
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
size="small"
|
|||
|
|
label={t('export.cotFieldName')}
|
|||
|
|
value={customFields.cotField}
|
|||
|
|
onChange={handleCustomFieldChange('cotField')}
|
|||
|
|
margin="normal"
|
|||
|
|
/>
|
|||
|
|
</Grid>
|
|||
|
|
</Grid>
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={
|
|||
|
|
<Checkbox checked={customFields.includeLabels} onChange={handleIncludeLabelsChange} size="small" />
|
|||
|
|
}
|
|||
|
|
label={t('export.includeLabels')}
|
|||
|
|
/>
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={<Checkbox checked={customFields.includeChunk} onChange={handleIncludeChunkChange} size="small" />}
|
|||
|
|
label={t('export.includeChunk')}
|
|||
|
|
/>
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={<Checkbox checked={customFields.questionOnly} onChange={handleQuestionOnlyChange} size="small" />}
|
|||
|
|
label={t('export.questionOnly')}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Box sx={{ mb: 3 }}>
|
|||
|
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|||
|
|
{t('export.example')}
|
|||
|
|
</Typography>
|
|||
|
|
|
|||
|
|
{fileFormat === 'csv' ? (
|
|||
|
|
<TableContainer component={Paper} sx={{ mb: 2 }}>
|
|||
|
|
{(() => {
|
|||
|
|
const { headers, rows } = getPreviewData();
|
|||
|
|
const tableKey = `${formatType}-${fileFormat}-${JSON.stringify(customFields)}`;
|
|||
|
|
return (
|
|||
|
|
<Table size="small" key={tableKey}>
|
|||
|
|
<TableHead>
|
|||
|
|
<TableRow>
|
|||
|
|
{headers.map(header => (
|
|||
|
|
<TableCell key={header}>{header}</TableCell>
|
|||
|
|
))}
|
|||
|
|
</TableRow>
|
|||
|
|
</TableHead>
|
|||
|
|
<TableBody>
|
|||
|
|
{rows.map((row, index) => (
|
|||
|
|
<TableRow key={index}>
|
|||
|
|
{headers.map(header => (
|
|||
|
|
<TableCell key={header}>
|
|||
|
|
{Array.isArray(row[header]) ? row[header].join(', ') : row[header] || ''}
|
|||
|
|
</TableCell>
|
|||
|
|
))}
|
|||
|
|
</TableRow>
|
|||
|
|
))}
|
|||
|
|
</TableBody>
|
|||
|
|
</Table>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
</TableContainer>
|
|||
|
|
) : (
|
|||
|
|
<Paper
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{
|
|||
|
|
p: 2,
|
|||
|
|
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.grey[900] : theme.palette.grey[100],
|
|||
|
|
overflowX: 'auto'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<pre style={{ margin: 0 }}>
|
|||
|
|
{formatType === 'custom'
|
|||
|
|
? getCustomFormatExample()
|
|||
|
|
: formatType === 'multilingualthinking'
|
|||
|
|
? fileFormat === 'json'
|
|||
|
|
? JSON.stringify(
|
|||
|
|
{
|
|||
|
|
reasoning_language: 'English',
|
|||
|
|
developer: '系统提示词(选填)',
|
|||
|
|
user: '人类指令', // 映射到 question 字段
|
|||
|
|
analysis: '模型的思维链内容', // 映射到 cot 字段
|
|||
|
|
final: '模型回答', // 映射到 answer 字段
|
|||
|
|
messages: [
|
|||
|
|
{
|
|||
|
|
content: t('export.sampleSystem', '系统提示词(选填)'),
|
|||
|
|
role: 'system',
|
|||
|
|
thinking: null
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
content: t('export.sampleUserMessage', '人类指令'),
|
|||
|
|
role: 'user',
|
|||
|
|
thinking: null
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
content: t('export.sampleAssistantMessage', '模型回答'),
|
|||
|
|
role: 'assistant',
|
|||
|
|
thinking: t('export.sampleThinking', '模型的思维链内容')
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
null,
|
|||
|
|
2
|
|||
|
|
)
|
|||
|
|
: '{"reasoning_language": "English","developer": "系统提示词(选填)", "user": "人类指令", "analysis": "模型的思维链内容", "final": "模型回答", "messages": [{"role": "user", "content": "人类指令", "thinking": "null"}, {"role": "assistant", "content": "模型回答", "thinking": "模型的思维链内容"}]}'
|
|||
|
|
: formatType === 'alpaca'
|
|||
|
|
? fileFormat === 'json'
|
|||
|
|
? JSON.stringify(
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
instruction: t('export.sampleInstruction', '人类指令(必填)'), // 映射到 question 字段
|
|||
|
|
input: t('export.sampleInputOptional', '人类输入(选填)'),
|
|||
|
|
output: t('export.sampleOutput', '模型回答(必填)'), // 映射到 cot+answer 字段
|
|||
|
|
system: t('export.sampleSystem', '系统提示词(选填)')
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
null,
|
|||
|
|
2
|
|||
|
|
)
|
|||
|
|
: '{"instruction": "人类指令(必填)", "input": "人类输入(选填)", "output": "模型回答(必填)", "system": "系统提示词(选填)"}\n{"instruction": "第二个指令", "input": "", "output": "第二个回答", "system": "系统提示词"}'
|
|||
|
|
: fileFormat === 'json'
|
|||
|
|
? JSON.stringify(
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
messages: [
|
|||
|
|
{
|
|||
|
|
role: 'system',
|
|||
|
|
content: t('export.sampleSystem', '系统提示词(选填)')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
role: 'user',
|
|||
|
|
content: t('export.sampleUserMessage', '人类指令') // 映射到 question 字段
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
role: 'assistant',
|
|||
|
|
content: t('export.sampleAssistantMessage', '模型回答') // 映射到 cot+answer 字段
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
null,
|
|||
|
|
2
|
|||
|
|
)
|
|||
|
|
: '{"messages": [{"role": "system", "content": "系统提示词(选填)"}, {"role": "user", "content": "人类指令"}, {"role": "assistant", "content": "模型回答"}]}\n{"messages": [{"role": "user", "content": "第二个问题"}, {"role": "assistant", "content": "第二个回答"}]}'}
|
|||
|
|
</pre>
|
|||
|
|
</Paper>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Box sx={{ mb: 3 }}>
|
|||
|
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|||
|
|
{t('export.systemPrompt')}
|
|||
|
|
</Typography>
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
multiline
|
|||
|
|
rows={3}
|
|||
|
|
variant="outlined"
|
|||
|
|
placeholder={t('export.systemPromptPlaceholder')}
|
|||
|
|
value={systemPrompt}
|
|||
|
|
onChange={handleSystemPromptChange}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
{/* Reasoning language – only for multilingual‑thinking */}
|
|||
|
|
{formatType === 'multilingualthinking' && (
|
|||
|
|
<Box sx={{ mb: 3 }}>
|
|||
|
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
|||
|
|
{t('export.Reasoninglanguage')}
|
|||
|
|
</Typography>
|
|||
|
|
<TextField
|
|||
|
|
fullWidth
|
|||
|
|
rows={3}
|
|||
|
|
multiline
|
|||
|
|
variant="outlined"
|
|||
|
|
placeholder={t('export.ReasoninglanguagePlaceholder')}
|
|||
|
|
value={reasoningLanguage}
|
|||
|
|
onChange={handleReasoningLanguageChange}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
<Box sx={{ mb: 2, display: 'flex', flexDirection: 'row', gap: 4 }}>
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={<Checkbox checked={confirmedOnly} onChange={handleConfirmedOnlyChange} />}
|
|||
|
|
label={t('export.onlyConfirmed')}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<FormControlLabel
|
|||
|
|
control={<Checkbox checked={includeCOT} onChange={handleIncludeCOTChange} />}
|
|||
|
|
label={t('export.includeCOT')}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 2 }}>
|
|||
|
|
<Button onClick={handleOpenBalanceDialog} variant="outlined" sx={{ borderRadius: 2 }}>
|
|||
|
|
{t('exportDialog.balancedExport')}
|
|||
|
|
</Button>
|
|||
|
|
<Button onClick={handleExport} variant="contained" sx={{ borderRadius: 2 }}>
|
|||
|
|
{t('export.confirmExport')}
|
|||
|
|
</Button>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 平衡导出对话框 */}
|
|||
|
|
<Dialog
|
|||
|
|
open={balanceDialogOpen}
|
|||
|
|
onClose={() => setBalanceDialogOpen(false)}
|
|||
|
|
maxWidth="md"
|
|||
|
|
fullWidth
|
|||
|
|
PaperProps={{
|
|||
|
|
sx: {
|
|||
|
|
borderRadius: 2
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<DialogTitle>{t('exportDialog.balancedExportTitle')}</DialogTitle>
|
|||
|
|
<DialogContent>
|
|||
|
|
<Typography variant="body2" color="text.secondary" gutterBottom sx={{ mb: 3 }}>
|
|||
|
|
{t('exportDialog.balancedExportDescription')}
|
|||
|
|
</Typography>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|||
|
|
{error}
|
|||
|
|
</Alert>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{loading ? (
|
|||
|
|
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
|
|||
|
|
<CircularProgress />
|
|||
|
|
</Box>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
{/* 批量设置 */}
|
|||
|
|
<Box sx={{ mb: 3, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
|
|||
|
|
<Typography variant="subtitle2" gutterBottom>
|
|||
|
|
{t('exportDialog.quickSettings')}
|
|||
|
|
</Typography>
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|||
|
|
<Button size="small" onClick={() => setAllToSameCount(50)}>
|
|||
|
|
{t('exportDialog.setAllTo50')}
|
|||
|
|
</Button>
|
|||
|
|
<Button size="small" onClick={() => setAllToSameCount(100)}>
|
|||
|
|
{t('exportDialog.setAllTo100')}
|
|||
|
|
</Button>
|
|||
|
|
<Button size="small" onClick={() => setAllToSameCount(200)}>
|
|||
|
|
{t('exportDialog.setAllTo200')}
|
|||
|
|
</Button>
|
|||
|
|
<TextField
|
|||
|
|
size="small"
|
|||
|
|
type="number"
|
|||
|
|
placeholder={t('exportDialog.customAmount')}
|
|||
|
|
sx={{ width: 120 }}
|
|||
|
|
onKeyPress={e => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
setAllToSameCount(e.target.value);
|
|||
|
|
e.target.value = '';
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 标签配置表格 */}
|
|||
|
|
<TableContainer component={Paper}>
|
|||
|
|
<Table size="small">
|
|||
|
|
<TableHead>
|
|||
|
|
<TableRow>
|
|||
|
|
<TableCell>{t('exportDialog.tagName')}</TableCell>
|
|||
|
|
<TableCell align="right">{t('exportDialog.availableCount')}</TableCell>
|
|||
|
|
<TableCell align="right">{t('exportDialog.exportCount')}</TableCell>
|
|||
|
|
<TableCell align="right">{t('exportDialog.settings')}</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
</TableHead>
|
|||
|
|
<TableBody>
|
|||
|
|
{balanceConfig.map(config => (
|
|||
|
|
<TableRow key={config.tagLabel}>
|
|||
|
|
<TableCell>
|
|||
|
|
<Chip label={config.tagLabel} size="small" variant="outlined" />
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell align="right">{config.availableCount}</TableCell>
|
|||
|
|
<TableCell align="right">
|
|||
|
|
<strong>{config.maxCount}</strong>
|
|||
|
|
</TableCell>
|
|||
|
|
<TableCell align="right">
|
|||
|
|
<TextField
|
|||
|
|
size="small"
|
|||
|
|
type="number"
|
|||
|
|
value={config.maxCount}
|
|||
|
|
onChange={e => updateBalanceConfig(config.tagLabel, e.target.value)}
|
|||
|
|
inputProps={{
|
|||
|
|
min: 0,
|
|||
|
|
max: config.availableCount,
|
|||
|
|
style: { textAlign: 'right' }
|
|||
|
|
}}
|
|||
|
|
sx={{ width: 80 }}
|
|||
|
|
/>
|
|||
|
|
</TableCell>
|
|||
|
|
</TableRow>
|
|||
|
|
))}
|
|||
|
|
</TableBody>
|
|||
|
|
</Table>
|
|||
|
|
</TableContainer>
|
|||
|
|
|
|||
|
|
{/* 统计信息 */}
|
|||
|
|
<Box sx={{ mt: 2, p: 2, bgcolor: 'primary.50', borderRadius: 1 }}>
|
|||
|
|
<Typography variant="body2">
|
|||
|
|
<strong>
|
|||
|
|
{t('exportDialog.totalExportCount')}: {totalCount}
|
|||
|
|
</strong>{' '}
|
|||
|
|
| {t('exportDialog.tagCount')}: {balanceConfig.filter(c => c.maxCount > 0).length} /{' '}
|
|||
|
|
{balanceConfig.length}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</DialogContent>
|
|||
|
|
<DialogActions>
|
|||
|
|
<Button onClick={() => setBalanceDialogOpen(false)}>{t('common.cancel', '取消')}</Button>
|
|||
|
|
<Button variant="contained" onClick={handleBalancedExport} disabled={loading || totalCount === 0}>
|
|||
|
|
{t('exportDialog.export')} ({totalCount})
|
|||
|
|
</Button>
|
|||
|
|
</DialogActions>
|
|||
|
|
</Dialog>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default LocalExportTab;
|