初步实现了知识库模块

main
mula.liu 2025-10-16 11:09:41 +08:00
parent 4926608588
commit 22b9856416
8 changed files with 1335 additions and 11 deletions

View File

@ -8,6 +8,7 @@ import MeetingDetails from './pages/MeetingDetails';
import CreateMeeting from './pages/CreateMeeting'; import CreateMeeting from './pages/CreateMeeting';
import EditMeeting from './pages/EditMeeting'; import EditMeeting from './pages/EditMeeting';
import AdminManagement from './pages/AdminManagement'; import AdminManagement from './pages/AdminManagement';
import KnowledgeBasePage from './pages/KnowledgeBasePage';
import './App.css'; import './App.css';
function App() { function App() {
@ -81,6 +82,9 @@ function App() {
<Route path="/admin/management" element={ <Route path="/admin/management" element={
user && user.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" /> user && user.role_id === 1 ? <AdminManagement user={user} /> : <Navigate to="/dashboard" />
} /> } />
<Route path="/knowledge-base" element={
user ? <KnowledgeBasePage user={user} /> : <Navigate to="/" />
} />
</Routes> </Routes>
</div> </div>
</Router> </Router>

View File

@ -0,0 +1,369 @@
import React, { useState, useEffect } from 'react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import { Plus, X, Eye, Trash2, Calendar, Database, Search } from 'lucide-react';
import { Link } from 'react-router-dom';
const PersonalKnowledgeBase = ({ user }) => {
const [kbs, setKbs] = useState([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [meetings, setMeetings] = useState([]);
const [selectedMeetings, setSelectedMeetings] = useState([]);
const [userPrompt, setUserPrompt] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [generating, setGenerating] = useState(false);
const [taskId, setTaskId] = useState(null);
const [progress, setProgress] = useState(0);
const [selectedKb, setSelectedKb] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deletingKb, setDeletingKb] = useState(null);
useEffect(() => {
fetchPersonalKbs();
fetchMeetings();
}, []);
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);
// Reset form
setUserPrompt('');
setSelectedMeetings([]);
setShowModal(false);
// Refresh the list
fetchPersonalKbs();
} else if (status === 'failed') {
clearInterval(interval);
setTaskId(null);
setGenerating(false);
setProgress(0);
alert('知识库生成失败,请稍后重试');
}
})
.catch(error => {
console.error("Error fetching task status:", error);
clearInterval(interval);
setTaskId(null);
setGenerating(false);
setProgress(0);
});
}, 2000); // 2
return () => clearInterval(interval);
}
}, [taskId]);
const fetchPersonalKbs = () => {
setLoading(true);
apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST, { is_shared: false }))
.then(response => {
setKbs(response.data.kbs);
setLoading(false);
})
.catch(error => {
console.error("Error fetching personal knowledge bases:", error);
setLoading(false);
});
};
const fetchTags = () => {
// Tags functionality removed - replaced with search
};
const fetchMeetings = () => {
apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST, { user_id: user.user_id }))
.then(response => {
setMeetings(response.data);
})
.catch(error => {
console.error("Error fetching meetings:", error);
});
};
const handleGenerate = async () => {
if (!selectedMeetings || selectedMeetings.length === 0) {
alert('请至少选择一个会议');
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
});
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 filteredMeetings = (meetings || []).filter(meeting => {
if (!searchQuery) return true;
return meeting.title.toLowerCase().includes(searchQuery.toLowerCase());
});
const handleDelete = async (kbId) => {
setDeletingKb(kbs.find(kb => kb.kb_id === kbId));
setShowDeleteConfirm(true);
};
const confirmDelete = async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DELETE(deletingKb.kb_id)));
setShowDeleteConfirm(false);
setDeletingKb(null);
fetchPersonalKbs(); // Refresh the list
} catch (error) {
console.error("Error deleting knowledge base:", error);
alert('删除失败,请稍后重试');
setShowDeleteConfirm(false);
setDeletingKb(null);
}
};
const handleViewDetail = async (kbId) => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kbId)));
setSelectedKb(response.data);
setShowDetailModal(true);
} catch (error) {
console.error("Error fetching knowledge base detail:", error);
alert('获取详情失败,请稍后重试');
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<div className="kb-list">
{kbs.length === 0 ? (
<div className="empty-state">
<p>暂无知识库条目</p>
</div>
) : (
kbs.map(kb => (
<div key={kb.kb_id} className="kb-card">
<div className="kb-card-header">
<h3 className="kb-title">{kb.title}</h3>
<div className="kb-actions">
<button
className="kb-action-btn view-btn"
onClick={() => handleViewDetail(kb.kb_id)}
title="查看详情"
>
<Eye size={18} />
</button>
<button
className="kb-action-btn delete-btn"
onClick={() => handleDelete(kb.kb_id)}
title="删除"
>
<Trash2 size={18} />
</button>
</div>
</div>
<div className="kb-card-body">
<div className="kb-meta">
<span className="kb-meta-item">
<Calendar size={14} />
{formatDate(kb.created_at)}
</span>
<span className="kb-meta-item">
<Database size={14} />
{kb.source_meeting_count || 0} 个数据源
</span>
</div>
{kb.user_prompt && (
<div className="kb-prompt">
<strong>提示词:</strong> {kb.user_prompt}
</div>
)}
<div className="kb-content-preview">
{kb.content ? kb.content.substring(0, 200) + '...' : '内容生成中...'}
</div>
</div>
</div>
))
)}
</div>
<div className="kb-generation-section">
<div className="generation-form">
<div className="generation-actions">
<div className="prompt-input-container">
<button className="add-meeting-btn" onClick={() => setShowModal(true)}>
<Plus size={18} />
</button>
<textarea
placeholder="请输入您的提示词..."
value={userPrompt}
onChange={(e) => setUserPrompt(e.target.value)}
className="kb-prompt-input"
rows={4}
/>
</div>
<button onClick={handleGenerate} className="generate-kb-btn" disabled={generating || selectedMeetings.length === 0}>
{generating ? `生成中... ${progress}%` : '生成'}
</button>
</div>
{selectedMeetings.length > 0 && (
<div className="selected-meetings-info">
已选择 {selectedMeetings.length} 个会议
</div>
)}
</div>
</div>
{showModal && (
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h2>选择会议</h2>
<button onClick={() => setShowModal(false)} className="close-btn"><X size={20} /></button>
</div>
<div className="search-wrapper">
<div className="search-input-container">
<Search size={18} className="search-icon" />
<input
type="text"
placeholder="搜索会议名称..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
{searchQuery && (
<button
className="clear-search-btn"
onClick={() => setSearchQuery('')}
>
<X size={16} />
</button>
)}
</div>
</div>
<div className="meeting-list">
{filteredMeetings.length === 0 ? (
<div className="empty-state">
<p>未找到匹配的会议</p>
</div>
) : (
filteredMeetings.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={() => toggleMeetingSelection(meeting.meeting_id)}
/>
<label>{meeting.title}</label>
</div>
))
)}
</div>
<div className="modal-actions">
<button onClick={() => setShowModal(false)}>确认</button>
</div>
</div>
</div>
)}
{showDetailModal && selectedKb && (
<div className="modal-overlay">
<div className="modal-content detail-modal">
<div className="modal-header">
<h2>{selectedKb.title}</h2>
<button onClick={() => setShowDetailModal(false)} className="close-btn">
<X size={20} />
</button>
</div>
<div className="detail-content">
<div className="detail-meta">
{selectedKb.source_meetings && selectedKb.source_meetings.length > 0 && (
<div className="detail-meta-item">
<strong>数据源列表:</strong>
<div className="source-meetings-list">
{selectedKb.source_meetings.map(meeting => (
<Link
key={meeting.meeting_id}
to={`/meetings/${meeting.meeting_id}`}
className="source-meeting-link"
onClick={() => setShowDetailModal(false)}
>
{meeting.title}
</Link>
))}
</div>
</div>
)}
{selectedKb.user_prompt && (
<div className="detail-meta-item">
<strong>提示词:</strong> {selectedKb.user_prompt}
</div>
)}
</div>
<div className="detail-body">
<h3>内容</h3>
<div className="kb-content-full">
{selectedKb.content || '内容生成中...'}
</div>
</div>
</div>
</div>
</div>
)}
{showDeleteConfirm && deletingKb && (
<div className="delete-modal-overlay" onClick={() => {setShowDeleteConfirm(false); setDeletingKb(null);}}>
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
<h3>确认删除</h3>
<p>确定要删除知识库条目 "{deletingKb.title}" 此操作无法撤销</p>
<div className="modal-actions">
<button className="btn-cancel" onClick={() => {setShowDeleteConfirm(false); setDeletingKb(null);}}>取消</button>
<button className="btn-delete" onClick={confirmDelete}>确定删除</button>
</div>
</div>
</div>
)}
</div>
);
};
export default PersonalKnowledgeBase;

