first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,374 @@
'use client';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
Checkbox,
IconButton,
Chip,
Tooltip,
Pagination,
Divider,
Paper,
CircularProgress,
TextField
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import EditIcon from '@mui/icons-material/Edit';
import ChatIcon from '@mui/icons-material/Chat';
import { useGenerateDataset } from '@/hooks/useGenerateDataset';
import { toast } from 'sonner';
import { useAtomValue } from 'jotai';
import { selectedModelInfoAtom } from '@/lib/store';
export default function QuestionListView({
questions = [],
currentPage,
totalQuestions = 0,
handlePageChange,
selectedQuestions = [],
onSelectQuestion,
onDeleteQuestion,
projectId,
onEditQuestion,
refreshQuestions
}) {
const { t } = useTranslation();
// 处理状态
const [processingQuestions, setProcessingQuestions] = useState({});
const { generateSingleDataset } = useGenerateDataset();
// 获取当前选中的模型
const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
// 获取文本块的标题
const getChunkTitle = content => {
const firstLine = content ? content.split('\n')[0].trim() : '';
if (firstLine.startsWith('# ')) {
return firstLine.substring(2);
} else if (firstLine.length > 0) {
return firstLine.length > 200 ? firstLine.substring(0, 200) + '...' : firstLine;
}
return '';
};
// 检查问题是否被选中
const isQuestionSelected = questionId => {
return selectedQuestions.includes(questionId);
};
// 处理生成数据集
const handleGenerateDataset = async (questionId, questionInfo, imageId, imageName) => {
// 设置处理状态
setProcessingQuestions(prev => ({
...prev,
[questionId]: true
}));
await generateSingleDataset({
projectId,
questionId,
questionInfo,
imageId,
imageName
});
// 重置处理状态
setProcessingQuestions(prev => ({
...prev,
[questionId]: false
}));
refreshQuestions();
};
// 处理生成多轮对话数据集
const handleGenerateMultiTurnDataset = async (questionId, questionInfo) => {
try {
// 设置处理状态
setProcessingQuestions(prev => ({
...prev,
[`${questionId}_multi`]: true
}));
// 首先检查项目是否配置了多轮对话设置
const configResponse = await fetch(`/api/projects/${projectId}/tasks`);
if (!configResponse.ok) {
throw new Error('获取项目配置失败');
}
const config = await configResponse.json();
const multiTurnConfig = {
systemPrompt: config.multiTurnSystemPrompt,
scenario: config.multiTurnScenario,
rounds: config.multiTurnRounds,
roleA: config.multiTurnRoleA,
roleB: config.multiTurnRoleB
};
console.log('multiTurnConfig:', multiTurnConfig);
// 检查是否已配置必要的多轮对话设置
// 系统提示词是可选的但场景、角色A、角色B和轮数是必需的
if (
!multiTurnConfig.scenario ||
!multiTurnConfig.roleA ||
!multiTurnConfig.roleB ||
!multiTurnConfig.rounds ||
multiTurnConfig.rounds < 1
) {
toast.error(t('questions.multiTurnNotConfigured', '请先在项目设置中配置多轮对话相关参数'));
return;
}
// 检查是否选中了模型
if (!selectedModelInfo) {
toast.error(t('datasets.selectModelFirst', '请先选择模型'));
return;
}
// 调用多轮对话生成API
const response = await fetch(`/api/projects/${projectId}/dataset-conversations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
questionId,
...multiTurnConfig,
model: selectedModelInfo
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '生成多轮对话数据集失败');
}
const result = await response.json();
toast.success(t('questions.multiTurnGenerated', '多轮对话数据集生成成功!'));
} catch (error) {
console.error('生成多轮对话数据集失败:', error);
toast.error(error.message || '生成多轮对话数据集失败');
} finally {
// 重置处理状态
setProcessingQuestions(prev => ({
...prev,
[`${questionId}_multi`]: false
}));
}
};
return (
<Box style={{ padding: '20px' }}>
{/* 问题列表 */}
<Paper
elevation={0}
sx={{
borderRadius: 2,
overflow: 'hidden',
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
}}
>
<Box sx={{ px: 2, py: 1, display: 'flex', alignItems: 'center', bgcolor: 'background.paper' }}>
<Typography variant="body2" sx={{ fontWeight: 500, ml: 1 }}>
{t('datasets.question')}
</Typography>
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ fontWeight: 500, mr: 2, display: { xs: 'none', sm: 'block' } }}>
{t('common.label')}
</Typography>
<Typography
variant="body2"
sx={{ fontWeight: 500, width: 150, mr: 2, display: { xs: 'none', md: 'block' } }}
>
{t('common.dataSource')}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 500, width: 100, textAlign: 'center' }}>
{t('common.actions')}
</Typography>
</Box>
</Box>
<Divider />
{questions.map((question, index) => {
const isSelected = isQuestionSelected(question.id);
const questionKey = question.id;
return (
<Box key={questionKey}>
<Box
sx={{
px: 2,
py: 1.5,
display: 'flex',
alignItems: 'center',
bgcolor: isSelected ? 'action.selected' : 'background.paper',
'&:hover': {
bgcolor: 'action.hover'
}
}}
>
<Checkbox
checked={isSelected}
onChange={() => {
onSelectQuestion(questionKey);
}}
size="small"
/>
<Box sx={{ ml: 1, flex: 1, mr: 2 }}>
<Typography variant="body2">
{question.question}
{question.datasetCount > 0 ? (
<Chip
label={t('datasets.answerCount', { count: question.datasetCount })}
size="small"
color="primary"
variant="outlined"
sx={{ fontSize: '0.75rem', maxWidth: 150 }}
/>
) : null}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: { xs: 'block', sm: 'none' } }}>
{question.label || t('datasets.noTag')} ID: {(question.question || '').substring(0, 8)}
</Typography>
</Box>
<Box sx={{ display: { xs: 'none', sm: 'block' }, mr: 2 }}>
{question.label ? (
<Chip
label={question.label}
size="small"
color="primary"
variant="outlined"
sx={{ fontSize: '0.75rem', maxWidth: 150 }}
/>
) : (
<Typography variant="caption" color="text.disabled">
{t('datasets.noTag')}
</Typography>
)}
</Box>
<Box sx={{ width: 150, mr: 2, display: { xs: 'none', md: 'block' } }}>
<Tooltip title={getChunkTitle(question.chunk?.content)}>
<Chip
label={
question.imageId
? `Image: ${question.imageName}`
: `${t('chunks.title')}: ${question.chunk?.name}`
}
size="small"
variant="outlined"
color="info"
sx={{
fontSize: '0.75rem',
maxWidth: '100%',
textOverflow: 'ellipsis'
}}
/>
</Tooltip>
</Box>
<Box sx={{ width: 160, display: 'flex', justifyContent: 'center' }}>
<Tooltip title={t('common.edit')}>
<IconButton
size="small"
color="primary"
onClick={() => onEditQuestion(question)}
disabled={processingQuestions[questionKey]}
>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('datasets.generateDataset')}>
<IconButton
size="small"
color="primary"
onClick={() =>
handleGenerateDataset(question.id, question.question, question.imageId, question.imageName)
}
disabled={processingQuestions[questionKey]}
>
{processingQuestions[questionKey] ? (
<CircularProgress size={16} />
) : (
<AutoFixHighIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
{!question.imageId && (
<Tooltip title={t('questions.generateMultiTurn', '生成多轮对话')}>
<IconButton
size="small"
color="secondary"
onClick={() => handleGenerateMultiTurnDataset(question.id, question.question)}
disabled={processingQuestions[`${questionKey}_multi`]}
>
{processingQuestions[`${questionKey}_multi`] ? (
<CircularProgress size={16} />
) : (
<ChatIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
)}
<Tooltip title={t('common.delete')}>
<IconButton
size="small"
color="error"
onClick={() => onDeleteQuestion(question.id)}
disabled={processingQuestions[questionKey]}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
{index < questions.length - 1 && <Divider />}
</Box>
);
})}
</Paper>
{/* 分页 */}
{totalQuestions > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', mt: 3, mb: 2 }}>
<Pagination
count={totalQuestions}
page={currentPage}
onChange={handlePageChange}
color="primary"
showFirstButton
showLastButton
shape="rounded"
size="medium"
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2">{t('common.jumpTo')}:</Typography>
<TextField
size="small"
type="number"
inputProps={{
min: 1,
max: totalQuestions,
style: { padding: '4px 8px', width: '50px' }
}}
onKeyPress={e => {
if (e.key === 'Enter') {
const pageNum = parseInt(e.target.value, 10);
if (pageNum >= 1 && pageNum <= totalQuestions) {
handlePageChange(null, pageNum);
e.target.value = '';
}
}
}}
/>
</Box>
</Box>
)}
</Box>
);
}

View File

@@ -0,0 +1,565 @@
'use client';
import { useState, useEffect, useCallback, useMemo, memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
Paper,
List,
ListItem,
ListItemText,
Checkbox,
IconButton,
Collapse,
Chip,
Tooltip,
Divider,
CircularProgress
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import DeleteIcon from '@mui/icons-material/Delete';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import EditIcon from '@mui/icons-material/Edit';
import FolderIcon from '@mui/icons-material/Folder';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import { useGenerateDataset } from '@/hooks/useGenerateDataset';
import axios from 'axios';
/**
* 问题树视图组件
* @param {Object} props
* @param {Array} props.tags - 标签树
* @param {Array} props.selectedQuestions - 已选择的问题ID列表
* @param {Function} props.onSelectQuestion - 选择问题的回调函数
* @param {Function} props.onDeleteQuestion - 删除问题的回调函数
*/
export default function QuestionTreeView({
tags = [],
selectedQuestions = [],
onSelectQuestion,
onDeleteQuestion,
onEditQuestion,
projectId,
searchTerm
}) {
const { t } = useTranslation();
const [expandedTags, setExpandedTags] = useState({});
const [questionsByTag, setQuestionsByTag] = useState({});
const [processingQuestions, setProcessingQuestions] = useState({});
const { generateSingleDataset } = useGenerateDataset();
const [questions, setQuestions] = useState([]);
const [loadedTags, setLoadedTags] = useState({});
// 初始化时,将所有标签设置为收起状态(而不是展开状态)
useEffect(() => {
async function fetchTagsInfo() {
try {
// 获取标签信息,仅用于标签统计
const response = await axios.get(`/api/projects/${projectId}/questions/tree?tagsOnly=true&input=${searchTerm}`);
setQuestions(response.data); // 设置数据仅用于标签统计
// 当搜索条件变化时,重新加载已展开标签的问题数据
const expandedTagLabels = Object.entries(expandedTags)
.filter(([_, isExpanded]) => isExpanded)
.map(([label]) => label);
// 重新加载已展开标签的数据
for (const label of expandedTagLabels) {
fetchTagQuestions(label);
}
} catch (error) {
console.error('获取标签信息失败:', error);
}
}
if (projectId) {
fetchTagsInfo();
}
const initialExpandedState = {};
const processTag = tag => {
// 将默认状态改为 false收起而不是 true展开
initialExpandedState[tag.label] = false;
if (tag.child && tag.child.length > 0) {
tag.child.forEach(processTag);
}
};
tags.forEach(processTag);
// 未分类问题也默认收起
initialExpandedState['uncategorized'] = false;
setExpandedTags(initialExpandedState);
}, [tags]);
// 根据标签对问题进行分类
useEffect(() => {
const taggedQuestions = {};
// 初始化标签映射
const initTagMap = tag => {
taggedQuestions[tag.label] = [];
if (tag.child && tag.child.length > 0) {
tag.child.forEach(initTagMap);
}
};
tags.forEach(initTagMap);
// 将问题分配到对应的标签下
questions.forEach(question => {
// 如果问题没有标签,添加到"未分类"
if (!question.label) {
if (!taggedQuestions['uncategorized']) {
taggedQuestions['uncategorized'] = [];
}
taggedQuestions['uncategorized'].push(question);
return;
}
// 将问题添加到匹配的标签下
const questionLabel = question.label;
// 查找最精确匹配的标签
// 使用一个数组来存储所有匹配的标签路径,以便找到最精确的匹配
const findAllMatchingTags = (tag, path = []) => {
const currentPath = [...path, tag.label];
// 存储所有匹配结果
const matches = [];
// 精确匹配当前标签
if (tag.label === questionLabel) {
matches.push({ label: tag.label, depth: currentPath.length });
}
// 检查子标签
if (tag.child && tag.child.length > 0) {
for (const childTag of tag.child) {
const childMatches = findAllMatchingTags(childTag, currentPath);
matches.push(...childMatches);
}
}
return matches;
};
// 在所有根标签中查找所有匹配
let allMatches = [];
for (const rootTag of tags) {
const matches = findAllMatchingTags(rootTag);
allMatches.push(...matches);
}
// 找到深度最大的匹配(最精确的匹配)
let matchedTagLabel = null;
if (allMatches.length > 0) {
// 按深度排序,深度最大的是最精确的匹配
allMatches.sort((a, b) => b.depth - a.depth);
matchedTagLabel = allMatches[0].label;
}
if (matchedTagLabel) {
// 如果找到匹配的标签,将问题添加到该标签下
if (!taggedQuestions[matchedTagLabel]) {
taggedQuestions[matchedTagLabel] = [];
}
taggedQuestions[matchedTagLabel].push(question);
} else {
// 如果找不到匹配的标签,添加到"未分类"
if (!taggedQuestions['uncategorized']) {
taggedQuestions['uncategorized'] = [];
}
taggedQuestions['uncategorized'].push(question);
}
});
setQuestionsByTag(taggedQuestions);
}, [questions, tags]);
// 处理展开/折叠标签 - 使用 useCallback 优化
const handleToggleExpand = useCallback(
tagLabel => {
// 检查是否需要加载此标签的问题数据
const shouldExpand = !expandedTags[tagLabel];
if (shouldExpand && !loadedTags[tagLabel]) {
// 如果要展开且尚未加载数据,则加载数据
fetchTagQuestions(tagLabel);
}
setExpandedTags(prev => ({
...prev,
[tagLabel]: shouldExpand
}));
},
[expandedTags, loadedTags, projectId]
);
// 获取特定标签的问题数据
const fetchTagQuestions = useCallback(
async tagLabel => {
try {
const response = await axios.get(
`/api/projects/${projectId}/questions/tree?tag=${encodeURIComponent(tagLabel)}${searchTerm ? `&input=${searchTerm}` : ''}`
);
// 更新问题数据,合并新获取的数据
setQuestions(prev => {
// 创建一个新数组,包含现有数据
const updatedQuestions = [...prev];
// 添加新获取的问题数据
response.data.forEach(newQuestion => {
// 检查是否已存在相同 ID 的问题
const existingIndex = updatedQuestions.findIndex(q => q.id === newQuestion.id);
if (existingIndex === -1) {
// 如果不存在,添加到数组
updatedQuestions.push(newQuestion);
} else {
// 如果已存在,更新数据
updatedQuestions[existingIndex] = newQuestion;
}
});
return updatedQuestions;
});
// 标记该标签已加载数据
setLoadedTags(prev => ({
...prev,
[tagLabel]: true
}));
} catch (error) {
console.error(`获取标签 "${tagLabel}" 的问题失败:`, error);
}
},
[projectId, searchTerm, expandedTags]
);
// 检查问题是否被选中 - 使用 useCallback 优化
const isQuestionSelected = useCallback(
questionKey => {
return selectedQuestions.includes(questionKey);
},
[selectedQuestions]
);
// 处理生成数据集 - 使用 useCallback 优化
const handleGenerateDataset = async (questionId, questionInfo) => {
// 设置处理状态
setProcessingQuestions(prev => ({
...prev,
[questionId]: true
}));
await generateSingleDataset({ projectId, questionId, questionInfo });
// 重置处理状态
setProcessingQuestions(prev => ({
...prev,
[questionId]: false
}));
};
// 渲染单个问题项 - 使用 useCallback 优化
const renderQuestionItem = useCallback(
(question, index, total) => {
const questionKey = question.id;
return (
<QuestionItem
key={questionKey}
question={question}
index={index}
total={total}
isSelected={isQuestionSelected(questionKey)}
onSelect={onSelectQuestion}
onDelete={onDeleteQuestion}
onGenerate={handleGenerateDataset}
onEdit={onEditQuestion}
isProcessing={processingQuestions[questionKey]}
t={t}
/>
);
},
[isQuestionSelected, onSelectQuestion, onDeleteQuestion, handleGenerateDataset, processingQuestions, t]
);
// 计算标签及其子标签下的所有问题数量 - 使用 useMemo 缓存计算结果
const tagQuestionCounts = useMemo(() => {
const counts = {};
const countQuestions = tag => {
const directQuestions = questionsByTag[tag.label] || [];
let total = directQuestions.length;
if (tag.child && tag.child.length > 0) {
for (const childTag of tag.child) {
total += countQuestions(childTag);
}
}
counts[tag.label] = total;
return total;
};
tags.forEach(countQuestions);
return counts;
}, [questionsByTag, tags]);
// 递归渲染标签树 - 使用 useCallback 优化
const renderTagTree = useCallback(
(tag, level = 0) => {
const questions = questionsByTag[tag.label] || [];
const hasQuestions = questions.length > 0;
const hasChildren = tag.child && tag.child.length > 0;
const isExpanded = expandedTags[tag.label];
const totalQuestions = tagQuestionCounts[tag.label] || 0;
return (
<Box key={tag.label}>
<TagItem
tag={tag}
level={level}
isExpanded={isExpanded}
totalQuestions={totalQuestions}
onToggle={handleToggleExpand}
t={t}
/>
{/* 只有当标签展开时才渲染子内容,减少不必要的渲染 */}
{isExpanded && (
<Collapse in={true}>
{hasChildren && (
<List disablePadding>{tag.child.map(childTag => renderTagTree(childTag, level + 1))}</List>
)}
{hasQuestions && (
<List disablePadding sx={{ mt: hasChildren ? 1 : 0 }}>
{questions.map((question, index) => renderQuestionItem(question, index, questions.length))}
</List>
)}
</Collapse>
)}
</Box>
);
},
[questionsByTag, expandedTags, tagQuestionCounts, handleToggleExpand, renderQuestionItem, t]
);
// 渲染未分类问题
const renderUncategorizedQuestions = () => {
const uncategorizedQuestions = questionsByTag['uncategorized'] || [];
if (uncategorizedQuestions.length === 0) return null;
return (
<Box>
<ListItem
button
onClick={() => handleToggleExpand('uncategorized')}
sx={{
py: 1,
bgcolor: 'primary.light',
color: 'primary.contrastText',
'&:hover': {
bgcolor: 'primary.main'
},
borderRadius: '4px',
mb: 0.5,
pr: 1
}}
>
<FolderIcon fontSize="small" sx={{ mr: 1, color: 'inherit' }} />
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body1" sx={{ fontWeight: 600, fontSize: '1rem' }}>
{t('datasets.uncategorized')}
</Typography>
<Chip
label={t('datasets.questionCount', { count: uncategorizedQuestions.length })}
size="small"
sx={{ ml: 1, height: 20, fontSize: '0.7rem', color: '#fff', backgroundColor: '#333' }}
/>
</Box>
}
/>
<IconButton size="small" edge="end" sx={{ color: 'inherit' }}>
{expandedTags['uncategorized'] ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</ListItem>
<Collapse in={expandedTags['uncategorized']}>
<List disablePadding>
{uncategorizedQuestions.map((question, index) =>
renderQuestionItem(question, index, uncategorizedQuestions.length)
)}
</List>
</Collapse>
</Box>
);
};
// 如果没有标签和问题,显示空状态
if (tags.length === 0 && Object.keys(questionsByTag).length === 0) {
return (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary">
{t('datasets.noTagsAndQuestions')}
</Typography>
</Box>
);
}
return (
<Paper
elevation={0}
sx={{
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
overflow: 'auto',
p: 2,
maxHeight: '75vh'
}}
>
<List disablePadding>
{renderUncategorizedQuestions()}
{tags.map(tag => renderTagTree(tag))}
</List>
</Paper>
);
}
// 使用 memo 优化问题项渲染
const QuestionItem = memo(
({ question, index, total, isSelected, onSelect, onDelete, onGenerate, onEdit, isProcessing, t }) => {
const questionKey = question.id;
return (
<Box key={question.id}>
<ListItem
sx={{
pl: 4,
py: 1,
borderRadius: '4px',
ml: 2,
mr: 1,
mb: 0.5,
bgcolor: isSelected ? 'action.selected' : 'transparent',
'&:hover': {
bgcolor: 'action.hover'
}
}}
>
<Checkbox checked={isSelected} onChange={() => onSelect(questionKey)} size="small" />
<QuestionMarkIcon fontSize="small" sx={{ mr: 1, color: 'primary.main' }} />
<ListItemText
primary={
<Typography variant="body2" sx={{ fontWeight: 400 }}>
{question.question}
{question.dataSites && question.dataSites.length > 0 && (
<Chip
label={t('datasets.answerCount', { count: question.dataSites.length })}
size="small"
color="primary"
variant="outlined"
sx={{ ml: 1, fontSize: '0.75rem', maxWidth: 150 }}
/>
)}
</Typography>
}
secondary={
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{t('datasets.source')}: {question.chunk?.name || question.chunkId || t('common.unknown')}
</Typography>
}
/>
<Box>
<Tooltip title={t('common.edit')}>
<IconButton
size="small"
sx={{ mr: 1 }}
onClick={() =>
onEdit({
question: question.question,
chunkId: question.chunkId,
label: question.label || 'other'
})
}
disabled={isProcessing}
>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t('datasets.generateDataset')}>
<IconButton
size="small"
sx={{ mr: 1 }}
onClick={() => onGenerate(question.id, question.question)}
disabled={isProcessing}
>
{isProcessing ? <CircularProgress size={16} /> : <AutoFixHighIcon fontSize="small" />}
</IconButton>
</Tooltip>
<Tooltip title={t('common.delete')}>
<IconButton size="small" onClick={() => onDelete(question.id)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</ListItem>
{index < total - 1 && <Divider component="li" variant="inset" sx={{ ml: 6 }} />}
</Box>
);
}
);
// 使用 memo 优化标签项渲染
const TagItem = memo(({ tag, level, isExpanded, totalQuestions, onToggle, t }) => {
return (
<ListItem
button
onClick={() => onToggle(tag.label)}
sx={{
pl: level * 2 + 1,
py: 1,
bgcolor: level === 0 ? 'primary.light' : 'background.paper',
color: level === 0 ? 'primary.contrastText' : 'inherit',
'&:hover': {
bgcolor: level === 0 ? 'primary.main' : 'action.hover'
},
borderRadius: '4px',
mb: 0.5,
pr: 1
}}
>
{/* 内部内容保持不变 */}
<FolderIcon fontSize="small" sx={{ mr: 1, color: level === 0 ? 'inherit' : 'primary.main' }} />
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography
variant="body1"
sx={{
fontWeight: level === 0 ? 600 : 400,
fontSize: level === 0 ? '1rem' : '0.9rem'
}}
>
{tag.label}
</Typography>
{totalQuestions > 0 && (
<Chip
label={t('datasets.questionCount', { count: totalQuestions })}
size="small"
color={level === 0 ? 'default' : 'primary'}
variant={level === 0 ? 'default' : 'outlined'}
sx={{ ml: 1, height: 20, fontSize: '0.7rem', color: '#fff', backgroundColor: '#333' }}
/>
)}
</Box>
}
/>
<IconButton size="small" edge="end" sx={{ color: level === 0 ? 'inherit' : 'action.active' }}>
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</ListItem>
);
});