877 lines
31 KiB
JavaScript
877 lines
31 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { Database, ChevronLeft, ChevronRight, Plus, Calendar, Trash2, Edit, FileText, Image, X } from 'lucide-react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import apiClient from '../utils/apiClient';
|
||
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||
import ContentViewer from '../components/ContentViewer';
|
||
import TagDisplay from '../components/TagDisplay';
|
||
import Toast from '../components/Toast';
|
||
import ConfirmDialog from '../components/ConfirmDialog';
|
||
import FormModal from '../components/FormModal';
|
||
import StepIndicator from '../components/StepIndicator';
|
||
import SimpleSearchInput from '../components/SimpleSearchInput';
|
||
import Breadcrumb from '../components/Breadcrumb';
|
||
import remarkGfm from 'remark-gfm';
|
||
import rehypeRaw from 'rehype-raw';
|
||
import rehypeSanitize from 'rehype-sanitize';
|
||
import exportService from '../services/exportService';
|
||
import tools from '../utils/tools';
|
||
import PageLoading from '../components/PageLoading';
|
||
import meetingCacheService from '../services/meetingCacheService';
|
||
import './KnowledgeBasePage.css';
|
||
|
||
const KnowledgeBasePage = ({ user }) => {
|
||
const navigate = useNavigate();
|
||
const [kbs, setKbs] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [selectedKb, setSelectedKb] = useState(null);
|
||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||
const [meetings, setMeetings] = useState([]);
|
||
const [selectedMeetings, setSelectedMeetings] = useState([]);
|
||
const [userPrompt, setUserPrompt] = useState('');
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [selectedTags, setSelectedTags] = useState([]);
|
||
const [availableTags, setAvailableTags] = useState([]);
|
||
const [generating, setGenerating] = useState(false);
|
||
const [taskId, setTaskId] = useState(null);
|
||
const [progress, setProgress] = useState(0);
|
||
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
|
||
const [toasts, setToasts] = useState([]);
|
||
const [createStep, setCreateStep] = useState(1); // 1: 选择会议, 2: 输入提示词
|
||
const [meetingsPagination, setMeetingsPagination] = useState({ page: 1, total: 0, has_more: false });
|
||
const [loadingMeetings, setLoadingMeetings] = useState(false);
|
||
const [availablePrompts, setAvailablePrompts] = useState([]); // 可用的提示词模版列表
|
||
const [selectedPromptId, setSelectedPromptId] = useState(null); // 选中的提示词模版ID
|
||
|
||
// 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(() => {
|
||
fetchAllKbs();
|
||
fetchAllTagsForFilter(); // 获取标签云数据
|
||
}, []);
|
||
|
||
// 当搜索或标签过滤变化时,重新加载第一页
|
||
useEffect(() => {
|
||
if (showCreateForm) {
|
||
fetchMeetings(1);
|
||
}
|
||
}, [searchQuery, selectedTags, showCreateForm]);
|
||
|
||
useEffect(() => {
|
||
if (taskId) {
|
||
const interval = setInterval(() => {
|
||
apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.TASK_STATUS(taskId)))
|
||
.then(response => {
|
||
const { status, progress } = response.data;
|
||
setProgress(progress || 0);
|
||
if (status === 'completed') {
|
||
clearInterval(interval);
|
||
setTaskId(null);
|
||
setGenerating(false);
|
||
setProgress(0);
|
||
setUserPrompt('');
|
||
setSelectedMeetings([]);
|
||
setShowCreateForm(false);
|
||
setCreateStep(1); // 重置步骤
|
||
setSearchQuery('');
|
||
setSelectedTags([]);
|
||
fetchAllKbs();
|
||
} else if (status === 'failed') {
|
||
clearInterval(interval);
|
||
setTaskId(null);
|
||
setGenerating(false);
|
||
setProgress(0);
|
||
showToast('知识库生成失败,请稍后重试', 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error("Error fetching task status:", error);
|
||
clearInterval(interval);
|
||
setTaskId(null);
|
||
setGenerating(false);
|
||
setProgress(0);
|
||
});
|
||
}, 2000);
|
||
return () => clearInterval(interval);
|
||
}
|
||
}, [taskId]);
|
||
|
||
const fetchAllKbs = () => {
|
||
setLoading(true);
|
||
// 获取所有知识库(个人和共享)
|
||
apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST))
|
||
.then(response => {
|
||
// 按创建时间倒序排序
|
||
const sortedKbs = response.data.kbs.sort((a, b) =>
|
||
new Date(b.created_at) - new Date(a.created_at)
|
||
);
|
||
setKbs(sortedKbs);
|
||
// 如果有知识库且没有选中,默认选中第一个
|
||
if (sortedKbs.length > 0 && !selectedKb) {
|
||
loadKbDetail(sortedKbs[0].kb_id);
|
||
}
|
||
setLoading(false);
|
||
})
|
||
.catch(error => {
|
||
console.error("Error fetching knowledge bases:", error);
|
||
setLoading(false);
|
||
});
|
||
};
|
||
|
||
const fetchMeetings = async (page = 1) => {
|
||
try {
|
||
// 生成当前过滤器的键(不包含filterType,因为知识库这里不需要)
|
||
const filterKey = meetingCacheService.generateFilterKey('all', searchQuery, selectedTags);
|
||
|
||
// 先检查缓存
|
||
const cachedPage = meetingCacheService.getPage(filterKey, page);
|
||
if (cachedPage) {
|
||
console.log('Using cached page:', page, 'for filter:', filterKey);
|
||
setMeetings(cachedPage.meetings);
|
||
setMeetingsPagination(cachedPage.pagination);
|
||
return;
|
||
}
|
||
|
||
// 没有缓存,从服务器获取
|
||
setLoadingMeetings(true);
|
||
|
||
const params = {
|
||
user_id: user.user_id,
|
||
page: page,
|
||
search: searchQuery || undefined,
|
||
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined
|
||
};
|
||
|
||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), { params });
|
||
|
||
const newMeetings = response.data.meetings || [];
|
||
const newPagination = {
|
||
page: response.data.page,
|
||
total: response.data.total,
|
||
has_more: response.data.has_more
|
||
};
|
||
|
||
// 缓存当前页数据
|
||
meetingCacheService.setPage(filterKey, page, newMeetings, newPagination);
|
||
|
||
setMeetings(newMeetings);
|
||
setMeetingsPagination(newPagination);
|
||
} catch (error) {
|
||
console.error("Error fetching meetings:", error);
|
||
setMeetings([]);
|
||
} finally {
|
||
setLoadingMeetings(false);
|
||
}
|
||
};
|
||
|
||
// 获取所有标签用于过滤器
|
||
const fetchAllTagsForFilter = async () => {
|
||
try {
|
||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TAGS.LIST));
|
||
const allTags = response.data || [];
|
||
// 取前6个热门标签
|
||
const topSixTags = allTags.slice(0, 6).map(tag => tag.name);
|
||
setAvailableTags(topSixTags);
|
||
} catch (error) {
|
||
console.error("Error fetching tags:", error);
|
||
}
|
||
};
|
||
|
||
// 获取知识库任务的启用提示词模版列表
|
||
const fetchAvailablePrompts = async () => {
|
||
try {
|
||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.ACTIVE('KNOWLEDGE_TASK')));
|
||
const promptsList = response.data.prompts || [];
|
||
setAvailablePrompts(promptsList);
|
||
|
||
// 自动选中默认模版
|
||
const defaultPrompt = promptsList.find(p => p.is_default);
|
||
if (defaultPrompt) {
|
||
setSelectedPromptId(defaultPrompt.id);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching available prompts:", error);
|
||
setAvailablePrompts([]);
|
||
}
|
||
};
|
||
|
||
const loadKbDetail = async (kbId) => {
|
||
try {
|
||
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kbId)));
|
||
setSelectedKb(response.data);
|
||
} catch (error) {
|
||
console.error("Error loading knowledge base detail:", error);
|
||
}
|
||
};
|
||
|
||
const handleKbSelect = (kb) => {
|
||
loadKbDetail(kb.kb_id);
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
if (!selectedMeetings || selectedMeetings.length === 0) {
|
||
showToast('请至少选择一个会议', 'warning');
|
||
return;
|
||
}
|
||
|
||
setGenerating(true);
|
||
try {
|
||
const response = await apiClient.post(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.CREATE), {
|
||
user_prompt: userPrompt,
|
||
source_meeting_ids: selectedMeetings.join(','),
|
||
is_shared: false,
|
||
prompt_id: selectedPromptId // 传递选中的模版ID
|
||
});
|
||
setTaskId(response.data.task_id);
|
||
} catch (error) {
|
||
console.error("Error creating knowledge base:", error);
|
||
setGenerating(false);
|
||
}
|
||
};
|
||
|
||
const toggleMeetingSelection = (meetingId) => {
|
||
setSelectedMeetings(prev =>
|
||
prev.includes(meetingId)
|
||
? prev.filter(id => id !== meetingId)
|
||
: [...prev, meetingId]
|
||
);
|
||
};
|
||
|
||
const handlePageChange = (newPage) => {
|
||
fetchMeetings(newPage);
|
||
};
|
||
|
||
const handleTagToggle = (tag) => {
|
||
setSelectedTags(prev =>
|
||
prev.includes(tag)
|
||
? prev.filter(t => t !== tag)
|
||
: [...prev, tag]
|
||
);
|
||
};
|
||
|
||
const clearFilters = () => {
|
||
setSearchQuery('');
|
||
setSelectedTags([]);
|
||
};
|
||
|
||
const handleOpenCreateModal = () => {
|
||
setCreateStep(1);
|
||
setSelectedMeetings([]);
|
||
setUserPrompt('');
|
||
setSearchQuery('');
|
||
setSelectedTags([]);
|
||
setSelectedPromptId(null);
|
||
setShowCreateForm(true);
|
||
// 获取可用的提示词模版
|
||
fetchAvailablePrompts();
|
||
};
|
||
|
||
const handleCloseCreateModal = () => {
|
||
setShowCreateForm(false);
|
||
setCreateStep(1);
|
||
setSelectedMeetings([]);
|
||
setUserPrompt('');
|
||
setSearchQuery('');
|
||
setSelectedTags([]);
|
||
};
|
||
|
||
const handleNextStep = () => {
|
||
if (selectedMeetings.length === 0) {
|
||
showToast('请至少选择一个会议', 'warning');
|
||
return;
|
||
}
|
||
setCreateStep(2);
|
||
};
|
||
|
||
const handlePrevStep = () => {
|
||
setCreateStep(1);
|
||
};
|
||
|
||
const handleDelete = async (kb) => {
|
||
setDeleteConfirmInfo({ kb_id: kb.kb_id, title: kb.title });
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
try {
|
||
await apiClient.delete(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DELETE(deleteConfirmInfo.kb_id)));
|
||
// 如果删除的是当前选中的,清除选中
|
||
if (selectedKb && selectedKb.kb_id === deleteConfirmInfo.kb_id) {
|
||
setSelectedKb(null);
|
||
}
|
||
setDeleteConfirmInfo(null);
|
||
fetchAllKbs();
|
||
} catch (error) {
|
||
console.error("Error deleting knowledge base:", error);
|
||
showToast('删除失败,请稍后重试', 'error');
|
||
setDeleteConfirmInfo(null);
|
||
}
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
const groupKbsByDate = (kbList) => {
|
||
const todayKbs = [];
|
||
const pastKbs = [];
|
||
|
||
kbList.forEach(kb => {
|
||
if (tools.isToday(kb.created_at)) {
|
||
todayKbs.push(kb);
|
||
} else {
|
||
pastKbs.push(kb);
|
||
}
|
||
});
|
||
|
||
return { todayKbs, pastKbs };
|
||
};
|
||
|
||
// 导出知识库内容为图片
|
||
const exportSummaryToImage = async () => {
|
||
try {
|
||
if (!selectedKb?.content) {
|
||
showToast('暂无知识库内容,请稍后再试。', 'warning');
|
||
return;
|
||
}
|
||
|
||
const createdAt = tools.formatDate(selectedKb.created_at);
|
||
const tags = selectedKb.tags?.join('、') || '';
|
||
|
||
await exportService.exportKnowledgeBaseToImage({
|
||
title: selectedKb.title || '知识库',
|
||
content: selectedKb.content,
|
||
metadata: {
|
||
creator: selectedKb.created_by_name || '未知',
|
||
createdTime: createdAt,
|
||
tags: tags,
|
||
sourceMeetings: selectedKb.source_meetings?.length || 0
|
||
}
|
||
});
|
||
|
||
showToast('内容已成功导出为图片', 'success');
|
||
} catch (error) {
|
||
console.error('图片导出失败:', error);
|
||
showToast('图片导出失败,请重试。', 'error');
|
||
}
|
||
};
|
||
|
||
// 导出思维导图为图片
|
||
const exportMindMapToImage = async () => {
|
||
try {
|
||
if (!selectedKb?.content) {
|
||
showToast('暂无内容,无法导出思维导图。', 'warning');
|
||
return;
|
||
}
|
||
|
||
await exportService.exportMindMapToImage({
|
||
title: selectedKb.title || '知识库'
|
||
});
|
||
|
||
showToast('思维导图已成功导出为图片', 'success');
|
||
} catch (error) {
|
||
console.error('思维导图导出失败:', error);
|
||
showToast(error.message || '思维导图导出失败,请重试。', 'error');
|
||
}
|
||
};
|
||
|
||
const isCreator = selectedKb && user && String(selectedKb.creator_id) === String(user.user_id);
|
||
|
||
if (loading) {
|
||
return <PageLoading message="加载中..." />;
|
||
}
|
||
|
||
return (
|
||
<div className="kb-management-page">
|
||
<Breadcrumb currentPage="知识库管理" icon={Database} />
|
||
|
||
<div className="kb-layout">
|
||
{/* 左侧知识库列表 */}
|
||
<div className={`kb-sidebar ${sidebarCollapsed ? 'collapsed' : ''}`}>
|
||
<div className="sidebar-header">
|
||
{!sidebarCollapsed && <h2>知识库列表</h2>}
|
||
<div className="sidebar-actions">
|
||
{!sidebarCollapsed && (
|
||
<button
|
||
className="btn-new-kb"
|
||
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>
|
||
</div>
|
||
|
||
{!sidebarCollapsed && (
|
||
<div className="kb-list-sidebar">
|
||
{kbs.length === 0 ? (
|
||
<div className="empty-state">
|
||
<p>暂无知识库条目</p>
|
||
</div>
|
||
) : (
|
||
(() => {
|
||
const { todayKbs, pastKbs } = groupKbsByDate(kbs);
|
||
return (
|
||
<>
|
||
{/* 今天的知识库 */}
|
||
{todayKbs.length > 0 && (
|
||
<div className="kb-date-group">
|
||
<div className="date-group-header">今天</div>
|
||
{todayKbs.map(kb => (
|
||
<div
|
||
key={kb.kb_id}
|
||
className={`kb-list-item ${selectedKb && selectedKb.kb_id === kb.kb_id ? 'active' : ''}`}
|
||
onClick={() => handleKbSelect(kb)}
|
||
>
|
||
<div className="kb-list-item-header">
|
||
<h3>{kb.title}</h3>
|
||
{String(kb.creator_id) === String(user.user_id) && (
|
||
<div className="kb-item-actions">
|
||
<button
|
||
className="btn-edit-kb"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
navigate(`/knowledge-base/edit/${kb.kb_id}`);
|
||
}}
|
||
title="编辑"
|
||
>
|
||
<Edit size={14} />
|
||
</button>
|
||
<button
|
||
className="btn-delete-kb"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleDelete(kb);
|
||
}}
|
||
title="删除"
|
||
>
|
||
<Trash2 size={14} />
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="kb-list-item-meta">
|
||
<span className="meta-time">{tools.formatTime(kb.created_at)}</span>
|
||
<span className="meta-item">
|
||
<Database size={12} />
|
||
{kb.source_meeting_count || 0} 个数据源
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 之前的知识库 */}
|
||
{pastKbs.length > 0 && (
|
||
<div className="kb-date-group">
|
||
<div className="date-group-header">之前</div>
|
||
{pastKbs.map(kb => (
|
||
<div
|
||
key={kb.kb_id}
|
||
className={`kb-list-item ${selectedKb && selectedKb.kb_id === kb.kb_id ? 'active' : ''}`}
|
||
onClick={() => handleKbSelect(kb)}
|
||
>
|
||
<div className="kb-list-item-header">
|
||
<h3>{kb.title}</h3>
|
||
{String(kb.creator_id) === String(user.user_id) && (
|
||
<div className="kb-item-actions">
|
||
<button
|
||
className="btn-edit-kb"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
navigate(`/knowledge-base/edit/${kb.kb_id}`);
|
||
}}
|
||
title="编辑"
|
||
>
|
||
<Edit size={14} />
|
||
</button>
|
||
<button
|
||
className="btn-delete-kb"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleDelete(kb);
|
||
}}
|
||
title="删除"
|
||
>
|
||
<Trash2 size={14} />
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="kb-list-item-meta">
|
||
<span className="meta-date">{tools.formatShortDate(kb.created_at)}</span>
|
||
<span className="meta-item">
|
||
<Database size={12} />
|
||
{kb.source_meeting_count || 0} 个数据源
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
})()
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧详情区 */}
|
||
<div className="kb-detail-area">
|
||
{selectedKb ? (
|
||
<>
|
||
<div className="kb-detail-header">
|
||
<div className="kb-header-title">
|
||
<h1>
|
||
{selectedKb.title}
|
||
{selectedKb.tags && selectedKb.tags.length > 0 && (
|
||
<TagDisplay
|
||
tags={selectedKb.tags.map(tag => tag.name)}
|
||
size="medium"
|
||
showIcon={true}
|
||
className="inline-title-tags"
|
||
/>
|
||
)}
|
||
</h1>
|
||
</div>
|
||
<div className="kb-detail-meta">
|
||
{selectedKb.created_by_name && (
|
||
<span className="meta-item">
|
||
创建者: {selectedKb.created_by_name}
|
||
</span>
|
||
)}
|
||
<span className="meta-item">
|
||
<Calendar size={14} />
|
||
{tools.formatDate(selectedKb.created_at)}
|
||
</span>
|
||
{selectedKb.source_meetings && selectedKb.source_meetings.length > 0 && (
|
||
<span className="meta-item">
|
||
<Database size={14} />
|
||
{selectedKb.source_meetings.length} 个数据源
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 用户提示词 */}
|
||
{selectedKb.user_prompt && (
|
||
<div className="kb-prompt-section">
|
||
<strong>用户提示词:</strong> {selectedKb.user_prompt}
|
||
</div>
|
||
)}
|
||
|
||
{/* 数据源列表 */}
|
||
{selectedKb.source_meetings && selectedKb.source_meetings.length > 0 && (
|
||
<div className="kb-sources-section">
|
||
<h3>数据源列表</h3>
|
||
<div className="source-meetings-list">
|
||
{selectedKb.source_meetings.map(meeting => (
|
||
<a
|
||
key={meeting.meeting_id}
|
||
href={`/meetings/${meeting.meeting_id}`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="source-meeting-link"
|
||
>
|
||
{meeting.title}
|
||
</a>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 内容区域 - Tabs */}
|
||
<div className="kb-content-tabs">
|
||
<ContentViewer
|
||
content={selectedKb.content}
|
||
title={selectedKb.title}
|
||
emptyMessage="内容生成中..."
|
||
summaryActions={
|
||
selectedKb.content && (
|
||
<button
|
||
className="action-btn export-btn"
|
||
onClick={exportSummaryToImage}
|
||
title="导出图片"
|
||
>
|
||
<Image size={16} />
|
||
<span>导出图片</span>
|
||
</button>
|
||
)
|
||
}
|
||
mindmapActions={
|
||
selectedKb.content && (
|
||
<button
|
||
className="action-btn export-btn"
|
||
onClick={exportMindMapToImage}
|
||
title="导出图片"
|
||
>
|
||
<Image size={16} />
|
||
<span>导出图片</span>
|
||
</button>
|
||
)
|
||
}
|
||
/>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="kb-empty-placeholder">
|
||
<FileText size={64} />
|
||
<p>请从左侧选择一个知识库查看详情</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 新增知识库表单弹窗 */}
|
||
<FormModal
|
||
isOpen={showCreateForm}
|
||
onClose={handleCloseCreateModal}
|
||
title="新增知识库"
|
||
size="large"
|
||
headerExtra={
|
||
<StepIndicator
|
||
steps={['选择会议', '自定义提示词']}
|
||
currentStep={createStep}
|
||
/>
|
||
}
|
||
actions={
|
||
<>
|
||
{createStep === 1 ? (
|
||
<>
|
||
<button type="button" className="btn btn-secondary" onClick={handleCloseCreateModal}>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
onClick={handleNextStep}
|
||
disabled={selectedMeetings.length === 0}
|
||
>
|
||
下一步
|
||
</button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<button type="button" className="btn btn-secondary" onClick={handlePrevStep}>
|
||
上一步
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
onClick={handleGenerate}
|
||
disabled={generating}
|
||
>
|
||
{generating ? `生成中... ${progress}%` : '生成知识库'}
|
||
</button>
|
||
</>
|
||
)}
|
||
</>
|
||
}
|
||
>
|
||
{/* 步骤 1: 选择会议 */}
|
||
{createStep === 1 && (
|
||
<div className="form-step">
|
||
<div className="form-group">
|
||
{/* 紧凑的搜索和过滤区 */}
|
||
<div className="search-filter-area">
|
||
<SimpleSearchInput
|
||
value={searchQuery}
|
||
onChange={setSearchQuery}
|
||
placeholder="搜索会议名称或创建人..."
|
||
realTimeSearch={true}
|
||
debounceDelay={500}
|
||
/>
|
||
|
||
{availableTags.length > 0 && (
|
||
<div className="tag-filter-section">
|
||
<div className="tag-filter-chips">
|
||
{availableTags.map(tag => (
|
||
<button
|
||
key={tag}
|
||
type="button"
|
||
className={`tag-chip ${selectedTags.includes(tag) ? 'selected' : ''}`}
|
||
onClick={() => handleTagToggle(tag)}
|
||
>
|
||
{tag}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{(searchQuery || selectedTags.length > 0) && (
|
||
<button
|
||
type="button"
|
||
className="clear-filters-btn"
|
||
onClick={clearFilters}
|
||
>
|
||
<X size={14} />
|
||
清除筛选
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="meeting-list">
|
||
{loadingMeetings ? (
|
||
<div className="loading-state">
|
||
<p>加载中...</p>
|
||
</div>
|
||
) : meetings.length === 0 ? (
|
||
<div className="empty-state">
|
||
<p>未找到匹配的会议</p>
|
||
</div>
|
||
) : (
|
||
meetings.map(meeting => (
|
||
<div
|
||
key={meeting.meeting_id}
|
||
className={`meeting-item ${selectedMeetings.includes(meeting.meeting_id) ? 'selected' : ''}`}
|
||
onClick={() => toggleMeetingSelection(meeting.meeting_id)}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedMeetings.includes(meeting.meeting_id)}
|
||
onChange={(e) => {
|
||
e.stopPropagation();
|
||
toggleMeetingSelection(meeting.meeting_id);
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
/>
|
||
<div className="meeting-item-content">
|
||
<div className="meeting-item-title">{meeting.title}</div>
|
||
<div className="meeting-item-meta">
|
||
{meeting.creator_username && (
|
||
<span className="meeting-item-creator">创建人: {meeting.creator_username}</span>
|
||
)}
|
||
{meeting.created_at && (
|
||
<span className="meeting-item-date">创建时间: {tools.formatMeetingDate(meeting.created_at)}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* 分页按钮 */}
|
||
{!loadingMeetings && meetings.length > 0 && (
|
||
<div className="pagination-controls">
|
||
<button
|
||
className="pagination-btn"
|
||
onClick={() => handlePageChange(meetingsPagination.page - 1)}
|
||
disabled={meetingsPagination.page === 1}
|
||
>
|
||
<ChevronLeft size={16} />
|
||
上一页
|
||
</button>
|
||
<span className="pagination-info">
|
||
第 {meetingsPagination.page} 页 · 共 {meetingsPagination.total} 条
|
||
</span>
|
||
<button
|
||
className="pagination-btn"
|
||
onClick={() => handlePageChange(meetingsPagination.page + 1)}
|
||
disabled={!meetingsPagination.has_more}
|
||
>
|
||
下一页
|
||
<ChevronRight size={16} />
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤 2: 输入提示词 */}
|
||
{createStep === 2 && (
|
||
<div className="form-step">
|
||
<div className="step-summary">
|
||
<div className="summary-item">
|
||
<span className="summary-label">已选择会议:</span>
|
||
<span className="summary-value">{selectedMeetings.length} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 模版选择 */}
|
||
{availablePrompts.length > 0 && (
|
||
<div className="form-group">
|
||
<label>* 选择生成模版(可选)</label>
|
||
<select
|
||
value={selectedPromptId || ''}
|
||
onChange={(e) => setSelectedPromptId(e.target.value ? parseInt(e.target.value) : null)}
|
||
className="template-select"
|
||
>
|
||
{availablePrompts.map(prompt => (
|
||
<option key={prompt.id} value={prompt.id}>
|
||
{prompt.name} {prompt.is_default ? '(默认)' : ''}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
)}
|
||
|
||
<div className="form-group">
|
||
<label>* 用户提示词(可选)</label>
|
||
<p className="field-hint">您可以添加额外的要求来定制知识库生成内容,例如重点关注某个主题、提取特定信息等。如不填写,系统将使用默认提示词。</p>
|
||
<textarea
|
||
placeholder="例如:请重点关注会议中的决策事项和待办任务..."
|
||
value={userPrompt}
|
||
onChange={(e) => setUserPrompt(e.target.value)}
|
||
className="kb-prompt-input"
|
||
rows={8}
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</FormModal>
|
||
|
||
{/* 删除确认对话框 */}
|
||
<ConfirmDialog
|
||
isOpen={!!deleteConfirmInfo}
|
||
onClose={() => setDeleteConfirmInfo(null)}
|
||
onConfirm={confirmDelete}
|
||
title="删除知识库"
|
||
message={`确定要删除知识库条目"${deleteConfirmInfo?.title}"吗?此操作无法撤销。`}
|
||
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 KnowledgeBasePage;
|