View File

@ -0,0 +1,157 @@
import React, { useState, useEffect } from 'react';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import { Eye, Calendar, Database, X } from 'lucide-react';
import { Link } from 'react-router-dom';
const SharedKnowledgeBase = () => {
const [kbs, setKbs] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedKb, setSelectedKb] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
useEffect(() => {
apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.LIST, { is_shared: true }))
.then(response => {
setKbs(response.data.kbs);
setLoading(false);
})
.catch(error => {
console.error("Error fetching shared knowledge bases:", error);
setLoading(false);
});
}, []);
const handleViewDetail = async (kbId) => {
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.KNOWLEDGE_BASE.DETAIL(kbId)));
setSelectedKb(response.data);
setShowDetailModal(true);
} catch (error) {
console.error("Error fetching knowledge base detail:", error);
alert('获取详情失败,请稍后重试');
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="kb-list">
{kbs.length === 0 ? (
<div className="empty-state">
<p>暂无共享知识库条目</p>
</div>
) : (
kbs.map(kb => (
<div key={kb.kb_id} className="kb-card">
<div className="kb-card-header">
<h3 className="kb-title">{kb.title}</h3>
<div className="kb-actions">
<button
className="kb-action-btn view-btn"
onClick={() => handleViewDetail(kb.kb_id)}
title="查看详情"
>
<Eye size={18} />
</button>
</div>
</div>
<div className="kb-card-body">
<div className="kb-meta">
<span className="kb-meta-item">
<Calendar size={14} />
{formatDate(kb.created_at)}
</span>
<span className="kb-meta-item">
<Database size={14} />
{kb.source_meeting_count || 0} 个数据源
</span>
{kb.created_by_name && (
<span className="kb-meta-item">
创建者: {kb.created_by_name}
</span>
)}
</div>
{kb.user_prompt && (
<div className="kb-prompt">
<strong>提示词:</strong> {kb.user_prompt}
</div>
)}
<div className="kb-content-preview">
{kb.content ? kb.content.substring(0, 200) + '...' : '内容生成中...'}
</div>
</div>
</div>
))
)}
{showDetailModal && selectedKb && (
<div className="modal-overlay">
<div className="modal-content detail-modal">
<div className="modal-header">
<h2>{selectedKb.title}</h2>
<button onClick={() => setShowDetailModal(false)} className="close-btn">
<X size={20} />
</button>
</div>
<div className="detail-content">
<div className="detail-meta">
<div className="detail-meta-item">
<strong>创建时间:</strong> {formatDate(selectedKb.created_at)}
</div>
{selectedKb.source_meetings && selectedKb.source_meetings.length > 0 && (
<div className="detail-meta-item">
<strong>数据源会议:</strong>
<div className="source-meetings-list">
{selectedKb.source_meetings.map(meeting => (
<Link
key={meeting.meeting_id}
to={`/meetings/${meeting.meeting_id}`}
className="source-meeting-link"
onClick={() => setShowDetailModal(false)}
>
{meeting.title}
</Link>
))}
</div>
</div>
)}
{selectedKb.created_by_name && (
<div className="detail-meta-item">
<strong>创建者:</strong> {selectedKb.created_by_name}
</div>
)}
{selectedKb.user_prompt && (
<div className="detail-meta-item">
<strong>提示词:</strong> {selectedKb.user_prompt}
</div>
)}
</div>
<div className="detail-body">
<h3>内容</h3>
<div className="kb-content-full">
{selectedKb.content || '内容生成中...'}
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default SharedKnowledgeBase;

View File

@ -47,13 +47,26 @@ const API_CONFIG = {
CREATE: '/api/prompts', CREATE: '/api/prompts',
UPDATE: (promptId) => `/api/prompts/${promptId}`, UPDATE: (promptId) => `/api/prompts/${promptId}`,
DELETE: (promptId) => `/api/prompts/${promptId}` DELETE: (promptId) => `/api/prompts/${promptId}`
},
KNOWLEDGE_BASE: {
LIST: '/api/knowledge-bases',
CREATE: '/api/knowledge-bases',
DETAIL: (kbId) => `/api/knowledge-bases/${kbId}`,
UPDATE: (kbId) => `/api/knowledge-bases/${kbId}`,
DELETE: (kbId) => `/api/knowledge-bases/${kbId}`,
TASK_STATUS: (taskId) => `/api/knowledge-bases/tasks/${taskId}`
} }
} }
}; };
// 构建完整的API URL // 构建完整的API URL
export const buildApiUrl = (endpoint) => { export const buildApiUrl = (endpoint, params = {}) => {
return `${API_CONFIG.BASE_URL}${endpoint}`; const url = `${API_CONFIG.BASE_URL}${endpoint}`;
const query = Object.entries(params)
.filter(([, value]) => value !== null && value !== undefined)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return query ? `${url}?${query}` : url;
}; };
// 导出API端点 // 导出API端点

