first-update
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Typography, Card, CardContent, Chip, TextField } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 多轮对话内容展示和编辑组件
|
||||
*/
|
||||
export default function ConversationContent({ messages, editMode, onMessageChange, conversation }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取角色显示信息
|
||||
const getRoleDisplay = role => {
|
||||
switch (role) {
|
||||
case 'system':
|
||||
return { name: t('datasets.system'), color: 'default' };
|
||||
case 'user':
|
||||
return { name: conversation?.roleA || t('datasets.user'), color: 'primary' };
|
||||
case 'assistant':
|
||||
return { name: conversation?.roleB || t('datasets.assistant'), color: 'secondary' };
|
||||
default:
|
||||
return { name: role, color: 'default' };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('datasets.conversationContent')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ maxHeight: editMode ? 'none' : '70vh', overflowY: 'auto' }}>
|
||||
{messages.map((message, index) => {
|
||||
const roleInfo = getRoleDisplay(message.role);
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Chip label={roleInfo.name} color={roleInfo.color} size="small" sx={{ fontSize: '0.75rem' }} />
|
||||
{message.role !== 'system' && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
|
||||
{t('datasets.round', { round: Math.floor((index + 1) / 2) + 1 })}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Card variant="outlined" sx={{ mb: 1 }}>
|
||||
<CardContent sx={{ p: 2, '&:last-child': { pb: 2 } }}>
|
||||
{editMode ? (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={10}
|
||||
value={message.content}
|
||||
onChange={e => onMessageChange && onMessageChange(index, e.target.value)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiInputBase-input': {
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: 1.5
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="pre"
|
||||
sx={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: 1.6,
|
||||
margin: 0
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Button, Divider, Typography, IconButton, CircularProgress, Paper, Tooltip } from '@mui/material';
|
||||
import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore';
|
||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* 多轮对话详情页面的头部导航组件
|
||||
*/
|
||||
export default function ConversationHeader({
|
||||
projectId,
|
||||
conversationId,
|
||||
conversation,
|
||||
editMode,
|
||||
saving,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onNavigate
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2, mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Button startIcon={<NavigateBeforeIcon />} onClick={() => router.push(`/projects/${projectId}/multi-turn`)}>
|
||||
{t('common.backToList')}
|
||||
</Button>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Typography variant="h6">{t('datasets.conversationDetail')}</Typography>
|
||||
{conversation && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{conversation.scenario && (
|
||||
<>
|
||||
{conversation.scenario} • {conversation.turnCount}/{conversation.maxTurns} 轮
|
||||
</>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{/* 翻页按钮 */}
|
||||
<IconButton onClick={() => onNavigate && onNavigate('prev')}>
|
||||
<NavigateBeforeIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => onNavigate && onNavigate('next')}>
|
||||
<NavigateNextIcon />
|
||||
</IconButton>
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||
|
||||
{/* 编辑/保存按钮 */}
|
||||
{editMode ? (
|
||||
<>
|
||||
<Button onClick={onCancel}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={saving ? <CircularProgress size={16} /> : <SaveIcon />}
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? t('datasets.saving') : t('common.save')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outlined" startIcon={<EditIcon />} onClick={onEdit}>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button variant="outlined" color="error" startIcon={<DeleteIcon />} onClick={onDelete}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Typography, Chip, Tooltip, alpha, Paper } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
/**
|
||||
* 多轮对话元数据展示组件
|
||||
*/
|
||||
export default function ConversationMetadata({ conversation }) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!conversation) return null;
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2, mb: 3 }}>
|
||||
<Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{t('datasets.metadata')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip label={`${t('datasets.modelUsed')}: ${conversation.model}`} variant="outlined" size="small" />
|
||||
|
||||
{conversation.scenario && (
|
||||
<Chip
|
||||
label={`${t('datasets.conversationScenario')}: ${conversation.scenario}`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Chip
|
||||
label={`${t('datasets.conversationRounds')}: ${conversation.turnCount}/${conversation.maxTurns}`}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{conversation.roleA && (
|
||||
<Chip
|
||||
label={`${t('settings.multiTurnRoleA')}: ${conversation.roleA}`}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
|
||||
{conversation.roleB && (
|
||||
<Chip
|
||||
label={`${t('settings.multiTurnRoleB')}: ${conversation.roleB}`}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Chip
|
||||
label={`${t('datasets.createdAt')}: ${new Date(conversation.createAt).toLocaleDateString()}`}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{conversation.confirmed && (
|
||||
<Chip
|
||||
label={t('datasets.confirmed')}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: alpha(theme.palette.success.main, 0.1),
|
||||
color: theme.palette.success.dark,
|
||||
fontWeight: 'medium'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Typography, Divider, Paper, TextField } from '@mui/material';
|
||||
import { toast } from 'sonner';
|
||||
import StarRating from '@/components/datasets/StarRating';
|
||||
import TagSelector from '@/components/datasets/TagSelector';
|
||||
import NoteInput from '@/components/datasets/NoteInput';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 多轮对话评分、标签、备注综合组件
|
||||
*/
|
||||
export default function ConversationRatingSection({ conversation, projectId, onUpdate }) {
|
||||
const { t } = useTranslation();
|
||||
const [availableTags, setAvailableTags] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 解析对话中的标签
|
||||
const parseConversationTags = tagsString => {
|
||||
try {
|
||||
if (typeof tagsString === 'string' && tagsString.trim()) {
|
||||
return tagsString.split(/\s+/).filter(tag => tag.length > 0);
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 本地状态管理
|
||||
const [localScore, setLocalScore] = useState(conversation.score || 0);
|
||||
const [localTags, setLocalTags] = useState(() => parseConversationTags(conversation.tags));
|
||||
const [localNote, setLocalNote] = useState(conversation.note || '');
|
||||
|
||||
// 获取项目中已使用的标签
|
||||
useEffect(() => {
|
||||
const fetchAvailableTags = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/dataset-conversations/tags`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAvailableTags(data.tags || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取可用标签失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (projectId) {
|
||||
fetchAvailableTags();
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// 同步props中的conversation到本地状态
|
||||
useEffect(() => {
|
||||
setLocalScore(conversation.score || 0);
|
||||
setLocalTags(parseConversationTags(conversation.tags));
|
||||
setLocalNote(conversation.note || '');
|
||||
}, [conversation]);
|
||||
|
||||
// 更新对话元数据
|
||||
const updateMetadata = async updates => {
|
||||
if (loading) return;
|
||||
|
||||
// 立即更新本地状态
|
||||
if (updates.score !== undefined) {
|
||||
setLocalScore(updates.score);
|
||||
}
|
||||
if (updates.tagsArray !== undefined) {
|
||||
setLocalTags(updates.tagsArray);
|
||||
}
|
||||
if (updates.note !== undefined) {
|
||||
setLocalNote(updates.note);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/dataset-conversations/${conversation.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
score: updates.score,
|
||||
tags: updates.tags,
|
||||
note: updates.note
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(t('datasets.saveFailed'));
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
toast.success(t('datasets.saveSuccess'));
|
||||
|
||||
// 如果有父组件的更新回调,调用它
|
||||
if (onUpdate) {
|
||||
onUpdate(result.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新对话元数据失败:', error);
|
||||
toast.error(error.message || t('datasets.saveFailed'));
|
||||
|
||||
// 出错时恢复本地状态
|
||||
if (updates.score !== undefined) {
|
||||
setLocalScore(conversation.score || 0);
|
||||
}
|
||||
if (updates.tagsArray !== undefined) {
|
||||
setLocalTags(parseConversationTags(conversation.tags));
|
||||
}
|
||||
if (updates.note !== undefined) {
|
||||
setLocalNote(conversation.note || '');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理评分变更
|
||||
const handleScoreChange = newScore => {
|
||||
updateMetadata({ score: newScore });
|
||||
};
|
||||
|
||||
// 处理标签变更
|
||||
const handleTagsChange = newTags => {
|
||||
const tagsString = Array.isArray(newTags) ? newTags.join(' ') : '';
|
||||
updateMetadata({ tags: tagsString, tagsArray: newTags });
|
||||
};
|
||||
|
||||
// 处理备注变更
|
||||
const handleNoteChange = newNote => {
|
||||
updateMetadata({ note: newNote });
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
{/* 评分区域 */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{t('datasets.rating')}
|
||||
</Typography>
|
||||
<StarRating value={localScore} onChange={handleScoreChange} readOnly={loading} />
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* 标签区域 */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{t('datasets.customTags')}
|
||||
</Typography>
|
||||
<TagSelector
|
||||
value={localTags}
|
||||
onChange={handleTagsChange}
|
||||
availableTags={availableTags}
|
||||
readOnly={loading}
|
||||
placeholder={t('datasets.addCustomTag', '添加自定义标签...')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* 备注区域 */}
|
||||
<NoteInput
|
||||
value={localNote}
|
||||
onChange={handleNoteChange}
|
||||
readOnly={loading}
|
||||
placeholder={t('datasets.addNote', '添加备注...')}
|
||||
/>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
{/* 确认状态 */}
|
||||
{/* <Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||
{t('datasets.confirmationStatus')}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{conversation.confirmed ? t('datasets.confirmed') : t('datasets.unconfirmed')}
|
||||
</Typography>
|
||||
</Box> */}
|
||||
|
||||
{/* AI评估 */}
|
||||
{conversation.aiEvaluation && (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box>
|
||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||
{t('datasets.aiEvaluation')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.6 }}>
|
||||
{conversation.aiEvaluation}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user