mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 09:28:16 +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 }) => {
|
const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => {
|
||||||
return (
|
return (
|
||||||
@ -17,7 +23,7 @@ const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSiz
|
|||||||
<option value='50'>50条/页</option>
|
<option value='50'>50条/页</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<nav aria-label='知识库分页'>
|
<nav aria-label='分页导航'>
|
||||||
<ul className='pagination mb-0'>
|
<ul className='pagination mb-0'>
|
||||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||||
<button
|
<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 React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
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 ChatSidebar from './ChatSidebar';
|
||||||
import NewChat from './NewChat';
|
import NewChat from './NewChat';
|
||||||
import ChatWindow from './ChatWindow';
|
import ChatWindow from './ChatWindow';
|
||||||
@ -7,22 +10,36 @@ import ChatWindow from './ChatWindow';
|
|||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const { knowledgeBaseId, chatId } = useParams();
|
const { knowledgeBaseId, chatId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [chatHistory, setChatHistory] = useState([
|
const dispatch = useDispatch();
|
||||||
{
|
|
||||||
id: '1',
|
// 从 Redux store 获取聊天记录列表
|
||||||
knowledgeBaseId: '1',
|
const { items: chatHistory, status, error } = useSelector((state) => state.chat.list);
|
||||||
title: 'Chat History 1',
|
const operationStatus = useSelector((state) => state.chat.operations.status);
|
||||||
lastMessage: '上次聊天内容的摘要...',
|
const operationError = useSelector((state) => state.chat.operations.error);
|
||||||
timestamp: '2025-01-20T10:30:00Z',
|
|
||||||
},
|
// 获取聊天记录列表
|
||||||
{
|
useEffect(() => {
|
||||||
id: '2',
|
dispatch(fetchChats());
|
||||||
knowledgeBaseId: '2',
|
}, [dispatch]);
|
||||||
title: 'Chat History 2',
|
|
||||||
lastMessage: '上次聊天内容的摘要...',
|
// 监听操作状态,显示通知
|
||||||
timestamp: '2025-01-19T14:45:00Z',
|
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
|
// If we have a knowledgeBaseId but no chatId, create a new chat
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -34,8 +51,8 @@ export default function Chat() {
|
|||||||
}, [knowledgeBaseId, chatId, navigate]);
|
}, [knowledgeBaseId, chatId, navigate]);
|
||||||
|
|
||||||
const handleDeleteChat = (id) => {
|
const handleDeleteChat = (id) => {
|
||||||
// In a real app, you would call an API to delete the chat
|
// 调用 Redux action 删除聊天
|
||||||
setChatHistory((prevHistory) => prevHistory.filter((chat) => chat.id !== id));
|
dispatch(deleteChat(id));
|
||||||
|
|
||||||
// If the deleted chat is the current one, navigate to the chat list
|
// If the deleted chat is the current one, navigate to the chat list
|
||||||
if (chatId === id) {
|
if (chatId === id) {
|
||||||
@ -51,7 +68,12 @@ export default function Chat() {
|
|||||||
className='col-md-3 col-lg-2 p-0 border-end'
|
className='col-md-3 col-lg-2 p-0 border-end'
|
||||||
style={{ height: 'calc(100vh - 73px)', overflowY: 'auto' }}
|
style={{ height: 'calc(100vh - 73px)', overflowY: 'auto' }}
|
||||||
>
|
>
|
||||||
<ChatSidebar chatHistory={chatHistory} onDeleteChat={handleDeleteChat} />
|
<ChatSidebar
|
||||||
|
chatHistory={chatHistory}
|
||||||
|
onDeleteChat={handleDeleteChat}
|
||||||
|
isLoading={status === 'loading'}
|
||||||
|
hasError={status === 'failed'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
|
@ -2,9 +2,9 @@ import React, { useState } from 'react';
|
|||||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
import SvgIcon from '../../components/SvgIcon';
|
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 navigate = useNavigate();
|
||||||
const { chatId } = useParams();
|
const { chatId, knowledgeBaseId } = useParams();
|
||||||
const [activeDropdown, setActiveDropdown] = useState(null);
|
const [activeDropdown, setActiveDropdown] = useState(null);
|
||||||
|
|
||||||
const handleNewChat = () => {
|
const handleNewChat = () => {
|
||||||
@ -28,10 +28,37 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) {
|
|||||||
setActiveDropdown(null);
|
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 (
|
return (
|
||||||
<div className='chat-sidebar d-flex flex-column h-100'>
|
<div className='chat-sidebar d-flex flex-column h-100'>
|
||||||
<div className='p-3 pb-0'>
|
<div className='p-3 pb-0'>
|
||||||
<h5 className='mb-0'>Chats</h5>
|
<h5 className='mb-0'>聊天记录</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='p-3'>
|
<div className='p-3'>
|
||||||
@ -40,54 +67,62 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) {
|
|||||||
onClick={handleNewChat}
|
onClick={handleNewChat}
|
||||||
>
|
>
|
||||||
<SvgIcon className='plus' color='#ffffff' />
|
<SvgIcon className='plus' color='#ffffff' />
|
||||||
<span>New Chat</span>
|
<span>新建聊天</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-auto flex-grow-1'>
|
<div className='overflow-auto flex-grow-1'>
|
||||||
<ul className='list-group list-group-flush'>
|
{isLoading ? (
|
||||||
{chatHistory.map((chat) => (
|
renderLoading()
|
||||||
<li
|
) : hasError ? (
|
||||||
key={chat.id}
|
renderError()
|
||||||
className={`list-group-item border-0 position-relative ${
|
) : chatHistory.length === 0 ? (
|
||||||
chatId === chat.id ? 'bg-light' : ''
|
renderEmpty()
|
||||||
}`}
|
) : (
|
||||||
>
|
<ul className='list-group list-group-flush'>
|
||||||
<Link
|
{chatHistory.map((chat) => (
|
||||||
to={`/chat/${chat.knowledgeBaseId}/${chat.id}`}
|
<li
|
||||||
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
|
key={chat.id}
|
||||||
chatId === chat.id ? 'fw-bold' : ''
|
className={`list-group-item border-0 position-relative ${
|
||||||
|
chatId === chat.id ? 'bg-light' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className='text-truncate'>{chat.title}</div>
|
<Link
|
||||||
</Link>
|
to={`/chat/${chat.knowledge_base_id}/${chat.id}`}
|
||||||
<div
|
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
|
||||||
className='dropdown-hover-area position-absolute end-0 top-0 bottom-0'
|
chatId === chat.id ? 'fw-bold' : ''
|
||||||
style={{ width: '40px' }}
|
}`}
|
||||||
onMouseEnter={() => handleMouseEnter(chat.id)}
|
>
|
||||||
onMouseLeave={handleMouseLeave}
|
<div className='text-truncate'>{chat.title}</div>
|
||||||
>
|
</Link>
|
||||||
<button className='btn btn-sm position-absolute end-0 top-50 translate-middle-y me-2'>
|
<div
|
||||||
<SvgIcon className='more-dot' width='5' height='16' />
|
className='dropdown-hover-area position-absolute end-0 top-0 bottom-0'
|
||||||
</button>
|
style={{ width: '40px' }}
|
||||||
{activeDropdown === chat.id && (
|
onMouseEnter={() => handleMouseEnter(chat.id)}
|
||||||
<div
|
onMouseLeave={handleMouseLeave}
|
||||||
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 position-absolute end-0 top-50 translate-middle-y me-2'>
|
||||||
>
|
<SvgIcon className='more-dot' width='5' height='16' />
|
||||||
<button
|
</button>
|
||||||
className='btn btn-sm text-danger d-flex align-items-center gap-2 w-100'
|
{activeDropdown === chat.id && (
|
||||||
onClick={(e) => handleDeleteChat(e, 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' />
|
<button
|
||||||
<span>删除</span>
|
className='btn btn-sm text-danger d-flex align-items-center gap-2 w-100'
|
||||||
</button>
|
onClick={(e) => handleDeleteChat(e, chat.id)}
|
||||||
</div>
|
>
|
||||||
)}
|
<SvgIcon className='trash' />
|
||||||
</div>
|
<span>删除</span>
|
||||||
</li>
|
</button>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,62 +1,52 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
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';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
|
||||||
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||||
const [messages, setMessages] = useState([]);
|
const dispatch = useDispatch();
|
||||||
const [inputMessage, setInputMessage] = useState('');
|
const [inputMessage, setInputMessage] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [knowledgeBase, setKnowledgeBase] = useState(null);
|
|
||||||
const messagesEndRef = useRef(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(() => {
|
useEffect(() => {
|
||||||
// In a real app, you would fetch the knowledge base details from the API
|
if (chatId) {
|
||||||
const mockKnowledgeBases = [
|
dispatch(fetchMessages(chatId));
|
||||||
{
|
|
||||||
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
|
// 组件卸载时重置消息状态
|
||||||
|
return () => {
|
||||||
|
dispatch(resetMessages());
|
||||||
|
};
|
||||||
|
}, [chatId, dispatch]);
|
||||||
|
|
||||||
|
// 监听发送消息状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (sendStatus === 'failed' && sendError) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `发送失败: ${sendError}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(resetSendMessageStatus());
|
||||||
|
}
|
||||||
|
}, [sendStatus, sendError, dispatch]);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
@ -64,68 +54,83 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
const handleSendMessage = (e) => {
|
const handleSendMessage = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!inputMessage.trim()) return;
|
if (!inputMessage.trim() || sendStatus === 'loading') return;
|
||||||
|
|
||||||
// Add user message
|
// 发送消息
|
||||||
const userMessage = {
|
dispatch(sendMessage({ chatId, content: inputMessage }));
|
||||||
id: Date.now().toString(),
|
|
||||||
sender: 'user',
|
|
||||||
content: inputMessage,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
setMessages((prev) => [...prev, userMessage]);
|
|
||||||
setInputMessage('');
|
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 (
|
return (
|
||||||
<div className='chat-window d-flex flex-column h-100'>
|
<div className='chat-window d-flex flex-column h-100'>
|
||||||
{/* Chat header */}
|
{/* Chat header */}
|
||||||
<div className='p-3 border-bottom'>
|
<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>
|
<small className='text-muted'>{knowledgeBase?.description}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat messages */}
|
{/* Chat messages */}
|
||||||
<div className='flex-grow-1 p-3 overflow-auto' >
|
<div className='flex-grow-1 p-3 overflow-auto'>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
{messages.map((message) => (
|
{messagesStatus === 'loading'
|
||||||
<div
|
? renderLoading()
|
||||||
key={message.id}
|
: messagesStatus === 'failed'
|
||||||
className={`d-flex ${
|
? renderError()
|
||||||
message.sender === 'user' ? 'justify-content-end' : 'justify-content-start'
|
: messages.length === 0
|
||||||
} mb-3`}
|
? renderEmpty()
|
||||||
>
|
: messages.map((message) => (
|
||||||
<div
|
<div
|
||||||
className={`p-3 rounded-3 ${
|
key={message.id}
|
||||||
message.sender === 'user' ? 'bg-primary text-white' : 'bg-white border'
|
className={`d-flex ${
|
||||||
}`}
|
message.sender === 'user' ? 'justify-content-end' : 'justify-content-start'
|
||||||
style={{ maxWidth: '75%' }}
|
} mb-3`}
|
||||||
>
|
>
|
||||||
{message.content}
|
<div
|
||||||
</div>
|
className={`p-3 rounded-3 ${
|
||||||
</div>
|
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='d-flex justify-content-start mb-3'>
|
||||||
<div className='p-3 rounded-3 bg-white border'>
|
<div className='p-3 rounded-3 bg-white border'>
|
||||||
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -144,15 +149,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
placeholder='输入你的问题...'
|
placeholder='输入你的问题...'
|
||||||
value={inputMessage}
|
value={inputMessage}
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={sendStatus === 'loading'}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
|
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' />
|
<SvgIcon className='send' color='#ffffff' />
|
||||||
<span className='ms-1' style={{ minWidth: 'fit-content' }}>发送</span>
|
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
|
||||||
|
发送
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,54 +1,47 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
|
||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [knowledgeBases, setKnowledgeBases] = useState([
|
const dispatch = useDispatch();
|
||||||
{
|
const [knowledgeBases, setKnowledgeBases] = useState([]);
|
||||||
id: '1',
|
const [loading, setLoading] = useState(true);
|
||||||
title: '产品开发知识库',
|
|
||||||
description: '产品开发流程及规范说明文档',
|
// 获取知识库列表
|
||||||
documents: 24,
|
useEffect(() => {
|
||||||
date: '2025-02-15',
|
const fetchKnowledgeBases = async () => {
|
||||||
access: 'full',
|
try {
|
||||||
},
|
setLoading(true);
|
||||||
{
|
const response = await get('/knowledge-bases/');
|
||||||
id: '2',
|
|
||||||
title: '市场分析知识库',
|
// 过滤出有 can_read 权限的知识库
|
||||||
description: '2025年Q1市场分析总结',
|
const readableKnowledgeBases = response.data.items.filter(
|
||||||
documents: 12,
|
(kb) => kb.permissions && kb.permissions.can_read === true
|
||||||
date: '2025-02-10',
|
);
|
||||||
access: 'read',
|
|
||||||
},
|
setKnowledgeBases(readableKnowledgeBases);
|
||||||
{
|
} catch (error) {
|
||||||
id: '3',
|
console.error('获取知识库列表失败:', error);
|
||||||
title: '财务知识库',
|
dispatch(
|
||||||
description: '月度财务分析报告',
|
showNotification({
|
||||||
documents: 8,
|
message: '获取知识库列表失败,请稍后重试',
|
||||||
date: '2025-02-01',
|
type: 'danger',
|
||||||
access: 'none',
|
})
|
||||||
},
|
);
|
||||||
{
|
} finally {
|
||||||
id: '4',
|
setLoading(false);
|
||||||
title: '技术架构知识库',
|
}
|
||||||
description: '系统架构设计文档',
|
};
|
||||||
documents: 15,
|
|
||||||
date: '2025-01-20',
|
fetchKnowledgeBases();
|
||||||
access: 'full',
|
}, [dispatch]);
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
title: '用户研究知识库',
|
|
||||||
description: '用户调研和反馈分析',
|
|
||||||
documents: 18,
|
|
||||||
date: '2025-01-15',
|
|
||||||
access: 'read',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleSelectKnowledgeBase = (knowledgeBaseId) => {
|
const handleSelectKnowledgeBase = (knowledgeBaseId) => {
|
||||||
// In a real app, you would create a new chat and get its ID from the API
|
// 创建新聊天并导航到聊天页面
|
||||||
navigate(`/chat/${knowledgeBaseId}`);
|
navigate(`/chat/${knowledgeBaseId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,9 +51,19 @@ export default function NewChat() {
|
|||||||
<h2 className='mb-4'>选择知识库开始聊天</h2>
|
<h2 className='mb-4'>选择知识库开始聊天</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
|
{loading ? (
|
||||||
{knowledgeBases.map((kb) =>
|
<div className='d-flex justify-content-center my-5'>
|
||||||
kb.access === 'full' || kb.access === 'read' ? (
|
<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 key={kb.id} className='col'>
|
||||||
<div
|
<div
|
||||||
className='card h-100 bg-light border-0 cursor-pointer'
|
className='card h-100 bg-light border-0 cursor-pointer'
|
||||||
@ -68,14 +71,17 @@ export default function NewChat() {
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<div className='card-body py-4'>
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
name: knowledgeBase.name,
|
name: knowledgeBase.name,
|
||||||
desc: knowledgeBase.desc || knowledgeBase.description || '',
|
desc: knowledgeBase.desc || knowledgeBase.description || '',
|
||||||
type: knowledgeBase.type || 'private', // 默认为私有知识库
|
type: knowledgeBase.type || 'private', // 默认为私有知识库
|
||||||
|
department: knowledgeBase.department || '',
|
||||||
|
group: knowledgeBase.group || '',
|
||||||
});
|
});
|
||||||
const [formErrors, setFormErrors] = useState({});
|
const [formErrors, setFormErrors] = useState({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@ -81,6 +83,8 @@ export default function SettingsTab({ knowledgeBase }) {
|
|||||||
desc: knowledgeBaseForm.desc,
|
desc: knowledgeBaseForm.desc,
|
||||||
description: knowledgeBaseForm.desc, // Add description field for compatibility
|
description: knowledgeBaseForm.desc, // Add description field for compatibility
|
||||||
type: knowledgeBaseForm.type,
|
type: knowledgeBaseForm.type,
|
||||||
|
department: knowledgeBaseForm.department,
|
||||||
|
group: knowledgeBaseForm.group,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -86,13 +86,13 @@ const FileUploadModal = ({
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='modal-footer'>
|
<div className='modal-footer gap-2'>
|
||||||
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
<button type='button' className='btn btn-secondary' onClick={onClose}>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='btn btn-primary'
|
className='btn btn-dark'
|
||||||
onClick={onUpload}
|
onClick={onUpload}
|
||||||
disabled={!newFile.file || isSubmitting}
|
disabled={!newFile.file || isSubmitting}
|
||||||
>
|
>
|
||||||
|
@ -78,6 +78,37 @@ const KnowledgeBaseForm = ({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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'>
|
<div className='d-flex justify-content-between'>
|
||||||
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
|
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
|
@ -9,11 +9,13 @@ import {
|
|||||||
} from '../../store/knowledgeBase/knowledgeBase.thunks';
|
} from '../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice';
|
import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice';
|
||||||
import SvgIcon from '../../components/SvgIcon';
|
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';
|
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
||||||
|
|
||||||
export default function KnowledgeBase() {
|
export default function KnowledgeBase() {
|
||||||
@ -22,18 +24,21 @@ export default function KnowledgeBase() {
|
|||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
|
const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
|
||||||
const [formErrors, setFormErrors] = useState({});
|
const [formErrors, setFormErrors] = useState({});
|
||||||
const [accessRequestErrors, setAccessRequestErrors] = useState({});
|
const [accessRequestKnowledgeBase, setAccessRequestKnowledgeBase] = useState({
|
||||||
const [accessRequestData, setAccessRequestData] = useState({
|
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
accessType: '只读访问',
|
|
||||||
duration: '一周',
|
|
||||||
projectInfo: '',
|
|
||||||
reason: '',
|
|
||||||
});
|
});
|
||||||
|
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
const currentUser = useSelector((state) => state.auth.user);
|
||||||
|
|
||||||
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
|
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
desc: '',
|
desc: '',
|
||||||
|
type: 'private', // 默认为私有知识库
|
||||||
|
department: currentUser?.department || '',
|
||||||
|
group: currentUser?.group || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
@ -175,6 +180,11 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
// 不允许修改部门和组别字段
|
||||||
|
if (name === 'department' || name === 'group') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setNewKnowledgeBase((prev) => ({
|
setNewKnowledgeBase((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: value,
|
[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 validateCreateForm = () => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
|
||||||
@ -216,25 +210,14 @@ export default function KnowledgeBase() {
|
|||||||
errors.desc = '请输入知识库描述';
|
errors.desc = '请输入知识库描述';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newKnowledgeBase.type) {
|
||||||
|
errors.type = '请选择知识库类型';
|
||||||
|
}
|
||||||
|
|
||||||
setFormErrors(errors);
|
setFormErrors(errors);
|
||||||
return Object.keys(errors).length === 0;
|
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 = () => {
|
const handleCreateKnowledgeBase = () => {
|
||||||
// Validate form
|
// Validate form
|
||||||
if (!validateCreateForm()) {
|
if (!validateCreateForm()) {
|
||||||
@ -247,53 +230,69 @@ export default function KnowledgeBase() {
|
|||||||
name: newKnowledgeBase.name,
|
name: newKnowledgeBase.name,
|
||||||
desc: newKnowledgeBase.desc,
|
desc: newKnowledgeBase.desc,
|
||||||
description: newKnowledgeBase.desc,
|
description: newKnowledgeBase.desc,
|
||||||
type: 'private', // Default type
|
type: newKnowledgeBase.type,
|
||||||
|
department: newKnowledgeBase.department,
|
||||||
|
group: newKnowledgeBase.group,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset form and close modal
|
// Reset form and close modal
|
||||||
setNewKnowledgeBase({ name: '', desc: '' });
|
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
|
||||||
setFormErrors({});
|
setFormErrors({});
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle card click to navigate to knowledge base detail
|
// 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`);
|
navigate(`/knowledge-base/${id}/datasets`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRequestAccess = (id, title) => {
|
const handleRequestAccess = (id, title) => {
|
||||||
setAccessRequestData((prev) => ({
|
setAccessRequestKnowledgeBase({
|
||||||
...prev,
|
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
}));
|
});
|
||||||
setAccessRequestErrors({});
|
|
||||||
setShowAccessRequestModal(true);
|
setShowAccessRequestModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitAccessRequest = () => {
|
const handleSubmitAccessRequest = async (requestData) => {
|
||||||
// Validate form
|
setIsSubmittingRequest(true);
|
||||||
if (!validateAccessRequestForm()) {
|
|
||||||
return;
|
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) => {
|
const handleDelete = (e, id) => {
|
||||||
@ -305,6 +304,17 @@ export default function KnowledgeBase() {
|
|||||||
// Calculate total pages
|
// Calculate total pages
|
||||||
const totalPages = Math.ceil(displayTotal / pagination.page_size);
|
const totalPages = Math.ceil(displayTotal / pagination.page_size);
|
||||||
|
|
||||||
|
// 打开创建知识库弹窗
|
||||||
|
const handleOpenCreateModal = () => {
|
||||||
|
// 确保部门和组别字段使用当前用户的信息
|
||||||
|
setNewKnowledgeBase((prev) => ({
|
||||||
|
...prev,
|
||||||
|
department: currentUser?.department || '',
|
||||||
|
group: currentUser?.group || '',
|
||||||
|
}));
|
||||||
|
setShowCreateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='knowledge-base container my-4'>
|
<div className='knowledge-base container my-4'>
|
||||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
@ -314,11 +324,9 @@ export default function KnowledgeBase() {
|
|||||||
onSearchChange={handleSearchInputChange}
|
onSearchChange={handleSearchInputChange}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onClearSearch={handleClearSearch}
|
onClearSearch={handleClearSearch}
|
||||||
|
placeholder='搜索知识库...'
|
||||||
/>
|
/>
|
||||||
<button
|
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
|
||||||
className='btn btn-dark d-flex align-items-center gap-1'
|
|
||||||
onClick={() => setShowCreateModal(true)}
|
|
||||||
>
|
|
||||||
<SvgIcon className={'plus'} />
|
<SvgIcon className={'plus'} />
|
||||||
新建知识库
|
新建知识库
|
||||||
</button>
|
</button>
|
||||||
@ -371,166 +379,14 @@ export default function KnowledgeBase() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 申请权限弹窗 */}
|
{/* 申请权限弹窗 */}
|
||||||
{showAccessRequestModal && (
|
<AccessRequestModal
|
||||||
<div
|
show={showAccessRequestModal}
|
||||||
className='modal-backdrop'
|
knowledgeBaseId={accessRequestKnowledgeBase.id}
|
||||||
style={{
|
knowledgeBaseTitle={accessRequestKnowledgeBase.title}
|
||||||
position: 'fixed',
|
onClose={() => setShowAccessRequestModal(false)}
|
||||||
top: 0,
|
onSubmit={handleSubmitAccessRequest}
|
||||||
left: 0,
|
isSubmitting={isSubmittingRequest}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
</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 || ''}
|
description={item.description || item.desc || ''}
|
||||||
documents={item.document_count || 0}
|
documents={item.document_count || 0}
|
||||||
date={new Date(item.create_time || item.created_at).toLocaleDateString()}
|
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'}
|
access={item.permissions?.can_edit ? 'full' : item.permissions?.can_read ? 'read' : 'none'}
|
||||||
onClick={() => onCardClick(item.id)}
|
onClick={() => onCardClick(item.id, item.permissions)}
|
||||||
onRequestAccess={onRequestAccess}
|
onRequestAccess={onRequestAccess}
|
||||||
onDelete={(e) => onDelete(e, item.id)}
|
onDelete={(e) => onDelete(e, item.id)}
|
||||||
/>
|
/>
|
||||||
|
@ -9,6 +9,7 @@ export default function KnowledgeCard({
|
|||||||
documents,
|
documents,
|
||||||
date,
|
date,
|
||||||
access,
|
access,
|
||||||
|
permissions,
|
||||||
onClick,
|
onClick,
|
||||||
onRequestAccess,
|
onRequestAccess,
|
||||||
onDelete,
|
onDelete,
|
||||||
@ -41,17 +42,19 @@ export default function KnowledgeCard({
|
|||||||
<div className='knowledge-card card shadow border-0 p-0 col' onClick={onClick}>
|
<div className='knowledge-card card shadow border-0 p-0 col' onClick={onClick}>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<h5 className='card-title'>{title}</h5>
|
<h5 className='card-title'>{title}</h5>
|
||||||
<div className='hoverdown position-absolute end-0 top-0'>
|
{permissions && permissions.can_delete && (
|
||||||
<button type='button' className='detail-btn btn'>
|
<div className='hoverdown position-absolute end-0 top-0'>
|
||||||
<SvgIcon className={'more-dot'} />
|
<button type='button' className='detail-btn btn'>
|
||||||
</button>
|
<SvgIcon className={'more-dot'} />
|
||||||
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
|
</button>
|
||||||
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
|
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
|
||||||
删除
|
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
|
||||||
<SvgIcon className={'trash'} />
|
删除
|
||||||
</li>
|
<SvgIcon className={'trash'} />
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
|
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</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() {
|
export default function Login() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [username, setUsername] = useState('member2');
|
const [username, setUsername] = useState('leader2');
|
||||||
const [password, setPassword] = useState('member123');
|
const [password, setPassword] = useState('leader123');
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { mockGet, mockPost, mockPut, mockDel } from './mockApi';
|
|
||||||
|
|
||||||
const secretKey = import.meta.env.VITE_SECRETKEY;
|
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
|
// Create Axios instance with base URL
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
@ -62,30 +58,12 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
// Define common HTTP methods
|
// Define common HTTP methods
|
||||||
const get = async (url, params = {}) => {
|
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 });
|
const res = await api.get(url, { params });
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle POST requests for JSON data
|
// Handle POST requests for JSON data
|
||||||
const post = async (url, data, isMultipart = false) => {
|
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
|
const headers = isMultipart
|
||||||
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
||||||
: { 'Content-Type': 'application/json' }; // For JSON data
|
: { 'Content-Type': 'application/json' }; // For JSON data
|
||||||
@ -96,15 +74,6 @@ const post = async (url, data, isMultipart = false) => {
|
|||||||
|
|
||||||
// Handle PUT requests
|
// Handle PUT requests
|
||||||
const put = async (url, data) => {
|
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, {
|
const res = await api.put(url, data, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
@ -113,25 +82,11 @@ const put = async (url, data) => {
|
|||||||
|
|
||||||
// Handle DELETE requests
|
// Handle DELETE requests
|
||||||
const del = async (url) => {
|
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);
|
const res = await api.delete(url);
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = async (url, 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({
|
const axiosInstance = await axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -12,10 +12,14 @@ const mockKnowledgeBases = [
|
|||||||
create_time: '2023-10-15T08:30:00Z',
|
create_time: '2023-10-15T08:30:00Z',
|
||||||
update_time: '2023-12-20T14:45:00Z',
|
update_time: '2023-12-20T14:45:00Z',
|
||||||
type: 'private',
|
type: 'private',
|
||||||
|
department: '研发部',
|
||||||
|
group: '前端开发组',
|
||||||
owner: {
|
owner: {
|
||||||
id: 'user-001',
|
id: 'user-001',
|
||||||
username: 'johndoe',
|
username: 'johndoe',
|
||||||
email: 'john@example.com',
|
email: 'john@example.com',
|
||||||
|
department: '研发部',
|
||||||
|
group: '前端开发组',
|
||||||
},
|
},
|
||||||
document_count: 15,
|
document_count: 15,
|
||||||
tags: ['react', 'javascript', 'frontend'],
|
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
|
// Mock API functions
|
||||||
export const mockGet = async (url, params = {}) => {
|
export const mockGet = async (url, config = {}) => {
|
||||||
console.log(`[MOCK API] GET ${url}`, params);
|
console.log(`[MOCK API] GET ${url}`, config);
|
||||||
|
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
// Knowledge bases list with pagination
|
// Get knowledge bases
|
||||||
if (url === '/knowledge-bases/') {
|
if (url === '/knowledge-bases/') {
|
||||||
const { page = 1, page_size = 10 } = params;
|
return {
|
||||||
return paginate(knowledgeBases, page_size, page);
|
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
|
// Knowledge base search
|
||||||
if (url === '/knowledge-bases/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(
|
const filtered = knowledgeBases.filter(
|
||||||
(kb) =>
|
(kb) =>
|
||||||
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||||
@ -241,18 +312,6 @@ export const mockGet = async (url, params = {}) => {
|
|||||||
return paginate(filtered, page_size, page);
|
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' } } };
|
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);
|
console.log(`[MOCK API] POST ${url}`, data);
|
||||||
|
|
||||||
// Simulate network delay
|
// Simulate network delay
|
||||||
await new Promise((resolve) => setTimeout(resolve, 700));
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
|
||||||
// Create knowledge base
|
// Create knowledge base
|
||||||
if (url === '/knowledge-bases/') {
|
if (url === '/knowledge-bases/') {
|
||||||
@ -274,6 +333,8 @@ export const mockPost = async (url, data) => {
|
|||||||
create_time: new Date().toISOString(),
|
create_time: new Date().toISOString(),
|
||||||
update_time: new Date().toISOString(),
|
update_time: new Date().toISOString(),
|
||||||
type: data.type || 'private',
|
type: data.type || 'private',
|
||||||
|
department: data.department || null,
|
||||||
|
group: data.group || null,
|
||||||
owner: {
|
owner: {
|
||||||
id: 'user-001',
|
id: 'user-001',
|
||||||
username: 'johndoe',
|
username: 'johndoe',
|
||||||
@ -289,7 +350,75 @@ export const mockPost = async (url, data) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
knowledgeBases.push(newKnowledgeBase);
|
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' } } };
|
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||||
@ -318,7 +447,22 @@ export const mockPut = async (url, data) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
knowledgeBases[index] = updatedKnowledgeBase;
|
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' } } };
|
throw { response: { status: 404, data: { message: 'Not found' } } };
|
||||||
@ -343,6 +487,12 @@ export const mockDel = async (url) => {
|
|||||||
return { success: true };
|
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' } } };
|
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 { createSlice } from '@reduxjs/toolkit';
|
||||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
||||||
|
import { mockCurrentUser } from './auth.mock';
|
||||||
|
|
||||||
const setPending = (state) => {
|
const setPending = (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
@ -20,7 +21,11 @@ const setRejected = (state, action) => {
|
|||||||
|
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
initialState: { loading: false, error: null, user: null },
|
initialState: {
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
user: mockCurrentUser, // 使用模拟的当前用户数据
|
||||||
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
login: (state, action) => {
|
login: (state, action) => {
|
||||||
state.user = action.payload;
|
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 }) => {
|
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
|
||||||
try {
|
try {
|
||||||
const { user, message } = await get('/auth/verify-token/');
|
const { user, message } = await post('/auth/verify-token/');
|
||||||
if (!user) {
|
if (!user) {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
throw new Error(message || 'No token found');
|
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 }) => {
|
async (knowledgeBaseData, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await post('/knowledge-bases/', knowledgeBaseData);
|
const response = await post('/knowledge-bases/', knowledgeBaseData);
|
||||||
|
|
||||||
|
// 处理新的返回格式
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data.knowledge_base;
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return rejectWithValue(error.response?.data || 'Failed to create knowledge base');
|
return rejectWithValue(error.response?.data || 'Failed to create knowledge base');
|
||||||
@ -83,6 +89,12 @@ export const updateKnowledgeBase = createAsyncThunk(
|
|||||||
async ({ id, data }, { rejectWithValue }) => {
|
async ({ id, data }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await put(`/knowledge-bases/${id}/`, data);
|
const response = await put(`/knowledge-bases/${id}/`, data);
|
||||||
|
|
||||||
|
// 处理新的返回格式
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data.knowledge_base;
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return rejectWithValue(error.response?.data || 'Failed to update knowledge base');
|
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 notificationReducer from './notification.slice.js';
|
||||||
import authReducer from './auth/auth.slice.js';
|
import authReducer from './auth/auth.slice.js';
|
||||||
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
||||||
|
import chatReducer from './chat/chat.slice.js';
|
||||||
|
|
||||||
const rootRducer = combineReducers({
|
const rootRducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
notification: notificationReducer,
|
notification: notificationReducer,
|
||||||
knowledgeBase: knowledgeBaseReducer,
|
knowledgeBase: knowledgeBaseReducer,
|
||||||
|
chat: chatReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
|
Loading…
Reference in New Issue
Block a user