import React, { useState, useEffect, useRef } from 'react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import { Plus, ChevronLeft, ChevronRight, Trash2, BookText, FileText, Star, Save, Check, Edit2, X, MessageSquare, Library } from 'lucide-react';
import './PromptManagement.css';
import ConfirmDialog from '../../components/ConfirmDialog';
import FormModal from '../../components/FormModal';
import Toast from '../../components/Toast';
import MarkdownEditor from '../../components/MarkdownEditor';
import Breadcrumb from '../../components/Breadcrumb';
import PageLoading from '../../components/PageLoading';
const TASK_TYPES = {
MEETING_TASK: { label: '会议任务', icon: },
KNOWLEDGE_TASK: { label: '知识库任务', icon: }
};
const PromptManagement = () => {
const [prompts, setPrompts] = useState([]);
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [editingPrompt, setEditingPrompt] = useState(null); // 正在编辑的提示词
const [editingTitle, setEditingTitle] = useState(false); // 是否正在编辑标题
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [newPromptData, setNewPromptData] = useState({ name: '', task_type: 'MEETING_TASK', desc: '' });
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
const [toasts, setToasts] = useState([]);
const [isSaving, setIsSaving] = useState(false);
// Toast helper functions
const showToast = (message, type = 'info') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
};
const removeToast = (id) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
useEffect(() => {
fetchPrompts();
}, []);
const fetchPrompts = async () => {
setLoading(true);
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST));
const promptList = response.data.prompts;
setPrompts(promptList);
// 如果有提示词且没有选中,默认选中第一个
if (promptList.length > 0 && !selectedPrompt) {
const firstPrompt = promptList[0];
setSelectedPrompt(firstPrompt);
setEditingPrompt({ ...firstPrompt });
}
} catch (err) {
setError(err.response?.data?.message || '无法加载提示词列表');
} finally {
setLoading(false);
}
};
const handleOpenCreateModal = () => {
setNewPromptData({ name: '', task_type: 'MEETING_TASK', desc: '' });
setShowCreateModal(true);
};
const handleCloseCreateModal = () => {
setShowCreateModal(false);
setNewPromptData({ name: '', task_type: 'MEETING_TASK', desc: '' });
};
const handleCreatePrompt = async () => {
if (!newPromptData.name.trim()) {
showToast('请输入提示词名称', 'warning');
return;
}
try {
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), {
name: newPromptData.name,
task_type: newPromptData.task_type,
desc: newPromptData.desc || '',
content: '',
is_default: false,
is_active: true
});
showToast('提示词创建成功', 'success');
handleCloseCreateModal();
await fetchPrompts();
// 自动选中新创建的提示词
const newPrompt = response.data;
if (newPrompt && newPrompt.id) {
const createdPrompt = prompts.find(p => p.id === newPrompt.id) || newPrompt;
setSelectedPrompt(createdPrompt);
setEditingPrompt({ ...createdPrompt });
}
} catch (err) {
showToast(err.response?.data?.message || '创建失败', 'error');
}
};
const handlePromptSelect = (prompt) => {
setSelectedPrompt(prompt);
setEditingPrompt({ ...prompt });
setEditingTitle(false); // 切换提示词时关闭标题编辑
};
const handleEditChange = (field, value) => {
setEditingPrompt(prev => ({ ...prev, [field]: value }));
};
// 保存标题和描述
const handleSaveTitle = async () => {
if (!editingPrompt || !editingPrompt.id || !editingPrompt.name.trim()) {
showToast('标题不能为空', 'warning');
return;
}
setIsSaving(true);
try {
const dataToSend = {
name: editingPrompt.name,
task_type: editingPrompt.task_type,
content: editingPrompt.content || '',
desc: editingPrompt.desc || '',
is_default: Boolean(editingPrompt.is_default),
is_active: editingPrompt.is_active !== false
};
await apiClient.put(
buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)),
dataToSend
);
showToast('标题更新成功', 'success');
// 先计算完整更新后的对象(在回调外部)
const fullUpdatedPrompt = {
...prompts.find(p => p.id === editingPrompt.id),
name: editingPrompt.name,
desc: editingPrompt.desc
};
// 更新本地状态
setPrompts(prevPrompts =>
prevPrompts.map(p => p.id === editingPrompt.id ? fullUpdatedPrompt : p)
);
// 使用完整数据更新状态
setSelectedPrompt(fullUpdatedPrompt);
setEditingPrompt(fullUpdatedPrompt);
setEditingTitle(false);
} catch (err) {
console.error('handleSaveTitle error:', err);
showToast(err.response?.data?.message || '更新失败', 'error');
} finally {
setIsSaving(false);
}
};
// 直接保存字段更新(用于 is_default 和 is_active)
const handleDirectSave = async (field, value) => {
if (!editingPrompt || !editingPrompt.id) {
showToast('数据异常,请重新选择提示词', 'error');
console.error('handleDirectSave: editingPrompt is invalid', editingPrompt);
return;
}
// 如果是默认模版,不允许设置为无效
if (field === 'is_active' && !value && editingPrompt.is_default) {
showToast('默认模版不能设置为无效,请先取消默认状态', 'warning');
return;
}
const updatedPrompt = { ...editingPrompt, [field]: value };
setIsSaving(true);
try {
const dataToSend = {
name: updatedPrompt.name,
task_type: updatedPrompt.task_type,
content: updatedPrompt.content || '',
desc: updatedPrompt.desc || '',
is_default: Boolean(updatedPrompt.is_default),
is_active: updatedPrompt.is_active !== false
};
await apiClient.put(
buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)),
dataToSend
);
showToast('更新成功', 'success');
// 先计算完整更新后的对象(在回调外部)
const fullUpdatedPrompt = {
...prompts.find(p => p.id === editingPrompt.id),
[field]: value
};
// 更新本地状态
setPrompts(prevPrompts =>
prevPrompts.map(p => p.id === editingPrompt.id ? fullUpdatedPrompt : p)
);
// 使用完整数据更新状态
setSelectedPrompt(fullUpdatedPrompt);
setEditingPrompt(fullUpdatedPrompt);
} catch (err) {
console.error('handleDirectSave error:', err);
showToast(err.response?.data?.message || '更新失败', 'error');
// 恢复原值
setEditingPrompt({ ...selectedPrompt });
} finally {
setIsSaving(false);
}
};
const handleSave = async () => {
if (!editingPrompt || !editingPrompt.id) {
showToast('数据异常,请重新选择提示词', 'error');
console.error('handleSave: editingPrompt is invalid', editingPrompt);
return;
}
// 验证必需字段
if (!editingPrompt.name || !editingPrompt.task_type || editingPrompt.content === undefined) {
showToast('数据不完整,请刷新页面重试', 'error');
console.error('handleSave: missing required fields', editingPrompt);
return;
}
console.log('handleSave: Saving prompt', {
id: editingPrompt.id,
name: editingPrompt.name,
task_type: editingPrompt.task_type,
content_length: editingPrompt.content?.length,
is_default: editingPrompt.is_default,
is_active: editingPrompt.is_active
});
setIsSaving(true);
try {
const dataToSend = {
name: editingPrompt.name,
task_type: editingPrompt.task_type,
content: editingPrompt.content || '',
desc: editingPrompt.desc || '',
is_default: Boolean(editingPrompt.is_default),
is_active: editingPrompt.is_active !== false
};
const url = buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id));
console.log('handleSave: Request URL:', url);
console.log('handleSave: Sending data', dataToSend);
console.log('handleSave: editingPrompt.id type:', typeof editingPrompt.id, 'value:', editingPrompt.id);
const response = await apiClient.put(url, dataToSend);
console.log('handleSave: Success response:', response);
showToast('保存成功', 'success');
// 先从当前列表中找到原始记录
const originalPrompt = prompts.find(p => p.id === editingPrompt.id);
if (!originalPrompt) {
console.error('handleSave: Cannot find prompt in list', editingPrompt.id);
console.error('handleSave: Current prompts list:', prompts.map(p => ({ id: p.id, name: p.name })));
// 如果找不到,重新加载列表
await fetchPrompts();
return;
}
// 计算完整更新后的对象(在回调外部)
const fullUpdatedPrompt = {
...originalPrompt,
...editingPrompt
};
console.log('handleSave: fullUpdatedPrompt', fullUpdatedPrompt);
// 更新本地状态
setPrompts(prevPrompts =>
prevPrompts.map(p => p.id === editingPrompt.id ? fullUpdatedPrompt : p)
);
// 使用完整数据更新状态
setSelectedPrompt(fullUpdatedPrompt);
setEditingPrompt(fullUpdatedPrompt);
} catch (err) {
console.error('handleSave error:', err);
console.error('handleSave error response:', err.response?.data);
console.error('handleSave error config:', err.config);
showToast(err.response?.data?.message || '保存失败', 'error');
} finally {
setIsSaving(false);
}
};
const handleDelete = async (prompt) => {
// 检查是否为默认模版
if (prompt.is_default) {
showToast('默认模版不允许删除,请先取消默认状态', 'warning');
return;
}
setDeleteConfirmInfo({
id: prompt.id,
name: prompt.name
});
};
const handleConfirmDelete = async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(deleteConfirmInfo.id)));
if (selectedPrompt && selectedPrompt.id === deleteConfirmInfo.id) {
setSelectedPrompt(null);
setEditingPrompt(null);
}
setDeleteConfirmInfo(null);
await fetchPrompts();
showToast('提示词删除成功', 'success');
} catch (err) {
showToast(err.response?.data?.message || '删除失败', 'error');
setDeleteConfirmInfo(null);
}
};
// 按任务类型分组
const groupedPrompts = {
MEETING_TASK: prompts.filter(p => p.task_type === 'MEETING_TASK'),
KNOWLEDGE_TASK: prompts.filter(p => p.task_type === 'KNOWLEDGE_TASK')
};
if (loading) {
return ;
}
return (
{/* 左侧提示词列表 */}
{!sidebarCollapsed && (
<>
提示词列表
>
)}
{!sidebarCollapsed && (
{prompts.length === 0 ? (
) : (
<>
{/* 会议任务 */}
{groupedPrompts.MEETING_TASK.length > 0 && (
{TASK_TYPES.MEETING_TASK.icon}
{TASK_TYPES.MEETING_TASK.label}
{groupedPrompts.MEETING_TASK.map(prompt => (
handlePromptSelect(prompt)}
>
{prompt.is_default ? (
) : null}
{prompt.name}
{!prompt.is_active ? (
未启用
) : null}
))}
)}
{/* 知识库任务 */}
{groupedPrompts.KNOWLEDGE_TASK.length > 0 && (
{TASK_TYPES.KNOWLEDGE_TASK.icon}
{TASK_TYPES.KNOWLEDGE_TASK.label}
{groupedPrompts.KNOWLEDGE_TASK.map(prompt => (
handlePromptSelect(prompt)}
>
{prompt.is_default ? (
) : null}
{prompt.name}
{!prompt.is_active ? (
未启用
) : null}
))}
)}
>
)}
)}
{/* 右侧编辑区 */}
{selectedPrompt && editingPrompt ? (
<>
{/* 第一行:标题 + 操作按钮 */}
{/* 描述字段 */}
{/* 第二行:任务类型 + 设为默认 + 启用开关 */}
{TASK_TYPES[selectedPrompt.task_type]?.icon} {TASK_TYPES[selectedPrompt.task_type]?.label}
{/* Markdown 编辑器 */}
handleEditChange('content', value)}
placeholder="请输入提示词内容(支持 Markdown 格式)..."
height={500}
showImageUpload={false}
/>
>
) : (
)}
{/* 新增提示词表单弹窗(简化版) */}
>
}
>
setNewPromptData(prev => ({ ...prev, name: e.target.value }))}
placeholder="请输入提示词名称"
autoFocus
/>
{/* 删除提示词确认对话框 */}
setDeleteConfirmInfo(null)}
onConfirm={handleConfirmDelete}
title="删除提示词"
message={`确定要删除提示词"${deleteConfirmInfo?.name}"吗?此操作无法撤销。`}
confirmText="删除"
cancelText="取消"
type="danger"
/>
{/* Toast notifications */}
{toasts.map(toast => (
removeToast(toast.id)}
/>
))}
);
};
export default PromptManagement;