mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-07 22:58:19 +08:00
[dev]connect knowledgebase & chat api
This commit is contained in:
parent
4915514bde
commit
c9236cfff4
199
src/components/AccessRequestModal.jsx
Normal file
199
src/components/AccessRequestModal.jsx
Normal file
@ -0,0 +1,199 @@
|
||||
import React, { useState } from 'react';
|
||||
import SvgIcon from './SvgIcon';
|
||||
|
||||
/**
|
||||
* 申请权限弹窗组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.show - 是否显示弹窗
|
||||
* @param {string} props.knowledgeBaseId - 知识库ID
|
||||
* @param {string} props.knowledgeBaseTitle - 知识库标题
|
||||
* @param {Function} props.onClose - 关闭弹窗的回调函数
|
||||
* @param {Function} props.onSubmit - 提交申请的回调函数,接收 requestData 参数
|
||||
* @param {boolean} props.isSubmitting - 是否正在提交
|
||||
*/
|
||||
export default function AccessRequestModal({
|
||||
show,
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseTitle,
|
||||
onClose,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
}) {
|
||||
const [accessRequestData, setAccessRequestData] = useState({
|
||||
accessType: '只读访问',
|
||||
duration: '一周',
|
||||
reason: '',
|
||||
});
|
||||
const [accessRequestErrors, setAccessRequestErrors] = useState({});
|
||||
|
||||
const handleAccessRequestInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setAccessRequestData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
// Clear error when user types
|
||||
if (accessRequestErrors[name]) {
|
||||
setAccessRequestErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: '',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validateAccessRequestForm = () => {
|
||||
const errors = {};
|
||||
|
||||
if (!accessRequestData.reason.trim()) {
|
||||
errors.reason = '请输入申请原因';
|
||||
}
|
||||
|
||||
setAccessRequestErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmitAccessRequest = () => {
|
||||
// Validate form
|
||||
if (!validateAccessRequestForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用父组件的提交函数
|
||||
onSubmit({
|
||||
id: knowledgeBaseId,
|
||||
title: knowledgeBaseTitle,
|
||||
...accessRequestData,
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
setAccessRequestData({
|
||||
accessType: '只读访问',
|
||||
duration: '一周',
|
||||
reason: '',
|
||||
});
|
||||
setAccessRequestErrors({});
|
||||
};
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<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={onClose}
|
||||
aria-label='Close'
|
||||
disabled={isSubmitting}
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>知识库名称</label>
|
||||
<input type='text' className='form-control' value={knowledgeBaseTitle} readOnly />
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='key' />
|
||||
访问级别 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<select
|
||||
className='form-select'
|
||||
name='accessType'
|
||||
value={accessRequestData.accessType}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
>
|
||||
<option value='只读访问'>只读访问</option>
|
||||
<option value='编辑权限'>编辑权限</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='calendar' />
|
||||
访问时长 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<select
|
||||
className='form-select'
|
||||
name='duration'
|
||||
value={accessRequestData.duration}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
>
|
||||
<option value='一周'>一周</option>
|
||||
<option value='一个月'>一个月</option>
|
||||
<option value='三个月'>三个月</option>
|
||||
<option value='六个月'>六个月</option>
|
||||
<option value='永久'>永久</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='chat' />
|
||||
申请原因 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${accessRequestErrors.reason ? 'is-invalid' : ''}`}
|
||||
id='reason'
|
||||
name='reason'
|
||||
rows='3'
|
||||
value={accessRequestData.reason}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
placeholder='请输入申请原因'
|
||||
></textarea>
|
||||
{accessRequestErrors.reason && (
|
||||
<div className='invalid-feedback'>{accessRequestErrors.reason}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onClose} disabled={isSubmitting}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-dark'
|
||||
onClick={handleSubmitAccessRequest}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
提交中...
|
||||
</>
|
||||
) : (
|
||||
'提交申请'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
168
src/components/CreateKnowledgeBaseModal.jsx
Normal file
168
src/components/CreateKnowledgeBaseModal.jsx
Normal file
@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from './SvgIcon';
|
||||
|
||||
/**
|
||||
* 创建知识库模态框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.show - 是否显示弹窗
|
||||
* @param {Object} props.formData - 表单数据
|
||||
* @param {Object} props.formErrors - 表单错误信息
|
||||
* @param {boolean} props.isSubmitting - 是否正在提交
|
||||
* @param {Function} props.onClose - 关闭弹窗的回调函数
|
||||
* @param {Function} props.onChange - 表单输入变化的回调函数
|
||||
* @param {Function} props.onSubmit - 提交表单的回调函数
|
||||
*/
|
||||
const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => {
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<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={onClose}
|
||||
aria-label='Close'
|
||||
disabled={isSubmitting}
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='name' className='form-label'>
|
||||
知识库名称 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control ${formErrors.name ? 'is-invalid' : ''}`}
|
||||
id='name'
|
||||
name='name'
|
||||
value={formData.name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{formErrors.name && <div className='invalid-feedback'>{formErrors.name}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='desc' className='form-label'>
|
||||
知识库描述 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${formErrors.desc ? 'is-invalid' : ''}`}
|
||||
id='desc'
|
||||
name='desc'
|
||||
rows='3'
|
||||
value={formData.desc}
|
||||
onChange={onChange}
|
||||
></textarea>
|
||||
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>
|
||||
知识库类型 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<div className='d-flex gap-3'>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='type'
|
||||
id='typePrivate'
|
||||
value='private'
|
||||
checked={formData.type === 'private'}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='typePrivate'>
|
||||
私有知识库
|
||||
</label>
|
||||
</div>
|
||||
<div className='form-check'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='radio'
|
||||
name='type'
|
||||
id='typePublic'
|
||||
value='public'
|
||||
checked={formData.type === 'public'}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='typePublic'>
|
||||
公共知识库
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{formErrors.type && <div className='text-danger small mt-1'>{formErrors.type}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='department' className='form-label'>
|
||||
部门
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control bg-light'
|
||||
id='department'
|
||||
name='department'
|
||||
value={formData.department || ''}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='group' className='form-label'>
|
||||
组别
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control bg-light'
|
||||
id='group'
|
||||
name='group'
|
||||
value={formData.group || ''}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onClose} disabled={isSubmitting}>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark' onClick={onSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
'创建'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateKnowledgeBaseModal;
|
@ -2,6 +2,12 @@ import React from 'react';
|
||||
|
||||
/**
|
||||
* 分页组件
|
||||
* @param {Object} props
|
||||
* @param {number} props.currentPage - 当前页码
|
||||
* @param {number} props.totalPages - 总页数
|
||||
* @param {number} props.pageSize - 每页显示的条目数
|
||||
* @param {Function} props.onPageChange - 页码变化的回调函数
|
||||
* @param {Function} props.onPageSizeChange - 每页条目数变化的回调函数
|
||||
*/
|
||||
const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => {
|
||||
return (
|
||||
@ -17,7 +23,7 @@ const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSiz
|
||||
<option value='50'>50条/页</option>
|
||||
</select>
|
||||
</div>
|
||||
<nav aria-label='知识库分页'>
|
||||
<nav aria-label='分页导航'>
|
||||
<ul className='pagination mb-0'>
|
||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||
<button
|
42
src/components/SearchBar.jsx
Normal file
42
src/components/SearchBar.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from './SvgIcon';
|
||||
|
||||
/**
|
||||
* 搜索栏组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.searchKeyword - 搜索关键词
|
||||
* @param {boolean} props.isSearching - 是否正在搜索
|
||||
* @param {Function} props.onSearchChange - 搜索关键词变化的回调函数
|
||||
* @param {Function} props.onSearch - 提交搜索的回调函数
|
||||
* @param {Function} props.onClearSearch - 清除搜索的回调函数
|
||||
* @param {string} props.placeholder - 搜索框占位文本
|
||||
* @param {string} props.className - 额外的 CSS 类名
|
||||
*/
|
||||
const SearchBar = ({
|
||||
searchKeyword,
|
||||
isSearching,
|
||||
onSearchChange,
|
||||
onSearch,
|
||||
onClearSearch,
|
||||
placeholder = '搜索...',
|
||||
className = 'w-50',
|
||||
}) => {
|
||||
return (
|
||||
<form className={`d-flex ${className}`} onSubmit={onSearch}>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
placeholder={placeholder}
|
||||
value={searchKeyword}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
{isSearching && (
|
||||
<button type='button' className='btn btn-outline-dark ms-2' onClick={onClearSearch}>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
@ -1,5 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchChats, deleteChat } from '../../store/chat/chat.thunks';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import ChatSidebar from './ChatSidebar';
|
||||
import NewChat from './NewChat';
|
||||
import ChatWindow from './ChatWindow';
|
||||
@ -7,22 +10,36 @@ 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',
|
||||
},
|
||||
]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// 从 Redux store 获取聊天记录列表
|
||||
const { items: chatHistory, status, error } = useSelector((state) => state.chat.list);
|
||||
const operationStatus = useSelector((state) => state.chat.operations.status);
|
||||
const operationError = useSelector((state) => state.chat.operations.error);
|
||||
|
||||
// 获取聊天记录列表
|
||||
useEffect(() => {
|
||||
dispatch(fetchChats());
|
||||
}, [dispatch]);
|
||||
|
||||
// 监听操作状态,显示通知
|
||||
useEffect(() => {
|
||||
if (operationStatus === 'succeeded') {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '操作成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
} else if (operationStatus === 'failed' && operationError) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `操作失败: ${operationError}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [operationStatus, operationError, dispatch]);
|
||||
|
||||
// If we have a knowledgeBaseId but no chatId, create a new chat
|
||||
useEffect(() => {
|
||||
@ -34,8 +51,8 @@ export default function Chat() {
|
||||
}, [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));
|
||||
// 调用 Redux action 删除聊天
|
||||
dispatch(deleteChat(id));
|
||||
|
||||
// If the deleted chat is the current one, navigate to the chat list
|
||||
if (chatId === id) {
|
||||
@ -51,7 +68,12 @@ export default function Chat() {
|
||||
className='col-md-3 col-lg-2 p-0 border-end'
|
||||
style={{ height: 'calc(100vh - 73px)', overflowY: 'auto' }}
|
||||
>
|
||||
<ChatSidebar chatHistory={chatHistory} onDeleteChat={handleDeleteChat} />
|
||||
<ChatSidebar
|
||||
chatHistory={chatHistory}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
isLoading={status === 'loading'}
|
||||
hasError={status === 'failed'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
|
@ -2,9 +2,9 @@ import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
|
||||
export default function ChatSidebar({ chatHistory, onDeleteChat }) {
|
||||
export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading = false, hasError = false }) {
|
||||
const navigate = useNavigate();
|
||||
const { chatId } = useParams();
|
||||
const { chatId, knowledgeBaseId } = useParams();
|
||||
const [activeDropdown, setActiveDropdown] = useState(null);
|
||||
|
||||
const handleNewChat = () => {
|
||||
@ -28,10 +28,37 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) {
|
||||
setActiveDropdown(null);
|
||||
};
|
||||
|
||||
// 渲染加载状态
|
||||
const renderLoading = () => (
|
||||
<div className='p-3 text-center'>
|
||||
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
<div className='mt-2 text-muted small'>加载聊天记录...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染错误状态
|
||||
const renderError = () => (
|
||||
<div className='p-3 text-center'>
|
||||
<div className='text-danger mb-2'>
|
||||
<SvgIcon className='error' width='24' height='24' />
|
||||
</div>
|
||||
<div className='text-muted small'>加载聊天记录失败,请重试</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmpty = () => (
|
||||
<div className='p-3 text-center'>
|
||||
<div className='text-muted small'>暂无聊天记录</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='chat-sidebar d-flex flex-column h-100'>
|
||||
<div className='p-3 pb-0'>
|
||||
<h5 className='mb-0'>Chats</h5>
|
||||
<h5 className='mb-0'>聊天记录</h5>
|
||||
</div>
|
||||
|
||||
<div className='p-3'>
|
||||
@ -40,54 +67,62 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) {
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<SvgIcon className='plus' color='#ffffff' />
|
||||
<span>New Chat</span>
|
||||
<span>新建聊天</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' : ''
|
||||
{isLoading ? (
|
||||
renderLoading()
|
||||
) : hasError ? (
|
||||
renderError()
|
||||
) : chatHistory.length === 0 ? (
|
||||
renderEmpty()
|
||||
) : (
|
||||
<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' : ''
|
||||
}`}
|
||||
>
|
||||
<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)}
|
||||
<Link
|
||||
to={`/chat/${chat.knowledge_base_id}/${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' }}
|
||||
>
|
||||
<SvgIcon className='trash' />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<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>
|
||||
);
|
||||
|
@ -1,62 +1,52 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchMessages, sendMessage } from '../../store/chat/chat.messages.thunks';
|
||||
import { resetMessages, resetSendMessageStatus } from '../../store/chat/chat.slice';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
|
||||
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const dispatch = useDispatch();
|
||||
const [inputMessage, setInputMessage] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [knowledgeBase, setKnowledgeBase] = useState(null);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
// Fetch knowledge base details
|
||||
// 从 Redux store 获取数据
|
||||
const {
|
||||
items: messages,
|
||||
status: messagesStatus,
|
||||
error: messagesError,
|
||||
} = useSelector((state) => state.chat.messages);
|
||||
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
|
||||
const knowledgeBase = useSelector((state) =>
|
||||
state.knowledgeBase.list.items.find((kb) => kb.id === knowledgeBaseId)
|
||||
);
|
||||
|
||||
// 获取聊天消息
|
||||
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(),
|
||||
},
|
||||
]);
|
||||
if (chatId) {
|
||||
dispatch(fetchMessages(chatId));
|
||||
}
|
||||
}, [chatId, knowledgeBaseId]);
|
||||
|
||||
// Scroll to bottom when messages change
|
||||
// 组件卸载时重置消息状态
|
||||
return () => {
|
||||
dispatch(resetMessages());
|
||||
};
|
||||
}, [chatId, dispatch]);
|
||||
|
||||
// 监听发送消息状态
|
||||
useEffect(() => {
|
||||
if (sendStatus === 'failed' && sendError) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `发送失败: ${sendError}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
dispatch(resetSendMessageStatus());
|
||||
}
|
||||
}, [sendStatus, sendError, dispatch]);
|
||||
|
||||
// 滚动到底部
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
@ -64,68 +54,83 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
const handleSendMessage = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!inputMessage.trim()) return;
|
||||
if (!inputMessage.trim() || sendStatus === 'loading') return;
|
||||
|
||||
// Add user message
|
||||
const userMessage = {
|
||||
id: Date.now().toString(),
|
||||
sender: 'user',
|
||||
content: inputMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
// 发送消息
|
||||
dispatch(sendMessage({ chatId, content: inputMessage }));
|
||||
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);
|
||||
};
|
||||
|
||||
// 渲染加载状态
|
||||
const renderLoading = () => (
|
||||
<div className='p-5 text-center'>
|
||||
<div className='spinner-border text-secondary' role='status'>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
<div className='mt-3 text-muted'>加载聊天记录...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染错误状态
|
||||
const renderError = () => (
|
||||
<div className='p-5 text-center'>
|
||||
<div className='text-danger mb-3'>
|
||||
<SvgIcon className='error' width='48' height='48' />
|
||||
</div>
|
||||
<div className='text-muted'>加载聊天记录失败,请重试</div>
|
||||
<button className='btn btn-outline-secondary mt-3' onClick={() => dispatch(fetchMessages(chatId))}>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmpty = () => (
|
||||
<div className='p-5 text-center'>
|
||||
<div className='text-muted'>暂无聊天记录,发送一条消息开始聊天吧</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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>
|
||||
<h5 className='mb-0'>{knowledgeBase?.name || '加载中...'}</h5>
|
||||
<small className='text-muted'>{knowledgeBase?.description}</small>
|
||||
</div>
|
||||
|
||||
{/* Chat messages */}
|
||||
<div className='flex-grow-1 p-3 overflow-auto' >
|
||||
<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>
|
||||
))}
|
||||
{messagesStatus === 'loading'
|
||||
? renderLoading()
|
||||
: messagesStatus === 'failed'
|
||||
? renderError()
|
||||
: messages.length === 0
|
||||
? renderEmpty()
|
||||
: 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 && (
|
||||
{sendStatus === 'loading' && (
|
||||
<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>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,15 +149,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
placeholder='输入你的问题...'
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
disabled={isLoading}
|
||||
disabled={sendStatus === 'loading'}
|
||||
/>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
|
||||
disabled={isLoading || !inputMessage.trim()}
|
||||
disabled={sendStatus === 'loading' || !inputMessage.trim()}
|
||||
>
|
||||
<SvgIcon className='send' color='#ffffff' />
|
||||
<span className='ms-1' style={{ minWidth: 'fit-content' }}>发送</span>
|
||||
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
|
||||
发送
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,54 +1,47 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import { get } from '../../services/api';
|
||||
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 dispatch = useDispatch();
|
||||
const [knowledgeBases, setKnowledgeBases] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 获取知识库列表
|
||||
useEffect(() => {
|
||||
const fetchKnowledgeBases = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await get('/knowledge-bases/');
|
||||
|
||||
// 过滤出有 can_read 权限的知识库
|
||||
const readableKnowledgeBases = response.data.items.filter(
|
||||
(kb) => kb.permissions && kb.permissions.can_read === true
|
||||
);
|
||||
|
||||
setKnowledgeBases(readableKnowledgeBases);
|
||||
} catch (error) {
|
||||
console.error('获取知识库列表失败:', error);
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '获取知识库列表失败,请稍后重试',
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchKnowledgeBases();
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSelectKnowledgeBase = (knowledgeBaseId) => {
|
||||
// In a real app, you would create a new chat and get its ID from the API
|
||||
// 创建新聊天并导航到聊天页面
|
||||
navigate(`/chat/${knowledgeBaseId}`);
|
||||
};
|
||||
|
||||
@ -58,9 +51,19 @@ export default function NewChat() {
|
||||
<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' ? (
|
||||
{loading ? (
|
||||
<div className='d-flex justify-content-center my-5'>
|
||||
<div className='spinner-border' role='status'>
|
||||
<span className='visually-hidden'>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : knowledgeBases.length === 0 ? (
|
||||
<div className='text-center my-5'>
|
||||
<p className='text-muted'>没有可用的知识库,请联系管理员获取权限</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
|
||||
{knowledgeBases.map((kb) => (
|
||||
<div key={kb.id} className='col'>
|
||||
<div
|
||||
className='card h-100 bg-light border-0 cursor-pointer'
|
||||
@ -68,14 +71,17 @@ export default function NewChat() {
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className='card-body py-4'>
|
||||
<p className='card-title h5'>{kb.title}</p>
|
||||
<p className='card-title h5'>{kb.name}</p>
|
||||
<p className='card-text text-muted'>{kb.description}</p>
|
||||
<div className='d-flex justify-content-between align-items-center mt-3'>
|
||||
<small className='text-muted'>文档数: {kb.document_count || 0}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
name: knowledgeBase.name,
|
||||
desc: knowledgeBase.desc || knowledgeBase.description || '',
|
||||
type: knowledgeBase.type || 'private', // 默认为私有知识库
|
||||
department: knowledgeBase.department || '',
|
||||
group: knowledgeBase.group || '',
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@ -81,6 +83,8 @@ export default function SettingsTab({ knowledgeBase }) {
|
||||
desc: knowledgeBaseForm.desc,
|
||||
description: knowledgeBaseForm.desc, // Add description field for compatibility
|
||||
type: knowledgeBaseForm.type,
|
||||
department: knowledgeBaseForm.department,
|
||||
group: knowledgeBaseForm.group,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
@ -86,13 +86,13 @@ const FileUploadModal = ({
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
className='btn btn-dark'
|
||||
onClick={onUpload}
|
||||
disabled={!newFile.file || isSubmitting}
|
||||
>
|
||||
|
@ -78,6 +78,37 @@ const KnowledgeBaseForm = ({
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='department' className='form-label'>
|
||||
部门
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control bg-light'
|
||||
id='department'
|
||||
name='department'
|
||||
value={formData.department || ''}
|
||||
readOnly
|
||||
/>
|
||||
<small className='text-muted'>部门信息根据知识库创建者自动填写</small>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='group' className='form-label'>
|
||||
组别
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control bg-light'
|
||||
id='group'
|
||||
name='group'
|
||||
value={formData.group || ''}
|
||||
readOnly
|
||||
/>
|
||||
<small className='text-muted'>组别信息根据知识库创建者自动填写</small>
|
||||
</div>
|
||||
|
||||
<div className='d-flex justify-content-between'>
|
||||
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
|
@ -9,11 +9,13 @@ import {
|
||||
} from '../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
import { requestKnowledgeBaseAccess } from '../../services/permissionService';
|
||||
import AccessRequestModal from '../../components/AccessRequestModal';
|
||||
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
|
||||
import Pagination from '../../components/Pagination';
|
||||
import SearchBar from '../../components/SearchBar';
|
||||
|
||||
// 导入拆分的组件
|
||||
import SearchBar from './components/SearchBar';
|
||||
import Pagination from './components/Pagination';
|
||||
import CreateKnowledgeBaseModal from './components/CreateKnowledgeBaseModal';
|
||||
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
||||
|
||||
export default function KnowledgeBase() {
|
||||
@ -22,18 +24,21 @@ export default function KnowledgeBase() {
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
|
||||
const [formErrors, setFormErrors] = useState({});
|
||||
const [accessRequestErrors, setAccessRequestErrors] = useState({});
|
||||
const [accessRequestData, setAccessRequestData] = useState({
|
||||
const [accessRequestKnowledgeBase, setAccessRequestKnowledgeBase] = useState({
|
||||
id: '',
|
||||
title: '',
|
||||
accessType: '只读访问',
|
||||
duration: '一周',
|
||||
projectInfo: '',
|
||||
reason: '',
|
||||
});
|
||||
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
|
||||
|
||||
// 获取当前用户信息
|
||||
const currentUser = useSelector((state) => state.auth.user);
|
||||
|
||||
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
|
||||
name: '',
|
||||
desc: '',
|
||||
type: 'private', // 默认为私有知识库
|
||||
department: currentUser?.department || '',
|
||||
group: currentUser?.group || '',
|
||||
});
|
||||
|
||||
// Search state
|
||||
@ -175,6 +180,11 @@ export default function KnowledgeBase() {
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
// 不允许修改部门和组别字段
|
||||
if (name === 'department' || name === 'group') {
|
||||
return;
|
||||
}
|
||||
|
||||
setNewKnowledgeBase((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
@ -189,22 +199,6 @@ export default function KnowledgeBase() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccessRequestInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setAccessRequestData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
// Clear error when user types
|
||||
if (accessRequestErrors[name]) {
|
||||
setAccessRequestErrors((prev) => ({
|
||||
...prev,
|
||||
[name]: '',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validateCreateForm = () => {
|
||||
const errors = {};
|
||||
|
||||
@ -216,25 +210,14 @@ export default function KnowledgeBase() {
|
||||
errors.desc = '请输入知识库描述';
|
||||
}
|
||||
|
||||
if (!newKnowledgeBase.type) {
|
||||
errors.type = '请选择知识库类型';
|
||||
}
|
||||
|
||||
setFormErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const validateAccessRequestForm = () => {
|
||||
const errors = {};
|
||||
|
||||
if (!accessRequestData.projectInfo.trim()) {
|
||||
errors.projectInfo = '请输入项目信息';
|
||||
}
|
||||
|
||||
if (!accessRequestData.reason.trim()) {
|
||||
errors.reason = '请输入申请原因';
|
||||
}
|
||||
|
||||
setAccessRequestErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleCreateKnowledgeBase = () => {
|
||||
// Validate form
|
||||
if (!validateCreateForm()) {
|
||||
@ -247,53 +230,69 @@ export default function KnowledgeBase() {
|
||||
name: newKnowledgeBase.name,
|
||||
desc: newKnowledgeBase.desc,
|
||||
description: newKnowledgeBase.desc,
|
||||
type: 'private', // Default type
|
||||
type: newKnowledgeBase.type,
|
||||
department: newKnowledgeBase.department,
|
||||
group: newKnowledgeBase.group,
|
||||
})
|
||||
);
|
||||
|
||||
// Reset form and close modal
|
||||
setNewKnowledgeBase({ name: '', desc: '' });
|
||||
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
|
||||
setFormErrors({});
|
||||
setShowCreateModal(false);
|
||||
};
|
||||
|
||||
// Handle card click to navigate to knowledge base detail
|
||||
const handleCardClick = (id) => {
|
||||
const handleCardClick = (id, permissions) => {
|
||||
// 检查用户是否有读取权限
|
||||
if (!permissions || permissions.can_read === false) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '您没有访问此知识库的权限,请先申请权限',
|
||||
type: 'warning',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 有权限则跳转到详情页
|
||||
navigate(`/knowledge-base/${id}/datasets`);
|
||||
};
|
||||
|
||||
const handleRequestAccess = (id, title) => {
|
||||
setAccessRequestData((prev) => ({
|
||||
...prev,
|
||||
setAccessRequestKnowledgeBase({
|
||||
id,
|
||||
title,
|
||||
}));
|
||||
setAccessRequestErrors({});
|
||||
});
|
||||
setShowAccessRequestModal(true);
|
||||
};
|
||||
|
||||
const handleSubmitAccessRequest = () => {
|
||||
// Validate form
|
||||
if (!validateAccessRequestForm()) {
|
||||
return;
|
||||
const handleSubmitAccessRequest = async (requestData) => {
|
||||
setIsSubmittingRequest(true);
|
||||
|
||||
try {
|
||||
// 使用权限服务发送请求
|
||||
await requestKnowledgeBaseAccess(requestData);
|
||||
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '权限申请已提交',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
// Close modal
|
||||
setShowAccessRequestModal(false);
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: `权限申请失败: ${error.response?.data?.message || '请稍后重试'}`,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsSubmittingRequest(false);
|
||||
}
|
||||
|
||||
// Here you would typically call an API to submit the access request
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '权限申请已提交',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
// Reset form and close modal
|
||||
setAccessRequestData((prev) => ({
|
||||
...prev,
|
||||
projectInfo: '',
|
||||
reason: '',
|
||||
}));
|
||||
setAccessRequestErrors({});
|
||||
setShowAccessRequestModal(false);
|
||||
};
|
||||
|
||||
const handleDelete = (e, id) => {
|
||||
@ -305,6 +304,17 @@ export default function KnowledgeBase() {
|
||||
// Calculate total pages
|
||||
const totalPages = Math.ceil(displayTotal / pagination.page_size);
|
||||
|
||||
// 打开创建知识库弹窗
|
||||
const handleOpenCreateModal = () => {
|
||||
// 确保部门和组别字段使用当前用户的信息
|
||||
setNewKnowledgeBase((prev) => ({
|
||||
...prev,
|
||||
department: currentUser?.department || '',
|
||||
group: currentUser?.group || '',
|
||||
}));
|
||||
setShowCreateModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='knowledge-base container my-4'>
|
||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||
@ -314,11 +324,9 @@ export default function KnowledgeBase() {
|
||||
onSearchChange={handleSearchInputChange}
|
||||
onSearch={handleSearch}
|
||||
onClearSearch={handleClearSearch}
|
||||
placeholder='搜索知识库...'
|
||||
/>
|
||||
<button
|
||||
className='btn btn-dark d-flex align-items-center gap-1'
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
|
||||
<SvgIcon className={'plus'} />
|
||||
新建知识库
|
||||
</button>
|
||||
@ -371,166 +379,14 @@ export default function KnowledgeBase() {
|
||||
/>
|
||||
|
||||
{/* 申请权限弹窗 */}
|
||||
{showAccessRequestModal && (
|
||||
<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={() => setShowAccessRequestModal(false)}
|
||||
aria-label='Close'
|
||||
></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>知识库名称</label>
|
||||
<input type='text' className='form-control' value={accessRequestData.title} readOnly />
|
||||
</div>
|
||||
{/* <div className='mb-4'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='key' />
|
||||
访问级别 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<div className='d-flex gap-2'>
|
||||
<div
|
||||
className={`p-3 rounded border bg-warning-subtle ${
|
||||
accessRequestData.accessType === '只读访问'
|
||||
? 'border-warning bg-warning-subtle'
|
||||
: 'border-warning-subtle opacity-50'
|
||||
}`}
|
||||
style={{ flex: 1, cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' }))
|
||||
}
|
||||
>
|
||||
<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 ${
|
||||
accessRequestData.accessType === '完全访问'
|
||||
? 'border-success'
|
||||
: 'border-success-subtle opacity-50'
|
||||
}`}
|
||||
style={{ flex: 1, cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' }))
|
||||
}
|
||||
>
|
||||
<div className='text-center text-success fw-bold mb-1'>完全访问</div>
|
||||
<div className='text-center text-muted small'>查看、编辑和管理数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='key' />
|
||||
访问级别 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<select
|
||||
className='form-select'
|
||||
name='accessType'
|
||||
value={accessRequestData.accessType}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
>
|
||||
<option value='只读访问'>只读访问</option>
|
||||
<option value='编辑权限'>编辑权限</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='calendar' />
|
||||
访问时长 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<select
|
||||
className='form-select'
|
||||
name='duration'
|
||||
value={accessRequestData.duration}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
>
|
||||
<option value='一周'>一周</option>
|
||||
<option value='一个月'>一个月</option>
|
||||
<option value='三个月'>三个月</option>
|
||||
<option value='六个月'>六个月</option>
|
||||
<option value='永久'>永久</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='clipboard' />
|
||||
项目信息 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control ${accessRequestErrors.projectInfo ? 'is-invalid' : ''}`}
|
||||
id='projectInfo'
|
||||
name='projectInfo'
|
||||
value={accessRequestData.projectInfo}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
placeholder='请输入项目信息'
|
||||
/>
|
||||
{accessRequestErrors.projectInfo && (
|
||||
<div className='invalid-feedback'>{accessRequestErrors.projectInfo}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label d-flex align-items-center gap-1'>
|
||||
<SvgIcon className='chat' />
|
||||
申请原因 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${accessRequestErrors.reason ? 'is-invalid' : ''}`}
|
||||
id='reason'
|
||||
name='reason'
|
||||
rows='3'
|
||||
value={accessRequestData.reason}
|
||||
onChange={handleAccessRequestInputChange}
|
||||
placeholder='请输入申请原因'
|
||||
></textarea>
|
||||
{accessRequestErrors.reason && (
|
||||
<div className='invalid-feedback'>{accessRequestErrors.reason}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-secondary'
|
||||
onClick={() => setShowAccessRequestModal(false)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark' onClick={handleSubmitAccessRequest}>
|
||||
提交申请
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AccessRequestModal
|
||||
show={showAccessRequestModal}
|
||||
knowledgeBaseId={accessRequestKnowledgeBase.id}
|
||||
knowledgeBaseTitle={accessRequestKnowledgeBase.title}
|
||||
onClose={() => setShowAccessRequestModal(false)}
|
||||
onSubmit={handleSubmitAccessRequest}
|
||||
isSubmitting={isSubmittingRequest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '../../../components/SvgIcon';
|
||||
|
||||
/**
|
||||
* 创建知识库模态框组件
|
||||
*/
|
||||
const CreateKnowledgeBaseModal = ({ show, formData, formErrors, isSubmitting, onClose, onChange, onSubmit }) => {
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<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={onClose} aria-label='Close'></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='name' className='form-label'>
|
||||
知识库名称 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control ${formErrors.name ? 'is-invalid' : ''}`}
|
||||
id='name'
|
||||
name='name'
|
||||
value={formData.name}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{formErrors.name && <div className='invalid-feedback'>{formErrors.name}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label htmlFor='desc' className='form-label'>
|
||||
知识库描述 <span className='text-danger'>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`form-control ${formErrors.desc ? 'is-invalid' : ''}`}
|
||||
id='desc'
|
||||
name='desc'
|
||||
rows='3'
|
||||
value={formData.desc}
|
||||
onChange={onChange}
|
||||
></textarea>
|
||||
{formErrors.desc && <div className='invalid-feedback'>{formErrors.desc}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark' onClick={onSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
className='spinner-border spinner-border-sm me-2'
|
||||
role='status'
|
||||
aria-hidden='true'
|
||||
></span>
|
||||
处理中...
|
||||
</>
|
||||
) : (
|
||||
'创建'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateKnowledgeBaseModal;
|
@ -23,8 +23,9 @@ const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequest
|
||||
description={item.description || item.desc || ''}
|
||||
documents={item.document_count || 0}
|
||||
date={new Date(item.create_time || item.created_at).toLocaleDateString()}
|
||||
permissions={item.permissions}
|
||||
access={item.permissions?.can_edit ? 'full' : item.permissions?.can_read ? 'read' : 'none'}
|
||||
onClick={() => onCardClick(item.id)}
|
||||
onClick={() => onCardClick(item.id, item.permissions)}
|
||||
onRequestAccess={onRequestAccess}
|
||||
onDelete={(e) => onDelete(e, item.id)}
|
||||
/>
|
||||
|
@ -9,6 +9,7 @@ export default function KnowledgeCard({
|
||||
documents,
|
||||
date,
|
||||
access,
|
||||
permissions,
|
||||
onClick,
|
||||
onRequestAccess,
|
||||
onDelete,
|
||||
@ -41,17 +42,19 @@ export default function KnowledgeCard({
|
||||
<div className='knowledge-card card shadow border-0 p-0 col' onClick={onClick}>
|
||||
<div className='card-body'>
|
||||
<h5 className='card-title'>{title}</h5>
|
||||
<div className='hoverdown position-absolute end-0 top-0'>
|
||||
<button type='button' className='detail-btn btn'>
|
||||
<SvgIcon className={'more-dot'} />
|
||||
</button>
|
||||
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
|
||||
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
|
||||
删除
|
||||
<SvgIcon className={'trash'} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{permissions && permissions.can_delete && (
|
||||
<div className='hoverdown position-absolute end-0 top-0'>
|
||||
<button type='button' className='detail-btn btn'>
|
||||
<SvgIcon className={'more-dot'} />
|
||||
</button>
|
||||
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
|
||||
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
|
||||
删除
|
||||
<SvgIcon className={'trash'} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
|
||||
{description}
|
||||
</p>
|
||||
|
@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import SvgIcon from '../../../components/SvgIcon';
|
||||
|
||||
/**
|
||||
* 知识库搜索栏组件
|
||||
*/
|
||||
const SearchBar = ({ searchKeyword, isSearching, onSearchChange, onSearch, onClearSearch }) => {
|
||||
return (
|
||||
<form className='d-flex w-50' onSubmit={onSearch}>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
placeholder='搜索知识库...'
|
||||
value={searchKeyword}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
{/* <button type='submit' className='btn btn-outline-dark ms-2'>
|
||||
<SvgIcon className={'search'} />
|
||||
</button> */}
|
||||
{isSearching && (
|
||||
<button type='button' className='btn btn-outline-dark ms-2' onClick={onClearSearch}>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
@ -6,8 +6,8 @@ import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
|
||||
export default function Login() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState('member2');
|
||||
const [password, setPassword] = useState('member123');
|
||||
const [username, setUsername] = useState('leader2');
|
||||
const [password, setPassword] = useState('leader123');
|
||||
const [errors, setErrors] = useState({});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
|
@ -1,12 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { mockGet, mockPost, mockPut, mockDel } from './mockApi';
|
||||
|
||||
const secretKey = import.meta.env.VITE_SECRETKEY;
|
||||
|
||||
// Flag to enable/disable mock API
|
||||
const USE_MOCK_API = true; // Set to false to use real API
|
||||
|
||||
// Create Axios instance with base URL
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
@ -62,30 +58,12 @@ api.interceptors.response.use(
|
||||
|
||||
// Define common HTTP methods
|
||||
const get = async (url, params = {}) => {
|
||||
if (USE_MOCK_API) {
|
||||
try {
|
||||
const response = await mockGet(url, params);
|
||||
return { data: response };
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await api.get(url, { params });
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// Handle POST requests for JSON data
|
||||
const post = async (url, data, isMultipart = false) => {
|
||||
if (USE_MOCK_API) {
|
||||
try {
|
||||
const response = await mockPost(url, data);
|
||||
return { data: response };
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const headers = isMultipart
|
||||
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
||||
: { 'Content-Type': 'application/json' }; // For JSON data
|
||||
@ -96,15 +74,6 @@ const post = async (url, data, isMultipart = false) => {
|
||||
|
||||
// Handle PUT requests
|
||||
const put = async (url, data) => {
|
||||
if (USE_MOCK_API) {
|
||||
try {
|
||||
const response = await mockPut(url, data);
|
||||
return { data: response };
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await api.put(url, data, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
@ -113,25 +82,11 @@ const put = async (url, data) => {
|
||||
|
||||
// Handle DELETE requests
|
||||
const del = async (url) => {
|
||||
if (USE_MOCK_API) {
|
||||
try {
|
||||
const response = await mockDel(url);
|
||||
return { data: response };
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await api.delete(url);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const upload = async (url, data) => {
|
||||
if (USE_MOCK_API) {
|
||||
console.warn('[MOCK API] Upload functionality is not mocked');
|
||||
return { success: true, message: 'Mock upload successful' };
|
||||
}
|
||||
|
||||
const axiosInstance = await axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
|
@ -12,10 +12,14 @@ const mockKnowledgeBases = [
|
||||
create_time: '2023-10-15T08:30:00Z',
|
||||
update_time: '2023-12-20T14:45:00Z',
|
||||
type: 'private',
|
||||
department: '研发部',
|
||||
group: '前端开发组',
|
||||
owner: {
|
||||
id: 'user-001',
|
||||
username: 'johndoe',
|
||||
email: 'john@example.com',
|
||||
department: '研发部',
|
||||
group: '前端开发组',
|
||||
},
|
||||
document_count: 15,
|
||||
tags: ['react', 'javascript', 'frontend'],
|
||||
@ -216,22 +220,89 @@ const paginate = (array, page_size, page) => {
|
||||
};
|
||||
};
|
||||
|
||||
// 导入聊天历史模拟数据和方法
|
||||
import {
|
||||
mockChatHistory,
|
||||
mockGetChatHistory,
|
||||
mockCreateChat,
|
||||
mockUpdateChat,
|
||||
mockDeleteChat,
|
||||
} from '../store/chatHistory/chatHistory.mock';
|
||||
|
||||
// 模拟聊天消息数据
|
||||
const chatMessages = {};
|
||||
|
||||
// Mock API functions
|
||||
export const mockGet = async (url, params = {}) => {
|
||||
console.log(`[MOCK API] GET ${url}`, params);
|
||||
export const mockGet = async (url, config = {}) => {
|
||||
console.log(`[MOCK API] GET ${url}`, config);
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Knowledge bases list with pagination
|
||||
// Get knowledge bases
|
||||
if (url === '/knowledge-bases/') {
|
||||
const { page = 1, page_size = 10 } = params;
|
||||
return paginate(knowledgeBases, page_size, page);
|
||||
return {
|
||||
data: {
|
||||
items: knowledgeBases,
|
||||
total: knowledgeBases.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get knowledge base details
|
||||
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
|
||||
const id = url.split('/')[2];
|
||||
const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
|
||||
}
|
||||
|
||||
return { data: knowledgeBase };
|
||||
}
|
||||
|
||||
// Get chat history
|
||||
if (url === '/chat-history/') {
|
||||
const params = config.params || { page: 1, page_size: 10 };
|
||||
return { data: mockGetChatHistory(params) };
|
||||
}
|
||||
|
||||
// Get chat messages
|
||||
if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) {
|
||||
const chatId = url.split('/')[2];
|
||||
|
||||
// 如果没有该聊天的消息记录,创建一个空数组
|
||||
if (!chatMessages[chatId]) {
|
||||
chatMessages[chatId] = [];
|
||||
|
||||
// 添加一条欢迎消息
|
||||
const chat = mockChatHistory.find((chat) => chat.id === chatId);
|
||||
if (chat) {
|
||||
chatMessages[chatId].push({
|
||||
id: uuidv4(),
|
||||
chat_id: chatId,
|
||||
sender: 'bot',
|
||||
content: `欢迎使用 ${chat.knowledge_base_name},有什么可以帮助您的?`,
|
||||
type: 'text',
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
messages: chatMessages[chatId] || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Knowledge base search
|
||||
if (url === '/knowledge-bases/search/') {
|
||||
const { keyword = '', page = 1, page_size = 10 } = params;
|
||||
const { keyword = '', page = 1, page_size = 10 } = config.params || {};
|
||||
const filtered = knowledgeBases.filter(
|
||||
(kb) =>
|
||||
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
@ -241,18 +312,6 @@ export const mockGet = async (url, params = {}) => {
|
||||
return paginate(filtered, page_size, page);
|
||||
}
|
||||
|
||||
// Knowledge base details
|
||||
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
|
||||
const id = url.split('/')[2];
|
||||
const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
|
||||
|
||||
if (!knowledgeBase) {
|
||||
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
|
||||
}
|
||||
|
||||
return knowledgeBase;
|
||||
}
|
||||
|
||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||
};
|
||||
|
||||
@ -260,7 +319,7 @@ export const mockPost = async (url, data) => {
|
||||
console.log(`[MOCK API] POST ${url}`, data);
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
// Create knowledge base
|
||||
if (url === '/knowledge-bases/') {
|
||||
@ -274,6 +333,8 @@ export const mockPost = async (url, data) => {
|
||||
create_time: new Date().toISOString(),
|
||||
update_time: new Date().toISOString(),
|
||||
type: data.type || 'private',
|
||||
department: data.department || null,
|
||||
group: data.group || null,
|
||||
owner: {
|
||||
id: 'user-001',
|
||||
username: 'johndoe',
|
||||
@ -289,7 +350,75 @@ export const mockPost = async (url, data) => {
|
||||
};
|
||||
|
||||
knowledgeBases.push(newKnowledgeBase);
|
||||
return newKnowledgeBase;
|
||||
|
||||
// 模拟后端返回格式
|
||||
return {
|
||||
code: 200,
|
||||
message: '知识库创建成功',
|
||||
data: {
|
||||
knowledge_base: newKnowledgeBase,
|
||||
external_id: uuidv4(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Create new chat
|
||||
if (url === '/chat-history/') {
|
||||
return { data: mockCreateChat(data) };
|
||||
}
|
||||
|
||||
// Send chat message
|
||||
if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) {
|
||||
const chatId = url.split('/')[2];
|
||||
|
||||
// 如果没有该聊天的消息记录,创建一个空数组
|
||||
if (!chatMessages[chatId]) {
|
||||
chatMessages[chatId] = [];
|
||||
}
|
||||
|
||||
// 创建用户消息
|
||||
const userMessage = {
|
||||
id: uuidv4(),
|
||||
chat_id: chatId,
|
||||
sender: 'user',
|
||||
content: data.content,
|
||||
type: data.type || 'text',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 添加用户消息
|
||||
chatMessages[chatId].push(userMessage);
|
||||
|
||||
// 创建机器人回复
|
||||
const botMessage = {
|
||||
id: uuidv4(),
|
||||
chat_id: chatId,
|
||||
sender: 'bot',
|
||||
content: `这是对您问题的回复:${data.content}`,
|
||||
type: 'text',
|
||||
created_at: new Date(Date.now() + 1000).toISOString(),
|
||||
};
|
||||
|
||||
// 添加机器人回复
|
||||
chatMessages[chatId].push(botMessage);
|
||||
|
||||
// 更新聊天的最后一条消息和时间
|
||||
const chatIndex = mockChatHistory.findIndex((chat) => chat.id === chatId);
|
||||
if (chatIndex !== -1) {
|
||||
mockChatHistory[chatIndex].message_count = (mockChatHistory[chatIndex].message_count || 0) + 2;
|
||||
mockChatHistory[chatIndex].updated_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: '发送成功',
|
||||
data: {
|
||||
user_message: userMessage,
|
||||
bot_message: botMessage,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||
@ -318,7 +447,22 @@ export const mockPut = async (url, data) => {
|
||||
};
|
||||
|
||||
knowledgeBases[index] = updatedKnowledgeBase;
|
||||
return updatedKnowledgeBase;
|
||||
|
||||
// 返回与 mockPost 类似的格式
|
||||
return {
|
||||
code: 200,
|
||||
message: '知识库更新成功',
|
||||
data: {
|
||||
knowledge_base: updatedKnowledgeBase,
|
||||
external_id: knowledgeBases[index].id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Update chat
|
||||
if (url.match(/^\/chat-history\/[^/]+\/$/)) {
|
||||
const id = url.split('/')[2];
|
||||
return { data: mockUpdateChat(id, data) };
|
||||
}
|
||||
|
||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||
@ -343,6 +487,12 @@ export const mockDel = async (url) => {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Delete chat
|
||||
if (url.match(/^\/chat-history\/[^/]+\/$/)) {
|
||||
const id = url.split('/')[2];
|
||||
return { data: mockDeleteChat(id) };
|
||||
}
|
||||
|
||||
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||
};
|
||||
|
||||
|
13
src/store/auth/auth.mock.js
Normal file
13
src/store/auth/auth.mock.js
Normal file
@ -0,0 +1,13 @@
|
||||
// 模拟的当前用户数据
|
||||
export const mockCurrentUser = {
|
||||
id: 'user-001',
|
||||
username: 'johndoe',
|
||||
email: 'john@example.com',
|
||||
name: 'John Doe',
|
||||
department: '研发部',
|
||||
group: '前端开发组',
|
||||
role: 'developer',
|
||||
avatar: 'https://via.placeholder.com/150',
|
||||
created_at: '2023-01-15T08:30:00Z',
|
||||
updated_at: '2023-12-20T14:45:00Z',
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
||||
import { mockCurrentUser } from './auth.mock';
|
||||
|
||||
const setPending = (state) => {
|
||||
state.loading = true;
|
||||
@ -20,7 +21,11 @@ const setRejected = (state, action) => {
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: { loading: false, error: null, user: null },
|
||||
initialState: {
|
||||
loading: false,
|
||||
error: null,
|
||||
user: mockCurrentUser, // 使用模拟的当前用户数据
|
||||
},
|
||||
reducers: {
|
||||
login: (state, action) => {
|
||||
state.user = action.payload;
|
||||
|
@ -54,7 +54,7 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje
|
||||
|
||||
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { user, message } = await get('/auth/verify-token/');
|
||||
const { user, message } = await post('/auth/verify-token/');
|
||||
if (!user) {
|
||||
dispatch(logout());
|
||||
throw new Error(message || 'No token found');
|
||||
|
45
src/store/chat/chat.messages.thunks.js
Normal file
45
src/store/chat/chat.messages.thunks.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post } from '../../services/api';
|
||||
|
||||
/**
|
||||
* 获取聊天消息
|
||||
* @param {string} chatId - 聊天ID
|
||||
*/
|
||||
export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (chatId, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get(`/chat-history/${chatId}/messages/`);
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data.messages;
|
||||
}
|
||||
|
||||
return response.data?.messages || [];
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to fetch messages');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
* @param {Object} params - 消息参数
|
||||
* @param {string} params.chatId - 聊天ID
|
||||
* @param {string} params.content - 消息内容
|
||||
*/
|
||||
export const sendMessage = createAsyncThunk('chat/sendMessage', async ({ chatId, content }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await post(`/chat-history/${chatId}/messages/`, {
|
||||
content,
|
||||
type: 'text',
|
||||
});
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return response.data || {};
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to send message');
|
||||
}
|
||||
});
|
190
src/store/chat/chat.slice.js
Normal file
190
src/store/chat/chat.slice.js
Normal file
@ -0,0 +1,190 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { fetchChats, createChat, deleteChat, updateChat } from './chat.thunks';
|
||||
import { fetchMessages, sendMessage } from './chat.messages.thunks';
|
||||
|
||||
// 初始状态
|
||||
const initialState = {
|
||||
// 聊天列表
|
||||
list: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
|
||||
// 当前聊天
|
||||
currentChat: {
|
||||
data: null,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
|
||||
// 聊天消息
|
||||
messages: {
|
||||
items: [],
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
|
||||
// 发送消息状态
|
||||
sendMessage: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
|
||||
// 操作状态(创建、更新、删除)
|
||||
operations: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
|
||||
// 创建 slice
|
||||
const chatSlice = createSlice({
|
||||
name: 'chat',
|
||||
initialState,
|
||||
reducers: {
|
||||
// 重置操作状态
|
||||
resetOperationStatus: (state) => {
|
||||
state.operations.status = 'idle';
|
||||
state.operations.error = null;
|
||||
},
|
||||
|
||||
// 重置当前聊天
|
||||
resetCurrentChat: (state) => {
|
||||
state.currentChat.data = null;
|
||||
state.currentChat.status = 'idle';
|
||||
state.currentChat.error = null;
|
||||
},
|
||||
|
||||
// 设置当前聊天
|
||||
setCurrentChat: (state, action) => {
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
},
|
||||
|
||||
// 重置消息状态
|
||||
resetMessages: (state) => {
|
||||
state.messages.items = [];
|
||||
state.messages.status = 'idle';
|
||||
state.messages.error = null;
|
||||
},
|
||||
|
||||
// 重置发送消息状态
|
||||
resetSendMessageStatus: (state) => {
|
||||
state.sendMessage.status = 'idle';
|
||||
state.sendMessage.error = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// 获取聊天列表
|
||||
builder
|
||||
.addCase(fetchChats.pending, (state) => {
|
||||
state.list.status = 'loading';
|
||||
})
|
||||
.addCase(fetchChats.fulfilled, (state, action) => {
|
||||
state.list.status = 'succeeded';
|
||||
state.list.items = action.payload.results;
|
||||
state.list.total = action.payload.total;
|
||||
state.list.page = action.payload.page;
|
||||
state.list.page_size = action.payload.page_size;
|
||||
})
|
||||
.addCase(fetchChats.rejected, (state, action) => {
|
||||
state.list.status = 'failed';
|
||||
state.list.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 创建聊天
|
||||
.addCase(createChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(createChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
state.list.items.unshift(action.payload);
|
||||
state.list.total += 1;
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
})
|
||||
.addCase(createChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 删除聊天
|
||||
.addCase(deleteChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(deleteChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
|
||||
state.list.total -= 1;
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
|
||||
state.currentChat.data = null;
|
||||
}
|
||||
})
|
||||
.addCase(deleteChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 更新聊天
|
||||
.addCase(updateChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(updateChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.list.items[index] = action.payload;
|
||||
}
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
|
||||
state.currentChat.data = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(updateChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 获取聊天消息
|
||||
.addCase(fetchMessages.pending, (state) => {
|
||||
state.messages.status = 'loading';
|
||||
})
|
||||
.addCase(fetchMessages.fulfilled, (state, action) => {
|
||||
state.messages.status = 'succeeded';
|
||||
state.messages.items = action.payload;
|
||||
})
|
||||
.addCase(fetchMessages.rejected, (state, action) => {
|
||||
state.messages.status = 'failed';
|
||||
state.messages.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 发送聊天消息
|
||||
.addCase(sendMessage.pending, (state) => {
|
||||
state.sendMessage.status = 'loading';
|
||||
})
|
||||
.addCase(sendMessage.fulfilled, (state, action) => {
|
||||
state.sendMessage.status = 'succeeded';
|
||||
// 添加用户消息和机器人回复
|
||||
if (action.payload.user_message) {
|
||||
state.messages.items.push(action.payload.user_message);
|
||||
}
|
||||
if (action.payload.bot_message) {
|
||||
state.messages.items.push(action.payload.bot_message);
|
||||
}
|
||||
})
|
||||
.addCase(sendMessage.rejected, (state, action) => {
|
||||
state.sendMessage.status = 'failed';
|
||||
state.sendMessage.error = action.payload || action.error.message;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const { resetOperationStatus, resetCurrentChat, setCurrentChat, resetMessages, resetSendMessageStatus } =
|
||||
chatSlice.actions;
|
||||
|
||||
// 导出 reducer
|
||||
export default chatSlice.reducer;
|
87
src/store/chat/chat.thunks.js
Normal file
87
src/store/chat/chat.thunks.js
Normal file
@ -0,0 +1,87 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post, put, del } from '../../services/api';
|
||||
|
||||
/**
|
||||
* 获取聊天列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.page_size - 每页数量
|
||||
*/
|
||||
export const fetchChats = createAsyncThunk(
|
||||
'chat/fetchChats',
|
||||
async (params = { page: 1, page_size: 10 }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get('/chat-history/', { params });
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return response.data || { results: [], total: 0, page: 1, page_size: 10 };
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to fetch chats');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建新聊天
|
||||
* @param {Object} chatData - 聊天数据
|
||||
* @param {string} chatData.knowledge_base_id - 知识库ID
|
||||
* @param {string} chatData.title - 聊天标题
|
||||
*/
|
||||
export const createChat = createAsyncThunk('chat/createChat', async (chatData, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await post('/chat-history/', chatData);
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data.chat;
|
||||
}
|
||||
|
||||
return response.data?.chat || {};
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to create chat');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新聊天
|
||||
* @param {Object} params - 更新参数
|
||||
* @param {string} params.id - 聊天ID
|
||||
* @param {Object} params.data - 更新数据
|
||||
*/
|
||||
export const updateChat = createAsyncThunk('chat/updateChat', async ({ id, data }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await put(`/chat-history/${id}/`, data);
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return response.data.chat;
|
||||
}
|
||||
|
||||
return response.data?.chat || {};
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to update chat');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除聊天
|
||||
* @param {string} id - 聊天ID
|
||||
*/
|
||||
export const deleteChat = createAsyncThunk('chat/deleteChat', async (id, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await del(`/chat-history/${id}/`);
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return id;
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to delete chat');
|
||||
}
|
||||
});
|
153
src/store/chatHistory/chatHistory.mock.js
Normal file
153
src/store/chatHistory/chatHistory.mock.js
Normal file
@ -0,0 +1,153 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// 模拟聊天历史数据
|
||||
export const mockChatHistory = [
|
||||
{
|
||||
id: '1',
|
||||
title: '关于产品开发流程的咨询',
|
||||
knowledge_base_id: '1',
|
||||
knowledge_base_name: '产品开发知识库',
|
||||
created_at: '2025-03-10T10:30:00Z',
|
||||
updated_at: '2025-03-10T11:45:00Z',
|
||||
message_count: 8,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '市场分析报告查询',
|
||||
knowledge_base_id: '2',
|
||||
knowledge_base_name: '市场分析知识库',
|
||||
created_at: '2025-03-09T14:20:00Z',
|
||||
updated_at: '2025-03-09T15:10:00Z',
|
||||
message_count: 5,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '技术架构设计讨论',
|
||||
knowledge_base_id: '4',
|
||||
knowledge_base_name: '技术架构知识库',
|
||||
created_at: '2025-03-08T09:15:00Z',
|
||||
updated_at: '2025-03-08T10:30:00Z',
|
||||
message_count: 12,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: '用户反馈分析',
|
||||
knowledge_base_id: '5',
|
||||
knowledge_base_name: '用户研究知识库',
|
||||
created_at: '2025-03-07T16:40:00Z',
|
||||
updated_at: '2025-03-07T17:25:00Z',
|
||||
message_count: 6,
|
||||
},
|
||||
];
|
||||
|
||||
// 内存存储,用于模拟数据库操作
|
||||
let chatHistoryStore = [...mockChatHistory];
|
||||
|
||||
/**
|
||||
* 模拟获取聊天历史列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Object} - 分页结果
|
||||
*/
|
||||
export const mockGetChatHistory = (params = { page: 1, page_size: 10 }) => {
|
||||
const { page, page_size } = params;
|
||||
const startIndex = (page - 1) * page_size;
|
||||
const endIndex = startIndex + page_size;
|
||||
const paginatedItems = chatHistoryStore.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
total: chatHistoryStore.length,
|
||||
page,
|
||||
page_size,
|
||||
results: paginatedItems,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 模拟创建新聊天
|
||||
* @param {Object} chatData - 聊天数据
|
||||
* @returns {Object} - 创建结果
|
||||
*/
|
||||
export const mockCreateChat = (chatData) => {
|
||||
const newChat = {
|
||||
id: uuidv4(),
|
||||
title: chatData.title || '新的聊天',
|
||||
knowledge_base_id: chatData.knowledge_base_id,
|
||||
knowledge_base_name: chatData.knowledge_base_name || '未知知识库',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
message_count: 0,
|
||||
};
|
||||
|
||||
chatHistoryStore.unshift(newChat);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '创建成功',
|
||||
data: {
|
||||
chat: newChat,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 模拟更新聊天
|
||||
* @param {string} id - 聊天ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Object} - 更新结果
|
||||
*/
|
||||
export const mockUpdateChat = (id, data) => {
|
||||
const index = chatHistoryStore.findIndex((chat) => chat.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '聊天不存在',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const updatedChat = {
|
||||
...chatHistoryStore[index],
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
chatHistoryStore[index] = updatedChat;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '更新成功',
|
||||
data: {
|
||||
chat: updatedChat,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 模拟删除聊天
|
||||
* @param {string} id - 聊天ID
|
||||
* @returns {Object} - 删除结果
|
||||
*/
|
||||
export const mockDeleteChat = (id) => {
|
||||
const index = chatHistoryStore.findIndex((chat) => chat.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '聊天不存在',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
chatHistoryStore.splice(index, 1);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '删除成功',
|
||||
data: null,
|
||||
};
|
||||
};
|
130
src/store/chatHistory/chatHistory.slice.js
Normal file
130
src/store/chatHistory/chatHistory.slice.js
Normal file
@ -0,0 +1,130 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { fetchChatHistory, createChat, deleteChat, updateChat } from './chatHistory.thunks';
|
||||
|
||||
// 初始状态
|
||||
const initialState = {
|
||||
// 聊天历史列表
|
||||
list: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
|
||||
// 当前聊天
|
||||
currentChat: {
|
||||
data: null,
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
|
||||
// 操作状态(创建、更新、删除)
|
||||
operations: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
|
||||
// 创建 slice
|
||||
const chatHistorySlice = createSlice({
|
||||
name: 'chatHistory',
|
||||
initialState,
|
||||
reducers: {
|
||||
// 重置操作状态
|
||||
resetOperationStatus: (state) => {
|
||||
state.operations.status = 'idle';
|
||||
state.operations.error = null;
|
||||
},
|
||||
|
||||
// 重置当前聊天
|
||||
resetCurrentChat: (state) => {
|
||||
state.currentChat.data = null;
|
||||
state.currentChat.status = 'idle';
|
||||
state.currentChat.error = null;
|
||||
},
|
||||
|
||||
// 设置当前聊天
|
||||
setCurrentChat: (state, action) => {
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// 获取聊天历史
|
||||
builder
|
||||
.addCase(fetchChatHistory.pending, (state) => {
|
||||
state.list.status = 'loading';
|
||||
})
|
||||
.addCase(fetchChatHistory.fulfilled, (state, action) => {
|
||||
state.list.status = 'succeeded';
|
||||
state.list.items = action.payload.results;
|
||||
state.list.total = action.payload.total;
|
||||
state.list.page = action.payload.page;
|
||||
state.list.page_size = action.payload.page_size;
|
||||
})
|
||||
.addCase(fetchChatHistory.rejected, (state, action) => {
|
||||
state.list.status = 'failed';
|
||||
state.list.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 创建聊天
|
||||
.addCase(createChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(createChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
state.list.items.unshift(action.payload);
|
||||
state.list.total += 1;
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
})
|
||||
.addCase(createChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 删除聊天
|
||||
.addCase(deleteChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(deleteChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
|
||||
state.list.total -= 1;
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
|
||||
state.currentChat.data = null;
|
||||
}
|
||||
})
|
||||
.addCase(deleteChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 更新聊天
|
||||
.addCase(updateChat.pending, (state) => {
|
||||
state.operations.status = 'loading';
|
||||
})
|
||||
.addCase(updateChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.list.items[index] = action.payload;
|
||||
}
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
|
||||
state.currentChat.data = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(updateChat.rejected, (state, action) => {
|
||||
state.operations.status = 'failed';
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const { resetOperationStatus, resetCurrentChat, setCurrentChat } = chatHistorySlice.actions;
|
||||
|
||||
// 导出 reducer
|
||||
export default chatHistorySlice.reducer;
|
87
src/store/chatHistory/chatHistory.thunks.js
Normal file
87
src/store/chatHistory/chatHistory.thunks.js
Normal file
@ -0,0 +1,87 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post, put, del } from '../../services/api';
|
||||
|
||||
/**
|
||||
* 获取聊天历史列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.page_size - 每页数量
|
||||
*/
|
||||
export const fetchChatHistory = createAsyncThunk(
|
||||
'chatHistory/fetchChatHistory',
|
||||
async (params = { page: 1, page_size: 10 }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get('/chat-history/', { params });
|
||||
|
||||
// 处理返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to fetch chat history');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建新聊天
|
||||
* @param {Object} chatData - 聊天数据
|
||||
* @param {string} chatData.knowledge_base_id - 知识库ID
|
||||
* @param {string} chatData.title - 聊天标题
|
||||
*/
|
||||
export const createChat = createAsyncThunk('chatHistory/createChat', async (chatData, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await post('/chat-history/', chatData);
|
||||
|
||||
// 处理返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data.chat;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to create chat');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新聊天
|
||||
* @param {Object} params - 更新参数
|
||||
* @param {string} params.id - 聊天ID
|
||||
* @param {Object} params.data - 更新数据
|
||||
*/
|
||||
export const updateChat = createAsyncThunk('chatHistory/updateChat', async ({ id, data }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await put(`/chat-history/${id}/`, data);
|
||||
|
||||
// 处理返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data.chat;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to update chat');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除聊天
|
||||
* @param {string} id - 聊天ID
|
||||
*/
|
||||
export const deleteChat = createAsyncThunk('chatHistory/deleteChat', async (id, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await del(`/chat-history/${id}/`);
|
||||
|
||||
// 处理返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return id;
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to delete chat');
|
||||
}
|
||||
});
|
@ -50,6 +50,12 @@ export const createKnowledgeBase = createAsyncThunk(
|
||||
async (knowledgeBaseData, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await post('/knowledge-bases/', knowledgeBaseData);
|
||||
|
||||
// 处理新的返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data.knowledge_base;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to create knowledge base');
|
||||
@ -83,6 +89,12 @@ export const updateKnowledgeBase = createAsyncThunk(
|
||||
async ({ id, data }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await put(`/knowledge-bases/${id}/`, data);
|
||||
|
||||
// 处理新的返回格式
|
||||
if (response.data && response.data.code === 200) {
|
||||
return response.data.data.knowledge_base;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data || 'Failed to update knowledge base');
|
||||
|
@ -4,11 +4,13 @@ import sessionStorage from 'redux-persist/lib/storage/session';
|
||||
import notificationReducer from './notification.slice.js';
|
||||
import authReducer from './auth/auth.slice.js';
|
||||
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
||||
import chatReducer from './chat/chat.slice.js';
|
||||
|
||||
const rootRducer = combineReducers({
|
||||
auth: authReducer,
|
||||
notification: notificationReducer,
|
||||
knowledgeBase: knowledgeBaseReducer,
|
||||
chat: chatReducer,
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
|
Loading…
Reference in New Issue
Block a user