[dev]chat pages

This commit is contained in:
susie-laptop 2025-03-04 14:46:45 -05:00
parent 6d0ebf169c
commit 546f4c4265
12 changed files with 1223 additions and 87 deletions

View File

@ -1,12 +1,46 @@
import React from 'react';
import { icons } from '../icons/icons';
export default function SvgIcon({ className }) {
export default function SvgIcon({ className, width, height, color, style }) {
// Create a new SVG string with custom attributes if provided
const customizeSvg = (svgString) => {
if (!svgString) return '';
// If no customization needed, return the original SVG
if (!width && !height && !color) return svgString;
// Parse the SVG to modify attributes
let modifiedSvg = svgString;
// Replace width if provided
if (width) {
modifiedSvg = modifiedSvg.replace(/width=['"]([^'"]*)['"]/g, `width="${width}"`);
}
// Replace height if provided
if (height) {
modifiedSvg = modifiedSvg.replace(/height=['"]([^'"]*)['"]/g, `height="${height}"`);
}
// Replace fill color if provided
if (color) {
modifiedSvg = modifiedSvg.replace(/fill=['"]currentColor['"]/g, `fill="${color}"`);
}
return modifiedSvg;
};
return (
<span
className={className}
style={{ display: 'inline-block', lineHeight: 0 }}
dangerouslySetInnerHTML={{ __html: icons[className] || '' }}
style={{
display: 'inline-block',
lineHeight: 0,
...style,
}}
dangerouslySetInnerHTML={{
__html: customizeSvg(icons[className] || ''),
}}
/>
);
}

View File

@ -94,9 +94,11 @@ export const icons = {
p-id='21341'
></path>
</svg>`,
'setting-fill': `<svg t="1741046883752" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="34037" width="24" height="24" fill='currentColor'><path d="M809.188 550.978c1.6-12.8 2.799-25.799 2.799-38.998s-1.2-26.198-2.799-38.998l84.596-66.197c7.6-6 9.8-16.799 4.8-25.599l-79.996-138.594c-5-8.6-15.399-12.199-24.399-8.6l-99.596 40.199c-20.599-15.8-43.198-29.198-67.597-39.398l-14.999-105.996c-1.8-9.399-9.999-16.799-20-16.799l-159.994 0c-9.999 0-18.199 7.4-19.799 16.799l-14.999 105.996c-24.399 10.2-46.999 23.399-67.597 39.398l-99.596-40.199c-9-3.4-19.399 0-24.399 8.6l-79.996 138.594c-5 8.6-2.8 19.399 4.8 25.599l84.397 66.197c-1.6 12.8-2.8 25.799-2.8 38.998 0 13.2 1.2 26.198 2.8 38.998l-84.397 66.197c-7.6 6-9.8 16.799-4.8 25.599l79.996 138.594c5 8.6 15.399 12.199 24.399 8.6l99.596-40.199c20.599 15.8 43.198 29.198 67.597 39.398l14.999 105.996c1.6 9.399 9.8 16.799 19.799 16.799l159.994 0c9.999 0 18.199-7.4 19.799-16.799l14.999-105.996c24.399-10.2 46.999-23.399 67.597-39.398l99.596 40.199c9 3.4 19.399 0 24.399-8.6l79.996-138.594c5-8.6 2.8-19.399-4.8-25.599l-84.396-66.197zM512 651.973c-77.396 0-139.994-62.598-139.994-139.994s62.598-139.994 139.994-139.994 139.994 62.598 139.994 139.994-62.598 139.994-139.994 139.994z" p-id="34038"></path></svg>`,
dataset: `<svg t="1741046791101" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="32858" width="24" height="24" fill='currentColor'><path d="M170.666667 798.464V262.101333C170.666667 235.178667 192.512 213.333333 219.434667 213.333333h585.130666C831.488 213.333333 853.333333 235.178667 853.333333 262.101333v536.362667c0 26.922667-21.845333 48.768-48.768 48.768H219.434667A48.768 48.768 0 0 1 170.666667 798.464z m48.768-146.261333v146.261333h585.130666v-146.261333H219.434667z m0-48.768h585.130666v-146.304H219.434667v146.304z m0-195.072h585.130666V262.101333H219.434667V408.32z m73.130666-97.493334h97.536a24.362667 24.362667 0 1 1 0 48.768H292.565333a24.362667 24.362667 0 1 1 0-48.768z m0 195.029334h97.536a24.362667 24.362667 0 1 1 0 48.768H292.565333a24.362667 24.362667 0 0 1 0-48.768z m0 195.072h97.536a24.362667 24.362667 0 0 1 0 48.725333H292.565333a24.362667 24.362667 0 0 1 0-48.725333z p-id="32859"></path></svg>`,
'setting-fill': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>`,
dataset: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zm64 0l0 64 64 0 0-64L64 96zm384 0L192 96l0 64 256 0 0-64zM64 224l0 64 64 0 0-64-64 0zm384 0l-256 0 0 64 256 0 0-64zM64 352l0 64 64 0 0-64-64 0zm384 0l-256 0 0 64 256 0 0-64z"/></svg>`,
calendar: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill='currentColor'><path d="M152 24c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40L64 64C28.7 64 0 92.7 0 128l0 16 0 48L0 448c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-256 0-48 0-16c0-35.3-28.7-64-64-64l-40 0 0-40c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 40L152 64l0-40zM48 192l352 0 0 256c0 8.8-7.2 16-16 16L64 464c-8.8 0-16-7.2-16-16l0-256z"/></svg>`,
clipboard: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="16" height="16" fill='currentColor'><path d="M280 64l40 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 128C0 92.7 28.7 64 64 64l40 0 9.6 0C121 27.5 153.3 0 192 0s71 27.5 78.4 64l9.6 0zM64 112c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l256 0c8.8 0 16-7.2 16-16l0-320c0-8.8-7.2-16-16-16l-16 0 0 24c0 13.3-10.7 24-24 24l-88 0-88 0c-13.3 0-24-10.7-24-24l0-24-16 0zm128-8a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>`,
chat: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M123.6 391.3c12.9-9.4 29.6-11.8 44.6-6.4c26.5 9.6 56.2 15.1 87.8 15.1c124.7 0 208-80.5 208-160s-83.3-160-208-160S48 160.5 48 240c0 32 12.4 62.8 35.7 89.2c8.6 9.7 12.8 22.5 11.8 35.5c-1.4 18.1-5.7 34.7-11.3 49.4c17-7.9 31.1-16.7 39.4-22.7zM21.2 431.9c1.8-2.7 3.5-5.4 5.1-8.1c10-16.6 19.5-38.4 21.4-62.9C17.7 326.8 0 285.1 0 240C0 125.1 114.6 32 256 32s256 93.1 256 208s-114.6 208-256 208c-37.1 0-72.3-6.4-104.1-17.9c-11.9 8.7-31.3 20.6-54.3 30.6c-15.1 6.6-32.3 12.6-50.1 16.1c-.8 .2-1.6 .3-2.4 .5c-4.4 .8-8.7 1.5-13.2 1.9c-.2 0-.5 .1-.7 .1c-5.1 .5-10.2 .8-15.3 .8c-6.5 0-12.3-3.9-14.8-9.9c-2.5-6-1.1-12.8 3.4-17.4c4.1-4.2 7.8-8.7 11.3-13.5c1.7-2.3 3.3-4.6 4.8-6.9l.3-.5z"/></svg>`,
'arrowup-upload': `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" fill='currentColor'><path d="M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3 192 320c0 17.7 14.3 32 32 32s32-14.3 32-32l0-210.7 73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64c0 17.7-14.3 32-32 32L96 448c-17.7 0-32-14.3-32-32l0-64z"/></svg>`,
send: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16" fill='currentColor'><path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z"/></svg>`,
};

View File

@ -1,11 +1,12 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { logoutThunk } from '../store/auth/auth.thunk';
export default function HeaderWithNav() {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const { user } = useSelector((state) => state.auth);
const handleLogout = async () => {
@ -16,9 +17,14 @@ export default function HeaderWithNav() {
} catch (error) {}
};
// Check if the current path starts with the given path
const isActive = (path) => {
return location.pathname.startsWith(path);
};
return (
<header className=' navbar navbar-expand-lg'>
<nav className='navbar navbar-expand-lg border-bottom p-3 mb-3 w-100'>
<header className=' navbar navbar-expand-lg p-0'>
<nav className='navbar navbar-expand-lg border-bottom p-3 w-100'>
<div className='container-fluid'>
<Link className='navbar-brand' to='/'>
OOIN 智能知识库
@ -37,12 +43,16 @@ export default function HeaderWithNav() {
<div className='collapse navbar-collapse' id='navbarText'>
<ul className='navbar-nav me-auto mb-lg-0'>
<li className='nav-item'>
<Link className='nav-link active' aria-current='page' to='/'>
<Link
className={`nav-link ${isActive('/') && !isActive('/chat') ? 'active' : ''}`}
aria-current='page'
to='/'
>
知识库
</Link>
</li>
<li className='nav-item'>
<Link className='nav-link' to='#'>
<Link className={`nav-link ${isActive('/chat') ? 'active' : ''}`} to='/chat'>
Chat
</Link>
</li>

67
src/pages/Chat/Chat.jsx Normal file
View File

@ -0,0 +1,67 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ChatSidebar from './ChatSidebar';
import NewChat from './NewChat';
import ChatWindow from './ChatWindow';
export default function Chat() {
const { knowledgeBaseId, chatId } = useParams();
const navigate = useNavigate();
const [chatHistory, setChatHistory] = useState([
{
id: '1',
knowledgeBaseId: '1',
title: 'Chat History 1',
lastMessage: '上次聊天内容的摘要...',
timestamp: '2025-01-20T10:30:00Z',
},
{
id: '2',
knowledgeBaseId: '2',
title: 'Chat History 2',
lastMessage: '上次聊天内容的摘要...',
timestamp: '2025-01-19T14:45:00Z',
},
]);
// If we have a knowledgeBaseId but no chatId, create a new chat
useEffect(() => {
if (knowledgeBaseId && !chatId) {
// In a real app, you would create a new chat and get its ID from the API
const newChatId = Date.now().toString();
navigate(`/chat/${knowledgeBaseId}/${newChatId}`);
}
}, [knowledgeBaseId, chatId, navigate]);
const handleDeleteChat = (id) => {
// In a real app, you would call an API to delete the chat
setChatHistory((prevHistory) => prevHistory.filter((chat) => chat.id !== id));
// If the deleted chat is the current one, navigate to the chat list
if (chatId === id) {
navigate('/chat');
}
};
return (
<div className='chat-container container-fluid h-100'>
<div className='row h-100'>
{/* Sidebar */}
<div
className='col-md-3 col-lg-2 p-0 border-end'
style={{ height: 'calc(100vh - 73px)', overflowY: 'auto' }}
>
<ChatSidebar chatHistory={chatHistory} onDeleteChat={handleDeleteChat} />
</div>
{/* Main Content */}
<div
className='chat-main col-md-9 col-lg-10 p-0'
style={{ height: 'calc(100vh - 73px)', overflowY: 'auto' }}
>
{!chatId ? <NewChat /> : <ChatWindow chatId={chatId} knowledgeBaseId={knowledgeBaseId} />}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import SvgIcon from '../../components/SvgIcon';
export default function ChatSidebar({ chatHistory, onDeleteChat }) {
const navigate = useNavigate();
const { chatId } = useParams();
const [activeDropdown, setActiveDropdown] = useState(null);
const handleNewChat = () => {
navigate('/chat');
};
const handleMouseEnter = (id) => {
setActiveDropdown(id);
};
const handleMouseLeave = () => {
setActiveDropdown(null);
};
const handleDeleteChat = (e, id) => {
e.preventDefault();
e.stopPropagation();
if (onDeleteChat) {
onDeleteChat(id);
}
setActiveDropdown(null);
};
return (
<div className='chat-sidebar d-flex flex-column h-100'>
<div className='p-3 pb-0'>
<h5 className='mb-0'>Chats</h5>
</div>
<div className='p-3'>
<button
className='btn btn-dark w-100 d-flex align-items-center justify-content-center gap-2'
onClick={handleNewChat}
>
<SvgIcon className='plus' color='#ffffff' />
<span>New Chat</span>
</button>
</div>
<div className='overflow-auto flex-grow-1'>
<ul className='list-group list-group-flush'>
{chatHistory.map((chat) => (
<li
key={chat.id}
className={`list-group-item border-0 position-relative ${
chatId === chat.id ? 'bg-light' : ''
}`}
>
<Link
to={`/chat/${chat.knowledgeBaseId}/${chat.id}`}
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
chatId === chat.id ? 'fw-bold' : ''
}`}
>
<div className='text-truncate'>{chat.title}</div>
</Link>
<div
className='dropdown-hover-area position-absolute end-0 top-0 bottom-0'
style={{ width: '40px' }}
onMouseEnter={() => handleMouseEnter(chat.id)}
onMouseLeave={handleMouseLeave}
>
<button className='btn btn-sm position-absolute end-0 top-50 translate-middle-y me-2'>
<SvgIcon className='more-dot' width='5' height='16' />
</button>
{activeDropdown === chat.id && (
<div
className='position-absolute end-0 top-100 bg-white shadow rounded p-1 z-3'
style={{ zIndex: 1000, minWidth: '80px' }}
>
<button
className='btn btn-sm text-danger d-flex align-items-center gap-2 w-100'
onClick={(e) => handleDeleteChat(e, chat.id)}
>
<SvgIcon className='trash' />
<span>删除</span>
</button>
</div>
)}
</div>
</li>
))}
</ul>
</div>
</div>
);
}

