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,25 @@
import React from 'react';
import { Tabs, Tab } from '@mui/material';
/**
* 顶部分类选择标签页组件
*/
const CategoryTabs = ({ categoryEntries, selectedCategory, currentLanguage, onCategoryChange }) => {
return (
<Tabs
value={selectedCategory}
onChange={(e, newValue) => {
onCategoryChange(newValue);
}}
variant="scrollable"
scrollButtons="auto"
sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}
>
{categoryEntries.map(([categoryKey, categoryConfig]) => (
<Tab key={categoryKey} label={categoryConfig.displayName[currentLanguage]} value={categoryKey} />
))}
</Tabs>
);
};
export default CategoryTabs;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, Box, Typography, Chip, Button, Paper } from '@mui/material';
import { Edit as EditIcon, Restore as RestoreIcon } from '@mui/icons-material';
import ReactMarkdown from 'react-markdown';
import 'github-markdown-css/github-markdown-light.css';
/**
* 右侧提示词详情展示组件
*/
const PromptDetail = ({
currentPromptConfig,
selectedPrompt,
promptContent,
isCustomized,
onEditClick,
onDeleteClick
}) => {
const { t } = useTranslation();
if (!currentPromptConfig) {
return (
<Box sx={{ p: 3, textAlign: 'center', color: 'text.secondary' }}>{t('settings.prompts.selectPromptFirst')}</Box>
);
}
const handleEditClick = () => {
onEditClick();
};
const handleDeleteClick = () => {
onDeleteClick();
};
return (
<Card>
<CardContent>
{/* 标题、描述与操作区域 */}
<Box sx={{ mb: 3 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 2,
flexWrap: 'wrap'
}}
>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6">{currentPromptConfig.name}</Typography>
{isCustomized(selectedPrompt) && (
<Chip label={t('settings.prompts.customized')} color="primary" size="small" />
)}
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Button startIcon={<EditIcon />} variant="contained" size="small" onClick={handleEditClick}>
{t('settings.prompts.editPrompt')}
</Button>
{isCustomized(selectedPrompt) && (
<Button startIcon={<RestoreIcon />} color="error" size="small" onClick={handleDeleteClick}>
{t('settings.prompts.restoreDefault')}
</Button>
)}
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{currentPromptConfig.description}
</Typography>
</Box>
{/* Markdown 渲染提示词内容 */}
<Paper
variant="outlined"
sx={{
p: 2,
overflow: 'auto'
}}
>
<div className="markdown-body">
<ReactMarkdown>{promptContent}</ReactMarkdown>
</div>
</Paper>
</CardContent>
</Card>
);
};
export default PromptDetail;

View File