View File

@ -632,4 +632,29 @@
.success-message { .success-message {
color: green; color: green;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.user-card-actions {
margin-top: 1rem;
}
.user-card-action-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
text-decoration: none;
padding: 0.75rem 1.5rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.95rem;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
white-space: nowrap;
}
.user-card-action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X } from 'lucide-react'; import { LogOut, User, Calendar, Users, TrendingUp, Clock, MessageSquare, Plus, ChevronDown, KeyRound, Shield, Filter, X, Library } from 'lucide-react';
import apiClient from '../utils/apiClient'; import apiClient from '../utils/apiClient';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import { buildApiUrl, API_ENDPOINTS } from '../config/api';
@ -9,7 +9,7 @@ import './Dashboard.css';
const Dashboard = ({ user, onLogout }) => { const Dashboard = ({ user, onLogout }) => {
const [userInfo, setUserInfo] = useState(null); const [userInfo, setUserInfo] = useState(null);
const [meetings, setMeetings] = useState([]); const [meetings, setMeetings] = useState(null);
const [filteredMeetings, setFilteredMeetings] = useState([]); const [filteredMeetings, setFilteredMeetings] = useState([]);
const [selectedTags, setSelectedTags] = useState([]); const [selectedTags, setSelectedTags] = useState([]);
const [filterType, setFilterType] = useState('all'); // 'all', 'created', 'attended' const [filterType, setFilterType] = useState('all'); // 'all', 'created', 'attended'
@ -34,6 +34,7 @@ const Dashboard = ({ user, onLogout }) => {
}, [meetings, selectedTags, filterType]); }, [meetings, selectedTags, filterType]);
const filterMeetings = () => { const filterMeetings = () => {
if (!meetings) return;
let filtered = [...meetings]; let filtered = [...meetings];
// / // /
@ -93,12 +94,10 @@ const Dashboard = ({ user, onLogout }) => {
const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id))); const userResponse = await apiClient.get(buildApiUrl(API_ENDPOINTS.USERS.DETAIL(user.user_id)));
console.log('User response:', userResponse.data); console.log('User response:', userResponse.data);
setUserInfo(userResponse.data); setUserInfo(userResponse.data);
const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
//console.log('Meetings response:', meetingsResponse.data);
setMeetings(meetingsResponse.data);
const meetingsResponse = await apiClient.get(buildApiUrl(`${API_ENDPOINTS.MEETINGS.LIST}?user_id=${user.user_id}`));
setMeetings(meetingsResponse.data);
} catch (err) { } catch (err) {
console.error('Error fetching data:', err); console.error('Error fetching data:', err);
setError('获取数据失败,请刷新重试'); setError('获取数据失败,请刷新重试');
@ -172,7 +171,7 @@ const Dashboard = ({ user, onLogout }) => {
}); });
}; };
if (loading) { if (loading || !meetings) {
return ( return (
<div className="dashboard"> <div className="dashboard">
<div className="loading-container"> <div className="loading-container">
@ -241,6 +240,12 @@ const Dashboard = ({ user, onLogout }) => {
<h2>{userInfo?.caption}</h2> <h2>{userInfo?.caption}</h2>
<p className="user-email">{userInfo?.email}</p> <p className="user-email">{userInfo?.email}</p>
<p className="join-date">加入时间{formatDate(userInfo?.created_at)}</p> <p className="join-date">加入时间{formatDate(userInfo?.created_at)}</p>
<div className="user-card-actions">
<Link to="/knowledge-base" className="user-card-action-btn">
<Library size={16} />
<span>知识库管理</span>
</Link>
</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,700 @@
.kb-management-page {
min-height: 100vh;
background: #f8fafc;
}
.kb-header {
background: white;
border-bottom: 1px solid #e2e8f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 0;
}
.kb-header .header-content {
max-width: 1200px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.kb-header .logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.5rem;
font-weight: bold;
color: #667eea;
}
.kb-header .logo-icon {
width: 32px;
height: 32px;
}
.kb-header h1 {
margin: 0;
font-size: 1.5rem;
color: #1e293b;
font-weight: 600;
}
.kb-content {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
.kb-wrapper {
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.kb-tabs .ant-tabs-nav {
padding: 0 2rem;
margin-bottom: 0 !important;
border-bottom: 1px solid #e2e8f0;
}
.kb-tabs .ant-tabs-tab {
font-size: 1rem;
color: #475569;
padding: 16px 4px;
margin: 0 16px;
}
.kb-tabs .ant-tabs-tab .ant-tabs-tab-btn {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.kb-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
color: #667eea;
}
.kb-tabs .ant-tabs-ink-bar {
background: #667eea;
height: 3px;
}
.kb-tabs .ant-tabs-content-holder {
padding: 2rem;
min-height: 60vh;
}
/* Knowledge Base List Styles */
.kb-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.kb-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.kb-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.kb-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.kb-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
flex: 1;
word-break: break-word;
}
.kb-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.kb-action-btn {
padding: 0.5rem;
border: none;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.kb-action-btn:hover {
background: #e2e8f0;
}
.view-btn:hover {
background: #dbeafe;
color: #3b82f6;
}
.delete-btn:hover {
background: #fee2e2;
color: #ef4444;
}
.kb-card-body {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.kb-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
color: #64748b;
font-size: 0.875rem;
}
.kb-meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.kb-prompt {
padding: 0.75rem;
background: #f8fafc;
border-left: 3px solid #667eea;
border-radius: 4px;
font-size: 0.875rem;
color: #475569;
}
.kb-content-preview {
color: #64748b;
font-size: 0.875rem;
line-height: 1.6;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: #94a3b8;
}
.empty-state p {
margin: 0.5rem 0;
font-size: 1rem;
}
.empty-hint {
font-size: 0.875rem;
color: #cbd5e1;
}
/* Generation Section */
.kb-generation-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 2px solid #e2e8f0;
}
.kb-generation-section h2 {
margin-bottom: 1.5rem;
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
.generation-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.kb-title-display {
padding: 0.75rem 1rem;
background: #f8fafc;
border-radius: 8px;
font-size: 0.95rem;
color: #475569;
}
.generation-actions {
display: flex;
gap: 1rem;
align-items: flex-end;
}
.prompt-input-container {
flex-grow: 1;
display: flex;
gap: 0.5rem;
align-items: flex-end;
}
.add-meeting-btn {
padding: 0.75rem;
border-radius: 8px;
border: 1px solid #e2e8f0;
background: #f8fafc;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.add-meeting-btn:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
.kb-prompt-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-family: inherit;
font-size: 0.95rem;
resize: vertical;
min-height: 100px;
transition: border-color 0.2s ease;
}
.kb-prompt-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.generate-kb-btn {
align-self: flex-end;
padding: 0.75rem 1.5rem;
border-radius: 8px;
border: none;
background: #667eea;
color: white;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
flex-shrink: 0;
}
.generate-kb-btn:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.generate-kb-btn:disabled {
background: #cbd5e1;
cursor: not-allowed;
}
.selected-meetings-info {
padding: 0.5rem 1rem;
background: #dbeafe;
color: #1e40af;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.detail-modal {
max-width: 900px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
}
.close-btn {
padding: 0.5rem;
border: none;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: #e2e8f0;
color: #1e293b;
}
.tag-cloud-wrapper {
flex-shrink: 0;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 1rem;
}
/* Search Wrapper Styles */
.search-wrapper {
flex-shrink: 0;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 1rem;
}
.search-input-container {
position: relative;
display: flex;
align-items: center;
gap: 0.5rem;
}
.search-icon {
position: absolute;
left: 12px;
color: #94a3b8;
pointer-events: none;
}
.search-input {
flex: 1;
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.95rem;
transition: all 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.clear-search-btn {
position: absolute;
right: 8px;
padding: 0.25rem;
border: none;
background: transparent;
color: #94a3b8;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.clear-search-btn:hover {
background: #f1f5f9;
color: #64748b;
}
.meeting-list {
flex-grow: 1;
overflow-y: auto;
padding-right: 0.5rem;
}
.meeting-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 6px;
transition: background-color 0.2s ease;
cursor: pointer;
}
.meeting-item:hover {
background: #f8fafc;
}
.meeting-item.selected {
background: #dbeafe;
border: 1px solid #3b82f6;
}
.meeting-item input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.meeting-item input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.meeting-item label {
cursor: pointer;
flex: 1;
font-size: 0.95rem;
color: #475569;
}
.modal-actions {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
text-align: right;
}
.modal-actions button {
padding: 0.75rem 1.5rem;
border-radius: 8px;
border: none;
background: #667eea;
color: white;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.modal-actions button:hover {
background: #5568d3;
}
/* Detail Modal Styles */
.detail-content {
overflow-y: auto;
flex-grow: 1;
}
.detail-meta {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
background: #f8fafc;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.detail-meta-item {
font-size: 0.95rem;
color: #475569;
}
.detail-meta-item strong {
color: #1e293b;
margin-right: 0.5rem;
}
.detail-body {
padding: 1rem 0;
}
.detail-body h3 {
margin: 0 0 1rem 0;
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
}
.kb-content-full {
line-height: 1.8;
color: #475569;
white-space: pre-wrap;
word-wrap: break-word;
padding: 1rem;
background: #f8fafc;
border-radius: 8px;
max-height: 400px;
overflow-y: auto;
}
/* Scrollbar Styles */
.meeting-list::-webkit-scrollbar,
.kb-content-full::-webkit-scrollbar {
width: 6px;
}
.meeting-list::-webkit-scrollbar-track,
.kb-content-full::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.meeting-list::-webkit-scrollbar-thumb,
.kb-content-full::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.meeting-list::-webkit-scrollbar-thumb:hover,
.kb-content-full::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Delete Modal Styles - consistent with MeetingTimeline */
.delete-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.delete-modal {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.delete-modal h3 {
margin: 0 0 1rem 0;
color: #1e293b;
font-size: 1.25rem;
}
.delete-modal p {
margin: 0 0 2rem 0;
color: #64748b;
line-height: 1.6;
}
.delete-modal .modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 0;
padding-top: 0;
border-top: none;
}
.btn-cancel, .btn-delete {
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: none;
}
.btn-cancel {
background: #f1f5f9;
color: #475569;
}
.btn-cancel:hover {
background: #e2e8f0;
}
.btn-delete {
background: #ef4444;
color: white;
}
.btn-delete:hover {
background: #dc2626;
}
/* Source Meetings List Styles */
.source-meetings-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.source-meeting-link {
display: inline-block;
padding: 0.5rem 0.75rem;
background: #f1f5f9;
border-radius: 6px;
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
transition: all 0.2s ease;
border-left: 3px solid #667eea;
}
.source-meeting-link:hover {
background: #e2e8f0;
transform: translateX(4px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

View File

@ -0,0 +1,51 @@
import React from 'react';
import { MessageSquare, Book, Users } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Tabs } from 'antd';
import PersonalKnowledgeBase from '../components/knowledgebase/PersonalKnowledgeBase';
import SharedKnowledgeBase from '../components/knowledgebase/SharedKnowledgeBase';
import './KnowledgeBasePage.css';
const { TabPane } = Tabs;
const KnowledgeBasePage = ({ user }) => {
const navigate = useNavigate();
const handleLogoClick = () => {
navigate('/dashboard');
};
return (
<div className="kb-management-page">
<header className="kb-header">
<div className="header-content">
<div className="logo" onClick={handleLogoClick} style={{ cursor: 'pointer' }}>
<MessageSquare className="logo-icon" />
<span className="logo-text">iMeeting</span>
</div>
<h1>知识库管理</h1>
</div>
</header>
<div className="kb-content">
<div className="kb-wrapper">
<Tabs defaultActiveKey="personal" className="kb-tabs">
<TabPane
tab={<span><Book size={16} /> 个人知识库</span>}
key="personal"
>
<PersonalKnowledgeBase user={user} />
</TabPane>
<TabPane
tab={<span><Users size={16} /> 共享知识库</span>}
key="shared"
>
<SharedKnowledgeBase />
</TabPane>
</Tabs>
</div>
</div>
</div>
);
};
export default KnowledgeBasePage;