View File

@ -0,0 +1,161 @@
import React, { useState, useEffect, useRef } from 'react';
import SvgIcon from '../../components/SvgIcon';
export default function ChatWindow({ chatId, knowledgeBaseId }) {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [knowledgeBase, setKnowledgeBase] = useState(null);
const messagesEndRef = useRef(null);
// Fetch knowledge base details
useEffect(() => {
// In a real app, you would fetch the knowledge base details from the API
const mockKnowledgeBases = [
{
id: '1',
title: '产品开发知识库',
description: '产品开发流程及规范说明文档',
},
{
id: '2',
title: '市场分析知识库',
description: '2025年Q1市场分析总结',
},
{
id: '3',
title: '财务知识库',
description: '月度财务分析报告',
},
{
id: '4',
title: '技术架构知识库',
description: '系统架构设计文档',
},
{
id: '5',
title: '用户研究知识库',
description: '用户调研和反馈分析',
},
];
const kb = mockKnowledgeBases.find((kb) => kb.id === knowledgeBaseId);
setKnowledgeBase(kb);
// In a real app, you would fetch the chat messages from the API
// For now, we'll just add a welcome message
if (kb) {
setMessages([
{
id: '1',
sender: 'bot',
content: `欢迎使用 ${kb.title},有什么可以帮助您的?`,
timestamp: new Date().toISOString(),
},
]);
}
}, [chatId, knowledgeBaseId]);
// Scroll to bottom when messages change
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSendMessage = (e) => {
e.preventDefault();
if (!inputMessage.trim()) return;
// Add user message
const userMessage = {
id: Date.now().toString(),
sender: 'user',
content: inputMessage,
timestamp: new Date().toISOString(),
};
setMessages((prev) => [...prev, userMessage]);
setInputMessage('');
setIsLoading(true);
// Simulate bot response after a delay
setTimeout(() => {
const botMessage = {
id: (Date.now() + 1).toString(),
sender: 'bot',
content: `这是来自 ${knowledgeBase?.title} 的回复:${inputMessage}`,
timestamp: new Date().toISOString(),
};
setMessages((prev) => [...prev, botMessage]);
setIsLoading(false);
}, 1000);
};
return (
<div className='chat-window d-flex flex-column h-100'>
{/* Chat header */}
<div className='p-3 border-bottom'>
<h5 className='mb-0'>{knowledgeBase?.title || 'Loading...'}</h5>
<small className='text-muted'>{knowledgeBase?.description}</small>
</div>
{/* Chat messages */}
<div className='flex-grow-1 p-3 overflow-auto' >
<div className='container'>
{messages.map((message) => (
<div
key={message.id}
className={`d-flex ${
message.sender === 'user' ? 'justify-content-end' : 'justify-content-start'
} mb-3`}
>
<div
className={`p-3 rounded-3 ${
message.sender === 'user' ? 'bg-primary text-white' : 'bg-white border'
}`}
style={{ maxWidth: '75%' }}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className='d-flex justify-content-start mb-3'>
<div className='p-3 rounded-3 bg-white border'>
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Chat input */}
<div className='p-3 border-top'>
<form onSubmit={handleSendMessage} className='d-flex gap-2'>
<input
type='text'
className='form-control'
placeholder='输入你的问题...'
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
disabled={isLoading}
/>
<button
type='submit'
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
disabled={isLoading || !inputMessage.trim()}
>
<SvgIcon className='send' color='#ffffff' />
<span className='ms-1' style={{ minWidth: 'fit-content' }}>发送</span>
</button>
</form>
</div>
</div>
);
}

102
src/pages/Chat/NewChat.jsx Normal file
View File

@ -0,0 +1,102 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import SvgIcon from '../../components/SvgIcon';
export default function NewChat() {
const navigate = useNavigate();
const [knowledgeBases, setKnowledgeBases] = useState([
{
id: '1',
title: '产品开发知识库',
description: '产品开发流程及规范说明文档',
documents: 24,
date: '2025-02-15',
access: 'full',
},
{
id: '2',
title: '市场分析知识库',
description: '2025年Q1市场分析总结',
documents: 12,
date: '2025-02-10',
access: 'read',
},
{
id: '3',
title: '财务知识库',
description: '月度财务分析报告',
documents: 8,
date: '2025-02-01',
access: 'none',
},
{
id: '4',
title: '技术架构知识库',
description: '系统架构设计文档',
documents: 15,
date: '2025-01-20',
access: 'full',
},
{
id: '5',
title: '用户研究知识库',
description: '用户调研和反馈分析',
documents: 18,
date: '2025-01-15',
access: 'read',
},
]);
const handleSelectKnowledgeBase = (knowledgeBaseId) => {
// In a real app, you would create a new chat and get its ID from the API
navigate(`/chat/${knowledgeBaseId}`);
};
return (
<div className='new-chat container py-4'>
<div className='text-center mb-5'>
<h2 className='mb-4'>选择知识库开始聊天</h2>
</div>
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
{knowledgeBases.map((kb) =>
kb.access === 'full' || kb.access === 'read' ? (
<div key={kb.id} className='col'>
<div
className='card h-100 shadow-sm border-0 cursor-pointer'
onClick={() => handleSelectKnowledgeBase(kb.id)}
style={{ cursor: 'pointer' }}
>
<div className='card-body'>
<h5 className='card-title'>{kb.title}</h5>
<p className='card-text text-muted'>{kb.description}</p>
<div className='text-muted d-flex align-items-center gap-1'>
<SvgIcon className='file' />
{kb.documents} 文档
<span className='ms-3 d-flex align-items-center gap-1'>
<SvgIcon className='clock' />
{kb.date}
</span>
</div>
<div className='mt-3 d-flex justify-content-between align-items-end'>
{kb.access === 'full' ? (
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
<SvgIcon className='circle-yes' />
完全访问
</span>
) : (
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
<SvgIcon className='eye' />
只读访问
</span>
)}
</div>
</div>
</div>
</div>
) : null
)}
</div>
</div>
);
}

View File

@ -7,7 +7,33 @@ export default function DatasetTab({ knowledgeBase }) {
const [selectedDocuments, setSelectedDocuments] = useState([]);
const [selectAll, setSelectAll] = useState(false);
const [showBatchDropdown, setShowBatchDropdown] = useState(false);
const [showAddFileModal, setShowAddFileModal] = useState(false);
const [newFile, setNewFile] = useState({
name: '',
description: '',
file: null,
});
const [fileErrors, setFileErrors] = useState({});
const dropdownRef = useRef(null);
const fileInputRef = useRef(null);
// Convert documents to state so we can update it
const [documents, setDocuments] = useState([
{
id: '1001',
name: '测试数据集 001',
description: '产品相关的所有文档和说明',
size: '124kb',
updatedAt: '2023-05-15',
},
{
id: '1002',
name: '产品分析数据',
description: '技术架构和API文档',
size: '89kb',
updatedAt: '2023-05-10',
},
]);
// Handle click outside dropdown
useEffect(() => {
@ -25,24 +51,6 @@ export default function DatasetTab({ knowledgeBase }) {
};
}, [dropdownRef]);
// Mock data for documents in this knowledge base
const documents = [
{
id: '1001',
name: '测试数据集 001',
description: '产品相关的所有文档和说明',
size: '124kb',
updatedAt: '2023-05-15',
},
{
id: '1002',
name: '产品分析数据',
description: '技术架构和API文档',
size: '89kb',
updatedAt: '2023-05-10',
},
];
// Handle select all checkbox
const handleSelectAll = () => {
if (selectAll) {
@ -73,12 +81,147 @@ export default function DatasetTab({ knowledgeBase }) {
// Here you would typically call an API to delete the selected documents
console.log('Deleting documents:', selectedDocuments);
// Update documents state by removing selected documents
setDocuments((prevDocuments) => prevDocuments.filter((doc) => !selectedDocuments.includes(doc.id)));
// Reset selection
setSelectedDocuments([]);
setSelectAll(false);
setShowBatchDropdown(false);
};
// Handle file input change
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
setNewFile({
...newFile,
name: selectedFile.name,
file: selectedFile,
});
// Clear file error if exists
if (fileErrors.file) {
setFileErrors((prev) => ({
...prev,
file: '',
}));
}
}
};
// Handle description input change
const handleDescriptionChange = (e) => {
setNewFile({
...newFile,
description: e.target.value,
});
};
// Handle file drop
const handleFileDrop = (e) => {
e.preventDefault();
e.stopPropagation();
const droppedFile = e.dataTransfer.files[0];
if (droppedFile) {
setNewFile({
...newFile,
name: droppedFile.name,
file: droppedFile,
});
// Clear file error if exists
if (fileErrors.file) {
setFileErrors((prev) => ({
...prev,
file: '',
}));
}
}
};
// Prevent default behavior for drag events
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
// Validate file form
const validateFileForm = () => {
const errors = {};
if (!newFile.file) {
errors.file = '请上传文件';
}
setFileErrors(errors);
return Object.keys(errors).length === 0;
};
// Handle file upload
const handleFileUpload = () => {
// Validate form
if (!validateFileForm()) {
return;
}
// Here you would typically call an API to upload the file
console.log('Uploading file:', newFile);
// Generate a new ID for the document
const newId = (Math.max(...documents.map((doc) => parseInt(doc.id)), 0) + 1).toString();
// Format file size
const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb';
// Get current date
const today = new Date();
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(
today.getDate()
).padStart(2, '0')}`;
// Create new document object
const newDocument = {
id: newId,
name: newFile.name,
description: newFile.description || '无描述',
size: fileSizeKB,
updatedAt: formattedDate,
};
// Add new document to the documents array
setDocuments((prevDocuments) => [...prevDocuments, newDocument]);
// Reset form and close modal
handleCloseAddFileModal();
};
// Open file selector when clicking on the upload area
const handleUploadAreaClick = () => {
fileInputRef.current.click();
};
// Handle close modal
const handleCloseAddFileModal = () => {
setNewFile({
name: '',
description: '',
file: null,
});
setFileErrors({});
setShowAddFileModal(false);
};
// Handle delete document
const handleDeleteDocument = (docId) => {
// Here you would typically call an API to delete the document
console.log('Deleting document:', docId);
// Update documents state by removing the deleted document
setDocuments((prevDocuments) => prevDocuments.filter((doc) => doc.id !== docId));
};
return (
<>
{/* Breadcrumb navigation */}
@ -118,7 +261,12 @@ export default function DatasetTab({ knowledgeBase }) {
aria-expanded={showBatchDropdown}
disabled={selectedDocuments.length === 0}
>
<SvgIcon className='stack-fill' />
<SvgIcon
className='stack-fill'
width='18'
height='18'
color={selectedDocuments.length > 0 ? '#212529' : '#6c757d'}
/>
批量操作 {selectedDocuments.length > 0 ? `(${selectedDocuments.length})` : ''}
</button>
{showBatchDropdown && (
@ -128,8 +276,8 @@ export default function DatasetTab({ knowledgeBase }) {
className='dropdown-item d-flex align-items-center text-danger'
onClick={handleBatchDelete}
>
<SvgIcon className='trash me-2' />
删除
<SvgIcon className='trash' width='16' height='16' color='#dc3545' />
<span className='ms-2'>删除</span>
</button>
</li>
</ul>
@ -144,12 +292,15 @@ export default function DatasetTab({ knowledgeBase }) {
onChange={(e) => setSearchQuery(e.target.value)}
/>
<span className='position-absolute top-50 end-0 translate-middle-y pe-3'>
<SvgIcon className='search' />
<SvgIcon className='search' color='#6c757d' />
</span>
</div>
</div>
<button className='btn btn-dark d-flex align-items-center gap-1'>
<SvgIcon className='plus' />
<button
className='btn btn-dark d-flex align-items-center gap-1'
onClick={() => setShowAddFileModal(true)}
>
<SvgIcon className='plus' color='#ffffff' />
新增文件
</button>
</div>
@ -201,13 +352,14 @@ export default function DatasetTab({ knowledgeBase }) {
<td>
<div className='d-flex gap-1'>
<button className='btn btn-sm text-primary' title='查看'>
<SvgIcon className='eye' />
<SvgIcon className='eye' width='18' height='18' />
</button>
<button className='btn btn-sm text-success' title='编辑'>
<SvgIcon className='edit' />
</button>
<button className='btn btn-sm text-danger' title='删除'>
<SvgIcon className='trash' />
<button
className='btn btn-sm text-danger'
title='删除'
onClick={() => handleDeleteDocument(doc.id)}
>
<SvgIcon className='trash' width='18' height='18' />
</button>
</div>
</td>
@ -222,7 +374,7 @@ export default function DatasetTab({ knowledgeBase }) {
<div className='d-flex justify-content-between align-items-center mt-3'>
<div>
每页行数:
<select className='form-select form-select-sm d-inline-block ms-2' style={{ width: '70px' }}>
<select className='form-select form-select d-inline-block ms-2' style={{ width: '70px' }}>
<option value='5'>5</option>
<option value='10'>10</option>
<option value='20'>20</option>
@ -231,7 +383,7 @@ export default function DatasetTab({ knowledgeBase }) {
<div className='d-flex align-items-center'>
<span className='me-3'>1-5 of 10</span>
<nav aria-label='Page navigation'>
<ul className='pagination pagination-sm mb-0'>
<ul className='pagination pagination mb-0'>
<li className='page-item'>
<button className='page-link' aria-label='Previous'>
<span aria-hidden='true'>&laquo;</span>
@ -246,6 +398,121 @@ export default function DatasetTab({ knowledgeBase }) {
</nav>
</div>
</div>
{/* Add File Modal */}
{showAddFileModal && (
<div
className='modal-backdrop'
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1050,
}}
>
<div
className='modal-content bg-white rounded shadow'
style={{
width: '500px',
maxWidth: '90%',
padding: '20px',
}}
>
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
<h5 className='modal-title m-0'>新增文件</h5>
<button
type='button'
className='btn-close'
onClick={handleCloseAddFileModal}
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<div className='mb-3'>
<label htmlFor='fileName' className='form-label'>
文件名称 <span className='text-danger'>*</span>
</label>
<input
type='text'
className='form-control bg-light'
id='fileName'
value={newFile.name}
placeholder='在此输入文件名称'
readOnly
/>
<small className='text-muted'>文件名称将自动填充为上传文件的名称</small>
</div>
<div className='mb-3'>
<label htmlFor='fileDescription' className='form-label'>
文件描述
</label>
<textarea
className='form-control'
id='fileDescription'
value={newFile.description}
onChange={handleDescriptionChange}
placeholder='在此输入文件描述...'
rows='3'
></textarea>
</div>
<div className='mb-3'>
<label className='form-label'>
上传文件 <span className='text-danger'>*</span>
</label>
<div
className={`border rounded p-4 text-center ${
fileErrors.file ? 'border-danger' : 'border-dashed'
}`}
style={{ cursor: 'pointer' }}
onClick={handleUploadAreaClick}
onDrop={handleFileDrop}
onDragOver={handleDragOver}
>
<input
type='file'
ref={fileInputRef}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
{newFile.file ? (
<div>
<p className='mb-1 fw-bold'>{newFile.name}</p>
<p className='text-muted mb-0'>
{(newFile.file.size / 1024).toFixed(2)} KB
</p>
</div>
) : (
<div>
<SvgIcon className='arrowup-upload' width='36' height='36' />
<p className='my-1'>点击或拖拽文件到此处上传</p>
<p className='text-muted mb-0'>支持 PDF, DOCX, TXT, CSV 等格式</p>
</div>
)}
</div>
{fileErrors.file && <div className='text-danger small mt-1'>{fileErrors.file}</div>}
</div>
</div>
<div className='modal-footer d-flex justify-content-end gap-2'>
<button
type='button'
className='btn btn-outline-secondary'
onClick={handleCloseAddFileModal}
>
取消
</button>
<button type='button' className='btn btn-dark' onClick={handleFileUpload}>
确定
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -1,8 +1,114 @@
import React from 'react';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import SvgIcon from '../../../components/SvgIcon';
import { useDispatch } from 'react-redux';
import { showNotification } from '../../../store/notification.slice';
export default function SettingsTab({ knowledgeBase }) {
const dispatch = useDispatch();
// State for pagination
const [currentPage, setCurrentPage] = useState(1);
const usersPerPage = 10;
// State for edit modal
const [showEditModal, setShowEditModal] = useState(false);
const [editUser, setEditUser] = useState(null);
const [formErrors, setFormErrors] = useState({});
// Mock data for users with permissions - convert to state so we can update it
const [users, setUsers] = useState([
{
id: '1001',
username: '张三',
email: 'zhang@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1002',
username: '李四',
email: 'li@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
{
id: '1003',
username: '王五',
email: 'wang@abc.com',
permissionType: '只读',
accessDuration: '三个月',
},
{
id: '1004',
username: '赵六',
email: 'zhao@abc.com',
permissionType: '完全访问',
accessDuration: '六个月',
},
{
id: '1005',
username: '钱七',
email: 'qian@abc.com',
permissionType: '只读',
accessDuration: '一周',
},
{
id: '1006',
username: '孙八',
email: 'sun@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1007',
username: '周九',
email: 'zhou@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
{
id: '1008',
username: '吴十',
email: 'wu@abc.com',
permissionType: '只读',
accessDuration: '三个月',
},
{
id: '1009',
username: '郑十一',
email: 'zheng@abc.com',
permissionType: '完全访问',
accessDuration: '六个月',
},
{
id: '1010',
username: '冯十二',
email: 'feng@abc.com',
permissionType: '只读',
accessDuration: '一周',
},
{
id: '1011',
username: '陈十三',
email: 'chen@abc.com',
permissionType: '只读',
accessDuration: '一个月',
},
{
id: '1012',
username: '褚十四',
email: 'chu@abc.com',
permissionType: '完全访问',
accessDuration: '永久',
},
]);
// Get current users for pagination
const indexOfLastUser = currentPage * usersPerPage;
const indexOfFirstUser = indexOfLastUser - usersPerPage;
const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser);
const totalPages = Math.ceil(users.length / usersPerPage);
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
@ -16,6 +122,83 @@ export default function SettingsTab({ knowledgeBase }) {
console.log('Deleting knowledge base:', knowledgeBase.id);
};
// Handle edit user permissions
const handleEditUser = (user) => {
setEditUser({ ...user });
setFormErrors({});
setShowEditModal(true);
};
// Handle input change in edit modal
const handleEditInputChange = (e) => {
const { name, value } = e.target;
setEditUser((prev) => ({
...prev,
[name]: value,
}));
// Clear error if exists
if (formErrors[name]) {
setFormErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
// Validate edit form
const validateEditForm = () => {
const errors = {};
if (!editUser.permissionType) {
errors.permissionType = '请选择权限类型';
}
if (!editUser.accessDuration) {
errors.accessDuration = '请选择访问时长';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// Handle save user permissions
const handleSaveUserPermissions = () => {
// Validate form
if (!validateEditForm()) {
return;
}
// Here you would typically call an API to update the user permissions
console.log('Updating user permissions:', editUser);
// Update the users array with the edited user
setUsers((prevUsers) => prevUsers.map((user) => (user.id === editUser.id ? { ...editUser } : user)));
// Show success notification (in a real app)
dispatch(showNotification({ message: '用户权限已更新', type: 'success' }));
// Close modal
setShowEditModal(false);
};
// Handle pagination
const handlePageChange = (pageNumber) => {
setCurrentPage(pageNumber);
};
// Handle delete user
const handleDeleteUser = (userId) => {
// Here you would typically call an API to delete the user
console.log('Deleting user:', userId);
// Update the users array by removing the deleted user
setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId));
// Show success notification (in a real app)
dispatch(showNotification({ message: '用户已删除', type: 'success' }));
};
return (
<>
{/* Breadcrumb navigation */}
@ -23,10 +206,17 @@ export default function SettingsTab({ knowledgeBase }) {
<nav aria-label='breadcrumb'>
<ol className='breadcrumb mb-0'>
<li className='breadcrumb-item'>
<Link className='text-secondary text-decoration-none' to='/'>知识库</Link>
<Link className='text-secondary text-decoration-none' to='/'>
知识库
</Link>
</li>
<li className='breadcrumb-item'>
<Link className='text-secondary text-decoration-none' to={`/knowledge-base/${knowledgeBase.id}`}>{knowledgeBase.title}</Link>
<Link
className='text-secondary text-decoration-none'
to={`/knowledge-base/${knowledgeBase.id}`}
>
{knowledgeBase.title}
</Link>
</li>
<li className='breadcrumb-item active text-dark' aria-current='page'>
设置
@ -108,53 +298,96 @@ export default function SettingsTab({ knowledgeBase }) {
</tr>
</thead>
<tbody>
<tr>
<td>#1001</td>
<td>测试数据集 001</td>
<td>zhang@abc.com</td>
<td>只读</td>
<td>一个月</td>
<td>
<div className='d-flex gap-2'>
<button
className='btn btn-sm text-primary'
title='编辑'
{currentUsers.map((user) => (
<tr key={user.id}>
<td>#{user.id}</td>
<td>{user.username}</td>
<td>{user.email}</td>
<td>
<span
className={
user.permissionType === '完全访问'
? 'text-success'
: 'text-warning'
}
>
<SvgIcon className='edit' />
</button>
<button className='btn btn-sm text-danger' title='删除'>
<SvgIcon className='trash' />
</button>
</div>
</td>
</tr>
<tr>
<td>#1002</td>
<td>产品分析数据</td>
<td>li@abc.com</td>
<td>完全访问</td>
<td>永久</td>
<td>
<div className='d-flex gap-2'>
<button
className='btn btn-sm text-primary'
title='编辑'
>
<SvgIcon className='edit' />
</button>
<button className='btn btn-sm text-danger' title='删除'>
<SvgIcon className='trash' />
</button>
</div>
</td>
</tr>
{user.permissionType}
</span>
</td>
<td>{user.accessDuration}</td>
<td>
<div className='d-flex gap-2'>
<button
className='btn btn-sm text-primary'
title='编辑'
onClick={() => handleEditUser(user)}
>
<SvgIcon className='edit' width='16' height='16' />
</button>
<button
className='btn btn-sm text-danger'
title='删除'
onClick={() => handleDeleteUser(user.id)}
>
<SvgIcon className='trash' width='16' height='16' />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
{users.length > usersPerPage && (
<div className='d-flex justify-content-end mt-3'>
<nav aria-label='Page navigation'>
<ul className='pagination pagination-sm mb-0'>
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<button
className='page-link'
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<span aria-hidden='true'>&laquo;</span>
</button>
</li>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNumber) => (
<li
key={pageNumber}
className={`page-item ${
currentPage === pageNumber ? 'active' : ''
}`}
>
<button
className='page-link'
onClick={() => handlePageChange(pageNumber)}
>
{pageNumber}
</button>
</li>
))}
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
<button
className='page-link'
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<span aria-hidden='true'>&raquo;</span>
</button>
</li>
</ul>
</nav>
</div>
)}
<button type='button' className='btn btn-outline-dark mt-2'>
<SvgIcon className='plus me-1' />
添加用户
<SvgIcon className='plus' width='16' height='16' />
<span className='ms-1'>添加用户</span>
</button>
</div>
@ -169,6 +402,143 @@ export default function SettingsTab({ knowledgeBase }) {
</form>
</div>
</div>
{/* Edit User Permissions Modal */}
{showEditModal && (
<div
className='modal-backdrop'
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1050,
}}
>
<div
className='modal-content bg-white rounded shadow'
style={{
width: '500px',
maxWidth: '90%',
padding: '20px',
}}
>
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
<h5 className='modal-title m-0'>编辑用户权限</h5>
<button
type='button'
className='btn-close'
onClick={() => setShowEditModal(false)}
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<div className='mb-3'>
<label className='form-label'>用户信息</label>
<div className='card bg-light'>
<div className='card-body'>
<div className='d-flex justify-content-between mb-2'>
<span className='text-muted'>ID:</span>
<span>#{editUser?.id}</span>
</div>
<div className='d-flex justify-content-between mb-2'>
<span className='text-muted'>用户名:</span>
<span>{editUser?.username}</span>
</div>
<div className='d-flex justify-content-between'>
<span className='text-muted'>邮箱:</span>
<span>{editUser?.email}</span>
</div>
</div>
</div>
</div>
<div className='mb-4'>
<label className='form-label d-flex align-items-center'>
<SvgIcon className='key me-2' width='16' height='16' />
权限类型 <span className='text-danger'>*</span>
</label>
<div className='d-flex gap-2'>
<div
className={`p-3 rounded border bg-warning-subtle ${
editUser?.permissionType === '只读'
? 'border-warning'
: 'border-warning-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
handleEditInputChange({ target: { name: 'permissionType', value: '只读' } })
}
>
<div className='text-center text-warning fw-bold mb-1'>只读</div>
<div className='text-center text-muted small'>仅查看数据集内容</div>
</div>
<div
className={`p-3 rounded border bg-success-subtle ${
editUser?.permissionType === '完全访问'
? 'border-success'
: 'border-success-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
handleEditInputChange({
target: { name: 'permissionType', value: '完全访问' },
})
}
>
<div className='text-center text-success fw-bold mb-1'>完全访问</div>
<div className='text-center text-muted small'>查看编辑和管理数据</div>
</div>
</div>
{formErrors.permissionType && (
<div className='text-danger small mt-1'>{formErrors.permissionType}</div>
)}
</div>
<div className='mb-3'>
<label className='form-label d-flex align-items-center'>
<SvgIcon className='clock me-2' width='16' height='16' />
访问时长 <span className='text-danger'>*</span>
</label>
<select
className={`form-select ${formErrors.accessDuration ? 'is-invalid' : ''}`}
name='accessDuration'
value={editUser?.accessDuration || ''}
onChange={handleEditInputChange}
required
>
<option value=''>请选择访问时长</option>
<option value='一周'>一周</option>
<option value='一个月'>一个月</option>
<option value='三个月'>三个月</option>
<option value='六个月'>六个月</option>
<option value='永久'>永久</option>
</select>
{formErrors.accessDuration && (
<div className='invalid-feedback'>{formErrors.accessDuration}</div>
)}
</div>
</div>
<div className='modal-footer d-flex justify-content-end gap-2'>
<button
type='button'
className='btn btn-outline-secondary'
onClick={() => setShowEditModal(false)}
>
取消
</button>
<button type='button' className='btn btn-dark' onClick={handleSaveUserPermissions}>
保存
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -180,7 +180,7 @@ export default function KnowledgeBase() {
};
return (
<div className='container'>
<div className='knowledge-base container mt-4'>
<div className='d-flex justify-content-between align-items-center mb-3'>
<input type='text' className='form-control w-50' placeholder='搜索知识库...' />
<button

View File

@ -1,10 +1,14 @@
import React from 'react';
import SvgIcon from '../../components/SvgIcon';
import { useNavigate } from 'react-router-dom';
export default function KnowledgeCard({ id, title, description, documents, date, access, onClick, onRequestAccess }) {
const navigate = useNavigate();
const handleNewChat = (e) => {
e.preventDefault();
e.stopPropagation();
navigate(`/chat/${id}`);
};
const handleRequestAccess = (e) => {

View File

@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import Mainlayout from '../layouts/Mainlayout';
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
import KnowledgeBaseDetail from '../pages/KnowledgeBase/KnowledgeBaseDetail';
import Chat from '../pages/Chat/Chat';
import Loading from '../components/Loading';
import Login from '../pages/Auth/Login';
import Signup from '../pages/Auth/Signup';
@ -40,6 +41,30 @@ function AppRouter() {
</Mainlayout>
}
/>
<Route
path='/chat'
element={
<Mainlayout>
<Chat />
</Mainlayout>
}
/>
<Route
path='/chat/:knowledgeBaseId'
element={
<Mainlayout>
<Chat />
</Mainlayout>
}
/>
<Route
path='/chat/:knowledgeBaseId/:chatId'
element={
<Mainlayout>
<Chat />
</Mainlayout>
}
/>
</Route>
<Route path='/login' element={<Login />} />
<Route path='/signup' element={<Signup />} />