first-update
This commit is contained in:
610
easy-dataset-main/components/mga/GaPairsManager.js
Normal file
610
easy-dataset-main/components/mga/GaPairsManager.js
Normal file
@@ -0,0 +1,610 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
TextField,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Divider,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon,
|
||||
AutoFixHigh as AutoFixHighIcon,
|
||||
Save as SaveIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
/**
|
||||
* GA Pairs Manager Component
|
||||
* @param {Object} props
|
||||
* @param {string} props.projectId - Project ID
|
||||
* @param {string} props.fileId - File ID
|
||||
* @param {Function} props.onGaPairsChange - Callback when GA pairs change
|
||||
*/
|
||||
export default function GaPairsManager({ projectId, fileId, onGaPairsChange }) {
|
||||
const { t } = useTranslation();
|
||||
const [gaPairs, setGaPairs] = useState([]);
|
||||
const [backupGaPairs, setBackupGaPairs] = useState([]); // 备份状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(null);
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
const [newGaPair, setNewGaPair] = useState({
|
||||
genreTitle: '',
|
||||
genreDesc: '',
|
||||
audienceTitle: '',
|
||||
audienceDesc: '',
|
||||
isActive: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadGaPairs();
|
||||
}, [projectId, fileId]);
|
||||
|
||||
const loadGaPairs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`);
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
console.warn('GA Pairs API not found, using empty data');
|
||||
setGaPairs([]);
|
||||
setBackupGaPairs([]);
|
||||
return;
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: Failed to load GA pairs`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Load GA pairs result:', result);
|
||||
|
||||
if (result.success) {
|
||||
const loadedData = result.data || [];
|
||||
setGaPairs(loadedData);
|
||||
setBackupGaPairs([...loadedData]); // 创建备份
|
||||
onGaPairsChange?.(loadedData);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to load GA pairs');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load GA pairs error:', error);
|
||||
setError(t('gaPairs.loadError', { error: error.message }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateGaPairs = async () => {
|
||||
try {
|
||||
setGenerating(true);
|
||||
setError(null);
|
||||
|
||||
console.log('Starting GA pairs generation...');
|
||||
|
||||
// Get current language from i18n
|
||||
const currentLanguage = i18n.language === 'en' ? 'en' : '中文';
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
regenerate: false,
|
||||
appendMode: true, // 新增:启用追加模式
|
||||
language: currentLanguage
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = t('gaPairs.generateError');
|
||||
|
||||
if (response.status === 404) {
|
||||
errorMessage = t('gaPairs.serviceNotAvailable');
|
||||
} else if (response.status === 400) {
|
||||
try {
|
||||
const errorResult = await response.json();
|
||||
if (errorResult.error?.includes('No active AI model')) {
|
||||
errorMessage = t('gaPairs.noActiveModel');
|
||||
} else if (errorResult.error?.includes('content might be too short')) {
|
||||
errorMessage = t('gaPairs.contentTooShort');
|
||||
} else {
|
||||
errorMessage = errorResult.error || errorMessage;
|
||||
}
|
||||
} catch (parseError) {
|
||||
errorMessage = t('gaPairs.requestFailed', { status: response.status });
|
||||
}
|
||||
} else if (response.status === 500) {
|
||||
try {
|
||||
const errorResult = await response.json();
|
||||
if (errorResult.error?.includes('model configuration') || errorResult.error?.includes('Module not found')) {
|
||||
errorMessage = t('gaPairs.configError');
|
||||
} else {
|
||||
errorMessage = errorResult.error || 'Internal server error occurred.';
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse error response:', parseError);
|
||||
errorMessage = errorResult.error || t('gaPairs.internalServerError');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// 处理成功响应
|
||||
const responseText = await response.text();
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
throw new Error(t('gaPairs.emptyResponse'));
|
||||
}
|
||||
|
||||
const result = JSON.parse(responseText);
|
||||
console.log('Generate GA pairs result:', result);
|
||||
|
||||
if (result.success) {
|
||||
// 在追加模式下,后端只返回新生成的GA对
|
||||
const newGaPairs = result.data || [];
|
||||
|
||||
// 将新生成的GA对追加到现有的GA对
|
||||
const updatedGaPairs = [...gaPairs, ...newGaPairs];
|
||||
|
||||
setGaPairs(updatedGaPairs);
|
||||
setBackupGaPairs([...updatedGaPairs]); // 更新备份
|
||||
onGaPairsChange?.(updatedGaPairs);
|
||||
setSuccess(
|
||||
t('gaPairs.additionalPairsGenerated', {
|
||||
count: newGaPairs.length,
|
||||
total: updatedGaPairs.length
|
||||
})
|
||||
);
|
||||
} else {
|
||||
throw new Error(result.error || t('gaPairs.generationFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Generate GA pairs error:', error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveGaPairs = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
// 验证GA对数据
|
||||
const validatedGaPairs = gaPairs.map((pair, index) => {
|
||||
// 处理不同的数据格式
|
||||
let genreTitle, genreDesc, audienceTitle, audienceDesc;
|
||||
|
||||
if (pair.genre && typeof pair.genre === 'object') {
|
||||
genreTitle = pair.genre.title;
|
||||
genreDesc = pair.genre.description;
|
||||
} else {
|
||||
genreTitle = pair.genreTitle || pair.genre;
|
||||
genreDesc = pair.genreDesc || '';
|
||||
}
|
||||
|
||||
if (pair.audience && typeof pair.audience === 'object') {
|
||||
audienceTitle = pair.audience.title;
|
||||
audienceDesc = pair.audience.description;
|
||||
} else {
|
||||
audienceTitle = pair.audienceTitle || pair.audience;
|
||||
audienceDesc = pair.audienceDesc || '';
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (!genreTitle || !audienceTitle) {
|
||||
throw new Error(t('gaPairs.validationError', { number: index + 1 }));
|
||||
}
|
||||
|
||||
return {
|
||||
id: pair.id,
|
||||
genreTitle: genreTitle.trim(),
|
||||
genreDesc: genreDesc.trim(),
|
||||
audienceTitle: audienceTitle.trim(),
|
||||
audienceDesc: audienceDesc.trim(),
|
||||
isActive: pair.isActive !== undefined ? pair.isActive : true
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Saving validated GA pairs:', validatedGaPairs);
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
updates: validatedGaPairs
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = t('gaPairs.saveError');
|
||||
|
||||
if (response.status === 404) {
|
||||
errorMessage = 'GA Pairs save service is not available.';
|
||||
} else {
|
||||
try {
|
||||
const errorResult = await response.json();
|
||||
errorMessage = errorResult.error || errorMessage;
|
||||
} catch (parseError) {
|
||||
errorMessage = t('gaPairs.serverError', { status: response.status });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
const result = responseText ? JSON.parse(responseText) : { success: true };
|
||||
|
||||
if (result.success) {
|
||||
// 更新本地状态为服务器返回的数据
|
||||
const savedData = result.data || validatedGaPairs;
|
||||
setGaPairs(savedData);
|
||||
|
||||
// 根据保存的GA对数量显示不同的成功消息
|
||||
if (savedData.length === 0) {
|
||||
setSuccess(t('gaPairs.allPairsDeleted'));
|
||||
} else {
|
||||
setSuccess(t('gaPairs.pairsSaved', { count: savedData.length }));
|
||||
}
|
||||
|
||||
onGaPairsChange?.(savedData);
|
||||
} else {
|
||||
throw new Error(result.error || t('gaPairs.saveOperationFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save GA pairs error:', error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGaPairChange = (index, field, value) => {
|
||||
const updatedGaPairs = [...gaPairs];
|
||||
|
||||
// 确保对象存在
|
||||
if (!updatedGaPairs[index]) {
|
||||
console.error(`GA pair at index ${index} does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
updatedGaPairs[index] = {
|
||||
...updatedGaPairs[index],
|
||||
[field]: value
|
||||
};
|
||||
|
||||
setGaPairs(updatedGaPairs);
|
||||
// 不立即调用 onGaPairsChange,等用户点击保存时再调用
|
||||
};
|
||||
|
||||
const handleDeleteGaPair = index => {
|
||||
const updatedGaPairs = gaPairs.filter((_, i) => i !== index);
|
||||
setGaPairs(updatedGaPairs);
|
||||
onGaPairsChange?.(updatedGaPairs);
|
||||
};
|
||||
|
||||
const handleAddGaPair = () => {
|
||||
// 验证输入
|
||||
if (!newGaPair.genreTitle?.trim() || !newGaPair.audienceTitle?.trim()) {
|
||||
setError(t('gaPairs.requiredFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新的GA对对象
|
||||
const newPair = {
|
||||
id: `temp_${Date.now()}`, // 临时ID
|
||||
genreTitle: newGaPair.genreTitle.trim(),
|
||||
genreDesc: newGaPair.genreDesc?.trim() || '',
|
||||
audienceTitle: newGaPair.audienceTitle.trim(),
|
||||
audienceDesc: newGaPair.audienceDesc?.trim() || '',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const updatedGaPairs = [...gaPairs, newPair];
|
||||
setGaPairs(updatedGaPairs);
|
||||
onGaPairsChange?.(updatedGaPairs);
|
||||
|
||||
// 重置表单并关闭对话框
|
||||
setNewGaPair({
|
||||
genreTitle: '',
|
||||
genreDesc: '',
|
||||
audienceTitle: '',
|
||||
audienceDesc: '',
|
||||
isActive: true
|
||||
});
|
||||
setAddDialogOpen(false);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const resetMessages = () => {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
};
|
||||
|
||||
const recoverFromBackup = () => {
|
||||
setGaPairs([...backupGaPairs]);
|
||||
setError(null);
|
||||
setSuccess(t('gaPairs.restoredFromBackup'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (error || success) {
|
||||
const timer = setTimeout(resetMessages, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [error, success]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 4 }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>{t('gaPairs.loading')}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
{/* Header with action buttons */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h6">{t('gaPairs.title')}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{/* 右上角按钮为手动添加GA对 */}
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setAddDialogOpen(true)}
|
||||
disabled={generating || saving}
|
||||
>
|
||||
{t('gaPairs.addPair')}
|
||||
</Button>
|
||||
<Button variant="contained" startIcon={<SaveIcon />} onClick={saveGaPairs} disabled={generating || saving}>
|
||||
{saving ? <CircularProgress size={20} /> : t('gaPairs.saveChanges')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Error/Success Messages */}
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 2 }}
|
||||
action={
|
||||
backupGaPairs.length > 0 && (
|
||||
<Button color="inherit" size="small" onClick={recoverFromBackup}>
|
||||
{t('gaPairs.restoreBackup')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
onClose={resetMessages}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
{success && (
|
||||
<Alert severity="success" sx={{ mb: 2 }} onClose={resetMessages}>
|
||||
{success}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Generate GA Pairs Section - 只在没有GA对时显示 */}
|
||||
{gaPairs.length === 0 && (
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('gaPairs.noGaPairsTitle')}
|
||||
</Typography>
|
||||
<Typography color="textSecondary" sx={{ mb: 2 }}>
|
||||
{t('gaPairs.noGaPairsDescription')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={generating ? <CircularProgress size={20} /> : <AutoFixHighIcon />}
|
||||
onClick={generateGaPairs}
|
||||
disabled={generating}
|
||||
size="large"
|
||||
>
|
||||
{generating ? t('gaPairs.generating') : t('gaPairs.generateGaPairs')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* GA Pairs List */}
|
||||
{gaPairs.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||
{t('gaPairs.activePairs', {
|
||||
active: gaPairs.filter(pair => pair.isActive).length,
|
||||
total: gaPairs.length
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{gaPairs.map((pair, index) => (
|
||||
<Grid item xs={12} key={pair.id || index}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
<Typography variant="subtitle2" color="primary">
|
||||
{t('gaPairs.pairNumber', { number: index + 1 })}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={pair.isActive}
|
||||
onChange={e => handleGaPairChange(index, 'isActive', e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={t('gaPairs.active')}
|
||||
/>
|
||||
{/* 添加删除按钮 */}
|
||||
<Tooltip title={t('gaPairs.deleteTooltip')}>
|
||||
<IconButton size="small" color="error" onClick={() => handleDeleteGaPair(index)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label={t('gaPairs.genre')}
|
||||
value={pair.genreTitle || pair.genre || ''}
|
||||
onChange={e => handleGaPairChange(index, 'genreTitle', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
fullWidth
|
||||
disabled={!pair.isActive}
|
||||
/>
|
||||
<TextField
|
||||
label={t('gaPairs.genreDescription')}
|
||||
value={pair.genreDesc || ''}
|
||||
onChange={e => handleGaPairChange(index, 'genreDesc', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
fullWidth
|
||||
disabled={!pair.isActive}
|
||||
/>
|
||||
<TextField
|
||||
label={t('gaPairs.audience')}
|
||||
value={pair.audienceTitle || pair.audience || ''}
|
||||
onChange={e => handleGaPairChange(index, 'audienceTitle', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
fullWidth
|
||||
disabled={!pair.isActive}
|
||||
/>
|
||||
<TextField
|
||||
label={t('gaPairs.audienceDescription')}
|
||||
value={pair.audienceDesc || ''}
|
||||
onChange={e => handleGaPairChange(index, 'audienceDesc', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
fullWidth
|
||||
disabled={!pair.isActive}
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* 在GA对列表下方添加生成按钮 */}
|
||||
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={generating ? <CircularProgress size={20} /> : <AutoFixHighIcon />}
|
||||
onClick={generateGaPairs}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? t('gaPairs.generating') : t('gaPairs.generateMore')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Add GA Pair Dialog */}
|
||||
<Dialog open={addDialogOpen} onClose={() => setAddDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{t('gaPairs.addDialogTitle')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label={t('gaPairs.genreTitle')}
|
||||
value={newGaPair.genreTitle || ''}
|
||||
onChange={e => setNewGaPair({ ...newGaPair, genreTitle: e.target.value })}
|
||||
fullWidth
|
||||
required
|
||||
placeholder={t('gaPairs.genreTitlePlaceholder')}
|
||||
/>
|
||||
<TextField
|
||||
label={t('gaPairs.genreDescription')}
|
||||
value={newGaPair.genreDesc || ''}
|
||||
onChange={e => setNewGaPair({ ...newGaPair, genreDesc: e.target.value })}
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
placeholder={t('gaPairs.genreDescPlaceholder')}
|
||||
/>
|
||||
<TextField
|
||||
label={t('gaPairs.audienceTitle')}
|
||||
value={newGaPair.audienceTitle || ''}
|
||||
onChange={e => setNewGaPair({ ...newGaPair, audienceTitle: e.target.value })}
|
||||
fullWidth
|
||||
required
|
||||
placeholder={t('gaPairs.audienceTitlePlaceholder')}
|
||||
/>
|
||||
<TextField
|
||||
label={t('gaPairs.audienceDescription')}
|
||||
value={newGaPair.audienceDesc || ''}
|
||||
onChange={e => setNewGaPair({ ...newGaPair, audienceDesc: e.target.value })}
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
placeholder={t('gaPairs.audienceDescPlaceholder')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={newGaPair.isActive}
|
||||
onChange={e => setNewGaPair({ ...newGaPair, isActive: e.target.checked })}
|
||||
/>
|
||||
}
|
||||
label={t('gaPairs.active')}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAddDialogOpen(false);
|
||||
// 重置表单
|
||||
setNewGaPair({
|
||||
genreTitle: '',
|
||||
genreDesc: '',
|
||||
audienceTitle: '',
|
||||
audienceDesc: '',
|
||||
isActive: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('gaPairs.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddGaPair}
|
||||
variant="contained"
|
||||
disabled={!newGaPair.genreTitle?.trim() || !newGaPair.audienceTitle?.trim()}
|
||||
>
|
||||
{t('gaPairs.addPairButton')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user