634 lines
22 KiB
React
634 lines
22 KiB
React
|
|
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: <MessageSquare size={18} /> },
|
|||
|
|
KNOWLEDGE_TASK: { label: '知识库任务', icon: <Library size={18} /> }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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' });
|
|||
|
|
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' });
|
|||
|
|
setShowCreateModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCloseCreateModal = () => {
|
|||
|
|
setShowCreateModal(false);
|
|||
|
|
setNewPromptData({ name: '', task_type: 'MEETING_TASK' });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
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 || '',
|
|||
|
|
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
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 更新本地状态
|
|||
|
|
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 || '',
|
|||
|
|
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 || '',
|
|||
|
|
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 <PageLoading message="加载中..." />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="prompt-management">
|
|||
|
|
<Breadcrumb currentPage="提示词管理" icon={BookText} />
|
|||
|
|
|
|||
|
|
<div className="prompt-layout">
|
|||
|
|
{/* 左侧提示词列表 */}
|
|||
|
|
<div className={`prompt-sidebar ${sidebarCollapsed ? 'collapsed' : ''}`}>
|
|||
|
|
<div className="sidebar-header">
|
|||
|
|
{!sidebarCollapsed && (
|
|||
|
|
<>
|
|||
|
|
<h3>提示词列表</h3>
|
|||
|
|
<button
|
|||
|
|
className="btn-new-prompt"
|
|||
|
|
onClick={handleOpenCreateModal}
|
|||
|
|
title="新增提示词"
|
|||
|
|
>
|
|||
|
|
<Plus size={18} />
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
<button
|
|||
|
|
className="btn-toggle-sidebar"
|
|||
|
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|||
|
|
title={sidebarCollapsed ? "展开" : "收起"}
|
|||
|
|
>
|
|||
|
|
{sidebarCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{!sidebarCollapsed && (
|
|||
|
|
<div className="prompt-list-sidebar">
|
|||
|
|
{prompts.length === 0 ? (
|
|||
|
|
<div className="empty-state">
|
|||
|
|
<p>暂无提示词</p>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
{/* 会议任务 */}
|
|||
|
|
{groupedPrompts.MEETING_TASK.length > 0 && (
|
|||
|
|
<div className="prompt-group">
|
|||
|
|
<div className="group-header">
|
|||
|
|
<span>{TASK_TYPES.MEETING_TASK.icon}</span>
|
|||
|
|
<span>{TASK_TYPES.MEETING_TASK.label}</span>
|
|||
|
|
</div>
|
|||
|
|
{groupedPrompts.MEETING_TASK.map(prompt => (
|
|||
|
|
<div
|
|||
|
|
key={prompt.id}
|
|||
|
|
className={`prompt-list-item ${selectedPrompt?.id === prompt.id ? 'active' : ''}`}
|
|||
|
|
onClick={() => handlePromptSelect(prompt)}
|
|||
|
|
>
|
|||
|
|
<div className="prompt-item-info">
|
|||
|
|
{prompt.is_default ? (
|
|||
|
|
<span className="badge badge-default">
|
|||
|
|
<Star size={12} fill="currentColor" />
|
|||
|
|
</span>
|
|||
|
|
) : null}
|
|||
|
|
<h4 title={prompt.name}>{prompt.name}</h4>
|
|||
|
|
{!prompt.is_active ? (
|
|||
|
|
<span className="badge badge-inactive">未启用</span>
|
|||
|
|
) : null}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 知识库任务 */}
|
|||
|
|
{groupedPrompts.KNOWLEDGE_TASK.length > 0 && (
|
|||
|
|
<div className="prompt-group">
|
|||
|
|
<div className="group-header">
|
|||
|
|
<span>{TASK_TYPES.KNOWLEDGE_TASK.icon}</span>
|
|||
|
|
<span>{TASK_TYPES.KNOWLEDGE_TASK.label}</span>
|
|||
|
|
</div>
|
|||
|
|
{groupedPrompts.KNOWLEDGE_TASK.map(prompt => (
|
|||
|
|
<div
|
|||
|
|
key={prompt.id}
|
|||
|
|
className={`prompt-list-item ${selectedPrompt?.id === prompt.id ? 'active' : ''}`}
|
|||
|
|
onClick={() => handlePromptSelect(prompt)}
|
|||
|
|
>
|
|||
|
|
<div className="prompt-item-info">
|
|||
|
|
{prompt.is_default ? (
|
|||
|
|
<span className="badge badge-default">
|
|||
|
|
<Star size={12} fill="currentColor" />
|
|||
|
|
</span>
|
|||
|
|
) : null}
|
|||
|
|
<h4 title={prompt.name}>{prompt.name}</h4>
|
|||
|
|
{!prompt.is_active ? (
|
|||
|
|
<span className="badge badge-inactive">未启用</span>
|
|||
|
|
) : null}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 右侧编辑区 */}
|
|||
|
|
<div className="prompt-detail-area">
|
|||
|
|
{selectedPrompt && editingPrompt ? (
|
|||
|
|
<>
|
|||
|
|
{/* 第一行:标题 + 操作按钮 */}
|
|||
|
|
<div className="prompt-title-row">
|
|||
|
|
<div className="title-edit-container">
|
|||
|
|
{editingTitle ? (
|
|||
|
|
<>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="title-input"
|
|||
|
|
value={editingPrompt.name}
|
|||
|
|
onChange={(e) => handleEditChange('name', e.target.value)}
|
|||
|
|
onKeyDown={(e) => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
handleSaveTitle();
|
|||
|
|
} else if (e.key === 'Escape') {
|
|||
|
|
setEditingPrompt({ ...selectedPrompt });
|
|||
|
|
setEditingTitle(false);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
autoFocus
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
className="icon-btn-small confirm-btn"
|
|||
|
|
onClick={handleSaveTitle}
|
|||
|
|
disabled={isSaving}
|
|||
|
|
title="确认"
|
|||
|
|
>
|
|||
|
|
<Check size={16} />
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
className="icon-btn-small cancel-btn"
|
|||
|
|
onClick={() => {
|
|||
|
|
setEditingPrompt({ ...selectedPrompt });
|
|||
|
|
setEditingTitle(false);
|
|||
|
|
}}
|
|||
|
|
title="取消"
|
|||
|
|
>
|
|||
|
|
<X size={16} />
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<h1>{selectedPrompt.name}</h1>
|
|||
|
|
<button
|
|||
|
|
className="icon-btn-small edit-btn"
|
|||
|
|
onClick={() => setEditingTitle(true)}
|
|||
|
|
title="编辑标题"
|
|||
|
|
>
|
|||
|
|
<Edit2 size={16} />
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="title-actions">
|
|||
|
|
<button
|
|||
|
|
className="icon-btn save-btn"
|
|||
|
|
onClick={handleSave}
|
|||
|
|
disabled={isSaving}
|
|||
|
|
title="保存"
|
|||
|
|
>
|
|||
|
|
<Save size={18} />
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
className="icon-btn delete-btn"
|
|||
|
|
onClick={() => handleDelete(selectedPrompt)}
|
|||
|
|
title="删除"
|
|||
|
|
>
|
|||
|
|
<Trash2 size={18} />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 第二行:任务类型 + 设为默认 + 启用开关 */}
|
|||
|
|
<div className="prompt-controls-row">
|
|||
|
|
<span className="task-type-badge">
|
|||
|
|
{TASK_TYPES[selectedPrompt.task_type]?.icon} {TASK_TYPES[selectedPrompt.task_type]?.label}
|
|||
|
|
</span>
|
|||
|
|
|
|||
|
|
<div className="prompt-actions-right">
|
|||
|
|
<button
|
|||
|
|
className={`btn-set-default ${editingPrompt.is_default ? 'active' : ''}`}
|
|||
|
|
onClick={() => handleDirectSave('is_default', !editingPrompt.is_default)}
|
|||
|
|
disabled={isSaving}
|
|||
|
|
>
|
|||
|
|
<Star size={14} />
|
|||
|
|
<span>{editingPrompt.is_default ? '默认模版' : '设为默认'}</span>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<label className="switch-label">
|
|||
|
|
<span>启用</span>
|
|||
|
|
<div className="switch-wrapper">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={editingPrompt.is_active}
|
|||
|
|
onChange={(e) => handleDirectSave('is_active', e.target.checked)}
|
|||
|
|
className="switch-input"
|
|||
|
|
disabled={isSaving}
|
|||
|
|
/>
|
|||
|
|
<span className="switch-slider"></span>
|
|||
|
|
</div>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Markdown 编辑器 */}
|
|||
|
|
<div className="prompt-editor-container">
|
|||
|
|
<MarkdownEditor
|
|||
|
|
value={editingPrompt.content || ''}
|
|||
|
|
onChange={(value) => handleEditChange('content', value)}
|
|||
|
|
placeholder="请输入提示词内容(支持 Markdown 格式)..."
|
|||
|
|
height={500}
|
|||
|
|
showImageUpload={false}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<div className="prompt-empty-placeholder">
|
|||
|
|
<FileText size={64} />
|
|||
|
|
<p>请从左侧选择一个提示词进行编辑</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 新增提示词表单弹窗(简化版) */}
|
|||
|
|
<FormModal
|
|||
|
|
isOpen={showCreateModal}
|
|||
|
|
onClose={handleCloseCreateModal}
|
|||
|
|
title="新增提示词"
|
|||
|
|
size="small"
|
|||
|
|
actions={
|
|||
|
|
<>
|
|||
|
|
<button type="button" className="btn btn-secondary" onClick={handleCloseCreateModal}>
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
<button type="button" className="btn btn-primary" onClick={handleCreatePrompt}>
|
|||
|
|
创建
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<div className="form-group">
|
|||
|
|
<label><FileText size={16} /> 名称</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={newPromptData.name}
|
|||
|
|
onChange={(e) => setNewPromptData(prev => ({ ...prev, name: e.target.value }))}
|
|||
|
|
placeholder="请输入提示词名称"
|
|||
|
|
autoFocus
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="form-group">
|
|||
|
|
<label><BookText size={16} /> 任务类型</label>
|
|||
|
|
<select
|
|||
|
|
value={newPromptData.task_type}
|
|||
|
|
onChange={(e) => setNewPromptData(prev => ({ ...prev, task_type: e.target.value }))}
|
|||
|
|
>
|
|||
|
|
<option value="MEETING_TASK">会议任务</option>
|
|||
|
|
<option value="KNOWLEDGE_TASK">知识库任务</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</FormModal>
|
|||
|
|
|
|||
|
|
{/* 删除提示词确认对话框 */}
|
|||
|
|
<ConfirmDialog
|
|||
|
|
isOpen={!!deleteConfirmInfo}
|
|||
|
|
onClose={() => setDeleteConfirmInfo(null)}
|
|||
|
|
onConfirm={handleConfirmDelete}
|
|||
|
|
title="删除提示词"
|
|||
|
|
message={`确定要删除提示词"${deleteConfirmInfo?.name}"吗?此操作无法撤销。`}
|
|||
|
|
confirmText="删除"
|
|||
|
|
cancelText="取消"
|
|||
|
|
type="danger"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* Toast notifications */}
|
|||
|
|
{toasts.map(toast => (
|
|||
|
|
<Toast
|
|||
|
|
key={toast.id}
|
|||
|
|
message={toast.message}
|
|||
|
|
type={toast.type}
|
|||
|
|
onClose={() => removeToast(toast.id)}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default PromptManagement;
|