@@ -0,0 +1,71 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Box,
Typography,
Chip
} from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import RestoreIcon from '@mui/icons-material/Restore';
/**
* 提示词编辑对话框组件
*/
const PromptEditDialog = ({
open,
title,
promptType,
promptKey,
content,
loading,
onClose,
onSave,
onRestore,
onContentChange
}) => {
const { t } = useTranslation();
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
{t('settings.prompts.promptType')}: {promptType}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('settings.prompts.keyName')}: {promptKey}
</Typography>
</Box>
<TextField
fullWidth
multiline
rows={15}
value={content}
onChange={e => onContentChange(e.target.value)}
placeholder={t('settings.prompts.contentPlaceholder')}
variant="outlined"
/>
<Box display="flex" gap={1}>
<Button startIcon={<RestoreIcon />} onClick={onRestore} size="small" variant="outlined">
{t('settings.prompts.restoreDefaultContent')}
</Button>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('common.cancel')}</Button>
<Button onClick={onSave} variant="contained" disabled={loading} startIcon={<SaveIcon />}>
{t('common.save')}
</Button>
</DialogActions>
</Dialog>
);
};
export default PromptEditDialog;

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Tabs, Tab, Typography, Chip } from '@mui/material';
import { shouldShowPrompt } from './promptUtils';
/**
* 左侧提示词列表组件
*/
const PromptList = ({
currentCategory,
currentCategoryConfig,
selectedPrompt,
currentLanguage,
isCustomized,
onPromptSelect
}) => {
const { t } = useTranslation();
if (!currentCategoryConfig?.prompts) {
return (
<Typography variant="body2" color="text.secondary" align="center">
{t('settings.prompts.noPromptsAvailable')}
</Typography>
);
}
return (
<Tabs
orientation="vertical"
value={selectedPrompt || ''}
onChange={(e, newValue) => onPromptSelect(newValue)}
variant="scrollable"
scrollButtons="auto"
sx={{
borderRight: 1,
borderColor: 'divider',
'& .MuiTabs-indicator': {
left: 0,
right: 'auto'
},
'& .MuiTab-root': {
alignItems: 'flex-start',
textAlign: 'left'
}
}}
>
{currentCategoryConfig &&
Object.entries(currentCategoryConfig.prompts).map(([promptKey, promptConfig]) => {
if (!shouldShowPrompt(promptKey, currentLanguage)) return null;
const customized = isCustomized(promptKey);
return (
<Tab
key={promptKey}
value={promptKey}
label={
<Box sx={{ textAlign: 'left', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{promptConfig.name}
</Typography>
{customized && (
<Chip label={t('settings.prompts.customized')} color="primary" size="small" sx={{ mt: 0.5 }} />
)}
</Box>
}
sx={{
alignItems: 'flex-start',
minHeight: 60,
px: 2,
justifyContent: 'flex-start',
width: '100%'
}}
/>
);
})}
</Tabs>
);
};
export default PromptList;

View File

@@ -0,0 +1,400 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { Box, Grid, Card, CardContent } from '@mui/material';
import { fetchWithRetry } from '@/lib/util/request';
import { useSnackbar } from '@/hooks/useSnackbar';
// 导入拆分后的组件
import CategoryTabs from './CategoryTabs';
import PromptList from './PromptList';
import PromptDetail from './PromptDetail';
import PromptEditDialog from './PromptEditDialog';
import { getLanguageFromPromptKey, shouldShowPrompt } from './promptUtils';
/**
* 提示词设置主组件
*/
export default function PromptSettings() {
const { projectId } = useParams();
const { i18n, t } = useTranslation();
const { showSuccess, showErrorMessage, SnackbarComponent } = useSnackbar();
// 基础状态
const [currentLanguage, setCurrentLanguage] = useState(i18n.language === 'en' ? 'en' : 'zh-CN');
const [loading, setLoading] = useState(false);
const [templates, setTemplates] = useState({});
const [customPrompts, setCustomPrompts] = useState([]);
// 当前选中状态
const [selectedCategory, setSelectedCategory] = useState(null);
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [promptContent, setPromptContent] = useState('');
// 编辑对话框状态
const [editDialog, setEditDialog] = useState({
open: false,
promptType: '',
promptKey: '',
language: '',
content: '',
defaultContent: '',
isNew: false
});
// ======= 数据加载与初始化 =======
// 加载提示词数据
useEffect(() => {
loadPromptData();
}, [projectId, currentLanguage]);
// 监听语言变化
useEffect(() => {
const newLang = i18n.language === 'en' ? 'en' : 'zh-CN';
if (newLang !== currentLanguage) {
setCurrentLanguage(newLang);
}
}, [i18n.language, currentLanguage]);
// 监听选中提示词变化
useEffect(() => {
if (selectedPrompt) {
loadPromptContent();
}
}, [selectedPrompt]);
// 初始化选择第一个分类和提示词
useEffect(() => {
if (Object.keys(templates).length > 0 && currentLanguage && !selectedCategory) {
const firstCategory = Object.keys(templates)[0];
setSelectedCategory(firstCategory);
// 根据当前语言环境选择第一个匹配的提示词
const promptEntries = Object.keys(templates[firstCategory]?.prompts || {});
const firstPrompt = promptEntries.find(promptKey => shouldShowPrompt(promptKey, currentLanguage));
if (firstPrompt) {
setSelectedPrompt(firstPrompt);
}
}
}, [templates, selectedCategory, currentLanguage]);
// ======= API 操作函数 =======
// 加载提示词数据
const loadPromptData = async () => {
try {
setLoading(true);
const response = await fetchWithRetry(`/api/projects/${projectId}/custom-prompts?language=${currentLanguage}`);
const data = await response.json();
if (data.success) {
setTemplates(data.templates);
setCustomPrompts(data.customPrompts);
} else {
showErrorMessage(data.message || '加载提示词数据失败');
}
} catch (error) {
console.error('加载提示词数据出错:', error);
showErrorMessage('加载提示词数据失败');
} finally {
setLoading(false);
}
};
// 加载提示词内容
const loadPromptContent = async (forceRefresh = false) => {
if (!selectedPrompt) return;
try {
setLoading(true);
const content = await getCurrentPromptContent(selectedPrompt, forceRefresh);
setPromptContent(content);
} catch (error) {
console.error('加载提示词内容出错:', error);
showErrorMessage('加载提示词内容失败');
} finally {
setLoading(false);
}
};
// 加载默认提示词内容
const loadDefaultContent = async (promptType, promptKey) => {
if (i18n.language === 'en' && !promptKey.endsWith('_EN')) {
promptKey += '_EN';
}
try {
const response = await fetchWithRetry(
`/api/projects/${projectId}/default-prompts?promptType=${promptType}&promptKey=${promptKey}`
);
const data = await response.json();
if (data.success) {
return data.content;
}
return '';
} catch (error) {
console.error('加载默认提示词内容出错:', error);
return '';
}
};
// ======= 交互处理函数 =======
// 处理编辑提示词
const handleEditPrompt = async (promptType, promptKey, language) => {
const existingPrompt = customPrompts.find(
p => p.promptType === promptType && p.promptKey === promptKey && p.language === language
);
const defaultContent = await loadDefaultContent(promptType, promptKey);
setEditDialog({
open: true,
promptType,
promptKey,
language,
content: existingPrompt?.content || defaultContent,
defaultContent,
isNew: !existingPrompt
});
};
// 处理删除提示词
const handleDeletePrompt = async (promptType, promptKey, language) => {
try {
setLoading(true);
const query = new URLSearchParams({
promptType,
promptKey,
language
}).toString();
const response = await fetchWithRetry(`/api/projects/${projectId}/custom-prompts?${query}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showSuccess(t('settings.prompts.restoreSuccess'));
// 先重新加载数据,然后强制刷新内容
await loadPromptData();
await loadPromptContent(true); // 强制刷新
} else {
showErrorMessage(data.message || t('settings.prompts.restoreFailed'));
}
} catch (error) {
console.error(t('settings.prompts.deleteError'), error);
showErrorMessage(t('settings.prompts.restoreFailed'));
} finally {
setLoading(false);
}
};
// 处理保存提示词
const handleSavePrompt = async () => {
try {
setLoading(true);
const { promptType, promptKey, language, content } = editDialog;
const response = await fetchWithRetry(`/api/projects/${projectId}/custom-prompts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ promptType, promptKey, language, content })
});
const data = await response.json();
if (data.success) {
showSuccess(t('settings.prompts.saveSuccess'));
setEditDialog({ ...editDialog, open: false });
// 先重新加载数据,然后强制刷新内容
await loadPromptData();
await loadPromptContent(true); // 强制刷新
} else {
showErrorMessage(data.message || t('settings.prompts.saveFailed'));
}
} catch (error) {
console.error(t('settings.prompts.saveError'), error);
showErrorMessage(t('settings.prompts.saveFailed'));
} finally {
setLoading(false);
}
};
// 恢复默认内容
const handleRestoreDefault = () => {
setEditDialog(prev => ({
...prev,
content: prev.defaultContent
}));
};
// ======= 工具函数 =======
// 检查提示词是否已自定义
const isCustomized = promptKey => {
if (!selectedCategory || !promptKey || !templates[selectedCategory]) return false;
const language = getLanguageFromPromptKey(promptKey);
const promptType = templates[selectedCategory]?.prompts?.[promptKey]?.type;
if (!promptType) return false;
return customPrompts.some(p => p.promptType === promptType && p.promptKey === promptKey && p.language === language);
};
// 获取当前提示词内容(直接从服务器获取最新数据)
const getCurrentPromptContent = async (promptKey, forceRefresh = false) => {
if (!selectedCategory || !promptKey || !templates[selectedCategory]) return '';
const language = getLanguageFromPromptKey(promptKey);
const promptType = templates[selectedCategory]?.prompts?.[promptKey]?.type;
if (!promptType) {
return '';
}
// 如果需要强制刷新,直接从服务器获取
if (forceRefresh) {
try {
const response = await fetchWithRetry(
`/api/projects/${projectId}/custom-prompts?promptType=${promptType}&language=${language}`
);
const data = await response.json();
if (data.success) {
const existingPrompt = data.customPrompts.find(
p => p.promptType === promptType && p.promptKey === promptKey && p.language === language
);
if (existingPrompt) {
return existingPrompt.content;
}
}
} catch (error) {
console.error(t('settings.prompts.fetchContentError'), error);
}
} else {
// 使用缓存的状态
const existingPrompt = customPrompts.find(
p => p.promptType === promptType && p.promptKey === promptKey && p.language === language
);
if (existingPrompt) {
return existingPrompt.content;
}
}
// 回退到默认内容
return await loadDefaultContent(promptType, promptKey);
};
// ======= 数据准备 =======
// 当前分类的配置
const currentCategoryConfig = templates[selectedCategory];
// 当前提示词的配置
const currentPromptConfig = currentCategoryConfig?.prompts?.[selectedPrompt];
// 分类配置项
const categoryEntries = Object.entries(templates);
// 处理分类变更
const handleCategoryChange = newCategory => {
setSelectedCategory(newCategory);
// 根据当前语言环境选择第一个匹配的提示词
const promptEntries = Object.keys(templates[newCategory]?.prompts || {});
console.log('所有提示词:', promptEntries);
const firstPrompt = promptEntries.find(promptKey => shouldShowPrompt(promptKey, currentLanguage));
setSelectedPrompt(firstPrompt);
};
// 处理编辑按钮点击
const handleEditButtonClick = () => {
const promptType = templates[selectedCategory]?.prompts?.[selectedPrompt]?.type;
// 使用当前界面语言而不是从 promptKey 推断的语言
const language = currentLanguage;
if (promptType) {
handleEditPrompt(promptType, selectedPrompt, language);
}
};
// 处理删除按钮点击
const handleDeleteButtonClick = () => {
const promptType = templates[selectedCategory]?.prompts?.[selectedPrompt]?.type;
// 使用当前界面语言而不是从 promptKey 推断的语言
const language = currentLanguage;
if (promptType) {
handleDeletePrompt(promptType, selectedPrompt, language);
}
};
// 处理对话框内容变更
const handleDialogContentChange = newContent => {
setEditDialog({ ...editDialog, content: newContent });
};
return (
<Box>
<SnackbarComponent />
{/* 主要分类选择 */}
<CategoryTabs
categoryEntries={categoryEntries}
selectedCategory={selectedCategory}
currentLanguage={currentLanguage}
onCategoryChange={handleCategoryChange}
/>
{/* 左右布局:左侧垂直提示词选择,右侧内容展示 */}
<Grid container spacing={3}>
{/* 左侧:垂直 TAB 选择具体提示词 */}
<Grid item xs={12} md={4} lg={3}>
<Card>
<CardContent>
<PromptList
currentCategory={selectedCategory}
currentCategoryConfig={currentCategoryConfig}
selectedPrompt={selectedPrompt}
currentLanguage={currentLanguage}
isCustomized={isCustomized}
onPromptSelect={setSelectedPrompt}
/>
</CardContent>
</Card>
</Grid>
{/* 右侧:提示词内容展示和操作 */}
<Grid item xs={12} md={8} lg={9}>
<PromptDetail
currentPromptConfig={currentPromptConfig}
selectedPrompt={selectedPrompt}
promptContent={promptContent}
isCustomized={isCustomized}
onEditClick={handleEditButtonClick}
onDeleteClick={handleDeleteButtonClick}
/>
</Grid>
</Grid>
{/* 编辑提示词对话框 */}
<PromptEditDialog
open={editDialog.open}
title={editDialog.isNew ? t('settings.prompts.createCustomPrompt') : t('settings.prompts.editPrompt')}
promptType={editDialog.promptType}
promptKey={editDialog.promptKey}
content={editDialog.content}
loading={loading}
onClose={() => setEditDialog({ ...editDialog, open: false })}
onSave={handleSavePrompt}
onRestore={handleRestoreDefault}
onContentChange={handleDialogContentChange}
/>
</Box>
);
}

View File

@@ -0,0 +1,34 @@
/**
* 提示词设置相关工具函数
*/
/**
* 从提示词键名解析语言
* @param {string} promptKey 提示词键名
* @returns {string} 语言代码 ('zh-CN' 或 'en')
*/
export const getLanguageFromPromptKey = promptKey => {
return promptKey?.endsWith('_EN') ? 'en' : 'zh-CN';
};
/**
* 判断是否应该显示当前提示词(基于语言)
* @param {string} promptKey 提示词键名
* @param {string} currentLanguage 当前界面语言
* @returns {boolean} 是否应该显示
*/
export const shouldShowPrompt = (promptKey, currentLanguage) => {
const promptLang = getLanguageFromPromptKey(promptKey);
return promptLang === currentLanguage;
};
/**
* 构建提示词标题显示组件
* @param {Object} options 配置项
* @param {string} options.name 提示词名称
* @param {boolean} options.customized 是否已自定义
* @returns {Object} 包含名称和自定义标记的显示配置
*/
export const buildPromptTitle = ({ name, customized }) => {
return { name, customized };
};