[dev]connect knowledgebase & chat api

This commit is contained in:
susie-laptop 2025-03-12 21:14:25 -04:00
parent 4915514bde
commit c9236cfff4
30 changed files with 1736 additions and 648 deletions

View 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>
);
}

View 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;

View File

@ -2,6 +2,12 @@ import React from 'react';
/**
* 分页组件
* @param {Object} props
* @param {number} props.currentPage - 当前页码
* @param {number} props.totalPages - 总页数
* @param {number} props.pageSize - 每页显示的条目数
* @param {Function} props.onPageChange - 页码变化的回调函数
* @param {Function} props.onPageSizeChange - 每页条目数变化的回调函数
*/
const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => {
return (
@ -17,7 +23,7 @@ const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSiz
<option value='50'>50/</option>
</select>
</div>
<nav aria-label='知识库分页'>
<nav aria-label='分页导航'>
<ul className='pagination mb-0'>
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<button

View 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;

View File

@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { fetchChats, deleteChat } from '../../store/chat/chat.thunks';
import { showNotification } from '../../store/notification.slice';
import ChatSidebar from './ChatSidebar';
import NewChat from './NewChat';
import ChatWindow from './ChatWindow';
@ -7,22 +10,36 @@ import ChatWindow from './ChatWindow';
export default function Chat() {
const { knowledgeBaseId, chatId } = useParams();
const navigate = useNavigate();
const [chatHistory, setChatHistory] = useState([
{
id: '1',
knowledgeBaseId: '1',
title: 'Chat History 1',
lastMessage: '上次聊天内容的摘要...',
timestamp: '2025-01-20T10:30:00Z',
},
{
id: '2',
knowledgeBaseId: '2',
title: 'Chat History 2',
lastMessage: '上次聊天内容的摘要...',
timestamp: '2025-01-19T14:45:00Z',
},
]);
const dispatch = useDispatch();
// Redux store
const { items: chatHistory, status, error } = useSelector((state) => state.chat.list);
const operationStatus = useSelector((state) => state.chat.operations.status);
const operationError = useSelector((state) => state.chat.operations.error);
//
useEffect(() => {
dispatch(fetchChats());
}, [dispatch]);
//
useEffect(() => {
if (operationStatus === 'succeeded') {
dispatch(
showNotification({
message: '操作成功',
type: 'success',
})
);
} else if (operationStatus === 'failed' && operationError) {
dispatch(
showNotification({
message: `操作失败: ${operationError}`,
type: 'danger',
})
);
}
}, [operationStatus, operationError, dispatch]);
// If we have a knowledgeBaseId but no chatId, create a new chat
useEffect(() => {
@ -34,8 +51,8 @@ export default function Chat() {
}, [knowledgeBaseId, chatId, navigate]);
const handleDeleteChat = (id) => {
// In a real app, you would call an API to delete the chat
setChatHistory((prevHistory) => prevHistory.filter((chat) => chat.id !== id));
// Redux action
dispatch(deleteChat(id));
// If the deleted chat is the current one, navigate to the chat list
if (chatId === id) {
@ -51,7 +68,12 @@ export default function Chat() {
className='col-md-3 col-lg-2 p-0 border-end'
style={{ height: 'calc(100vh - 73px)', overflowY: 'auto' }}
>
<ChatSidebar chatHistory={chatHistory} onDeleteChat={handleDeleteChat} />
<ChatSidebar
chatHistory={chatHistory}
onDeleteChat={handleDeleteChat}
isLoading={status === 'loading'}
hasError={status === 'failed'}
/>
</div>
{/* Main Content */}

View File

@ -2,9 +2,9 @@ import React, { useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import SvgIcon from '../../components/SvgIcon';
export default function ChatSidebar({ chatHistory, onDeleteChat }) {
export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading = false, hasError = false }) {
const navigate = useNavigate();
const { chatId } = useParams();
const { chatId, knowledgeBaseId } = useParams();
const [activeDropdown, setActiveDropdown] = useState(null);
const handleNewChat = () => {
@ -28,10 +28,37 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) {
setActiveDropdown(null);
};
//
const renderLoading = () => (
<div className='p-3 text-center'>
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
<div className='mt-2 text-muted small'>加载聊天记录...</div>
</div>
);
//
const renderError = () => (
<div className='p-3 text-center'>
<div className='text-danger mb-2'>
<SvgIcon className='error' width='24' height='24' />
</div>
<div className='text-muted small'>加载聊天记录失败请重试</div>
</div>
);
//
const renderEmpty = () => (
<div className='p-3 text-center'>
<div className='text-muted small'>暂无聊天记录</div>
</div>
);
return (
<div className='chat-sidebar d-flex flex-column h-100'>
<div className='p-3 pb-0'>
<h5 className='mb-0'>Chats</h5>
<h5 className='mb-0'>聊天记录</h5>
</div>
<div className='p-3'>
@ -40,54 +67,62 @@ export default function ChatSidebar({ chatHistory, onDeleteChat }) {
onClick={handleNewChat}
>
<SvgIcon className='plus' color='#ffffff' />
<span>New Chat</span>
<span>新建聊天</span>
</button>
</div>
<div className='overflow-auto flex-grow-1'>
<ul className='list-group list-group-flush'>
{chatHistory.map((chat) => (
<li
key={chat.id}
className={`list-group-item border-0 position-relative ${
chatId === chat.id ? 'bg-light' : ''
}`}
>
<Link
to={`/chat/${chat.knowledgeBaseId}/${chat.id}`}
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
chatId === chat.id ? 'fw-bold' : ''
{isLoading ? (
renderLoading()
) : hasError ? (
renderError()
) : chatHistory.length === 0 ? (
renderEmpty()
) : (
<ul className='list-group list-group-flush'>
{chatHistory.map((chat) => (
<li
key={chat.id}
className={`list-group-item border-0 position-relative ${
chatId === chat.id ? 'bg-light' : ''
}`}
>
<div className='text-truncate'>{chat.title}</div>
</Link>
<div
className='dropdown-hover-area position-absolute end-0 top-0 bottom-0'
style={{ width: '40px' }}
onMouseEnter={() => handleMouseEnter(chat.id)}
onMouseLeave={handleMouseLeave}
>
<button className='btn btn-sm position-absolute end-0 top-50 translate-middle-y me-2'>
<SvgIcon className='more-dot' width='5' height='16' />
</button>
{activeDropdown === chat.id && (
<div
className='position-absolute end-0 top-100 bg-white shadow rounded p-1 z-3'
style={{ zIndex: 1000, minWidth: '80px' }}
>
<button
className='btn btn-sm text-danger d-flex align-items-center gap-2 w-100'
onClick={(e) => handleDeleteChat(e, chat.id)}
<Link
to={`/chat/${chat.knowledge_base_id}/${chat.id}`}
className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${
chatId === chat.id ? 'fw-bold' : ''
}`}
>
<div className='text-truncate'>{chat.title}</div>
</Link>
<div
className='dropdown-hover-area position-absolute end-0 top-0 bottom-0'
style={{ width: '40px' }}
onMouseEnter={() => handleMouseEnter(chat.id)}
onMouseLeave={handleMouseLeave}
>
<button className='btn btn-sm position-absolute end-0 top-50 translate-middle-y me-2'>
<SvgIcon className='more-dot' width='5' height='16' />
</button>
{activeDropdown === chat.id && (
<div
className='position-absolute end-0 top-100 bg-white shadow rounded p-1 z-3'
style={{ zIndex: 1000, minWidth: '80px' }}
>
<SvgIcon className='trash' />
<span>删除</span>
</button>
</div>
)}
</div>
</li>
))}
</ul>
<button
className='btn btn-sm text-danger d-flex align-items-center gap-2 w-100'
onClick={(e) => handleDeleteChat(e, chat.id)}
>
<SvgIcon className='trash' />
<span>删除</span>
</button>
</div>
)}
</div>
</li>
))}
</ul>
)}
</div>
</div>
);

View File

@ -1,62 +1,52 @@
import React, { useState, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchMessages, sendMessage } from '../../store/chat/chat.messages.thunks';
import { resetMessages, resetSendMessageStatus } from '../../store/chat/chat.slice';
import { showNotification } from '../../store/notification.slice';
import SvgIcon from '../../components/SvgIcon';
export default function ChatWindow({ chatId, knowledgeBaseId }) {
const [messages, setMessages] = useState([]);
const dispatch = useDispatch();
const [inputMessage, setInputMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [knowledgeBase, setKnowledgeBase] = useState(null);
const messagesEndRef = useRef(null);
// Fetch knowledge base details
// Redux store
const {
items: messages,
status: messagesStatus,
error: messagesError,
} = useSelector((state) => state.chat.messages);
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
const knowledgeBase = useSelector((state) =>
state.knowledgeBase.list.items.find((kb) => kb.id === knowledgeBaseId)
);
//
useEffect(() => {
// In a real app, you would fetch the knowledge base details from the API
const mockKnowledgeBases = [
{
id: '1',
title: '产品开发知识库',
description: '产品开发流程及规范说明文档',
},
{
id: '2',
title: '市场分析知识库',
description: '2025年Q1市场分析总结',
},
{
id: '3',
title: '财务知识库',
description: '月度财务分析报告',
},
{
id: '4',
title: '技术架构知识库',
description: '系统架构设计文档',
},
{
id: '5',
title: '用户研究知识库',
description: '用户调研和反馈分析',
},
];
const kb = mockKnowledgeBases.find((kb) => kb.id === knowledgeBaseId);
setKnowledgeBase(kb);
// In a real app, you would fetch the chat messages from the API
// For now, we'll just add a welcome message
if (kb) {
setMessages([
{
id: '1',
sender: 'bot',
content: `欢迎使用 ${kb.title},有什么可以帮助您的?`,
timestamp: new Date().toISOString(),
},
]);
if (chatId) {
dispatch(fetchMessages(chatId));
}
}, [chatId, knowledgeBaseId]);
// Scroll to bottom when messages change
//
return () => {
dispatch(resetMessages());
};
}, [chatId, dispatch]);
//
useEffect(() => {
if (sendStatus === 'failed' && sendError) {
dispatch(
showNotification({
message: `发送失败: ${sendError}`,
type: 'danger',
})
);
dispatch(resetSendMessageStatus());
}
}, [sendStatus, sendError, dispatch]);
//
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
@ -64,68 +54,83 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
const handleSendMessage = (e) => {
e.preventDefault();
if (!inputMessage.trim()) return;
if (!inputMessage.trim() || sendStatus === 'loading') return;
// Add user message
const userMessage = {
id: Date.now().toString(),
sender: 'user',
content: inputMessage,
timestamp: new Date().toISOString(),
};
setMessages((prev) => [...prev, userMessage]);
//
dispatch(sendMessage({ chatId, content: inputMessage }));
setInputMessage('');
setIsLoading(true);
// Simulate bot response after a delay
setTimeout(() => {
const botMessage = {
id: (Date.now() + 1).toString(),
sender: 'bot',
content: `这是来自 ${knowledgeBase?.title} 的回复:${inputMessage}`,
timestamp: new Date().toISOString(),
};
setMessages((prev) => [...prev, botMessage]);
setIsLoading(false);
}, 1000);
};
//
const renderLoading = () => (
<div className='p-5 text-center'>
<div className='spinner-border text-secondary' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
<div className='mt-3 text-muted'>加载聊天记录...</div>
</div>
);
//
const renderError = () => (
<div className='p-5 text-center'>
<div className='text-danger mb-3'>
<SvgIcon className='error' width='48' height='48' />
</div>
<div className='text-muted'>加载聊天记录失败请重试</div>
<button className='btn btn-outline-secondary mt-3' onClick={() => dispatch(fetchMessages(chatId))}>
重新加载
</button>
</div>
);
//
const renderEmpty = () => (
<div className='p-5 text-center'>
<div className='text-muted'>暂无聊天记录发送一条消息开始聊天吧</div>
</div>
);
return (
<div className='chat-window d-flex flex-column h-100'>
{/* Chat header */}
<div className='p-3 border-bottom'>
<h5 className='mb-0'>{knowledgeBase?.title || 'Loading...'}</h5>
<h5 className='mb-0'>{knowledgeBase?.name || '加载中...'}</h5>
<small className='text-muted'>{knowledgeBase?.description}</small>
</div>
{/* Chat messages */}
<div className='flex-grow-1 p-3 overflow-auto' >
<div className='flex-grow-1 p-3 overflow-auto'>
<div className='container'>
{messages.map((message) => (
<div
key={message.id}
className={`d-flex ${
message.sender === 'user' ? 'justify-content-end' : 'justify-content-start'
} mb-3`}
>
<div
className={`p-3 rounded-3 ${
message.sender === 'user' ? 'bg-primary text-white' : 'bg-white border'
}`}
style={{ maxWidth: '75%' }}
>
{message.content}
</div>
</div>
))}
{messagesStatus === 'loading'
? renderLoading()
: messagesStatus === 'failed'
? renderError()
: messages.length === 0
? renderEmpty()
: messages.map((message) => (
<div
key={message.id}
className={`d-flex ${
message.sender === 'user' ? 'justify-content-end' : 'justify-content-start'
} mb-3`}
>
<div
className={`p-3 rounded-3 ${
message.sender === 'user' ? 'bg-primary text-white' : 'bg-white border'
}`}
style={{ maxWidth: '75%' }}
>
{message.content}
</div>
</div>
))}
{isLoading && (
{sendStatus === 'loading' && (
<div className='d-flex justify-content-start mb-3'>
<div className='p-3 rounded-3 bg-white border'>
<div className='spinner-border spinner-border-sm text-secondary' role='status'>
<span className='visually-hidden'>Loading...</span>
<span className='visually-hidden'>加载中...</span>
</div>
</div>
</div>
@ -144,15 +149,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
placeholder='输入你的问题...'
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
disabled={isLoading}
disabled={sendStatus === 'loading'}
/>
<button
type='submit'
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
disabled={isLoading || !inputMessage.trim()}
disabled={sendStatus === 'loading' || !inputMessage.trim()}
>
<SvgIcon className='send' color='#ffffff' />
<span className='ms-1' style={{ minWidth: 'fit-content' }}>发送</span>
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
发送
</span>
</button>
</form>
</div>

View File

@ -1,54 +1,47 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { showNotification } from '../../store/notification.slice';
import { get } from '../../services/api';
import SvgIcon from '../../components/SvgIcon';
export default function NewChat() {
const navigate = useNavigate();
const [knowledgeBases, setKnowledgeBases] = useState([
{
id: '1',
title: '产品开发知识库',
description: '产品开发流程及规范说明文档',
documents: 24,
date: '2025-02-15',
access: 'full',
},
{
id: '2',
title: '市场分析知识库',
description: '2025年Q1市场分析总结',
documents: 12,
date: '2025-02-10',
access: 'read',
},
{
id: '3',
title: '财务知识库',
description: '月度财务分析报告',
documents: 8,
date: '2025-02-01',
access: 'none',
},
{
id: '4',
title: '技术架构知识库',
description: '系统架构设计文档',
documents: 15,
date: '2025-01-20',
access: 'full',
},
{
id: '5',
title: '用户研究知识库',
description: '用户调研和反馈分析',
documents: 18,
date: '2025-01-15',
access: 'read',
},
]);
const dispatch = useDispatch();
const [knowledgeBases, setKnowledgeBases] = useState([]);
const [loading, setLoading] = useState(true);
//
useEffect(() => {
const fetchKnowledgeBases = async () => {
try {
setLoading(true);
const response = await get('/knowledge-bases/');
// can_read
const readableKnowledgeBases = response.data.items.filter(
(kb) => kb.permissions && kb.permissions.can_read === true
);
setKnowledgeBases(readableKnowledgeBases);
} catch (error) {
console.error('获取知识库列表失败:', error);
dispatch(
showNotification({
message: '获取知识库列表失败,请稍后重试',
type: 'danger',
})
);
} finally {
setLoading(false);
}
};
fetchKnowledgeBases();
}, [dispatch]);
const handleSelectKnowledgeBase = (knowledgeBaseId) => {
// In a real app, you would create a new chat and get its ID from the API
//
navigate(`/chat/${knowledgeBaseId}`);
};
@ -58,9 +51,19 @@ export default function NewChat() {
<h2 className='mb-4'>选择知识库开始聊天</h2>
</div>
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
{knowledgeBases.map((kb) =>
kb.access === 'full' || kb.access === 'read' ? (
{loading ? (
<div className='d-flex justify-content-center my-5'>
<div className='spinner-border' role='status'>
<span className='visually-hidden'>加载中...</span>
</div>
</div>
) : knowledgeBases.length === 0 ? (
<div className='text-center my-5'>
<p className='text-muted'>没有可用的知识库请联系管理员获取权限</p>
</div>
) : (
<div className='row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 justify-content-center'>
{knowledgeBases.map((kb) => (
<div key={kb.id} className='col'>
<div
className='card h-100 bg-light border-0 cursor-pointer'
@ -68,14 +71,17 @@ export default function NewChat() {
style={{ cursor: 'pointer' }}
>
<div className='card-body py-4'>
<p className='card-title h5'>{kb.title}</p>
<p className='card-title h5'>{kb.name}</p>
<p className='card-text text-muted'>{kb.description}</p>
<div className='d-flex justify-content-between align-items-center mt-3'>
<small className='text-muted'>文档数: {kb.document_count || 0}</small>
</div>
</div>
</div>
</div>
) : null
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@ -19,6 +19,8 @@ export default function SettingsTab({ knowledgeBase }) {
name: knowledgeBase.name,
desc: knowledgeBase.desc || knowledgeBase.description || '',
type: knowledgeBase.type || 'private', //
department: knowledgeBase.department || '',
group: knowledgeBase.group || '',
});
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
@ -81,6 +83,8 @@ export default function SettingsTab({ knowledgeBase }) {
desc: knowledgeBaseForm.desc,
description: knowledgeBaseForm.desc, // Add description field for compatibility
type: knowledgeBaseForm.type,
department: knowledgeBaseForm.department,
group: knowledgeBaseForm.group,
},
})
)

View File

@ -86,13 +86,13 @@ const FileUploadModal = ({
></textarea>
</div>
</div>
<div className='modal-footer'>
<div className='modal-footer gap-2'>
<button type='button' className='btn btn-secondary' onClick={onClose}>
取消
</button>
<button
type='button'
className='btn btn-primary'
className='btn btn-dark'
onClick={onUpload}
disabled={!newFile.file || isSubmitting}
>

View File

@ -78,6 +78,37 @@ const KnowledgeBaseForm = ({
</label>
</div>
</div>
<div className='mb-3'>
<label htmlFor='department' className='form-label'>
部门
</label>
<input
type='text'
className='form-control bg-light'
id='department'
name='department'
value={formData.department || ''}
readOnly
/>
<small className='text-muted'>部门信息根据知识库创建者自动填写</small>
</div>
<div className='mb-3'>
<label htmlFor='group' className='form-label'>
组别
</label>
<input
type='text'
className='form-control bg-light'
id='group'
name='group'
value={formData.group || ''}
readOnly
/>
<small className='text-muted'>组别信息根据知识库创建者自动填写</small>
</div>
<div className='d-flex justify-content-between'>
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
{isSubmitting ? (

View File

@ -9,11 +9,13 @@ import {
} from '../../store/knowledgeBase/knowledgeBase.thunks';
import { resetSearchState } from '../../store/knowledgeBase/knowledgeBase.slice';
import SvgIcon from '../../components/SvgIcon';
import { requestKnowledgeBaseAccess } from '../../services/permissionService';
import AccessRequestModal from '../../components/AccessRequestModal';
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
import Pagination from '../../components/Pagination';
import SearchBar from '../../components/SearchBar';
//
import SearchBar from './components/SearchBar';
import Pagination from './components/Pagination';
import CreateKnowledgeBaseModal from './components/CreateKnowledgeBaseModal';
import KnowledgeBaseList from './components/KnowledgeBaseList';
export default function KnowledgeBase() {
@ -22,18 +24,21 @@ export default function KnowledgeBase() {
const [showCreateModal, setShowCreateModal] = useState(false);
const [showAccessRequestModal, setShowAccessRequestModal] = useState(false);
const [formErrors, setFormErrors] = useState({});
const [accessRequestErrors, setAccessRequestErrors] = useState({});
const [accessRequestData, setAccessRequestData] = useState({
const [accessRequestKnowledgeBase, setAccessRequestKnowledgeBase] = useState({
id: '',
title: '',
accessType: '只读访问',
duration: '一周',
projectInfo: '',
reason: '',
});
const [isSubmittingRequest, setIsSubmittingRequest] = useState(false);
//
const currentUser = useSelector((state) => state.auth.user);
const [newKnowledgeBase, setNewKnowledgeBase] = useState({
name: '',
desc: '',
type: 'private', //
department: currentUser?.department || '',
group: currentUser?.group || '',
});
// Search state
@ -175,6 +180,11 @@ export default function KnowledgeBase() {
const handleInputChange = (e) => {
const { name, value } = e.target;
//
if (name === 'department' || name === 'group') {
return;
}
setNewKnowledgeBase((prev) => ({
...prev,
[name]: value,
@ -189,22 +199,6 @@ export default function KnowledgeBase() {
}
};
const handleAccessRequestInputChange = (e) => {
const { name, value } = e.target;
setAccessRequestData((prev) => ({
...prev,
[name]: value,
}));
// Clear error when user types
if (accessRequestErrors[name]) {
setAccessRequestErrors((prev) => ({
...prev,
[name]: '',
}));
}
};
const validateCreateForm = () => {
const errors = {};
@ -216,25 +210,14 @@ export default function KnowledgeBase() {
errors.desc = '请输入知识库描述';
}
if (!newKnowledgeBase.type) {
errors.type = '请选择知识库类型';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const validateAccessRequestForm = () => {
const errors = {};
if (!accessRequestData.projectInfo.trim()) {
errors.projectInfo = '请输入项目信息';
}
if (!accessRequestData.reason.trim()) {
errors.reason = '请输入申请原因';
}
setAccessRequestErrors(errors);
return Object.keys(errors).length === 0;
};
const handleCreateKnowledgeBase = () => {
// Validate form
if (!validateCreateForm()) {
@ -247,53 +230,69 @@ export default function KnowledgeBase() {
name: newKnowledgeBase.name,
desc: newKnowledgeBase.desc,
description: newKnowledgeBase.desc,
type: 'private', // Default type
type: newKnowledgeBase.type,
department: newKnowledgeBase.department,
group: newKnowledgeBase.group,
})
);
// Reset form and close modal
setNewKnowledgeBase({ name: '', desc: '' });
setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' });
setFormErrors({});
setShowCreateModal(false);
};
// Handle card click to navigate to knowledge base detail
const handleCardClick = (id) => {
const handleCardClick = (id, permissions) => {
//
if (!permissions || permissions.can_read === false) {
dispatch(
showNotification({
message: '您没有访问此知识库的权限,请先申请权限',
type: 'warning',
})
);
return;
}
//
navigate(`/knowledge-base/${id}/datasets`);
};
const handleRequestAccess = (id, title) => {
setAccessRequestData((prev) => ({
...prev,
setAccessRequestKnowledgeBase({
id,
title,
}));
setAccessRequestErrors({});
});
setShowAccessRequestModal(true);
};
const handleSubmitAccessRequest = () => {
// Validate form
if (!validateAccessRequestForm()) {
return;
const handleSubmitAccessRequest = async (requestData) => {
setIsSubmittingRequest(true);
try {
// 使
await requestKnowledgeBaseAccess(requestData);
dispatch(
showNotification({
message: '权限申请已提交',
type: 'success',
})
);
// Close modal
setShowAccessRequestModal(false);
} catch (error) {
dispatch(
showNotification({
message: `权限申请失败: ${error.response?.data?.message || '请稍后重试'}`,
type: 'danger',
})
);
} finally {
setIsSubmittingRequest(false);
}
// Here you would typically call an API to submit the access request
dispatch(
showNotification({
message: '权限申请已提交',
type: 'success',
})
);
// Reset form and close modal
setAccessRequestData((prev) => ({
...prev,
projectInfo: '',
reason: '',
}));
setAccessRequestErrors({});
setShowAccessRequestModal(false);
};
const handleDelete = (e, id) => {
@ -305,6 +304,17 @@ export default function KnowledgeBase() {
// Calculate total pages
const totalPages = Math.ceil(displayTotal / pagination.page_size);
//
const handleOpenCreateModal = () => {
// 使
setNewKnowledgeBase((prev) => ({
...prev,
department: currentUser?.department || '',
group: currentUser?.group || '',
}));
setShowCreateModal(true);
};
return (
<div className='knowledge-base container my-4'>
<div className='d-flex justify-content-between align-items-center mb-3'>
@ -314,11 +324,9 @@ export default function KnowledgeBase() {
onSearchChange={handleSearchInputChange}
onSearch={handleSearch}
onClearSearch={handleClearSearch}
placeholder='搜索知识库...'
/>
<button
className='btn btn-dark d-flex align-items-center gap-1'
onClick={() => setShowCreateModal(true)}
>
<button className='btn btn-dark d-flex align-items-center gap-1' onClick={handleOpenCreateModal}>
<SvgIcon className={'plus'} />
新建知识库
</button>
@ -371,166 +379,14 @@ export default function KnowledgeBase() {
/>
{/* 申请权限弹窗 */}
{showAccessRequestModal && (
<div
className='modal-backdrop'
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1050,
}}
>
<div
className='modal-content bg-white rounded shadow'
style={{
width: '500px',
maxWidth: '90%',
padding: '20px',
}}
>
<div className='modal-header d-flex justify-content-between align-items-center mb-3'>
<h5 className='modal-title m-0'>申请访问权限</h5>
<button
type='button'
className='btn-close'
onClick={() => setShowAccessRequestModal(false)}
aria-label='Close'
></button>
</div>
<div className='modal-body'>
<div className='mb-3'>
<label className='form-label'>知识库名称</label>
<input type='text' className='form-control' value={accessRequestData.title} readOnly />
</div>
{/* <div className='mb-4'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='key' />
访问级别 <span className='text-danger'>*</span>
</label>
<div className='d-flex gap-2'>
<div
className={`p-3 rounded border bg-warning-subtle ${
accessRequestData.accessType === '只读访问'
? 'border-warning bg-warning-subtle'
: 'border-warning-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
setAccessRequestData((prev) => ({ ...prev, accessType: '只读访问' }))
}
>
<div className='text-center text-warning fw-bold mb-1'>只读访问</div>
<div className='text-center text-muted small'>仅查看数据集内容</div>
</div>
<div
className={`p-3 rounded border bg-success-subtle ${
accessRequestData.accessType === '完全访问'
? 'border-success'
: 'border-success-subtle opacity-50'
}`}
style={{ flex: 1, cursor: 'pointer' }}
onClick={() =>
setAccessRequestData((prev) => ({ ...prev, accessType: '完全访问' }))
}
>
<div className='text-center text-success fw-bold mb-1'>完全访问</div>
<div className='text-center text-muted small'>查看编辑和管理数据</div>
</div>
</div>
</div> */}
<div className='mb-3'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='key' />
访问级别 <span className='text-danger'>*</span>
</label>
<select
className='form-select'
name='accessType'
value={accessRequestData.accessType}
onChange={handleAccessRequestInputChange}
>
<option value='只读访问'>只读访问</option>
<option value='编辑权限'>编辑权限</option>
</select>
</div>
<div className='mb-3'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='calendar' />
访问时长 <span className='text-danger'>*</span>
</label>
<select
className='form-select'
name='duration'
value={accessRequestData.duration}
onChange={handleAccessRequestInputChange}
>
<option value='一周'>一周</option>
<option value='一个月'>一个月</option>
<option value='三个月'>三个月</option>
<option value='六个月'>六个月</option>
<option value='永久'>永久</option>
</select>
</div>
<div className='mb-3'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='clipboard' />
项目信息 <span className='text-danger'>*</span>
</label>
<input
type='text'
className={`form-control ${accessRequestErrors.projectInfo ? 'is-invalid' : ''}`}
id='projectInfo'
name='projectInfo'
value={accessRequestData.projectInfo}
onChange={handleAccessRequestInputChange}
placeholder='请输入项目信息'
/>
{accessRequestErrors.projectInfo && (
<div className='invalid-feedback'>{accessRequestErrors.projectInfo}</div>
)}
</div>
<div className='mb-3'>
<label className='form-label d-flex align-items-center gap-1'>
<SvgIcon className='chat' />
申请原因 <span className='text-danger'>*</span>
</label>
<textarea
className={`form-control ${accessRequestErrors.reason ? 'is-invalid' : ''}`}
id='reason'
name='reason'
rows='3'
value={accessRequestData.reason}
onChange={handleAccessRequestInputChange}
placeholder='请输入申请原因'
></textarea>
{accessRequestErrors.reason && (
<div className='invalid-feedback'>{accessRequestErrors.reason}</div>
)}
</div>
</div>
<div className='modal-footer gap-2'>
<button
type='button'
className='btn btn-secondary'
onClick={() => setShowAccessRequestModal(false)}
>
取消
</button>
<button type='button' className='btn btn-dark' onClick={handleSubmitAccessRequest}>
提交申请
</button>
</div>
</div>
</div>
)}
<AccessRequestModal
show={showAccessRequestModal}
knowledgeBaseId={accessRequestKnowledgeBase.id}
knowledgeBaseTitle={accessRequestKnowledgeBase.title}
onClose={() => setShowAccessRequestModal(false)}
onSubmit={handleSubmitAccessRequest}
isSubmitting={isSubmittingRequest}
/>
</div>
);
}

View File

@ -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;

View File

@ -23,8 +23,9 @@ const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequest
description={item.description || item.desc || ''}
documents={item.document_count || 0}
date={new Date(item.create_time || item.created_at).toLocaleDateString()}
permissions={item.permissions}
access={item.permissions?.can_edit ? 'full' : item.permissions?.can_read ? 'read' : 'none'}
onClick={() => onCardClick(item.id)}
onClick={() => onCardClick(item.id, item.permissions)}
onRequestAccess={onRequestAccess}
onDelete={(e) => onDelete(e, item.id)}
/>

View File

@ -9,6 +9,7 @@ export default function KnowledgeCard({
documents,
date,
access,
permissions,
onClick,
onRequestAccess,
onDelete,
@ -41,17 +42,19 @@ export default function KnowledgeCard({
<div className='knowledge-card card shadow border-0 p-0 col' onClick={onClick}>
<div className='card-body'>
<h5 className='card-title'>{title}</h5>
<div className='hoverdown position-absolute end-0 top-0'>
<button type='button' className='detail-btn btn'>
<SvgIcon className={'more-dot'} />
</button>
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
删除
<SvgIcon className={'trash'} />
</li>
</ul>
</div>
{permissions && permissions.can_delete && (
<div className='hoverdown position-absolute end-0 top-0'>
<button type='button' className='detail-btn btn'>
<SvgIcon className={'more-dot'} />
</button>
<ul className='hoverdown-menu shadow bg-white p-1 rounded'>
<li className='p-1 hoverdown-item px-2' onClick={onDelete}>
删除
<SvgIcon className={'trash'} />
</li>
</ul>
</div>
)}
<p className='card-text text-muted mb-3' style={descriptionStyle} title={description}>
{description}
</p>

View File

@ -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;

View File

@ -6,8 +6,8 @@ import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
export default function Login() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState('member2');
const [password, setPassword] = useState('member123');
const [username, setUsername] = useState('leader2');
const [password, setPassword] = useState('leader123');
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);

View File

@ -1,12 +1,8 @@
import axios from 'axios';
import CryptoJS from 'crypto-js';
import { mockGet, mockPost, mockPut, mockDel } from './mockApi';
const secretKey = import.meta.env.VITE_SECRETKEY;
// Flag to enable/disable mock API
const USE_MOCK_API = true; // Set to false to use real API
// Create Axios instance with base URL
const api = axios.create({
baseURL: '/api',
@ -62,30 +58,12 @@ api.interceptors.response.use(
// Define common HTTP methods
const get = async (url, params = {}) => {
if (USE_MOCK_API) {
try {
const response = await mockGet(url, params);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const res = await api.get(url, { params });
return res.data;
};
// Handle POST requests for JSON data
const post = async (url, data, isMultipart = false) => {
if (USE_MOCK_API) {
try {
const response = await mockPost(url, data);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const headers = isMultipart
? { 'Content-Type': 'multipart/form-data' } // For file uploads
: { 'Content-Type': 'application/json' }; // For JSON data
@ -96,15 +74,6 @@ const post = async (url, data, isMultipart = false) => {
// Handle PUT requests
const put = async (url, data) => {
if (USE_MOCK_API) {
try {
const response = await mockPut(url, data);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const res = await api.put(url, data, {
headers: { 'Content-Type': 'application/json' },
});
@ -113,25 +82,11 @@ const put = async (url, data) => {
// Handle DELETE requests
const del = async (url) => {
if (USE_MOCK_API) {
try {
const response = await mockDel(url);
return { data: response };
} catch (error) {
return Promise.reject(error);
}
}
const res = await api.delete(url);
return res.data;
};
const upload = async (url, data) => {
if (USE_MOCK_API) {
console.warn('[MOCK API] Upload functionality is not mocked');
return { success: true, message: 'Mock upload successful' };
}
const axiosInstance = await axios.create({
baseURL: '/api',
headers: {

View File

@ -12,10 +12,14 @@ const mockKnowledgeBases = [
create_time: '2023-10-15T08:30:00Z',
update_time: '2023-12-20T14:45:00Z',
type: 'private',
department: '研发部',
group: '前端开发组',
owner: {
id: 'user-001',
username: 'johndoe',
email: 'john@example.com',
department: '研发部',
group: '前端开发组',
},
document_count: 15,
tags: ['react', 'javascript', 'frontend'],
@ -216,22 +220,89 @@ const paginate = (array, page_size, page) => {
};
};
// 导入聊天历史模拟数据和方法
import {
mockChatHistory,
mockGetChatHistory,
mockCreateChat,
mockUpdateChat,
mockDeleteChat,
} from '../store/chatHistory/chatHistory.mock';
// 模拟聊天消息数据
const chatMessages = {};
// Mock API functions
export const mockGet = async (url, params = {}) => {
console.log(`[MOCK API] GET ${url}`, params);
export const mockGet = async (url, config = {}) => {
console.log(`[MOCK API] GET ${url}`, config);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));
// Knowledge bases list with pagination
// Get knowledge bases
if (url === '/knowledge-bases/') {
const { page = 1, page_size = 10 } = params;
return paginate(knowledgeBases, page_size, page);
return {
data: {
items: knowledgeBases,
total: knowledgeBases.length,
},
};
}
// Get knowledge base details
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
const id = url.split('/')[2];
const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
if (!knowledgeBase) {
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
}
return { data: knowledgeBase };
}
// Get chat history
if (url === '/chat-history/') {
const params = config.params || { page: 1, page_size: 10 };
return { data: mockGetChatHistory(params) };
}
// Get chat messages
if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) {
const chatId = url.split('/')[2];
// 如果没有该聊天的消息记录,创建一个空数组
if (!chatMessages[chatId]) {
chatMessages[chatId] = [];
// 添加一条欢迎消息
const chat = mockChatHistory.find((chat) => chat.id === chatId);
if (chat) {
chatMessages[chatId].push({
id: uuidv4(),
chat_id: chatId,
sender: 'bot',
content: `欢迎使用 ${chat.knowledge_base_name},有什么可以帮助您的?`,
type: 'text',
created_at: new Date().toISOString(),
});
}
}
return {
data: {
code: 200,
message: '获取成功',
data: {
messages: chatMessages[chatId] || [],
},
},
};
}
// Knowledge base search
if (url === '/knowledge-bases/search/') {
const { keyword = '', page = 1, page_size = 10 } = params;
const { keyword = '', page = 1, page_size = 10 } = config.params || {};
const filtered = knowledgeBases.filter(
(kb) =>
kb.name.toLowerCase().includes(keyword.toLowerCase()) ||
@ -241,18 +312,6 @@ export const mockGet = async (url, params = {}) => {
return paginate(filtered, page_size, page);
}
// Knowledge base details
if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) {
const id = url.split('/')[2];
const knowledgeBase = knowledgeBases.find((kb) => kb.id === id);
if (!knowledgeBase) {
throw { response: { status: 404, data: { message: 'Knowledge base not found' } } };
}
return knowledgeBase;
}
throw { response: { status: 404, data: { message: 'Not found' } } };
};
@ -260,7 +319,7 @@ export const mockPost = async (url, data) => {
console.log(`[MOCK API] POST ${url}`, data);
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 700));
await new Promise((resolve) => setTimeout(resolve, 800));
// Create knowledge base
if (url === '/knowledge-bases/') {
@ -274,6 +333,8 @@ export const mockPost = async (url, data) => {
create_time: new Date().toISOString(),
update_time: new Date().toISOString(),
type: data.type || 'private',
department: data.department || null,
group: data.group || null,
owner: {
id: 'user-001',
username: 'johndoe',
@ -289,7 +350,75 @@ export const mockPost = async (url, data) => {
};
knowledgeBases.push(newKnowledgeBase);
return newKnowledgeBase;
// 模拟后端返回格式
return {
code: 200,
message: '知识库创建成功',
data: {
knowledge_base: newKnowledgeBase,
external_id: uuidv4(),
},
};
}
// Create new chat
if (url === '/chat-history/') {
return { data: mockCreateChat(data) };
}
// Send chat message
if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) {
const chatId = url.split('/')[2];
// 如果没有该聊天的消息记录,创建一个空数组
if (!chatMessages[chatId]) {
chatMessages[chatId] = [];
}
// 创建用户消息
const userMessage = {
id: uuidv4(),
chat_id: chatId,
sender: 'user',
content: data.content,
type: data.type || 'text',
created_at: new Date().toISOString(),
};
// 添加用户消息
chatMessages[chatId].push(userMessage);
// 创建机器人回复
const botMessage = {
id: uuidv4(),
chat_id: chatId,
sender: 'bot',
content: `这是对您问题的回复:${data.content}`,
type: 'text',
created_at: new Date(Date.now() + 1000).toISOString(),
};
// 添加机器人回复
chatMessages[chatId].push(botMessage);
// 更新聊天的最后一条消息和时间
const chatIndex = mockChatHistory.findIndex((chat) => chat.id === chatId);
if (chatIndex !== -1) {
mockChatHistory[chatIndex].message_count = (mockChatHistory[chatIndex].message_count || 0) + 2;
mockChatHistory[chatIndex].updated_at = new Date().toISOString();
}
return {
data: {
code: 200,
message: '发送成功',
data: {
user_message: userMessage,
bot_message: botMessage,
},
},
};
}
throw { response: { status: 404, data: { message: 'Not found' } } };
@ -318,7 +447,22 @@ export const mockPut = async (url, data) => {
};
knowledgeBases[index] = updatedKnowledgeBase;
return updatedKnowledgeBase;
// 返回与 mockPost 类似的格式
return {
code: 200,
message: '知识库更新成功',
data: {
knowledge_base: updatedKnowledgeBase,
external_id: knowledgeBases[index].id,
},
};
}
// Update chat
if (url.match(/^\/chat-history\/[^/]+\/$/)) {
const id = url.split('/')[2];
return { data: mockUpdateChat(id, data) };
}
throw { response: { status: 404, data: { message: 'Not found' } } };
@ -343,6 +487,12 @@ export const mockDel = async (url) => {
return { success: true };
}
// Delete chat
if (url.match(/^\/chat-history\/[^/]+\/$/)) {
const id = url.split('/')[2];
return { data: mockDeleteChat(id) };
}
throw { response: { status: 404, data: { message: 'Not found' } } };
};

View 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',
};

View File

@ -1,5 +1,6 @@
import { createSlice } from '@reduxjs/toolkit';
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
import { mockCurrentUser } from './auth.mock';
const setPending = (state) => {
state.loading = true;
@ -20,7 +21,11 @@ const setRejected = (state, action) => {
const authSlice = createSlice({
name: 'auth',
initialState: { loading: false, error: null, user: null },
initialState: {
loading: false,
error: null,
user: mockCurrentUser, // 使用模拟的当前用户数据
},
reducers: {
login: (state, action) => {
state.user = action.payload;

View File

@ -54,7 +54,7 @@ export const signupThunk = createAsyncThunk('auth/signup', async (config, { reje
export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => {
try {
const { user, message } = await get('/auth/verify-token/');
const { user, message } = await post('/auth/verify-token/');
if (!user) {
dispatch(logout());
throw new Error(message || 'No token found');

View 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');
}
});

View 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;

View 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');
}
});

View 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,
};
};

View 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;

View 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');
}
});

View File

@ -50,6 +50,12 @@ export const createKnowledgeBase = createAsyncThunk(
async (knowledgeBaseData, { rejectWithValue }) => {
try {
const response = await post('/knowledge-bases/', knowledgeBaseData);
// 处理新的返回格式
if (response.data && response.data.code === 200) {
return response.data.data.knowledge_base;
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to create knowledge base');
@ -83,6 +89,12 @@ export const updateKnowledgeBase = createAsyncThunk(
async ({ id, data }, { rejectWithValue }) => {
try {
const response = await put(`/knowledge-bases/${id}/`, data);
// 处理新的返回格式
if (response.data && response.data.code === 200) {
return response.data.data.knowledge_base;
}
return response.data;
} catch (error) {
return rejectWithValue(error.response?.data || 'Failed to update knowledge base');

View File

@ -4,11 +4,13 @@ import sessionStorage from 'redux-persist/lib/storage/session';
import notificationReducer from './notification.slice.js';
import authReducer from './auth/auth.slice.js';
import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
import chatReducer from './chat/chat.slice.js';
const rootRducer = combineReducers({
auth: authReducer,
notification: notificationReducer,
knowledgeBase: knowledgeBaseReducer,
chat: chatReducer,
});
const persistConfig = {