mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-07 23:29:42 +08:00
[dev]chat pages
This commit is contained in:
parent
6d0ebf169c
commit
546f4c4265
@ -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] || ''),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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>`,
|
||||
};
|
||||
|
@ -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
67
src/pages/Chat/Chat.jsx
Normal 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>
|
||||
);
|
||||
}
|
94
src/pages/Chat/ChatSidebar.jsx
Normal file
94
src/pages/Chat/ChatSidebar.jsx
Normal 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>
|
||||
);
|
||||
}
|
161
src/pages/Chat/ChatWindow.jsx
Normal file
161
src/pages/Chat/ChatWindow.jsx
Normal 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
102
src/pages/Chat/NewChat.jsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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'>«</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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'>«</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'>»</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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 />} />
|
||||
|
Loading…
Reference in New Issue
Block a user