mirror of
https://github.com/Funkoala14/knowledgebase_law.git
synced 2025-06-08 00:08:15 +08:00
[dev]change pass & upload file & resource
This commit is contained in:
parent
37b2bc1557
commit
50304328ad
151
src/components/ChangePasswordModal.jsx
Normal file
151
src/components/ChangePasswordModal.jsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { changePasswordThunk } from '../store/auth/auth.thunk';
|
||||
|
||||
function ChangePasswordModal({ show, onClose }) {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.old_password) {
|
||||
newErrors.old_password = '请输入当前密码';
|
||||
}
|
||||
|
||||
if (!formData.new_password) {
|
||||
newErrors.new_password = '请输入新密码';
|
||||
} else if (formData.new_password.length < 6) {
|
||||
newErrors.new_password = '新密码长度不能少于6个字符';
|
||||
}
|
||||
|
||||
if (!formData.confirm_password) {
|
||||
newErrors.confirm_password = '请确认新密码';
|
||||
} else if (formData.new_password !== formData.confirm_password) {
|
||||
newErrors.confirm_password = '两次输入的密码不一致';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
|
||||
if (validateForm()) {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await dispatch(
|
||||
changePasswordThunk({
|
||||
old_password: formData.old_password,
|
||||
new_password: formData.new_password,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// 成功后会由thunk中的代码重定向到登录页
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
// 错误处理已经在thunk中完成
|
||||
console.error('Change password failed:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className='modal-dialog modal-dialog-centered'>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-header border-0'>
|
||||
<h5 className='modal-title'>修改密码</h5>
|
||||
<button type='button' className='btn-close' onClick={onClose}></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>当前密码</label>
|
||||
<input
|
||||
value={formData.old_password}
|
||||
name='old_password'
|
||||
type='password'
|
||||
className={`form-control${submitted && errors.old_password ? ' is-invalid' : ''}`}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{submitted && errors.old_password && (
|
||||
<div className='invalid-feedback'>{errors.old_password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>新密码</label>
|
||||
<input
|
||||
value={formData.new_password}
|
||||
name='new_password'
|
||||
type='password'
|
||||
className={`form-control${submitted && errors.new_password ? ' is-invalid' : ''}`}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{submitted && errors.new_password && (
|
||||
<div className='invalid-feedback'>{errors.new_password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-3'>
|
||||
<label className='form-label'>确认新密码</label>
|
||||
<input
|
||||
value={formData.confirm_password}
|
||||
name='confirm_password'
|
||||
type='password'
|
||||
className={`form-control${
|
||||
submitted && errors.confirm_password ? ' is-invalid' : ''
|
||||
}`}
|
||||
required
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{submitted && errors.confirm_password && (
|
||||
<div className='invalid-feedback'>{errors.confirm_password}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='modal-footer border-0'>
|
||||
<button type='button' className='btn btn-outline-primary' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button type='submit' className='btn btn-primary' disabled={isSubmitting}>
|
||||
{isSubmitting ? '提交中...' : '修改密码'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangePasswordModal;
|
69
src/components/ResourceList.jsx
Normal file
69
src/components/ResourceList.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import SvgIcon from './SvgIcon';
|
||||
|
||||
/**
|
||||
* 资源列表组件 - 显示聊天回复中引用的资源
|
||||
* @param {Object} props - 组件属性
|
||||
* @param {Array} props.resources - 资源列表
|
||||
*/
|
||||
const ResourceList = ({ resources = [] }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
if (!resources || resources.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 最多显示3个资源,超过3个时折叠
|
||||
const visibleResources = expanded ? resources : resources.slice(0, 3);
|
||||
const hasMore = resources.length > 3;
|
||||
|
||||
return (
|
||||
<div className='resource-list mt-3 border-top pt-3'>
|
||||
<div className='d-flex align-items-center mb-2'>
|
||||
<span className='text-muted me-2' style={{ fontSize: '0.9rem' }}>
|
||||
资源引用
|
||||
</span>
|
||||
<span className='badge bg-light text-dark'>{resources.length}</span>
|
||||
</div>
|
||||
<div className='resource-items'>
|
||||
{visibleResources.map((resource, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='resource-item p-2 border rounded mb-2 d-flex align-items-center'
|
||||
style={{ background: '#f8f9fa' }}
|
||||
>
|
||||
<div className='resource-icon me-2'>
|
||||
<span className='badge bg-secondary me-1'>{(resource.similarity * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className='resource-content flex-grow-1'>
|
||||
<div className='resource-title fw-medium'>{resource.document_name}</div>
|
||||
<div className='resource-source text-muted small'>{resource.dataset_name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasMore && !expanded && (
|
||||
<button
|
||||
className='btn btn-sm btn-light d-flex align-items-center'
|
||||
onClick={() => setExpanded(true)}
|
||||
>
|
||||
<span className='me-1'>查看更多</span>
|
||||
<SvgIcon className='chevron-down' width='14' height='14' />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{expanded && (
|
||||
<button
|
||||
className='btn btn-sm btn-light d-flex align-items-center'
|
||||
onClick={() => setExpanded(false)}
|
||||
>
|
||||
<span className='me-1'>收起</span>
|
||||
<SvgIcon className='chevron-up' width='14' height='14' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceList;
|
@ -1,36 +1,202 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import '../styles/style.scss';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateProfileThunk } from '../store/auth/auth.thunk';
|
||||
import ChangePasswordModal from './ChangePasswordModal';
|
||||
|
||||
// 部门和组别的映射关系
|
||||
const departmentGroups = {
|
||||
达人部门: ['达人'],
|
||||
商务部门: ['商务'],
|
||||
样本中心: ['样本'],
|
||||
产品部门: ['产品'],
|
||||
AI自媒体: ['AI自媒体'],
|
||||
HR: ['HR'],
|
||||
技术部门: ['技术'],
|
||||
};
|
||||
|
||||
export default function UserSettingsModal({ show, onClose }) {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { user, loading } = useSelector((state) => state.auth);
|
||||
const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app
|
||||
const [formData, setFormData] = useState({});
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
// 可选的组别列表
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setFormData({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
department: user.department,
|
||||
group: user.group,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// 当部门变化时更新可用的组别
|
||||
useEffect(() => {
|
||||
if (formData.department && departmentGroups[formData.department]) {
|
||||
setAvailableGroups(departmentGroups[formData.department]);
|
||||
} else {
|
||||
setAvailableGroups([]);
|
||||
}
|
||||
}, [formData.department]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
if (name === 'department') {
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
['group']: '',
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
}
|
||||
|
||||
// 清除对应的错误信息
|
||||
if (errors[name]) {
|
||||
setErrors({
|
||||
...errors,
|
||||
[name]: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
|
||||
if (validateForm()) {
|
||||
console.log('Form submitted successfully!');
|
||||
console.log('Update data:', formData);
|
||||
try {
|
||||
await dispatch(updateProfileThunk(formData)).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Signup failed:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
if (!formData.email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) {
|
||||
newErrors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (!formData.name) {
|
||||
newErrors.name = 'Name is required';
|
||||
}
|
||||
|
||||
if (!formData.department) {
|
||||
newErrors.department = '请选择部门';
|
||||
}
|
||||
|
||||
if (!formData.group) {
|
||||
newErrors.group = '请选择组别';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='modal show d-block' style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className='modal-dialog modal-dialog-centered'>
|
||||
<div className='modal-content'>
|
||||
<form className='modal-content' onSubmit={handleSubmit}>
|
||||
<div className='modal-header border-0'>
|
||||
<h5 className='modal-title'>管理员个人设置</h5>
|
||||
<h5 className='modal-title'>个人设置</h5>
|
||||
<button type='button' className='btn-close' onClick={onClose}></button>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div className='mb-4'>
|
||||
<h6 className='text-secondary mb-3'>个人信息</h6>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label text-secondary'>姓名</label>
|
||||
<label className='form-label text-secondary'>用户名</label>
|
||||
<input type='text' className='form-control' value={user?.username || ''} readOnly />
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label text-secondary'>姓名</label>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
value={formData?.name || ''}
|
||||
onChange={handleInputChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
{submitted && errors.name && <div className='invalid-feedback'>{errors.name}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<label className='form-label text-secondary'>邮箱</label>
|
||||
<input
|
||||
type='email'
|
||||
className='form-control'
|
||||
value={user?.email || 'admin@ooin.com'}
|
||||
readOnly
|
||||
value={formData?.email || 'admin@ooin.com'}
|
||||
onChange={handleInputChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
{submitted && errors.email && <div className='invalid-feedback'>{errors.email}</div>}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<select
|
||||
className={`form-select form-select-lg${
|
||||
submitted && errors.department ? ' is-invalid' : ''
|
||||
}`}
|
||||
id='department'
|
||||
name='department'
|
||||
value={formData.department}
|
||||
onChange={handleInputChange}
|
||||
disabled={loading}
|
||||
required
|
||||
>
|
||||
<option value='' disabled>
|
||||
选择部门
|
||||
</option>
|
||||
{Object.keys(departmentGroups).map((dept, index) => (
|
||||
<option key={index} value={dept}>
|
||||
{dept}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{submitted && errors.department && (
|
||||
<div className='invalid-feedback'>{errors.department}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<select
|
||||
className={`form-select form-select-lg${
|
||||
submitted && errors.group ? ' is-invalid' : ''
|
||||
}`}
|
||||
id='group'
|
||||
name='group'
|
||||
value={formData.group}
|
||||
onChange={handleInputChange}
|
||||
disabled={loading || !formData.department}
|
||||
required
|
||||
>
|
||||
<option value='' disabled>
|
||||
{formData.department ? '选择组别' : '请先选择部门'}
|
||||
</option>
|
||||
{availableGroups.map((group, index) => (
|
||||
<option key={index} value={group}>
|
||||
{group}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{submitted && errors.group && <div className='invalid-feedback'>{errors.group}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -44,58 +210,31 @@ export default function UserSettingsModal({ show, onClose }) {
|
||||
</div>
|
||||
<small className='text-secondary'>上次修改:{lastPasswordChange}</small>
|
||||
</div>
|
||||
<button className='btn btn-outline-dark btn-sm'>修改</button>
|
||||
</div>
|
||||
<div className='d-flex justify-content-between align-items-center p-3 bg-light rounded mt-3'>
|
||||
<div>
|
||||
<div className='d-flex align-items-center gap-2'>
|
||||
<i className='bi bi-shield-check'></i>
|
||||
<span>双重认证</span>
|
||||
</div>
|
||||
<small className='text-secondary'>增强账户安全性</small>
|
||||
</div>
|
||||
<button className='btn btn-outline-dark btn-sm'>设置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6 className='text-secondary mb-3'>通知设置</h6>
|
||||
<div className='form-check form-switch mb-3 dark-switch'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
id='notificationSwitch1'
|
||||
defaultChecked
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='notificationSwitch1'>
|
||||
访问请求通知
|
||||
</label>
|
||||
<div className='text-secondary small'>新的数据集访问申请通知</div>
|
||||
</div>
|
||||
<div className='form-check form-switch dark-switch'>
|
||||
<input
|
||||
className='form-check-input'
|
||||
type='checkbox'
|
||||
id='notificationSwitch2'
|
||||
defaultChecked
|
||||
/>
|
||||
<label className='form-check-label' htmlFor='notificationSwitch2'>
|
||||
安全提醒
|
||||
</label>
|
||||
<div className='text-secondary small'>异常登录和权限变更提醒</div>
|
||||
<button
|
||||
className='btn btn-outline-primary btn-sm'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowChangePassword(true);
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-footer border-0'>
|
||||
<button type='button' className='btn btn-outline-dark' onClick={onClose}>
|
||||
<button type='button' disabled={loading} className='btn btn-outline-primary-custom' onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
<button type='button' className='btn btn-dark'>
|
||||
<button type='submit' className='btn btn-primary' disabled={loading}>
|
||||
保存更改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{showChangePassword && (
|
||||
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import NotificationCenter from '../components/NotificationCenter';
|
||||
import FullscreenLoading from '../components/FullscreenLoading';
|
||||
import SvgIcon from '../components/SvgIcon';
|
||||
import { fetchNotifications } from '../store/notificationCenter/notificationCenter.thunks';
|
||||
import ChangePasswordModal from '../components/ChangePasswordModal';
|
||||
|
||||
export default function HeaderWithNav() {
|
||||
const dispatch = useDispatch();
|
||||
@ -15,6 +16,7 @@ export default function HeaderWithNav() {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter);
|
||||
|
||||
@ -109,7 +111,7 @@ export default function HeaderWithNav() {
|
||||
className='btn btn-link text-dark p-0'
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
>
|
||||
<SvgIcon className={'bell'} color="#A32B23" />
|
||||
<SvgIcon className={'bell'} color='#A32B23' />
|
||||
{unreadCount > 0 && (
|
||||
<span className='position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger'>
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
@ -145,15 +147,24 @@ export default function HeaderWithNav() {
|
||||
transform: 'translate(0px, 34px)',
|
||||
}}
|
||||
>
|
||||
<li className='d-none'>
|
||||
<li>
|
||||
<Link
|
||||
className='dropdown-item'
|
||||
to='#'
|
||||
// onClick={() => setShowSettings(true)}
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
个人设置
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className='dropdown-item'
|
||||
to='#'
|
||||
onClick={() => setShowChangePassword(true)}
|
||||
>
|
||||
修改密码
|
||||
</Link>
|
||||
</li>
|
||||
<li className='d-none'>
|
||||
<hr className='dropdown-divider' />
|
||||
</li>
|
||||
@ -187,6 +198,7 @@ export default function HeaderWithNav() {
|
||||
</nav>
|
||||
<UserSettingsModal show={showSettings} onClose={() => setShowSettings(false)} />
|
||||
<NotificationCenter show={showNotifications} onClose={() => setShowNotifications(false)} />
|
||||
<ChangePasswordModal show={showChangePassword} onClose={() => setShowChangePassword(false)} />
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ export default function Chat() {
|
||||
items: chatHistory,
|
||||
status,
|
||||
error,
|
||||
} = useSelector((state) => state.chat.history || { items: [], status: 'idle', error: null });
|
||||
} = useSelector((state) => state.chat.list || { items: [], status: 'idle', error: null });
|
||||
const operationStatus = useSelector((state) => state.chat.createSession?.status);
|
||||
const operationError = useSelector((state) => state.chat.createSession?.error);
|
||||
|
||||
|
@ -2,22 +2,7 @@ import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
|
||||
/**
|
||||
* 聊天侧边栏组件
|
||||
* @param {Object} props
|
||||
* @param {Array} props.chatHistory - 聊天历史记录
|
||||
* @param {Function} props.onDeleteChat - 删除聊天的回调
|
||||
* @param {boolean} props.isLoading - 是否正在加载
|
||||
* @param {boolean} props.hasError - 是否有错误
|
||||
* @param {boolean} props.isNewChatView - 是否在选择知识库页面
|
||||
*/
|
||||
export default function ChatSidebar({
|
||||
chatHistory = [],
|
||||
onDeleteChat,
|
||||
isLoading = false,
|
||||
hasError = false,
|
||||
isNewChatView = false
|
||||
}) {
|
||||
export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading = false, hasError = false, isNewChatView = false }) {
|
||||
const navigate = useNavigate();
|
||||
const { chatId, knowledgeBaseId } = useParams();
|
||||
const [activeDropdown, setActiveDropdown] = useState(null);
|
||||
@ -70,9 +55,9 @@ export default function ChatSidebar({
|
||||
|
||||
return (
|
||||
<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'>聊天记录</h5>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className='p-3'>
|
||||
<button
|
||||
@ -110,8 +95,9 @@ export default function ChatSidebar({
|
||||
chatId === chat.conversation_id ? 'fw-bold' : ''
|
||||
}`}
|
||||
>
|
||||
<div className='d-flex flex-column'>
|
||||
<div className='text-truncate fw-medium'>
|
||||
<div className='d-flex flex-column w-100'>
|
||||
{chat.title && <div className='text-truncate mb-1'>{chat.title}</div>}
|
||||
<div className='text-truncate fw-medium small text-secondary'>
|
||||
{chat.datasets?.map((ds) => ds.name).join(', ') || '未命名知识库'}
|
||||
</div>
|
||||
</div>
|
||||
@ -132,7 +118,7 @@ export default function ChatSidebar({
|
||||
style={{ zIndex: 1000, minWidth: '80px' }}
|
||||
>
|
||||
<button
|
||||
className='btn btn-sm text-primary-custom d-flex align-items-center gap-2 w-100'
|
||||
className='btn btn-sm text-primary-custom d-flex align-items-center gap-2 w-100'
|
||||
onClick={(e) => handleDeleteChat(e, chat.conversation_id)}
|
||||
>
|
||||
<SvgIcon className='trash' />
|
||||
|
@ -1,13 +1,11 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchMessages } from '../../store/chat/chat.messages.thunks';
|
||||
import { resetMessages, resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice';
|
||||
import { resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice';
|
||||
import { showNotification } from '../../store/notification.slice';
|
||||
import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
|
||||
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
import SvgIcon from '../../components/SvgIcon';
|
||||
import SafeMarkdown from '../../components/SafeMarkdown';
|
||||
import { get } from '../../services/api';
|
||||
import ResourceList from '../../components/ResourceList';
|
||||
|
||||
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
const dispatch = useDispatch();
|
||||
@ -17,11 +15,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
|
||||
|
||||
// 从 Redux store 获取消息
|
||||
const messages = useSelector((state) => state.chat.messages.items);
|
||||
const messageStatus = useSelector((state) => state.chat.messages.status);
|
||||
const messageError = useSelector((state) => state.chat.messages.error);
|
||||
const chatList = useSelector((state) => state.chat.list.items);
|
||||
const currentChatId = useSelector((state) => state.chat.currentChat.conversationId || chatId);
|
||||
const currentChat = chatList.find((chat) => chat.conversation_id === currentChatId);
|
||||
const messages = currentChat?.messages || [];
|
||||
const messageStatus = useSelector((state) => state.chat.list.messageStatus);
|
||||
const messageError = useSelector((state) => state.chat.list.messageError);
|
||||
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
|
||||
|
||||
// 获取消息资源
|
||||
const resources = useSelector((state) => state.chat.resources);
|
||||
|
||||
// 使用新的Redux状态结构
|
||||
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
|
||||
const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId);
|
||||
@ -43,6 +47,17 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
// 监听知识库ID变更,确保保存在组件状态中
|
||||
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
|
||||
|
||||
// 当chatId改变时设置当前会话ID
|
||||
useEffect(() => {
|
||||
if (chatId) {
|
||||
// 通过设置currentChat.conversationId确保消息显示在正确的会话下
|
||||
dispatch({
|
||||
type: 'chat/setCurrentChat',
|
||||
payload: { conversation_id: chatId },
|
||||
});
|
||||
}
|
||||
}, [chatId, dispatch]);
|
||||
|
||||
// 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds
|
||||
useEffect(() => {
|
||||
// 优先使用conversation中的知识库列表
|
||||
@ -105,7 +120,8 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
|
||||
// 组件卸载时清空消息
|
||||
return () => {
|
||||
dispatch(resetMessages());
|
||||
// Don't reset messages when switching chats
|
||||
// dispatch(resetMessages());
|
||||
// 不要清空hasLoadedDetailRef,否则会导致重复加载
|
||||
// hasLoadedDetailRef.current = {}; // 清理ref缓存
|
||||
};
|
||||
@ -253,19 +269,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染错误状态
|
||||
const renderError = () => (
|
||||
<div className='alert alert-danger'>
|
||||
<p className='mb-0'>
|
||||
<strong>加载消息失败</strong>
|
||||
</p>
|
||||
<p className='mb-0 small'>{messageError}</p>
|
||||
<button className='btn btn-outline-secondary mt-3' onClick={() => dispatch(fetchMessages(chatId))}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染空消息状态
|
||||
const renderEmpty = () => {
|
||||
if (loading) return null;
|
||||
@ -283,16 +286,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
<div className='p-3 border-bottom'>
|
||||
{conversation && conversation.datasets ? (
|
||||
<>
|
||||
<h5 className='mb-0'>{conversation.datasets.map((dataset) => dataset.name).join(', ')}</h5>
|
||||
<h5 className='mb-0'>{conversation.title}</h5>
|
||||
{conversation.datasets.length > 0 && conversation.datasets[0].type && (
|
||||
<small className='text-muted'>类型: {conversation.datasets[0].type}</small>
|
||||
)}
|
||||
</>
|
||||
) : knowledgeBase ? (
|
||||
<>
|
||||
<h5 className='mb-0'>{knowledgeBase.name}</h5>
|
||||
<small className='text-muted'>{knowledgeBase.description}</small>
|
||||
</>
|
||||
) : (
|
||||
<h5 className='mb-0'>{loading || availableDatasetsLoading ? '加载中...' : '聊天'}</h5>
|
||||
)}
|
||||
@ -303,8 +301,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
<div className='container'>
|
||||
{messageStatus === 'loading'
|
||||
? renderLoading()
|
||||
: messageStatus === 'failed'
|
||||
? renderError()
|
||||
: messages.length === 0
|
||||
? renderEmpty()
|
||||
: messages.map((message) => (
|
||||
@ -327,7 +323,10 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
{message.role === 'user' ? (
|
||||
message.content
|
||||
) : (
|
||||
<SafeMarkdown content={message.content} />
|
||||
<SafeMarkdown
|
||||
content={message.content}
|
||||
isStreaming={message.is_streaming}
|
||||
/>
|
||||
)}
|
||||
{message.is_streaming && (
|
||||
<span className='streaming-indicator'>
|
||||
@ -336,6 +335,14 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||
<span className='dot dot3'></span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 只在AI回复消息下方显示资源列表 */}
|
||||
{message.role === 'assistant' &&
|
||||
!message.is_streaming &&
|
||||
resources.messageId === message.id &&
|
||||
resources.items.length > 0 && (
|
||||
<ResourceList resources={resources.items} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='message-time small text-muted mt-1'>
|
||||
|
@ -17,9 +17,8 @@ export default function NewChat() {
|
||||
const error = useSelector((state) => state.chat.availableDatasets.error);
|
||||
|
||||
// 获取聊天历史记录
|
||||
const chatHistory = useSelector((state) => state.chat.history.items || []);
|
||||
const chatHistoryLoading = useSelector((state) => state.chat.history.status === 'loading');
|
||||
const chatCreationStatus = useSelector((state) => state.chat.sendMessage?.status);
|
||||
const chatHistory = useSelector((state) => state.chat.list.items || []);
|
||||
const chatHistoryLoading = useSelector((state) => state.chat.list.status === 'loading');
|
||||
|
||||
// 获取可用知识库列表和聊天历史
|
||||
useEffect(() => {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { uploadDocument, getKnowledgeBaseDocuments } from '../../../../store/knowledgeBase/knowledgeBase.thunks';
|
||||
import { setIsUploading } from '../../../../store/upload/upload.slice';
|
||||
|
||||
/**
|
||||
* 文件上传模态框组件
|
||||
*/
|
||||
const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
const FileUploadModal = ({ show, knowledgeBaseId, onClose, onMinimizeChange, isMinimized: externalIsMinimized }) => {
|
||||
const dispatch = useDispatch();
|
||||
const fileInputRef = useRef(null);
|
||||
const modalRef = useRef(null);
|
||||
@ -13,6 +14,14 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [fileError, setFileError] = useState('');
|
||||
const [uploadResults, setUploadResults] = useState(null);
|
||||
const [isMinimized, setIsMinimized] = useState(externalIsMinimized || false);
|
||||
|
||||
// 同步外部的最小化状态
|
||||
useEffect(() => {
|
||||
if (externalIsMinimized !== undefined && externalIsMinimized !== isMinimized) {
|
||||
setIsMinimized(externalIsMinimized);
|
||||
}
|
||||
}, [externalIsMinimized]);
|
||||
|
||||
// 处理上传区域点击事件
|
||||
const handleUploadAreaClick = () => {
|
||||
@ -67,9 +76,12 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
// Update Redux state
|
||||
dispatch({ type: 'upload/setIsUploading', payload: true });
|
||||
setUploadResults(null);
|
||||
|
||||
try {
|
||||
// Upload the files
|
||||
const result = await dispatch(
|
||||
uploadDocument({
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
@ -77,13 +89,17 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// 成功上传后刷新文档列表
|
||||
dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId }));
|
||||
// Refresh the documents list
|
||||
dispatch(
|
||||
getKnowledgeBaseDocuments({
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
})
|
||||
);
|
||||
|
||||
// 显示上传结果
|
||||
// Update UI with results
|
||||
setUploadResults(result);
|
||||
|
||||
// 如果没有失败的文件,就在3秒后自动关闭模态窗
|
||||
// Close dialog after success
|
||||
if (result.failed_count === 0) {
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
@ -94,6 +110,8 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
setFileError('文件上传失败: ' + (error?.message || '未知错误'));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
// Reset Redux state
|
||||
dispatch({ type: 'upload/setIsUploading', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
@ -101,25 +119,96 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
// 只有在非上传状态才允许关闭
|
||||
if (!isUploading) {
|
||||
resetFileInput();
|
||||
setIsMinimized(false); // 重置最小化状态
|
||||
onClose();
|
||||
} else {
|
||||
// 如果正在上传,提示用户
|
||||
alert('正在上传文件,请等待上传完成后再关闭。');
|
||||
}
|
||||
};
|
||||
|
||||
// 清理函数
|
||||
// 处理最小化/最大化切换
|
||||
const toggleMinimize = () => {
|
||||
const newMinimizedState = !isMinimized;
|
||||
setIsMinimized(newMinimizedState);
|
||||
|
||||
// 通知父组件最小化状态变化
|
||||
if (onMinimizeChange) {
|
||||
onMinimizeChange(newMinimizedState);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理显示状态变化
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 确保在组件卸载时清理所有引用
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
// 当弹窗被显示时,如果之前是最小化状态,则恢复
|
||||
if (show && isMinimized) {
|
||||
setIsMinimized(false);
|
||||
if (onMinimizeChange) {
|
||||
onMinimizeChange(false);
|
||||
}
|
||||
if (modalRef.current) {
|
||||
modalRef.current = null;
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
// 添加离开页面的警告
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e) => {
|
||||
if (isUploading) {
|
||||
// 标准方式阻止页面关闭
|
||||
e.preventDefault();
|
||||
// Chrome需要这个
|
||||
e.returnValue = '正在上传文件,离开页面可能会中断上传。确定要离开吗?';
|
||||
return e.returnValue;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isUploading) {
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [isUploading]);
|
||||
|
||||
// 如果不显示且不是最小化状态,则不渲染任何内容
|
||||
if (!show && !isMinimized) return null;
|
||||
|
||||
// 最小化模式的渲染
|
||||
if (isMinimized) {
|
||||
return (
|
||||
<div
|
||||
className='minimized-upload-modal bg-white rounded shadow d-flex align-items-center'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
padding: '10px 15px',
|
||||
zIndex: 1050,
|
||||
border: '1px solid #ddd',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={toggleMinimize}
|
||||
>
|
||||
<div className='d-flex align-items-center'>
|
||||
{isUploading ? (
|
||||
<span className='spinner-border spinner-border-sm me-2' role='status' aria-hidden='true'></span>
|
||||
) : (
|
||||
<span className='me-2'>📁</span>
|
||||
)}
|
||||
<span>
|
||||
{isUploading
|
||||
? `正在上传文件 (${selectedFiles.length})...`
|
||||
: `文件上传 (${selectedFiles.length})`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 只有在显示状态下渲染完整弹窗
|
||||
if (!show) return null;
|
||||
|
||||
// 完整模式的渲染
|
||||
return (
|
||||
<div
|
||||
ref={modalRef}
|
||||
@ -149,13 +238,48 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
>
|
||||
<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={handleClose}
|
||||
disabled={isUploading}
|
||||
aria-label='Close'
|
||||
></button>
|
||||
<div className='d-flex align-items-center' style={{ gap: '8px' }}>
|
||||
{/* Minimize button */}
|
||||
<button
|
||||
type='button'
|
||||
onClick={toggleMinimize}
|
||||
aria-label='Minimize'
|
||||
className='btn p-0 d-flex align-items-center justify-content-center'
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
opacity: 0.5,
|
||||
}}
|
||||
onMouseOver={(e) => (e.currentTarget.style.opacity = '0.75')}
|
||||
onMouseOut={(e) => (e.currentTarget.style.opacity = '0.5')}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='14'
|
||||
height='14'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M32 416c-17.7 0-32 14.3-32 32s14.3 32 32 32l448 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 416z' />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
type='button'
|
||||
className='btn-close'
|
||||
onClick={handleClose}
|
||||
disabled={isUploading}
|
||||
aria-label='Close'
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
padding: 0,
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='modal-body'>
|
||||
<div
|
||||
@ -262,12 +386,12 @@ const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className='modal-footer gap-2'>
|
||||
<button type='button' className='btn btn-secondary' onClick={handleClose} disabled={isUploading}>
|
||||
<button type='button' className='btn btn-outline-secondary' onClick={handleClose} disabled={isUploading}>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-dark'
|
||||
className='btn btn-outline-primary-custom'
|
||||
onClick={handleUpload}
|
||||
disabled={selectedFiles.length === 0 || isUploading}
|
||||
>
|
||||
|
@ -246,7 +246,7 @@ const KnowledgeBaseForm = ({
|
||||
)}
|
||||
|
||||
<div className='d-flex justify-content-between mt-4'>
|
||||
<button type='submit' className='btn btn-dark' disabled={isSubmitting|| !formData.permissions.can_edit}>
|
||||
<button type='submit' className='btn btn-primary' disabled={isSubmitting|| !formData.permissions.can_edit}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span
|
||||
|
@ -100,7 +100,7 @@ const get = async (url, params = {}) => {
|
||||
console.log(`[MOCK MODE] GET ${url}`);
|
||||
return await mockGet(url, params);
|
||||
}
|
||||
|
||||
|
||||
const res = await api.get(url, { ...params });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@ -159,11 +159,6 @@ const put = async (url, data) => {
|
||||
// Handle DELETE requests with fallback to mock API
|
||||
const del = async (url) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] DELETE ${url}`);
|
||||
return await mockDelete(url);
|
||||
}
|
||||
|
||||
const res = await api.delete(url);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@ -217,16 +212,6 @@ export const switchToRealApi = async () => {
|
||||
// Handle streaming requests
|
||||
const streamRequest = async (url, data, onChunk, onError) => {
|
||||
try {
|
||||
if (isServerDown) {
|
||||
console.log(`[MOCK MODE] STREAM ${url}`);
|
||||
// 模拟流式响应
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"这是模拟的","conversation_id":"mock-1234"}}'), 300);
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"流式","conversation_id":"mock-1234"}}'), 600);
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"响应","conversation_id":"mock-1234"}}'), 900);
|
||||
setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"数据","conversation_id":"mock-1234","is_end":true}}'), 1200);
|
||||
return { success: true, conversation_id: 'mock-1234' };
|
||||
}
|
||||
|
||||
// 获取认证Token
|
||||
const encryptedToken = sessionStorage.getItem('token') || '';
|
||||
let token = '';
|
||||
@ -239,7 +224,6 @@ const streamRequest = async (url, data, onChunk, onError) => {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Token ${token}` : '',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
@ -248,50 +232,104 @@ const streamRequest = async (url, data, onChunk, onError) => {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取响应体的reader
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let conversationId = null;
|
||||
// 检查是否为SSE (Server-Sent Events)格式
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
const isSSE = contentType && contentType.includes('text/event-stream');
|
||||
console.log('响应内容类型:', contentType, '是否SSE:', isSSE);
|
||||
|
||||
// 处理流式数据
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// 处理SSE格式
|
||||
if (isSSE) {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let conversationId = null;
|
||||
|
||||
// 解码并处理数据
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
// 处理流式数据
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// 按行分割并处理JSON
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
|
||||
// 解码并处理数据 - 不使用stream选项以确保完整解码
|
||||
const chunk = decoder.decode(value, { stream: false });
|
||||
buffer += chunk;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
// 检查是否为SSE格式(data: {...})
|
||||
let jsonStr = line;
|
||||
if (line.startsWith('data:')) {
|
||||
// 按行分割并处理JSON
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
try {
|
||||
// 提取data:后面的JSON部分
|
||||
jsonStr = line.substring(5).trim();
|
||||
console.log('检测到SSE格式数据,提取JSON:', jsonStr);
|
||||
let jsonStr = line;
|
||||
if (line.startsWith('data:')) {
|
||||
jsonStr = line.substring(5).trim();
|
||||
console.log('SSE数据块:', jsonStr);
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const parsedData = JSON.parse(jsonStr);
|
||||
if (parsedData.code === 200 && parsedData.data && parsedData.data.conversation_id) {
|
||||
conversationId = parsedData.data.conversation_id;
|
||||
}
|
||||
|
||||
// 立即调用处理函数
|
||||
onChunk(jsonStr);
|
||||
} catch (e) {
|
||||
console.warn('解析JSON失败:', line, e);
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const data = JSON.parse(jsonStr);
|
||||
if (data.code === 200 && data.data && data.data.conversation_id) {
|
||||
conversationId = data.data.conversation_id;
|
||||
}
|
||||
onChunk(jsonStr);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse JSON:', line, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, conversation_id: conversationId };
|
||||
return { success: true, conversation_id: conversationId };
|
||||
}
|
||||
// 处理常规JSON响应
|
||||
else {
|
||||
// 原始响应可能是单个JSON对象而不是流
|
||||
const responseData = await response.json();
|
||||
console.log('接收到非流式响应:', responseData);
|
||||
|
||||
if (responseData.code === 200) {
|
||||
// 模拟分段处理
|
||||
const content = responseData.data?.content || '';
|
||||
const conversationId = responseData.data?.conversation_id;
|
||||
|
||||
// 每100个字符分段处理
|
||||
let offset = 0;
|
||||
const chunkSize = 100;
|
||||
|
||||
while (offset < content.length) {
|
||||
const isLast = offset + chunkSize >= content.length;
|
||||
const chunk = content.substring(offset, offset + chunkSize);
|
||||
|
||||
// 构造类似流式传输的JSON
|
||||
const chunkData = {
|
||||
code: 200,
|
||||
message: 'partial',
|
||||
data: {
|
||||
content: chunk,
|
||||
conversation_id: conversationId,
|
||||
is_end: isLast,
|
||||
},
|
||||
};
|
||||
|
||||
// 调用处理函数
|
||||
onChunk(JSON.stringify(chunkData));
|
||||
|
||||
// 暂停一下让UI有时间更新
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
offset += chunkSize;
|
||||
}
|
||||
|
||||
return { success: true, conversation_id: conversationId };
|
||||
}
|
||||
|
||||
// 如果不是成功响应,直接传递原始数据
|
||||
onChunk(JSON.stringify(responseData));
|
||||
return { success: responseData.code === 200, conversation_id: responseData.data?.conversation_id };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Streaming request failed:', error);
|
||||
if (onError) {
|
||||
@ -318,4 +356,21 @@ export const rejectPermission = (permissionId) => {
|
||||
return post(`/permissions/reject_permission/${permissionId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取聊天回复相关资源列表
|
||||
* @param {Object} data - 请求参数
|
||||
* @param {Array} data.dataset_id_list - 知识库ID列表
|
||||
* @param {string} data.question - 用户问题
|
||||
* @returns {Promise<Object>} API响应
|
||||
*/
|
||||
export const fetchChatResources = async (data) => {
|
||||
try {
|
||||
const response = await post('/chat-history/hit_test/', data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('获取聊天资源失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { get, post, put, del, upload, streamRequest };
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk, updateProfileThunk } from './auth.thunk';
|
||||
|
||||
const setPending = (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
state.user = null;
|
||||
};
|
||||
|
||||
const setFulfilled = (state, action) => {
|
||||
@ -49,6 +48,16 @@ const authSlice = createSlice({
|
||||
.addCase(signupThunk.fulfilled, setFulfilled)
|
||||
.addCase(signupThunk.rejected, setRejected)
|
||||
|
||||
.addCase(updateProfileThunk.pending, setPending)
|
||||
.addCase(updateProfileThunk.fulfilled, (state, action) => {
|
||||
state.user = {
|
||||
...state.user,
|
||||
...action.payload,
|
||||
};
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(updateProfileThunk.rejected, setRejected)
|
||||
|
||||
.addCase(logoutThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post } from '../../services/api';
|
||||
import { get, post, put } from '../../services/api';
|
||||
import { showNotification } from '../notification.slice';
|
||||
import { logout } from './auth.slice';
|
||||
import CryptoJS from 'crypto-js';
|
||||
@ -40,29 +40,29 @@ export const loginThunk = createAsyncThunk(
|
||||
export const signupThunk = createAsyncThunk('auth/signup', async (userData, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
// 使用新的注册 API
|
||||
const response = await post('/auth/register/', userData);
|
||||
console.log('注册返回数据:', response);
|
||||
const { data, message, code } = await post('/auth/register/', userData);
|
||||
console.log('注册返回数据:', data);
|
||||
|
||||
// 处理新的返回格式
|
||||
if (response && response.code === 200) {
|
||||
// // 将 token 加密存储到 sessionStorage
|
||||
// const { token } = response.data;
|
||||
// if (token) {
|
||||
// const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
|
||||
// sessionStorage.setItem('token', encryptedToken);
|
||||
// }
|
||||
if (code !== 200) {
|
||||
throw new Error(message);
|
||||
}
|
||||
// 将 token 加密存储到 sessionStorage
|
||||
const { token } = data;
|
||||
|
||||
// 显示注册成功通知
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '注册成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
return response.data;
|
||||
if (token) {
|
||||
const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString();
|
||||
sessionStorage.setItem('token', encryptedToken);
|
||||
}
|
||||
|
||||
return rejectWithValue(response.message || '注册失败');
|
||||
// 显示注册成功通知
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '注册成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || '注册失败,请稍后重试';
|
||||
dispatch(
|
||||
@ -107,3 +107,73 @@ export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWit
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新个人资料
|
||||
export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async (userData, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { data, message, code } = await put('/users/profile/', userData);
|
||||
|
||||
if (code !== 200) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// 显示更新成功通知
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '个人信息更新成功',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || '更新失败,请稍后重试';
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: errorMessage,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
// 修改密码
|
||||
export const changePasswordThunk = createAsyncThunk(
|
||||
'auth/changePassword',
|
||||
async ({ old_password, new_password }, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { message, code } = await post('/auth/change-password/', {
|
||||
old_password,
|
||||
new_password
|
||||
});
|
||||
|
||||
if (code !== 200) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// 显示密码修改成功通知
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: '密码修改成功,请重新登录',
|
||||
type: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
// 修改成功后清除登录状态
|
||||
sessionStorage.removeItem('token');
|
||||
dispatch(logout());
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || '密码修改失败,请稍后重试';
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: errorMessage,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
@ -1,25 +1,6 @@
|
||||
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?.message || 'Failed to fetch messages');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
* @param {Object} params - 消息参数
|
||||
|
@ -9,28 +9,16 @@ import {
|
||||
fetchConversationDetail,
|
||||
createConversation,
|
||||
} from './chat.thunks';
|
||||
import { fetchMessages, sendMessage } from './chat.messages.thunks';
|
||||
import { sendMessage } from './chat.messages.thunks';
|
||||
|
||||
// 初始状态
|
||||
const initialState = {
|
||||
// Chat history state
|
||||
history: {
|
||||
items: [],
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// Chat session creation state
|
||||
createSession: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
sessionId: null,
|
||||
},
|
||||
// Chat messages state
|
||||
messages: {
|
||||
items: [],
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// Send message state
|
||||
sendMessage: {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
@ -47,20 +35,28 @@ const initialState = {
|
||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
error: null,
|
||||
},
|
||||
// 兼容旧版本的state结构
|
||||
// 聊天列表状态
|
||||
list: {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
messageStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
messageError: null,
|
||||
},
|
||||
// 当前聊天
|
||||
currentChat: {
|
||||
data: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
conversationId: null, // 当前选择的会话ID
|
||||
},
|
||||
// 聊天资源引用
|
||||
resources: {
|
||||
messageId: null,
|
||||
items: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
|
||||
@ -80,19 +76,31 @@ const chatSlice = createSlice({
|
||||
state.currentChat.data = null;
|
||||
state.currentChat.status = 'idle';
|
||||
state.currentChat.error = null;
|
||||
state.currentChat.conversationId = null;
|
||||
},
|
||||
|
||||
// 设置当前聊天
|
||||
setCurrentChat: (state, action) => {
|
||||
state.currentChat.data = action.payload;
|
||||
state.currentChat.status = 'succeeded';
|
||||
if (action.payload && action.payload.conversation_id) {
|
||||
state.currentChat.conversationId = action.payload.conversation_id;
|
||||
}
|
||||
},
|
||||
|
||||
// 重置消息状态
|
||||
resetMessages: (state) => {
|
||||
state.messages.items = [];
|
||||
state.messages.status = 'idle';
|
||||
state.messages.error = null;
|
||||
if (state.currentChat.conversationId) {
|
||||
const chatIndex = state.list.items.findIndex(
|
||||
(chat) => chat.conversation_id === state.currentChat.conversationId
|
||||
);
|
||||
if (chatIndex !== -1) {
|
||||
// 只重置当前会话的消息
|
||||
state.list.items[chatIndex].messages = [];
|
||||
}
|
||||
}
|
||||
state.list.messageStatus = 'idle';
|
||||
state.list.messageError = null;
|
||||
},
|
||||
|
||||
// 重置发送消息状态
|
||||
@ -103,64 +111,138 @@ const chatSlice = createSlice({
|
||||
|
||||
// 添加消息
|
||||
addMessage: (state, action) => {
|
||||
state.messages.items.push(action.payload);
|
||||
const conversationId = state.currentChat.conversationId;
|
||||
if (conversationId) {
|
||||
const chatIndex = state.list.items.findIndex((chat) => chat.conversation_id === conversationId);
|
||||
console.log(chatIndex, 'chatIndex');
|
||||
if (chatIndex !== -1) {
|
||||
// 确保messages数组存在
|
||||
if (!state.list.items[chatIndex].messages) {
|
||||
state.list.items[chatIndex].messages = [];
|
||||
}
|
||||
// 添加消息到对应会话
|
||||
state.list.items[chatIndex].messages.push(action.payload);
|
||||
|
||||
// 更新最后一条消息和消息数量
|
||||
if (action.payload.role === 'user') {
|
||||
state.list.items[chatIndex].last_message = action.payload.content;
|
||||
if (state.list.items[chatIndex].message_count) {
|
||||
state.list.items[chatIndex].message_count += 1;
|
||||
} else {
|
||||
state.list.items[chatIndex].message_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 更新消息(用于流式传输)
|
||||
// 更新消息
|
||||
updateMessage: (state, action) => {
|
||||
const { id, ...updates } = action.payload;
|
||||
const messageIndex = state.messages.items.findIndex((msg) => msg.id === id);
|
||||
const conversationId = state.currentChat.conversationId;
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
// 更新现有消息
|
||||
state.messages.items[messageIndex] = {
|
||||
...state.messages.items[messageIndex],
|
||||
...updates,
|
||||
};
|
||||
if (conversationId) {
|
||||
const chatIndex = state.list.items.findIndex((chat) => chat.conversation_id === conversationId);
|
||||
|
||||
// 如果流式传输结束,更新发送消息状态
|
||||
if (updates.is_streaming === false) {
|
||||
state.sendMessage.status = 'succeeded';
|
||||
if (chatIndex !== -1 && state.list.items[chatIndex].messages) {
|
||||
const messageIndex = state.list.items[chatIndex].messages.findIndex((msg) => msg.id === id);
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
// 更新现有消息
|
||||
state.list.items[chatIndex].messages[messageIndex] = {
|
||||
...state.list.items[chatIndex].messages[messageIndex],
|
||||
...updates,
|
||||
};
|
||||
|
||||
// 如果流式传输结束,更新发送消息状态
|
||||
if (updates.is_streaming === false) {
|
||||
state.sendMessage.status = 'succeeded';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 添加资源
|
||||
setMessageResources: (state, action) => {
|
||||
const { messageId, resources } = action.payload;
|
||||
state.resources = {
|
||||
messageId,
|
||||
items: resources,
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
// 清除资源
|
||||
clearMessageResources: (state) => {
|
||||
state.resources = {
|
||||
messageId: null,
|
||||
items: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
// 设置资源加载状态
|
||||
setResourcesLoading: (state, action) => {
|
||||
state.resources.status = 'loading';
|
||||
state.resources.messageId = action.payload;
|
||||
state.resources.error = null;
|
||||
},
|
||||
|
||||
// 设置资源加载失败
|
||||
setResourcesError: (state, action) => {
|
||||
state.resources.status = 'failed';
|
||||
state.resources.error = action.payload;
|
||||
},
|
||||
|
||||
// 更新特定会话的消息
|
||||
updateChatMessages: (state, action) => {
|
||||
const { conversationId, messages } = action.payload;
|
||||
const chatIndex = state.list.items.findIndex((chat) => chat.conversation_id === conversationId);
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
state.list.items[chatIndex].messages = messages;
|
||||
// 同时更新当前会话ID
|
||||
state.currentChat.conversationId = conversationId;
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// 获取聊天列表
|
||||
builder
|
||||
.addCase(fetchChats.pending, (state) => {
|
||||
state.list.status = 'loading';
|
||||
state.history.status = 'loading';
|
||||
})
|
||||
.addCase(fetchChats.fulfilled, (state, action) => {
|
||||
state.list.status = 'succeeded';
|
||||
console.log(action.payload, '当前list.items:', state.list.items);
|
||||
|
||||
// 检查是否是追加模式
|
||||
if (action.payload.append) {
|
||||
// 追加模式:将新结果添加到现有列表的前面
|
||||
state.list.items = [...action.payload.results, ...state.list.items];
|
||||
state.history.items = [...action.payload.results, ...state.history.items];
|
||||
// 确保每个聊天项都有messages数组
|
||||
const newResults = action.payload.results.map((chat) => ({
|
||||
...chat,
|
||||
messages: chat.messages || [],
|
||||
}));
|
||||
state.list.items = [...newResults, ...state.list.items];
|
||||
} else {
|
||||
// 替换模式:使用新结果替换整个列表
|
||||
state.list.items = action.payload.results;
|
||||
// 确保每个聊天项都有messages数组
|
||||
state.list.items = (action.payload.results || []).map((chat) => ({
|
||||
...chat,
|
||||
messages: chat.messages || [],
|
||||
}));
|
||||
state.list.total = action.payload.total;
|
||||
state.list.page = action.payload.page;
|
||||
state.list.page_size = action.payload.page_size;
|
||||
|
||||
// 同时更新新的状态结构
|
||||
state.history.items = action.payload.results;
|
||||
}
|
||||
|
||||
state.history.status = 'succeeded';
|
||||
state.history.error = null;
|
||||
state.list.error = null;
|
||||
})
|
||||
.addCase(fetchChats.rejected, (state, action) => {
|
||||
state.list.status = 'failed';
|
||||
state.list.error = action.payload || action.error.message;
|
||||
|
||||
// 同时更新新的状态结构
|
||||
state.history.status = 'failed';
|
||||
state.history.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 创建聊天
|
||||
@ -185,16 +267,14 @@ const chatSlice = createSlice({
|
||||
})
|
||||
.addCase(deleteChat.fulfilled, (state, action) => {
|
||||
state.operations.status = 'succeeded';
|
||||
// 更新旧的状态结构
|
||||
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
|
||||
// 更新新的状态结构
|
||||
state.history.items = state.history.items.filter((chat) => chat.conversation_id !== action.payload);
|
||||
// 更新聊天列表
|
||||
state.list.items = state.list.items.filter((chat) => chat.conversation_id !== action.payload);
|
||||
|
||||
if (state.list.total > 0) {
|
||||
state.list.total -= 1;
|
||||
}
|
||||
|
||||
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
|
||||
if (state.currentChat.data && state.currentChat.data.conversation_id === action.payload) {
|
||||
state.currentChat.data = null;
|
||||
}
|
||||
})
|
||||
@ -222,20 +302,6 @@ const chatSlice = createSlice({
|
||||
state.operations.error = action.payload || action.error.message;
|
||||
})
|
||||
|
||||
// 获取聊天消息
|
||||
.addCase(fetchMessages.pending, (state) => {
|
||||
state.messages.status = 'loading';
|
||||
state.messages.error = null;
|
||||
})
|
||||
.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.error.message;
|
||||
})
|
||||
|
||||
// 发送聊天消息
|
||||
.addCase(sendMessage.pending, (state) => {
|
||||
state.sendMessage.status = 'loading';
|
||||
@ -348,6 +414,11 @@ export const {
|
||||
resetSendMessageStatus,
|
||||
addMessage,
|
||||
updateMessage,
|
||||
setMessageResources,
|
||||
clearMessageResources,
|
||||
setResourcesLoading,
|
||||
setResourcesError,
|
||||
updateChatMessages,
|
||||
} = chatSlice.actions;
|
||||
|
||||
// 导出 reducer
|
||||
|
@ -1,29 +1,32 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post, put, del, streamRequest } from '../../services/api';
|
||||
import { get, post, put, del, streamRequest, fetchChatResources } from '../../services/api';
|
||||
import { showNotification } from '../notification.slice';
|
||||
import { addMessage, updateMessage, setCurrentChat } from './chat.slice';
|
||||
import {
|
||||
addMessage,
|
||||
updateMessage,
|
||||
setCurrentChat,
|
||||
setMessageResources,
|
||||
setResourcesLoading,
|
||||
setResourcesError,
|
||||
} from './chat.slice';
|
||||
|
||||
/**
|
||||
* 获取聊天列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.page - 页码
|
||||
* @param {number} params.page_size - 每页数量
|
||||
*/
|
||||
export const fetchChats = createAsyncThunk('chat/fetchChats', async (params = {}, { rejectWithValue }) => {
|
||||
export const fetchChats = createAsyncThunk('chat/fetchChats', async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await get('/chat-history/', { params });
|
||||
const response = await get('/chat-history/');
|
||||
|
||||
// 处理返回格式
|
||||
if (response && response.code === 200) {
|
||||
return {
|
||||
results: response.data.results,
|
||||
total: response.data.total,
|
||||
page: response.data.page || 1,
|
||||
page_size: response.data.page_size || 10,
|
||||
results: response.data,
|
||||
total: response.data.length,
|
||||
};
|
||||
}
|
||||
|
||||
return { results: [], total: 0, page: 1, page_size: 10 };
|
||||
return { results: [], total: 0 };
|
||||
} catch (error) {
|
||||
console.error('Error fetching chats:', error);
|
||||
return rejectWithValue(error.response?.data?.message || 'Failed to fetch chats');
|
||||
@ -134,6 +137,15 @@ export const createChatRecord = createAsyncThunk(
|
||||
|
||||
// 先添加用户消息到聊天窗口
|
||||
const userMessageId = Date.now().toString();
|
||||
|
||||
// 先设置当前会话ID,这样addMessage可以找到正确的聊天项
|
||||
if (conversation_id) {
|
||||
dispatch({
|
||||
type: 'chat/setCurrentChat',
|
||||
payload: { conversation_id },
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addMessage({
|
||||
id: userMessageId,
|
||||
@ -158,6 +170,15 @@ export const createChatRecord = createAsyncThunk(
|
||||
let finalMessage = '';
|
||||
let conversationId = conversation_id;
|
||||
|
||||
// 同时获取聊天资源 - 在后台发送请求
|
||||
dispatch(
|
||||
getChatResources({
|
||||
dataset_id_list,
|
||||
question,
|
||||
messageId: assistantMessageId,
|
||||
})
|
||||
);
|
||||
|
||||
// 使用流式请求函数处理
|
||||
const result = await streamRequest(
|
||||
'/chat-history/',
|
||||
@ -168,11 +189,17 @@ export const createChatRecord = createAsyncThunk(
|
||||
const data = JSON.parse(chunkText);
|
||||
console.log('收到聊天数据块:', data);
|
||||
|
||||
if (data.code === 200) {
|
||||
if (data.code === 200 || data.code === 201) {
|
||||
// 保存会话ID (无论消息类型,只要找到会话ID就保存)
|
||||
if (data.data && data.data.conversation_id && !conversationId) {
|
||||
conversationId = data.data.conversation_id;
|
||||
console.log('获取到会话ID:', conversationId);
|
||||
|
||||
// 设置当前会话ID,使消息更新能找到正确的聊天项
|
||||
dispatch({
|
||||
type: 'chat/setCurrentChat',
|
||||
payload: { conversation_id: conversationId },
|
||||
});
|
||||
}
|
||||
|
||||
// 处理各种可能的消息类型
|
||||
@ -190,6 +217,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
content: finalMessage,
|
||||
is_streaming: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -234,6 +262,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
updateMessage({
|
||||
id: assistantMessageId,
|
||||
content: finalMessage,
|
||||
is_streaming: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -277,7 +306,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
// 获取知识库信息
|
||||
const state = getState();
|
||||
const availableDatasets = state.chat.availableDatasets.items || [];
|
||||
const existingChats = state.chat.history.items || [];
|
||||
const existingChats = state.chat.list.items || [];
|
||||
|
||||
// 检查是否已存在此会话ID的记录
|
||||
const existingChat = existingChats.find((chat) => chat.conversation_id === chatInfo.conversation_id);
|
||||
@ -303,6 +332,7 @@ export const createChatRecord = createAsyncThunk(
|
||||
create_time: new Date().toISOString(),
|
||||
last_message: question,
|
||||
message_count: 2, // 用户问题和助手回复
|
||||
messages: [], // 确保有消息数组
|
||||
};
|
||||
|
||||
// 更新当前聊天
|
||||
@ -375,6 +405,16 @@ export const fetchConversationDetail = createAsyncThunk(
|
||||
currentChat?.conversation_id === conversationId
|
||||
) {
|
||||
console.log('使用新创建的会话数据,跳过详情请求:', conversationId);
|
||||
|
||||
// 确保设置当前会话ID
|
||||
dispatch({
|
||||
type: 'chat/setCurrentChat',
|
||||
payload: {
|
||||
...currentChat,
|
||||
conversation_id: conversationId,
|
||||
},
|
||||
});
|
||||
|
||||
return currentChat;
|
||||
}
|
||||
|
||||
@ -383,12 +423,25 @@ export const fetchConversationDetail = createAsyncThunk(
|
||||
});
|
||||
|
||||
if (response && response.code === 200) {
|
||||
// 如果存在消息,更新Redux状态
|
||||
// 找到对应的chat item并添加消息
|
||||
if (response.data.messages) {
|
||||
dispatch({
|
||||
type: 'chat/fetchMessages/fulfilled',
|
||||
payload: response.data.messages,
|
||||
});
|
||||
const chatList = state.chat.list.items;
|
||||
const chatIndex = chatList.findIndex((chat) => chat.conversation_id === conversationId);
|
||||
console.log(chatIndex, 'chatIndex');
|
||||
|
||||
if (chatIndex !== -1) {
|
||||
// 直接更新该聊天的消息
|
||||
dispatch({
|
||||
type: 'chat/updateChatMessages',
|
||||
payload: {
|
||||
conversationId,
|
||||
messages: response.data.messages,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 如果不存在该聊天,先通过fetchChats刷新列表
|
||||
await dispatch(fetchChats());
|
||||
}
|
||||
}
|
||||
|
||||
return response.data;
|
||||
@ -450,6 +503,7 @@ export const createConversation = createAsyncThunk(
|
||||
create_time: new Date().toISOString(),
|
||||
last_message: '',
|
||||
message_count: 0,
|
||||
messages: [], // 确保有消息数组
|
||||
};
|
||||
|
||||
// 更新聊天历史列表
|
||||
@ -489,3 +543,58 @@ export const createConversation = createAsyncThunk(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取聊天回复的相关资源
|
||||
* @param {Object} params - 聊天参数
|
||||
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
||||
* @param {string} params.question - 用户问题
|
||||
* @param {string} params.messageId - 消息ID,用于关联资源
|
||||
*/
|
||||
export const getChatResources = createAsyncThunk(
|
||||
'chat/getChatResources',
|
||||
async ({ dataset_id_list, question, messageId }, { dispatch }) => {
|
||||
try {
|
||||
// 设置资源加载状态
|
||||
dispatch(setResourcesLoading(messageId));
|
||||
|
||||
// 调用API获取资源
|
||||
const response = await fetchChatResources({
|
||||
dataset_id_list,
|
||||
question,
|
||||
});
|
||||
|
||||
// 处理响应
|
||||
if (response && response.code === 200 && response.data) {
|
||||
const { matched_documents } = response.data;
|
||||
|
||||
// 将资源添加到store
|
||||
dispatch(
|
||||
setMessageResources({
|
||||
messageId,
|
||||
resources: matched_documents || [],
|
||||
})
|
||||
);
|
||||
|
||||
return matched_documents;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取聊天资源失败:', error);
|
||||
|
||||
// 设置错误状态
|
||||
dispatch(setResourcesError(error.message || '获取资源失败'));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 获取存在的聊天(如果存在)
|
||||
export const getExistingChat = (conversationId) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const existingChats = state.chat.list.items;
|
||||
|
||||
// 只通过会话ID查找聊天
|
||||
return existingChats.find((chat) => chat.conversation_id === conversationId);
|
||||
};
|
||||
|
19
src/store/upload/upload.slice.js
Normal file
19
src/store/upload/upload.slice.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
isUploading: false,
|
||||
};
|
||||
|
||||
const uploadSlice = createSlice({
|
||||
name: 'upload',
|
||||
initialState,
|
||||
reducers: {
|
||||
setIsUploading: (state, action) => {
|
||||
console.log('Setting isUploading to:', action.payload);
|
||||
state.isUploading = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setIsUploading } = uploadSlice.actions;
|
||||
export default uploadSlice.reducer;
|
Loading…
Reference in New Issue
Block a user