diff --git a/src/components/ChangePasswordModal.jsx b/src/components/ChangePasswordModal.jsx new file mode 100644 index 0000000..eafbad1 --- /dev/null +++ b/src/components/ChangePasswordModal.jsx @@ -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 ( +
+
+
+
+
修改密码
+ +
+
+
+
+ + + {submitted && errors.old_password && ( +
{errors.old_password}
+ )} +
+ +
+ + + {submitted && errors.new_password && ( +
{errors.new_password}
+ )} +
+ +
+ + + {submitted && errors.confirm_password && ( +
{errors.confirm_password}
+ )} +
+ +
+ + +
+
+
+
+
+
+ ); +} + +export default ChangePasswordModal; diff --git a/src/components/ResourceList.jsx b/src/components/ResourceList.jsx new file mode 100644 index 0000000..2abc2b7 --- /dev/null +++ b/src/components/ResourceList.jsx @@ -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 ( +
+
+ + 资源引用 + + {resources.length} +
+
+ {visibleResources.map((resource, index) => ( +
+
+ {(resource.similarity * 100).toFixed(0)}% +
+
+
{resource.document_name}
+
{resource.dataset_name}
+
+
+ ))} + + {hasMore && !expanded && ( + + )} + + {expanded && ( + + )} +
+
+ ); +}; + +export default ResourceList; diff --git a/src/components/UserSettingsModal.jsx b/src/components/UserSettingsModal.jsx index 73a0828..d10e2d9 100644 --- a/src/components/UserSettingsModal.jsx +++ b/src/components/UserSettingsModal.jsx @@ -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 (
-
+
-
管理员个人设置
+
个人设置
个人信息
- +
+
+ + + {submitted && errors.name &&
{errors.name}
} +
+ {submitted && errors.email &&
{errors.email}
} +
+
+ + {submitted && errors.department && ( +
{errors.department}
+ )} +
+
+ + {submitted && errors.group &&
{errors.group}
}
@@ -44,58 +210,31 @@ export default function UserSettingsModal({ show, onClose }) {
上次修改:{lastPasswordChange}
- -
-
-
-
- - 双重认证 -
- 增强账户安全性 -
- -
-
- -
-
通知设置
-
- - -
新的数据集访问申请通知
-
-
- - -
异常登录和权限变更提醒
+
- -
- + + {showChangePassword && ( + setShowChangePassword(false)} /> + )} ); -} +} \ No newline at end of file diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx index 81c0959..af1c689 100644 --- a/src/layouts/HeaderWithNav.jsx +++ b/src/layouts/HeaderWithNav.jsx @@ -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)} > - + {unreadCount > 0 && ( {unreadCount > 99 ? '99+' : unreadCount} @@ -145,15 +147,24 @@ export default function HeaderWithNav() { transform: 'translate(0px, 34px)', }} > -
  • +
  • setShowSettings(true)} + onClick={() => setShowSettings(true)} > 个人设置
  • +
  • + setShowChangePassword(true)} + > + 修改密码 + +

  • @@ -187,6 +198,7 @@ export default function HeaderWithNav() { setShowSettings(false)} /> setShowNotifications(false)} /> + setShowChangePassword(false)} /> ); diff --git a/src/pages/Chat/Chat.jsx b/src/pages/Chat/Chat.jsx index 35d1aee..0407532 100644 --- a/src/pages/Chat/Chat.jsx +++ b/src/pages/Chat/Chat.jsx @@ -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); diff --git a/src/pages/Chat/ChatSidebar.jsx b/src/pages/Chat/ChatSidebar.jsx index 84dd3d1..64a82e6 100644 --- a/src/pages/Chat/ChatSidebar.jsx +++ b/src/pages/Chat/ChatSidebar.jsx @@ -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 (
    - {/*
    +
    聊天记录
    -
    */} +
    ); - // 渲染错误状态 - const renderError = () => ( -
    -

    - 加载消息失败 -

    -

    {messageError}

    - -
    - ); - // 渲染空消息状态 const renderEmpty = () => { if (loading) return null; @@ -283,16 +286,11 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
    {conversation && conversation.datasets ? ( <> -
    {conversation.datasets.map((dataset) => dataset.name).join(', ')}
    +
    {conversation.title}
    {conversation.datasets.length > 0 && conversation.datasets[0].type && ( 类型: {conversation.datasets[0].type} )} - ) : knowledgeBase ? ( - <> -
    {knowledgeBase.name}
    - {knowledgeBase.description} - ) : (
    {loading || availableDatasetsLoading ? '加载中...' : '聊天'}
    )} @@ -303,8 +301,6 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
    {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 ) : ( - + )} {message.is_streaming && ( @@ -336,6 +335,14 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) { )} + + {/* 只在AI回复消息下方显示资源列表 */} + {message.role === 'assistant' && + !message.is_streaming && + resources.messageId === message.id && + resources.items.length > 0 && ( + + )}
    diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx index 5b84ff9..276d49e 100644 --- a/src/pages/Chat/NewChat.jsx +++ b/src/pages/Chat/NewChat.jsx @@ -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(() => { diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx index 301526c..8959a65 100644 --- a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx +++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx @@ -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 ( +
    +
    + {isUploading ? ( + + ) : ( + 📁 + )} + + {isUploading + ? `正在上传文件 (${selectedFiles.length})...` + : `文件上传 (${selectedFiles.length})`} + +
    +
    + ); + } + + // 只有在显示状态下渲染完整弹窗 if (!show) return null; + // 完整模式的渲染 return (
    { >
    上传文档
    - +
    + {/* Minimize button */} + + + {/* Close button */} + +
    { )}
    -