初步实现了知识库模块
parent
4926608588
commit
22b9856416
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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端点
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue