mirror of
https://github.com/Funkoala14/knowledgebase_influencer.git
synced 2025-06-08 03:08:14 +08:00
Compare commits
2 Commits
97203b4bcd
...
6a654950a5
Author | SHA1 | Date | |
---|---|---|---|
6a654950a5 | |||
2b89db7301 |
@ -1,83 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { switchToMockApi, switchToRealApi, checkServerStatus } from '../services/api';
|
|
||||||
|
|
||||||
export default function ApiModeSwitch() {
|
|
||||||
const [isMockMode, setIsMockMode] = useState(false);
|
|
||||||
const [isChecking, setIsChecking] = useState(false);
|
|
||||||
const [showNotification, setShowNotification] = useState(false);
|
|
||||||
const [notification, setNotification] = useState({ message: '', type: 'info' });
|
|
||||||
|
|
||||||
// 组件加载时检查服务器状态
|
|
||||||
useEffect(() => {
|
|
||||||
const checkStatus = async () => {
|
|
||||||
setIsChecking(true);
|
|
||||||
const isServerUp = await checkServerStatus();
|
|
||||||
setIsMockMode(!isServerUp);
|
|
||||||
setIsChecking(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 切换API模式
|
|
||||||
const handleToggleMode = async () => {
|
|
||||||
setIsChecking(true);
|
|
||||||
|
|
||||||
if (isMockMode) {
|
|
||||||
// 尝试切换回真实API
|
|
||||||
const isServerUp = await switchToRealApi();
|
|
||||||
if (isServerUp) {
|
|
||||||
setIsMockMode(false);
|
|
||||||
showNotificationMessage('已切换到真实API模式', 'success');
|
|
||||||
} else {
|
|
||||||
showNotificationMessage('服务器连接失败,继续使用模拟数据', 'warning');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 切换到模拟API
|
|
||||||
switchToMockApi();
|
|
||||||
setIsMockMode(true);
|
|
||||||
showNotificationMessage('已切换到模拟API模式', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsChecking(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 显示通知消息
|
|
||||||
const showNotificationMessage = (message, type) => {
|
|
||||||
setNotification({ message, type });
|
|
||||||
setShowNotification(true);
|
|
||||||
|
|
||||||
// 3秒后自动隐藏通知
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowNotification(false);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='api-mode-switch'>
|
|
||||||
<div className='d-flex align-items-center'>
|
|
||||||
<div className='form-check form-switch me-2'>
|
|
||||||
<input
|
|
||||||
className='form-check-input'
|
|
||||||
type='checkbox'
|
|
||||||
id='apiModeToggle'
|
|
||||||
checked={isMockMode}
|
|
||||||
onChange={handleToggleMode}
|
|
||||||
disabled={isChecking}
|
|
||||||
/>
|
|
||||||
<label className='form-check-label' htmlFor='apiModeToggle'>
|
|
||||||
{isChecking ? '检查中...' : isMockMode ? '模拟API模式' : '真实API模式'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{isMockMode && <span className='badge bg-warning text-dark'>使用本地模拟数据</span>}
|
|
||||||
{!isMockMode && <span className='badge bg-success'>已连接到后端服务器</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showNotification && (
|
|
||||||
<div className={`alert alert-${notification.type} mt-2 py-2 px-3`} style={{ fontSize: '0.85rem' }}>
|
|
||||||
{notification.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { fetchChats, deleteChat, createChatRecord, createConversation } from '../../store/chat/chat.thunks';
|
import { fetchChats, deleteChat, createConversation } from '../../store/chat/chat.thunks';
|
||||||
import { showNotification } from '../../store/notification.slice';
|
import { showNotification } from '../../store/notification.slice';
|
||||||
import ChatSidebar from './ChatSidebar';
|
import ChatSidebar from './ChatSidebar';
|
||||||
import NewChat from './NewChat';
|
import NewChat from './NewChat';
|
||||||
@ -17,12 +17,13 @@ export default function Chat() {
|
|||||||
items: chatHistory,
|
items: chatHistory,
|
||||||
status,
|
status,
|
||||||
error,
|
error,
|
||||||
} = useSelector((state) => state.chat.history || { items: [], status: 'idle', error: null });
|
} = useSelector((state) => state.chat.chats || { items: [], status: 'idle', error: null });
|
||||||
const operationStatus = useSelector((state) => state.chat.createSession?.status);
|
const operationStatus = useSelector((state) => state.chat.chatOperation?.status);
|
||||||
const operationError = useSelector((state) => state.chat.createSession?.error);
|
const operationError = useSelector((state) => state.chat.chatOperation?.error);
|
||||||
|
|
||||||
// 获取聊天记录列表
|
// 获取聊天记录列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(chatHistory);
|
||||||
dispatch(fetchChats({ page: 1, page_size: 20 }));
|
dispatch(fetchChats({ page: 1, page_size: 20 }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
252
src/pages/Chat/ChatSidePanel.jsx
Normal file
252
src/pages/Chat/ChatSidePanel.jsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { setUserGoal, getConversationSummary, getRecommendedReply } from '../../store/talentChat/talentChat.thunks';
|
||||||
|
import { showNotification } from '../../store/notification.slice';
|
||||||
|
import SafeMarkdown from '../../components/SafeMarkdown';
|
||||||
|
|
||||||
|
export default function ChatSidePanel({ chatId, talentEmail }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [goal, setGoal] = useState('');
|
||||||
|
const [loading, setLoading] = useState({
|
||||||
|
goal: false,
|
||||||
|
summary: false,
|
||||||
|
reply: false,
|
||||||
|
});
|
||||||
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
|
|
||||||
|
// Get data from Redux store
|
||||||
|
const userGoal = useSelector((state) => state.talentChat?.userGoal?.data);
|
||||||
|
const conversationSummary = useSelector((state) => state.talentChat?.conversationSummary?.data);
|
||||||
|
const recommendedReply = useSelector((state) => state.talentChat?.recommendedReply?.data);
|
||||||
|
|
||||||
|
// Load conversation summary and recommended reply when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (talentEmail && chatId) {
|
||||||
|
handleGetSummary();
|
||||||
|
handleGetRecommendedReply();
|
||||||
|
}
|
||||||
|
}, [chatId, talentEmail]);
|
||||||
|
|
||||||
|
// Reset copy success state after 2 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (copySuccess) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setCopySuccess(false);
|
||||||
|
}, 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [copySuccess]);
|
||||||
|
|
||||||
|
const handleGoalSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!goal.trim()) return;
|
||||||
|
|
||||||
|
setLoading((prev) => ({ ...prev, goal: true }));
|
||||||
|
try {
|
||||||
|
await dispatch(setUserGoal(goal)).unwrap();
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '聊天目标设置成功',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `设置目标失败: ${error}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, goal: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetSummary = async () => {
|
||||||
|
if (!talentEmail) return;
|
||||||
|
|
||||||
|
setLoading((prev) => ({ ...prev, summary: true }));
|
||||||
|
try {
|
||||||
|
await dispatch(getConversationSummary(talentEmail)).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `获取聊天总结失败: ${error}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, summary: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetRecommendedReply = async () => {
|
||||||
|
if (!chatId || !talentEmail) return;
|
||||||
|
|
||||||
|
setLoading((prev) => ({ ...prev, reply: true }));
|
||||||
|
try {
|
||||||
|
await dispatch(
|
||||||
|
getRecommendedReply({
|
||||||
|
conversation_id: chatId,
|
||||||
|
talent_email: talentEmail,
|
||||||
|
})
|
||||||
|
).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `获取推荐话术失败: ${error}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, reply: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyReply = () => {
|
||||||
|
// Get reply text from the appropriate property path
|
||||||
|
const replyText = recommendedReply?.reply || '';
|
||||||
|
|
||||||
|
if (replyText) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(replyText)
|
||||||
|
.then(() => {
|
||||||
|
setCopySuccess(true);
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '已复制到剪贴板',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '复制失败,请手动复制',
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='chat-side-panel d-flex flex-column h-100'>
|
||||||
|
{/* 聊天目标 */}
|
||||||
|
<div className='panel-section p-3 border-bottom' style={{ flex: '0 0 auto' }}>
|
||||||
|
<h5 className='section-title mb-3'>
|
||||||
|
<span className='bg-warning bg-opacity-25 px-2 py-1 rounded'>聊天目标</span>
|
||||||
|
</h5>
|
||||||
|
<form onSubmit={handleGoalSubmit}>
|
||||||
|
<textarea
|
||||||
|
className='form-control mb-2'
|
||||||
|
rows='3'
|
||||||
|
placeholder='请输入聊天目标...'
|
||||||
|
value={goal}
|
||||||
|
onChange={(e) => setGoal(e.target.value)}
|
||||||
|
></textarea>
|
||||||
|
<button type='submit' className='btn btn-sm btn-primary' disabled={loading.goal || !goal.trim()}>
|
||||||
|
{loading.goal ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-2'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
提交中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'设置目标'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{userGoal && (
|
||||||
|
<div className='mt-2 p-2 bg-light rounded'>
|
||||||
|
<small className='fw-bold'>当前目标:</small>
|
||||||
|
<p className='mb-0 small'>
|
||||||
|
{userGoal.content ||
|
||||||
|
(typeof userGoal === 'string' ? userGoal : userGoal?.data?.content || '')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 历史聊天记录总结 */}
|
||||||
|
<div className='panel-section p-3 border-bottom' style={{ flex: '1 0 50%', overflowY: 'auto' }}>
|
||||||
|
<h5 className='section-title mb-3'>
|
||||||
|
<span className='bg-warning bg-opacity-25 px-2 py-1 rounded'>历史聊天记录总结</span>
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-outline-secondary ms-2'
|
||||||
|
onClick={handleGetSummary}
|
||||||
|
disabled={loading.summary || !talentEmail}
|
||||||
|
>
|
||||||
|
{loading.summary ? (
|
||||||
|
<span className='spinner-border spinner-border-sm' role='status' aria-hidden='true'></span>
|
||||||
|
) : (
|
||||||
|
'刷新'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
{loading.summary ? (
|
||||||
|
<div className='text-center py-3'>
|
||||||
|
<div className='spinner-border text-secondary' role='status'>
|
||||||
|
<span className='visually-hidden'>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<p className='mt-2 text-muted'>获取聊天总结...</p>
|
||||||
|
</div>
|
||||||
|
) : conversationSummary?.summary ? (
|
||||||
|
<div className='summary-content'>
|
||||||
|
<SafeMarkdown content={conversationSummary.summary} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className='text-muted'>暂无聊天总结</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 推荐聊天话术 */}
|
||||||
|
<div className='panel-section p-3' style={{ flex: '1 0 50%', overflowY: 'auto' }}>
|
||||||
|
<h5 className='section-title mb-3'>
|
||||||
|
<span className='bg-warning bg-opacity-25 px-2 py-1 rounded'>推荐聊天话术</span>
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-outline-secondary ms-2'
|
||||||
|
onClick={handleGetRecommendedReply}
|
||||||
|
disabled={loading.reply || !chatId || !talentEmail}
|
||||||
|
>
|
||||||
|
{loading.reply ? (
|
||||||
|
<span className='spinner-border spinner-border-sm' role='status' aria-hidden='true'></span>
|
||||||
|
) : (
|
||||||
|
'刷新'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
{loading.reply ? (
|
||||||
|
<div className='text-center py-3'>
|
||||||
|
<div className='spinner-border text-secondary' role='status'>
|
||||||
|
<span className='visually-hidden'>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<p className='mt-2 text-muted'>生成话术中...</p>
|
||||||
|
</div>
|
||||||
|
) : recommendedReply?.reply || recommendedReply?.status === 'success' ? (
|
||||||
|
<div
|
||||||
|
className='recommended-reply bg-light p-3 rounded position-relative'
|
||||||
|
style={{ whiteSpace: 'pre-line', fontFamily: 'Arial, sans-serif' }}
|
||||||
|
>
|
||||||
|
<SafeMarkdown content={recommendedReply.reply} />
|
||||||
|
<div className='position-absolute top-0 end-0 m-2'>
|
||||||
|
<button
|
||||||
|
className='btn btn-sm btn-outline-primary'
|
||||||
|
onClick={handleCopyReply}
|
||||||
|
title='复制话术内容'
|
||||||
|
>
|
||||||
|
{copySuccess ? '已复制 ✓' : '复制'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className='text-muted'>暂无推荐话术</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { fetchMessages } from '../../store/chat/chat.messages.thunks';
|
import { fetchMessages } from '../../store/chat/chat.messages.thunks';
|
||||||
import { resetMessages, resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice';
|
import { resetMessageOperation, addMessage } from '../../store/chat/chat.slice';
|
||||||
import { showNotification } from '../../store/notification.slice';
|
import { showNotification } from '../../store/notification.slice';
|
||||||
import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
|
import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks';
|
||||||
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
|
import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks';
|
||||||
import SvgIcon from '../../components/SvgIcon';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
import SafeMarkdown from '../../components/SafeMarkdown';
|
import SafeMarkdown from '../../components/SafeMarkdown';
|
||||||
|
import ChatSidePanel from './ChatSidePanel';
|
||||||
import { get } from '../../services/api';
|
import { get } from '../../services/api';
|
||||||
|
|
||||||
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
||||||
@ -16,11 +17,23 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
|
const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话
|
||||||
|
|
||||||
// 从 Redux store 获取消息
|
// Gmail无邮件警告状态
|
||||||
const messages = useSelector((state) => state.chat.messages.items);
|
const [noEmailsWarning, setNoEmailsWarning] = useState(null);
|
||||||
const messageStatus = useSelector((state) => state.chat.messages.status);
|
const [troubleshooting, setTroubleshooting] = useState(null);
|
||||||
const messageError = useSelector((state) => state.chat.messages.error);
|
|
||||||
const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage);
|
// 从 Redux store 获取聊天数据
|
||||||
|
const chats = useSelector((state) => state.chat.chats.items);
|
||||||
|
const currentChat = chats.find((chat) => chat.conversation_id === chatId);
|
||||||
|
const messages = currentChat?.messages || [];
|
||||||
|
|
||||||
|
// 获取消息操作状态
|
||||||
|
const messageStatus = useSelector((state) => state.chat.messageOperation.status);
|
||||||
|
const messageError = useSelector((state) => state.chat.messageOperation.error);
|
||||||
|
|
||||||
|
// Gmail集成状态
|
||||||
|
const gmailSetupStatus = useSelector((state) => state.gmailChat?.setup?.status);
|
||||||
|
const gmailNoEmailsWarning = useSelector((state) => state.gmailChat?.setup?.noEmailsWarning);
|
||||||
|
const gmailTroubleshooting = useSelector((state) => state.gmailChat?.setup?.troubleshooting);
|
||||||
|
|
||||||
// 使用新的Redux状态结构
|
// 使用新的Redux状态结构
|
||||||
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
|
const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []);
|
||||||
@ -31,23 +44,55 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []);
|
const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []);
|
||||||
const availableDatasetsLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
|
const availableDatasetsLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
|
||||||
|
|
||||||
// 获取会话详情
|
// 获取当前活跃聊天ID
|
||||||
const conversation = useSelector((state) => state.chat.currentChat.data);
|
const activeConversationId = useSelector((state) => state.chat.activeConversationId);
|
||||||
const conversationStatus = useSelector((state) => state.chat.currentChat.status);
|
|
||||||
const conversationError = useSelector((state) => state.chat.currentChat.error);
|
|
||||||
|
|
||||||
// 获取会话创建状态
|
// 聊天操作状态
|
||||||
const createSessionStatus = useSelector((state) => state.chat.createSession?.status);
|
const chatOperationStatus = useSelector((state) => state.chat.chatOperation.status);
|
||||||
const createSessionId = useSelector((state) => state.chat.createSession?.sessionId);
|
|
||||||
|
// 提取达人邮箱信息(用于侧边栏功能)
|
||||||
|
const [talentEmail, setTalentEmail] = useState('');
|
||||||
|
|
||||||
// 监听知识库ID变更,确保保存在组件状态中
|
// 监听知识库ID变更,确保保存在组件状态中
|
||||||
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
|
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]);
|
||||||
|
|
||||||
// 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds
|
// 检查是否存在Gmail无邮件警告
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 优先使用conversation中的知识库列表
|
// 优先使用Redux状态中的警告
|
||||||
if (conversation && conversation.datasets && conversation.datasets.length > 0) {
|
if (gmailNoEmailsWarning) {
|
||||||
const datasetIds = conversation.datasets.map((ds) => ds.id);
|
setNoEmailsWarning(gmailNoEmailsWarning);
|
||||||
|
setTroubleshooting(gmailTroubleshooting);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从localStorage中获取警告信息
|
||||||
|
try {
|
||||||
|
const savedWarning = localStorage.getItem('gmailNoEmailsWarning');
|
||||||
|
if (savedWarning) {
|
||||||
|
const warningData = JSON.parse(savedWarning);
|
||||||
|
// 检查是否是当前会话的警告且未过期(24小时内)
|
||||||
|
const isCurrentChat = warningData.chatId === chatId;
|
||||||
|
const isStillValid = Date.now() - warningData.timestamp < 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
if (isCurrentChat && isStillValid) {
|
||||||
|
setNoEmailsWarning(warningData.message);
|
||||||
|
setTroubleshooting(warningData.troubleshooting);
|
||||||
|
} else if (!isStillValid) {
|
||||||
|
// 警告过期,清除
|
||||||
|
localStorage.removeItem('gmailNoEmailsWarning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析Gmail警告信息失败:', error);
|
||||||
|
localStorage.removeItem('gmailNoEmailsWarning');
|
||||||
|
}
|
||||||
|
}, [chatId, gmailNoEmailsWarning, gmailTroubleshooting]);
|
||||||
|
|
||||||
|
// 当currentChat或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds
|
||||||
|
useEffect(() => {
|
||||||
|
// 优先使用currentChat中的知识库列表
|
||||||
|
if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
|
||||||
|
const datasetIds = currentChat.datasets.map((ds) => ds.id);
|
||||||
console.log('从会话中获取知识库列表:', datasetIds);
|
console.log('从会话中获取知识库列表:', datasetIds);
|
||||||
setSelectedKnowledgeBaseIds(datasetIds);
|
setSelectedKnowledgeBaseIds(datasetIds);
|
||||||
}
|
}
|
||||||
@ -58,7 +103,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
console.log('从URL参数中获取知识库列表:', ids);
|
console.log('从URL参数中获取知识库列表:', ids);
|
||||||
setSelectedKnowledgeBaseIds(ids);
|
setSelectedKnowledgeBaseIds(ids);
|
||||||
}
|
}
|
||||||
}, [conversation, knowledgeBaseId]);
|
}, [currentChat, knowledgeBaseId]);
|
||||||
|
|
||||||
// 获取聊天详情
|
// 获取聊天详情
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -70,12 +115,12 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是新创建的会话
|
// 检查是否是已存在的聊天
|
||||||
const isNewlyCreatedChat = createSessionStatus === 'succeeded' && createSessionId === chatId;
|
const existingChat = chats.find((chat) => chat.conversation_id === chatId);
|
||||||
|
|
||||||
// 如果是新创建的会话且已经有会话数据,则跳过详情获取
|
// 如果已经有这个聊天的消息,则不需要获取详情
|
||||||
if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) {
|
if (existingChat && existingChat.messages && existingChat.messages.length > 0) {
|
||||||
console.log('跳过新创建会话的详情获取:', chatId);
|
console.log('聊天已存在且有消息,跳过详情获取:', chatId);
|
||||||
hasLoadedDetailRef.current[chatId] = true;
|
hasLoadedDetailRef.current[chatId] = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -102,14 +147,7 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
}, [chatId, dispatch, chats]);
|
||||||
// 组件卸载时清空消息
|
|
||||||
return () => {
|
|
||||||
dispatch(resetMessages());
|
|
||||||
// 不要清空hasLoadedDetailRef,否则会导致重复加载
|
|
||||||
// hasLoadedDetailRef.current = {}; // 清理ref缓存
|
|
||||||
};
|
|
||||||
}, [chatId, dispatch, createSessionStatus, createSessionId]);
|
|
||||||
|
|
||||||
// 组件销毁时完全清空ref缓存
|
// 组件销毁时完全清空ref缓存
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -122,32 +160,81 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果是新聊天且没有任何消息,添加一条系统欢迎消息
|
// 如果是新聊天且没有任何消息,添加一条系统欢迎消息
|
||||||
if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
|
if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') {
|
||||||
const selectedKb = knowledgeBase ||
|
// 检查是否有无邮件警告作为首条消息
|
||||||
availableDatasets.find((ds) => ds.id === knowledgeBaseId) || { name: '知识库' };
|
let isNoEmailsFirstMessage = false;
|
||||||
|
let noEmailsMessage = null;
|
||||||
|
|
||||||
dispatch(
|
try {
|
||||||
addMessage({
|
const savedWarning = localStorage.getItem('gmailNoEmailsWarning');
|
||||||
id: 'welcome-' + Date.now(),
|
if (savedWarning) {
|
||||||
role: 'assistant',
|
const warningData = JSON.parse(savedWarning);
|
||||||
content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`,
|
// 检查是否是当前会话的警告、是否是首条消息
|
||||||
created_at: new Date().toISOString(),
|
if (warningData.chatId === chatId && warningData.isFirstMessage) {
|
||||||
})
|
isNoEmailsFirstMessage = true;
|
||||||
);
|
noEmailsMessage = warningData.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析Gmail警告信息失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNoEmailsFirstMessage && noEmailsMessage) {
|
||||||
|
// 使用警告消息作为首条消息
|
||||||
|
dispatch(
|
||||||
|
addMessage({
|
||||||
|
conversationId: chatId,
|
||||||
|
message: {
|
||||||
|
id: 'gmail-warning-' + Date.now(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: `⚠️ ${noEmailsMessage}\n\n您仍然可以在此聊天中提问,但可能无法获得与邮件内容相关的回答。`,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 移除isFirstMessage标记,防止再次显示
|
||||||
|
try {
|
||||||
|
const savedWarning = localStorage.getItem('gmailNoEmailsWarning');
|
||||||
|
if (savedWarning) {
|
||||||
|
const warningData = JSON.parse(savedWarning);
|
||||||
|
warningData.isFirstMessage = false;
|
||||||
|
localStorage.setItem('gmailNoEmailsWarning', JSON.stringify(warningData));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新Gmail警告信息失败:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用常规欢迎消息
|
||||||
|
const selectedKb = knowledgeBase ||
|
||||||
|
availableDatasets.find((ds) => ds.id === knowledgeBaseId) || { name: '知识库' };
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
addMessage({
|
||||||
|
conversationId: chatId,
|
||||||
|
message: {
|
||||||
|
id: 'welcome-' + Date.now(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [chatId, messages.length, loading, messageStatus, knowledgeBase, knowledgeBaseId, availableDatasets, dispatch]);
|
}, [chatId, messages.length, loading, messageStatus, knowledgeBase, knowledgeBaseId, availableDatasets, dispatch]);
|
||||||
|
|
||||||
// 监听发送消息状态
|
// 监听发送消息状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sendStatus === 'failed' && sendError) {
|
if (messageStatus === 'failed' && messageError) {
|
||||||
dispatch(
|
dispatch(
|
||||||
showNotification({
|
showNotification({
|
||||||
message: `发送失败: ${sendError}`,
|
message: `发送失败: ${messageError}`,
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(resetSendMessageStatus());
|
dispatch(resetMessageOperation());
|
||||||
}
|
}
|
||||||
}, [sendStatus, sendError, dispatch]);
|
}, [messageStatus, messageError, dispatch]);
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -156,8 +243,8 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
|
|
||||||
// 获取当前会话的知识库信息
|
// 获取当前会话的知识库信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果conversation有数据集信息,优先使用它
|
// 如果currentChat有数据集信息,优先使用它
|
||||||
if (conversation && conversation.datasets && conversation.datasets.length > 0) {
|
if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,12 +252,36 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) {
|
if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) {
|
||||||
dispatch(fetchAvailableDatasets());
|
dispatch(fetchAvailableDatasets());
|
||||||
}
|
}
|
||||||
}, [dispatch, knowledgeBaseId, knowledgeBases, conversation, availableDatasets]);
|
}, [dispatch, knowledgeBaseId, knowledgeBases, currentChat, availableDatasets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 尝试从聊天数据或知识库中提取达人邮箱
|
||||||
|
if (currentChat?.talent_email) {
|
||||||
|
setTalentEmail(currentChat.talent_email);
|
||||||
|
} else if (currentChat?.datasets?.[0]?.talent_email) {
|
||||||
|
setTalentEmail(currentChat.datasets[0].talent_email);
|
||||||
|
} else if (currentChat?.datasets?.[0]?.name && currentChat?.datasets[0]?.name.includes('@')) {
|
||||||
|
// 如果知识库名称中包含邮箱格式,提取出来
|
||||||
|
const emailMatch = currentChat.datasets[0].name.match(/[\w.-]+@[\w.-]+\.\w+/);
|
||||||
|
if (emailMatch) {
|
||||||
|
setTalentEmail(emailMatch[0]);
|
||||||
|
}
|
||||||
|
} else if (messages.length > 0) {
|
||||||
|
// 从消息中查找可能包含的达人邮箱
|
||||||
|
for (const message of messages) {
|
||||||
|
const emailMatch = message.content?.match(/[\w.-]+@[\w.-]+\.\w+/);
|
||||||
|
if (emailMatch) {
|
||||||
|
setTalentEmail(emailMatch[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentChat, messages]);
|
||||||
|
|
||||||
const handleSendMessage = (e) => {
|
const handleSendMessage = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!inputMessage.trim() || sendStatus === 'loading') return;
|
if (!inputMessage.trim() || messageStatus === 'loading') return;
|
||||||
|
|
||||||
console.log('准备发送消息:', inputMessage);
|
console.log('准备发送消息:', inputMessage);
|
||||||
console.log('当前会话ID:', chatId);
|
console.log('当前会话ID:', chatId);
|
||||||
@ -183,9 +294,9 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
// 使用已保存的知识库列表
|
// 使用已保存的知识库列表
|
||||||
dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, ''));
|
dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, ''));
|
||||||
console.log('使用组件状态中的知识库列表:', dataset_id_list);
|
console.log('使用组件状态中的知识库列表:', dataset_id_list);
|
||||||
} else if (conversation && conversation.datasets && conversation.datasets.length > 0) {
|
} else if (currentChat && currentChat.datasets && currentChat.datasets.length > 0) {
|
||||||
// 如果已有会话,使用会话中的知识库
|
// 如果已有会话,使用会话中的知识库
|
||||||
dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, ''));
|
dataset_id_list = currentChat.datasets.map((ds) => ds.id.replace(/-/g, ''));
|
||||||
console.log('使用会话中的知识库列表:', dataset_id_list);
|
console.log('使用会话中的知识库列表:', dataset_id_list);
|
||||||
} else if (knowledgeBaseId) {
|
} else if (knowledgeBaseId) {
|
||||||
// 如果是新会话,使用当前选择的知识库
|
// 如果是新会话,使用当前选择的知识库
|
||||||
@ -278,125 +389,136 @@ export default function ChatWindow({ chatId, knowledgeBaseId }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='chat-window d-flex flex-column h-100'>
|
<div className='d-flex h-100'>
|
||||||
{/* Chat header */}
|
{/* Main Chat Area */}
|
||||||
<div className='p-3 border-bottom'>
|
<div className='chat-window d-flex flex-column h-100 flex-grow-1'>
|
||||||
{conversation && conversation.datasets ? (
|
{/* Chat header */}
|
||||||
<>
|
<div className='p-3 border-bottom'>
|
||||||
<h5 className='mb-0'>{conversation.datasets.map((dataset) => dataset.name).join(', ')}</h5>
|
{currentChat && currentChat.datasets ? (
|
||||||
{conversation.datasets.length > 0 && conversation.datasets[0].type && (
|
<>
|
||||||
<small className='text-muted'>类型: {conversation.datasets[0].type}</small>
|
<h5 className='mb-0'>{currentChat.datasets.map((dataset) => dataset.name).join(', ')}</h5>
|
||||||
)}
|
{currentChat.datasets.length > 0 && currentChat.datasets[0].type && (
|
||||||
</>
|
<small className='text-muted'>类型: {currentChat.datasets[0].type}</small>
|
||||||
) : knowledgeBase ? (
|
)}
|
||||||
<>
|
</>
|
||||||
<h5 className='mb-0'>{knowledgeBase.name}</h5>
|
) : knowledgeBase ? (
|
||||||
<small className='text-muted'>{knowledgeBase.description}</small>
|
<>
|
||||||
</>
|
<h5 className='mb-0'>{knowledgeBase.name}</h5>
|
||||||
) : (
|
<small className='text-muted'>{knowledgeBase.description}</small>
|
||||||
<h5 className='mb-0'>{loading || availableDatasetsLoading ? '加载中...' : '聊天'}</h5>
|
</>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<h5 className='mb-0'>{loading || availableDatasetsLoading ? '加载中...' : '聊天'}</h5>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Chat messages */}
|
{/* Chat messages */}
|
||||||
<div className='flex-grow-1 p-3 overflow-auto'>
|
<div className='flex-grow-1 p-3 overflow-auto'>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
{messageStatus === 'loading'
|
{messageStatus === 'loading' && !messages.length
|
||||||
? renderLoading()
|
? renderLoading()
|
||||||
: messageStatus === 'failed'
|
: messageStatus === 'failed' && !messages.length
|
||||||
? renderError()
|
? renderError()
|
||||||
: messages.length === 0
|
: messages.length === 0
|
||||||
? renderEmpty()
|
? renderEmpty()
|
||||||
: messages.map((message) => (
|
: messages.map((message) => (
|
||||||
<div
|
|
||||||
key={message.id}
|
|
||||||
className={`d-flex ${
|
|
||||||
message.role === 'user' ? 'align-items-end' : 'align-items-start'
|
|
||||||
} mb-3 flex-column`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`chat-message p-3 rounded-3 ${
|
key={message.id}
|
||||||
message.role === 'user' ? 'bg-dark text-white' : 'bg-light'
|
className={`d-flex ${
|
||||||
}`}
|
message.role === 'user' ? 'align-items-end' : 'align-items-start'
|
||||||
style={{
|
} mb-3 flex-column`}
|
||||||
maxWidth: '75%',
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className='message-content'>
|
<div
|
||||||
{message.role === 'user' ? (
|
className={`chat-message p-3 rounded-3 ${
|
||||||
message.content
|
message.role === 'user' ? 'bg-dark text-white' : 'bg-light'
|
||||||
) : (
|
}`}
|
||||||
<SafeMarkdown content={message.content} />
|
style={{
|
||||||
)}
|
maxWidth: '75%',
|
||||||
{message.is_streaming && (
|
position: 'relative',
|
||||||
<span className='streaming-indicator'>
|
}}
|
||||||
<span className='dot dot1'></span>
|
>
|
||||||
<span className='dot dot2'></span>
|
<div className='message-content'>
|
||||||
<span className='dot dot3'></span>
|
{message.role === 'user' ? (
|
||||||
</span>
|
message.content
|
||||||
)}
|
) : (
|
||||||
|
<SafeMarkdown content={message.content} />
|
||||||
|
)}
|
||||||
|
{message.is_streaming && (
|
||||||
|
<span className='streaming-indicator'>
|
||||||
|
<span className='dot dot1'></span>
|
||||||
|
<span className='dot dot2'></span>
|
||||||
|
<span className='dot dot3'></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='message-time small text-muted mt-1'>
|
||||||
|
{message.created_at &&
|
||||||
|
(() => {
|
||||||
|
const messageDate = new Date(message.created_at);
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
// 检查是否是今天
|
||||||
|
const isToday =
|
||||||
|
messageDate.getDate() === today.getDate() &&
|
||||||
|
messageDate.getMonth() === today.getMonth() &&
|
||||||
|
messageDate.getFullYear() === today.getFullYear();
|
||||||
|
|
||||||
|
// 如果是今天,只显示时间;否则显示年月日和时间
|
||||||
|
if (isToday) {
|
||||||
|
return messageDate.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return messageDate.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
{message.is_streaming && ' · 正在生成...'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='message-time small text-muted mt-1'>
|
))}
|
||||||
{message.created_at &&
|
|
||||||
(() => {
|
|
||||||
const messageDate = new Date(message.created_at);
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
// 检查是否是今天
|
<div ref={messagesEndRef} />
|
||||||
const isToday =
|
</div>
|
||||||
messageDate.getDate() === today.getDate() &&
|
</div>
|
||||||
messageDate.getMonth() === today.getMonth() &&
|
|
||||||
messageDate.getFullYear() === today.getFullYear();
|
|
||||||
|
|
||||||
// 如果是今天,只显示时间;否则显示年月日和时间
|
{/* Chat input */}
|
||||||
if (isToday) {
|
<div className='p-3 border-top'>
|
||||||
return messageDate.toLocaleTimeString('zh-CN', {
|
<form onSubmit={handleSendMessage} className='d-flex gap-2'>
|
||||||
hour: '2-digit',
|
<input
|
||||||
minute: '2-digit',
|
type='text'
|
||||||
});
|
className='form-control'
|
||||||
} else {
|
placeholder='输入你的问题...'
|
||||||
return messageDate.toLocaleString('zh-CN', {
|
value={inputMessage}
|
||||||
year: 'numeric',
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
month: '2-digit',
|
disabled={messageStatus === 'loading'}
|
||||||
day: '2-digit',
|
/>
|
||||||
hour: '2-digit',
|
<button
|
||||||
minute: '2-digit',
|
type='submit'
|
||||||
});
|
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
|
||||||
}
|
disabled={messageStatus === 'loading' || !inputMessage.trim()}
|
||||||
})()}
|
>
|
||||||
{message.is_streaming && ' · 正在生成...'}
|
<SvgIcon className='send' color='#ffffff' />
|
||||||
</div>
|
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
|
||||||
</div>
|
发送
|
||||||
))}
|
</span>
|
||||||
|
</button>
|
||||||
<div ref={messagesEndRef} />
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat input */}
|
{/* Right Side Panel */}
|
||||||
<div className='p-3 border-top'>
|
<div
|
||||||
<form onSubmit={handleSendMessage} className='d-flex gap-2'>
|
className='chat-side-panel-container border-start'
|
||||||
<input
|
style={{ width: '350px', height: '100%', overflow: 'hidden' }}
|
||||||
type='text'
|
>
|
||||||
className='form-control'
|
<ChatSidePanel chatId={chatId} talentEmail={talentEmail} />
|
||||||
placeholder='输入你的问题...'
|
|
||||||
value={inputMessage}
|
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
|
||||||
disabled={sendStatus === 'loading'}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type='submit'
|
|
||||||
className='btn btn-dark d-flex align-items-center justify-content-center gap-2'
|
|
||||||
disabled={sendStatus === 'loading' || !inputMessage.trim()}
|
|
||||||
>
|
|
||||||
<SvgIcon className='send' color='#ffffff' />
|
|
||||||
<span className='ms-1' style={{ minWidth: 'fit-content' }}>
|
|
||||||
发送
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { showNotification } from '../../store/notification.slice';
|
import { showNotification } from '../../store/notification.slice';
|
||||||
import { fetchAvailableDatasets, fetchChats, createConversation } from '../../store/chat/chat.thunks';
|
import { fetchAvailableDatasets, fetchChats, createConversation } from '../../store/chat/chat.thunks';
|
||||||
|
import { setupGmailChat } from '../../store/gmailChat/gmailChat.thunks';
|
||||||
import SvgIcon from '../../components/SvgIcon';
|
import SvgIcon from '../../components/SvgIcon';
|
||||||
|
|
||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
@ -11,15 +12,26 @@ export default function NewChat() {
|
|||||||
const [selectedDatasetIds, setSelectedDatasetIds] = useState([]);
|
const [selectedDatasetIds, setSelectedDatasetIds] = useState([]);
|
||||||
const [isNavigating, setIsNavigating] = useState(false);
|
const [isNavigating, setIsNavigating] = useState(false);
|
||||||
|
|
||||||
|
// Gmail集成相关状态
|
||||||
|
const [talentEmail, setTalentEmail] = useState('');
|
||||||
|
const [showAuthCodeInput, setShowAuthCodeInput] = useState(false);
|
||||||
|
const [authCode, setAuthCode] = useState('');
|
||||||
|
const [authUrl, setAuthUrl] = useState('');
|
||||||
|
const [isGmailLoading, setIsGmailLoading] = useState(false);
|
||||||
|
|
||||||
// 从 Redux store 获取可用知识库数据
|
// 从 Redux store 获取可用知识库数据
|
||||||
const datasets = useSelector((state) => state.chat.availableDatasets.items || []);
|
const datasets = useSelector((state) => state.chat.availableDatasets.items || []);
|
||||||
const isLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
|
const isLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading');
|
||||||
const error = useSelector((state) => state.chat.availableDatasets.error);
|
const error = useSelector((state) => state.chat.availableDatasets.error);
|
||||||
|
|
||||||
// 获取聊天历史记录
|
// 获取聊天历史记录
|
||||||
const chatHistory = useSelector((state) => state.chat.history.items || []);
|
const chatHistory = useSelector((state) => state.chat.chats.items || []);
|
||||||
const chatHistoryLoading = useSelector((state) => state.chat.history.status === 'loading');
|
const chatHistoryLoading = useSelector((state) => state.chat.chats.status === 'loading');
|
||||||
const chatCreationStatus = useSelector((state) => state.chat.sendMessage?.status);
|
const chatCreationStatus = useSelector((state) => state.chat.messageOperation?.status);
|
||||||
|
|
||||||
|
// Gmail集成状态
|
||||||
|
const gmailSetupStatus = useSelector((state) => state.gmailChat?.setup?.status);
|
||||||
|
const gmailSetupError = useSelector((state) => state.gmailChat?.setup?.error);
|
||||||
|
|
||||||
// 获取可用知识库列表和聊天历史
|
// 获取可用知识库列表和聊天历史
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,6 +69,157 @@ export default function NewChat() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理Gmail集成
|
||||||
|
const handleGmailIntegration = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!talentEmail) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '请输入达人邮箱',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGmailLoading(true);
|
||||||
|
try {
|
||||||
|
// 调用Gmail设置API
|
||||||
|
const response = await dispatch(setupGmailChat(talentEmail)).unwrap();
|
||||||
|
|
||||||
|
// 如果是没有邮件往来的情况
|
||||||
|
if (response.message && response.message.includes('没有找到') && response.knowledge_base_id) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: response.message,
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用获取的知识库ID创建新会话
|
||||||
|
try {
|
||||||
|
const conversationResponse = await dispatch(
|
||||||
|
createConversation({
|
||||||
|
dataset_id_list: [response.knowledge_base_id],
|
||||||
|
})
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
console.log('使用Gmail知识库创建会话成功:', conversationResponse);
|
||||||
|
|
||||||
|
if (conversationResponse && conversationResponse.conversation_id) {
|
||||||
|
// 保存警告信息到localStorage
|
||||||
|
localStorage.setItem(
|
||||||
|
'gmailNoEmailsWarning',
|
||||||
|
JSON.stringify({
|
||||||
|
message: response.message,
|
||||||
|
troubleshooting: response.troubleshooting || null,
|
||||||
|
chatId: conversationResponse.conversation_id,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
isFirstMessage: true, // 标记这是首次消息
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 导航到新会话
|
||||||
|
navigate(`/chat/${response.knowledge_base_id}/${conversationResponse.conversation_id}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('未能获取会话ID');
|
||||||
|
}
|
||||||
|
} catch (convError) {
|
||||||
|
console.error('创建会话失败:', convError);
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `创建会话失败: ${convError.message || '请重试'}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果成功返回知识库ID和会话ID
|
||||||
|
else if (response && response.data && response.data.knowledge_base_id && response.data.conversation_id) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: response.message || 'Gmail集成成功',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// 导航到新会话
|
||||||
|
navigate(`/chat/${response.data.knowledge_base_id}/${response.data.conversation_id}`);
|
||||||
|
} else if (response && response.status === 'authorization_required' && response.auth_url) {
|
||||||
|
// 需要授权
|
||||||
|
setAuthUrl(response.auth_url);
|
||||||
|
setShowAuthCodeInput(true);
|
||||||
|
// 打开授权URL
|
||||||
|
window.open(response.auth_url, '_blank');
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '需要Gmail授权,请在新打开的窗口中完成授权并获取授权码',
|
||||||
|
type: 'info',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gmail集成失败:', error);
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `Gmail集成失败: ${error.message || '请重试'}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsGmailLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理授权码提交
|
||||||
|
const handleAuthCodeSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!authCode) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: '请输入授权码',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGmailLoading(true);
|
||||||
|
try {
|
||||||
|
// 使用授权码再次调用Gmail设置API
|
||||||
|
const response = await dispatch(
|
||||||
|
setupGmailChat({
|
||||||
|
talent_gmail: talentEmail,
|
||||||
|
auth_code: authCode,
|
||||||
|
})
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
if (response && response.data && response.data.knowledge_base_id && response.data.conversation_id) {
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: response.message || 'Gmail集成成功',
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// 导航到新会话
|
||||||
|
navigate(`/chat/${response.data.knowledge_base_id}/${response.data.conversation_id}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('未能获取会话信息');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('授权码提交失败:', error);
|
||||||
|
dispatch(
|
||||||
|
showNotification({
|
||||||
|
message: `授权码提交失败: ${error.message || '请重试'}`,
|
||||||
|
type: 'danger',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsGmailLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 开始聊天
|
// 开始聊天
|
||||||
const handleStartChat = async () => {
|
const handleStartChat = async () => {
|
||||||
if (selectedDatasetIds.length === 0) {
|
if (selectedDatasetIds.length === 0) {
|
||||||
@ -188,6 +351,121 @@ export default function NewChat() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Gmail集成模块 */}
|
||||||
|
<div className='card mb-5 shadow-sm'>
|
||||||
|
<div className='card-body'>
|
||||||
|
<h4 className='card-title mb-4'>Gmail邮件集成</h4>
|
||||||
|
|
||||||
|
{!showAuthCodeInput ? (
|
||||||
|
<form onSubmit={handleGmailIntegration} className='mb-3'>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='talentEmail' className='form-label'>
|
||||||
|
达人邮箱地址
|
||||||
|
</label>
|
||||||
|
<div className='input-group'>
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
className='form-control'
|
||||||
|
id='talentEmail'
|
||||||
|
placeholder='请输入正在联络的达人邮箱'
|
||||||
|
value={talentEmail}
|
||||||
|
onChange={(e) => setTalentEmail(e.target.value)}
|
||||||
|
disabled={isGmailLoading}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btn-outline-dark'
|
||||||
|
type='submit'
|
||||||
|
disabled={isGmailLoading || !talentEmail}
|
||||||
|
>
|
||||||
|
{isGmailLoading ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-2'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
处理中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'创建邮件会话'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='form-text'>将创建一个包含您与该达人Gmail邮件沟通记录的知识库及会话</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleAuthCodeSubmit} className='mb-3'>
|
||||||
|
<div className='alert alert-info mb-3'>
|
||||||
|
<p className='mb-2'>
|
||||||
|
<strong>需要Gmail授权</strong>
|
||||||
|
</p>
|
||||||
|
<p className='mb-2'>系统已在新窗口打开Gmail授权页面,请完成授权并获取授权码。</p>
|
||||||
|
<p className='mb-0'>
|
||||||
|
<a
|
||||||
|
href={authUrl}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='btn btn-sm btn-outline-primary'
|
||||||
|
>
|
||||||
|
重新打开授权页面
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor='authCode' className='form-label'>
|
||||||
|
授权码
|
||||||
|
</label>
|
||||||
|
<div className='input-group'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='form-control'
|
||||||
|
id='authCode'
|
||||||
|
placeholder='请输入Gmail授权页面提供的授权码'
|
||||||
|
value={authCode}
|
||||||
|
onChange={(e) => setAuthCode(e.target.value)}
|
||||||
|
disabled={isGmailLoading}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btn-outline-success'
|
||||||
|
type='submit'
|
||||||
|
disabled={isGmailLoading || !authCode}
|
||||||
|
>
|
||||||
|
{isGmailLoading ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className='spinner-border spinner-border-sm me-2'
|
||||||
|
role='status'
|
||||||
|
aria-hidden='true'
|
||||||
|
></span>
|
||||||
|
处理中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'提交授权码'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-link ps-0'
|
||||||
|
onClick={() => {
|
||||||
|
setShowAuthCodeInput(false);
|
||||||
|
setAuthCode('');
|
||||||
|
setAuthUrl('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
« 返回输入达人邮箱
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='d-flex justify-content-between align-items-center mb-4'>
|
<div className='d-flex justify-content-between align-items-center mb-4'>
|
||||||
<h4 className='m-0'>选择知识库开始聊天</h4>
|
<h4 className='m-0'>选择知识库开始聊天</h4>
|
||||||
<button
|
<button
|
||||||
|
@ -15,7 +15,6 @@ import AccessRequestModal from '../../components/AccessRequestModal';
|
|||||||
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
|
import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal';
|
||||||
import Pagination from '../../components/Pagination';
|
import Pagination from '../../components/Pagination';
|
||||||
import SearchBar from '../../components/SearchBar';
|
import SearchBar from '../../components/SearchBar';
|
||||||
import ApiModeSwitch from '../../components/ApiModeSwitch';
|
|
||||||
|
|
||||||
// 导入拆分的组件
|
// 导入拆分的组件
|
||||||
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
import KnowledgeBaseList from './components/KnowledgeBaseList';
|
||||||
@ -461,9 +460,6 @@ export default function KnowledgeBase() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='knowledge-base container my-4'>
|
<div className='knowledge-base container my-4'>
|
||||||
{/* <div className='api-mode-control mb-3'>
|
|
||||||
<ApiModeSwitch />
|
|
||||||
</div> */}
|
|
||||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={searchKeyword}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { mockGet, mockPost, mockPut, mockDelete } from './mockApi';
|
|
||||||
|
|
||||||
const secretKey = import.meta.env.VITE_SECRETKEY;
|
const secretKey = import.meta.env.VITE_SECRETKEY;
|
||||||
|
|
||||||
@ -42,13 +41,6 @@ api.interceptors.response.use(
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 处理服务器无法连接的情况
|
|
||||||
if (!error.response || error.code === 'ECONNABORTED' || error.message.includes('Network Error')) {
|
|
||||||
console.error('Server appears to be down. Switching to mock data.');
|
|
||||||
isServerDown = true;
|
|
||||||
hasCheckedServer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle errors in the response
|
// Handle errors in the response
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// monitor /verify
|
// monitor /verify
|
||||||
@ -74,52 +66,20 @@ api.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 检查服务器状态
|
|
||||||
export const checkServerStatus = async () => {
|
|
||||||
try {
|
|
||||||
// await api.get('/health-check', { timeout: 3000 });
|
|
||||||
isServerDown = false;
|
|
||||||
hasCheckedServer = true;
|
|
||||||
console.log('Server connection established');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
isServerDown = true;
|
|
||||||
hasCheckedServer = true;
|
|
||||||
console.error('Server connection failed, using mock data');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始检查服务器状态
|
// Define common HTTP methods
|
||||||
checkServerStatus();
|
|
||||||
|
|
||||||
// Define common HTTP methods with fallback to mock API
|
|
||||||
const get = async (url, params = {}) => {
|
const get = async (url, params = {}) => {
|
||||||
try {
|
try {
|
||||||
if (isServerDown) {
|
|
||||||
console.log(`[MOCK MODE] GET ${url}`);
|
|
||||||
return await mockGet(url, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api.get(url, { ...params });
|
const res = await api.get(url, { ...params });
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
|
||||||
console.log(`Failed to connect to server. Falling back to mock API for GET ${url}`);
|
|
||||||
return await mockGet(url, params);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle POST requests for JSON data with fallback to mock API
|
// Handle POST requests for JSON data
|
||||||
const post = async (url, data, isMultipart = false) => {
|
const post = async (url, data, isMultipart = false) => {
|
||||||
try {
|
try {
|
||||||
if (isServerDown) {
|
|
||||||
console.log(`[MOCK MODE] POST ${url}`);
|
|
||||||
return await mockPost(url, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = isMultipart
|
const headers = isMultipart
|
||||||
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
||||||
: { 'Content-Type': 'application/json' }; // For JSON data
|
: { 'Content-Type': 'application/json' }; // For JSON data
|
||||||
@ -127,61 +87,34 @@ const post = async (url, data, isMultipart = false) => {
|
|||||||
const res = await api.post(url, data, { headers });
|
const res = await api.post(url, data, { headers });
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
|
||||||
console.log(`Failed to connect to server. Falling back to mock API for POST ${url}`);
|
|
||||||
return await mockPost(url, data);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle PUT requests with fallback to mock API
|
// Handle PUT requests
|
||||||
const put = async (url, data) => {
|
const put = async (url, data) => {
|
||||||
try {
|
try {
|
||||||
if (isServerDown) {
|
|
||||||
console.log(`[MOCK MODE] PUT ${url}`);
|
|
||||||
return await mockPut(url, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api.put(url, data, {
|
const res = await api.put(url, data, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
|
||||||
console.log(`Failed to connect to server. Falling back to mock API for PUT ${url}`);
|
|
||||||
return await mockPut(url, data);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle DELETE requests with fallback to mock API
|
// Handle DELETE requests
|
||||||
const del = async (url) => {
|
const del = async (url) => {
|
||||||
try {
|
try {
|
||||||
if (isServerDown) {
|
|
||||||
console.log(`[MOCK MODE] DELETE ${url}`);
|
|
||||||
return await mockDelete(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await api.delete(url);
|
const res = await api.delete(url);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
|
||||||
console.log(`Failed to connect to server. Falling back to mock API for DELETE ${url}`);
|
|
||||||
return await mockDelete(url);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = async (url, data) => {
|
const upload = async (url, data) => {
|
||||||
try {
|
try {
|
||||||
if (isServerDown) {
|
|
||||||
console.log(`[MOCK MODE] Upload ${url}`);
|
|
||||||
return await mockPost(url, data, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const axiosInstance = await axios.create({
|
const axiosInstance = await axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
headers: {
|
headers: {
|
||||||
@ -191,41 +124,12 @@ const upload = async (url, data) => {
|
|||||||
const res = await axiosInstance.post(url, data);
|
const res = await axiosInstance.post(url, data);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCheckedServer || (error.request && !error.response)) {
|
|
||||||
console.log(`Failed to connect to server. Falling back to mock API for Upload ${url}`);
|
|
||||||
return await mockPost(url, data, true);
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 手动切换到模拟API(为调试目的)
|
|
||||||
export const switchToMockApi = () => {
|
|
||||||
isServerDown = true;
|
|
||||||
hasCheckedServer = true;
|
|
||||||
console.log('Manually switched to mock API');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 手动切换回真实API
|
|
||||||
export const switchToRealApi = async () => {
|
|
||||||
// 重新检查服务器状态
|
|
||||||
const isServerUp = await checkServerStatus();
|
|
||||||
console.log(isServerUp ? 'Switched back to real API' : 'Server still down, continuing with mock API');
|
|
||||||
return isServerUp;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle streaming requests
|
// Handle streaming requests
|
||||||
const streamRequest = async (url, data, onChunk, onError) => {
|
const streamRequest = async (url, data, onChunk, onError) => {
|
||||||
try {
|
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
|
// 获取认证Token
|
||||||
const encryptedToken = sessionStorage.getItem('token') || '';
|
const encryptedToken = sessionStorage.getItem('token') || '';
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,18 +3,20 @@ import { get, post } from '../../services/api';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取聊天消息
|
* 获取聊天消息
|
||||||
* @param {string} chatId - 聊天ID
|
* @param {string} conversationId - 会话ID
|
||||||
*/
|
*/
|
||||||
export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (chatId, { rejectWithValue }) => {
|
export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (conversationId, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await get(`/chat-history/${chatId}/messages/`);
|
const response = await get(`/chat-history/conversation_detail`, {
|
||||||
|
params: { conversation_id: conversationId },
|
||||||
|
});
|
||||||
|
|
||||||
// 处理返回格式
|
// 处理返回格式
|
||||||
if (response && response.code === 200) {
|
if (response && response.code === 200) {
|
||||||
return response.data.messages;
|
return response.data.messages || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data?.messages || [];
|
return [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return rejectWithValue(error.response?.data?.message || 'Failed to fetch messages');
|
return rejectWithValue(error.response?.data?.message || 'Failed to fetch messages');
|
||||||
}
|
}
|
||||||
@ -35,7 +37,10 @@ export const sendMessage = createAsyncThunk('chat/sendMessage', async ({ chatId,
|
|||||||
|
|
||||||
// 处理返回格式
|
// 处理返回格式
|
||||||
if (response && response.code === 200) {
|
if (response && response.code === 200) {
|
||||||
return response.data;
|
return {
|
||||||
|
...response.data,
|
||||||
|
role: response.data.role || 'user', // 确保有角色字段
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data || {};
|
return response.data || {};
|
||||||
|
@ -2,7 +2,6 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||||||
import {
|
import {
|
||||||
fetchAvailableDatasets,
|
fetchAvailableDatasets,
|
||||||
fetchChats,
|
fetchChats,
|
||||||
createChat,
|
|
||||||
updateChat,
|
updateChat,
|
||||||
deleteChat,
|
deleteChat,
|
||||||
createChatRecord,
|
createChatRecord,
|
||||||
@ -13,28 +12,24 @@ import { fetchMessages, sendMessage } from './chat.messages.thunks';
|
|||||||
|
|
||||||
// 初始状态
|
// 初始状态
|
||||||
const initialState = {
|
const initialState = {
|
||||||
// Chat history state
|
// 聊天列表,包含所有聊天及其消息
|
||||||
history: {
|
chats: {
|
||||||
items: [],
|
items: [], // 每个chat对象包含conversation_id, datasets, create_time, messages等
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
error: null,
|
error: null,
|
||||||
|
pagination: {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
page_size: 10,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Chat session creation state
|
// 当前活跃聊天的ID
|
||||||
createSession: {
|
activeConversationId: null,
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
// 消息发送状态
|
||||||
error: null,
|
messageOperation: {
|
||||||
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'
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
error: null,
|
error: null,
|
||||||
|
streamingMessageId: null, // 当前正在流式传输的消息ID(如果有)
|
||||||
},
|
},
|
||||||
// 可用于聊天的知识库列表
|
// 可用于聊天的知识库列表
|
||||||
availableDatasets: {
|
availableDatasets: {
|
||||||
@ -42,26 +37,16 @@ const initialState = {
|
|||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
// 操作状态(创建、更新、删除)
|
// 聊天操作状态(创建、更新、删除)
|
||||||
operations: {
|
chatOperation: {
|
||||||
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
// 兼容旧版本的state结构
|
};
|
||||||
list: {
|
|
||||||
items: [],
|
// 辅助函数:查找聊天索引
|
||||||
total: 0,
|
const findChatIndex = (state, conversationId) => {
|
||||||
page: 1,
|
return state.chats.items.findIndex((chat) => chat.conversation_id === conversationId);
|
||||||
page_size: 10,
|
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
// 当前聊天
|
|
||||||
currentChat: {
|
|
||||||
data: null,
|
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建 slice
|
// 创建 slice
|
||||||
@ -70,57 +55,98 @@ const chatSlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
// 重置操作状态
|
// 重置操作状态
|
||||||
resetOperationStatus: (state) => {
|
resetChatOperation: (state) => {
|
||||||
state.operations.status = 'idle';
|
state.chatOperation.status = 'idle';
|
||||||
state.operations.error = null;
|
state.chatOperation.error = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重置当前聊天
|
// 设置当前活跃聊天
|
||||||
resetCurrentChat: (state) => {
|
setActiveChat: (state, action) => {
|
||||||
state.currentChat.data = null;
|
state.activeConversationId = action.payload;
|
||||||
state.currentChat.status = 'idle';
|
|
||||||
state.currentChat.error = null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置当前聊天
|
// 重置消息操作状态
|
||||||
setCurrentChat: (state, action) => {
|
resetMessageOperation: (state) => {
|
||||||
state.currentChat.data = action.payload;
|
state.messageOperation.status = 'idle';
|
||||||
state.currentChat.status = 'succeeded';
|
state.messageOperation.error = null;
|
||||||
|
state.messageOperation.streamingMessageId = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重置消息状态
|
// 添加消息到特定聊天
|
||||||
resetMessages: (state) => {
|
|
||||||
state.messages.items = [];
|
|
||||||
state.messages.status = 'idle';
|
|
||||||
state.messages.error = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 重置发送消息状态
|
|
||||||
resetSendMessageStatus: (state) => {
|
|
||||||
state.sendMessage.status = 'idle';
|
|
||||||
state.sendMessage.error = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 添加消息
|
|
||||||
addMessage: (state, action) => {
|
addMessage: (state, action) => {
|
||||||
state.messages.items.push(action.payload);
|
const { conversationId, message } = action.payload;
|
||||||
|
const chatIndex = findChatIndex(state, conversationId);
|
||||||
|
|
||||||
|
if (chatIndex !== -1) {
|
||||||
|
// 确保chat有messages数组
|
||||||
|
if (!state.chats.items[chatIndex].messages) {
|
||||||
|
state.chats.items[chatIndex].messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加消息
|
||||||
|
state.chats.items[chatIndex].messages.push(message);
|
||||||
|
|
||||||
|
// 更新最后一条消息和消息计数
|
||||||
|
state.chats.items[chatIndex].last_message = message.content;
|
||||||
|
state.chats.items[chatIndex].message_count = (state.chats.items[chatIndex].message_count || 0) + 1;
|
||||||
|
|
||||||
|
// 如果是助手消息且正在流式传输,记录ID
|
||||||
|
if (message.role === 'assistant' && message.is_streaming) {
|
||||||
|
state.messageOperation.streamingMessageId = message.id;
|
||||||
|
state.messageOperation.status = 'loading';
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新消息(用于流式传输)
|
// 更新消息(用于流式传输)
|
||||||
updateMessage: (state, action) => {
|
updateMessage: (state, action) => {
|
||||||
const { id, ...updates } = action.payload;
|
const { conversationId, messageId, updates, serverMessageId } = action.payload;
|
||||||
const messageIndex = state.messages.items.findIndex((msg) => msg.id === id);
|
const chatIndex = findChatIndex(state, conversationId);
|
||||||
|
|
||||||
if (messageIndex !== -1) {
|
if (chatIndex !== -1 && state.chats.items[chatIndex].messages) {
|
||||||
// 更新现有消息
|
// 首先尝试使用服务器返回的ID找到消息
|
||||||
state.messages.items[messageIndex] = {
|
let messageIndex = -1;
|
||||||
...state.messages.items[messageIndex],
|
|
||||||
...updates,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果流式传输结束,更新发送消息状态
|
if (serverMessageId) {
|
||||||
if (updates.is_streaming === false) {
|
// 如果提供了服务器ID,优先使用它查找消息
|
||||||
state.sendMessage.status = 'succeeded';
|
messageIndex = state.chats.items[chatIndex].messages.findIndex(
|
||||||
|
(msg) => msg.id === serverMessageId || msg.server_id === serverMessageId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没找到或没提供服务器ID,则使用客户端生成的ID
|
||||||
|
if (messageIndex === -1) {
|
||||||
|
messageIndex = state.chats.items[chatIndex].messages.findIndex((msg) => msg.id === messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageIndex !== -1) {
|
||||||
|
// 更新现有消息
|
||||||
|
const updatedMessage = {
|
||||||
|
...state.chats.items[chatIndex].messages[messageIndex],
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果收到了服务器ID且消息没有server_id字段,添加它
|
||||||
|
if (serverMessageId && !updatedMessage.server_id) {
|
||||||
|
updatedMessage.server_id = serverMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.chats.items[chatIndex].messages[messageIndex] = updatedMessage;
|
||||||
|
|
||||||
|
// 如果流式传输结束,更新状态
|
||||||
|
if (
|
||||||
|
updates.is_streaming === false &&
|
||||||
|
(messageId === state.messageOperation.streamingMessageId ||
|
||||||
|
serverMessageId === state.messageOperation.streamingMessageId)
|
||||||
|
) {
|
||||||
|
state.messageOperation.status = 'succeeded';
|
||||||
|
state.messageOperation.streamingMessageId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果更新了内容,更新最后一条消息
|
||||||
|
if (updates.content) {
|
||||||
|
state.chats.items[chatIndex].last_message = updates.content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -129,189 +155,161 @@ const chatSlice = createSlice({
|
|||||||
// 获取聊天列表
|
// 获取聊天列表
|
||||||
builder
|
builder
|
||||||
.addCase(fetchChats.pending, (state) => {
|
.addCase(fetchChats.pending, (state) => {
|
||||||
state.list.status = 'loading';
|
state.chats.status = 'loading';
|
||||||
state.history.status = 'loading';
|
|
||||||
})
|
})
|
||||||
.addCase(fetchChats.fulfilled, (state, action) => {
|
.addCase(fetchChats.fulfilled, (state, action) => {
|
||||||
state.list.status = 'succeeded';
|
state.chats.status = 'succeeded';
|
||||||
|
|
||||||
// 检查是否是追加模式
|
// 检查是否是追加模式
|
||||||
if (action.payload.append) {
|
if (action.payload.append) {
|
||||||
// 追加模式:将新结果添加到现有列表的前面
|
// 追加模式:将新结果添加到现有列表的前面
|
||||||
state.list.items = [...action.payload.results, ...state.list.items];
|
state.chats.items = [...action.payload.results, ...state.chats.items];
|
||||||
state.history.items = [...action.payload.results, ...state.history.items];
|
|
||||||
} else {
|
} else {
|
||||||
// 替换模式:使用新结果替换整个列表
|
// 替换模式:使用新结果替换整个列表
|
||||||
state.list.items = action.payload.results;
|
state.chats.items = action.payload.results;
|
||||||
state.list.total = action.payload.total;
|
state.chats.pagination.total = action.payload.total;
|
||||||
state.list.page = action.payload.page;
|
state.chats.pagination.page = action.payload.page;
|
||||||
state.list.page_size = action.payload.page_size;
|
state.chats.pagination.page_size = action.payload.page_size;
|
||||||
|
|
||||||
// 同时更新新的状态结构
|
|
||||||
state.history.items = action.payload.results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.history.status = 'succeeded';
|
|
||||||
state.history.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(fetchChats.rejected, (state, action) => {
|
.addCase(fetchChats.rejected, (state, action) => {
|
||||||
state.list.status = 'failed';
|
state.chats.status = 'failed';
|
||||||
state.list.error = action.payload || action.error.message;
|
state.chats.error = action.payload || action.error.message;
|
||||||
|
|
||||||
// 同时更新新的状态结构
|
|
||||||
state.history.status = 'failed';
|
|
||||||
state.history.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) => {
|
.addCase(deleteChat.pending, (state) => {
|
||||||
state.operations.status = 'loading';
|
state.chatOperation.status = 'loading';
|
||||||
})
|
})
|
||||||
.addCase(deleteChat.fulfilled, (state, action) => {
|
.addCase(deleteChat.fulfilled, (state, action) => {
|
||||||
state.operations.status = 'succeeded';
|
state.chatOperation.status = 'succeeded';
|
||||||
// 更新旧的状态结构
|
// 删除聊天
|
||||||
state.list.items = state.list.items.filter((chat) => chat.id !== action.payload);
|
state.chats.items = state.chats.items.filter((chat) => chat.conversation_id !== action.payload);
|
||||||
// 更新新的状态结构
|
|
||||||
state.history.items = state.history.items.filter((chat) => chat.conversation_id !== action.payload);
|
|
||||||
|
|
||||||
if (state.list.total > 0) {
|
if (state.chats.pagination.total > 0) {
|
||||||
state.list.total -= 1;
|
state.chats.pagination.total -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.currentChat.data && state.currentChat.data.id === action.payload) {
|
// 如果删除的是当前活跃聊天,重置activeConversationId
|
||||||
state.currentChat.data = null;
|
if (state.activeConversationId === action.payload) {
|
||||||
|
state.activeConversationId = null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCase(deleteChat.rejected, (state, action) => {
|
.addCase(deleteChat.rejected, (state, action) => {
|
||||||
state.operations.status = 'failed';
|
state.chatOperation.status = 'failed';
|
||||||
state.operations.error = action.payload || action.error.message;
|
state.chatOperation.error = action.payload || action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新聊天
|
// 更新聊天
|
||||||
.addCase(updateChat.pending, (state) => {
|
.addCase(updateChat.pending, (state) => {
|
||||||
state.operations.status = 'loading';
|
state.chatOperation.status = 'loading';
|
||||||
})
|
})
|
||||||
.addCase(updateChat.fulfilled, (state, action) => {
|
.addCase(updateChat.fulfilled, (state, action) => {
|
||||||
state.operations.status = 'succeeded';
|
state.chatOperation.status = 'succeeded';
|
||||||
const index = state.list.items.findIndex((chat) => chat.id === action.payload.id);
|
const chatIndex = findChatIndex(state, action.payload.conversation_id);
|
||||||
if (index !== -1) {
|
if (chatIndex !== -1) {
|
||||||
state.list.items[index] = action.payload;
|
// 保留messages字段,避免覆盖
|
||||||
}
|
const existingMessages = state.chats.items[chatIndex].messages;
|
||||||
if (state.currentChat.data && state.currentChat.data.id === action.payload.id) {
|
state.chats.items[chatIndex] = {
|
||||||
state.currentChat.data = action.payload;
|
...action.payload,
|
||||||
|
messages: existingMessages || [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCase(updateChat.rejected, (state, action) => {
|
.addCase(updateChat.rejected, (state, action) => {
|
||||||
state.operations.status = 'failed';
|
state.chatOperation.status = 'failed';
|
||||||
state.operations.error = action.payload || action.error.message;
|
state.chatOperation.error = action.payload || action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取聊天消息
|
// 获取聊天消息
|
||||||
.addCase(fetchMessages.pending, (state) => {
|
.addCase(fetchMessages.pending, (state) => {
|
||||||
state.messages.status = 'loading';
|
state.messageOperation.status = 'loading';
|
||||||
state.messages.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(fetchMessages.fulfilled, (state, action) => {
|
.addCase(fetchMessages.fulfilled, (state, action) => {
|
||||||
state.messages.status = 'succeeded';
|
state.messageOperation.status = 'succeeded';
|
||||||
state.messages.items = action.payload;
|
// 假设action.meta.arg是conversationId
|
||||||
|
const conversationId = action.meta.arg;
|
||||||
|
const chatIndex = findChatIndex(state, conversationId);
|
||||||
|
|
||||||
|
if (chatIndex !== -1) {
|
||||||
|
state.chats.items[chatIndex].messages = action.payload;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(fetchMessages.rejected, (state, action) => {
|
.addCase(fetchMessages.rejected, (state, action) => {
|
||||||
state.messages.status = 'failed';
|
state.messageOperation.status = 'failed';
|
||||||
state.messages.error = action.error.message;
|
state.messageOperation.error = action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 发送聊天消息
|
// 发送聊天消息
|
||||||
.addCase(sendMessage.pending, (state) => {
|
.addCase(sendMessage.pending, (state) => {
|
||||||
state.sendMessage.status = 'loading';
|
state.messageOperation.status = 'loading';
|
||||||
state.sendMessage.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(sendMessage.fulfilled, (state, action) => {
|
.addCase(sendMessage.fulfilled, (state, action) => {
|
||||||
state.sendMessage.status = 'succeeded';
|
state.messageOperation.status = 'succeeded';
|
||||||
// 更新消息列表
|
// 假设action.meta.arg包含chatId
|
||||||
const index = state.messages.items.findIndex(
|
const { chatId } = action.meta.arg;
|
||||||
(msg) => msg.content === action.payload.content && msg.sender === action.payload.sender
|
const chatIndex = findChatIndex(state, chatId);
|
||||||
);
|
|
||||||
if (index === -1) {
|
if (chatIndex !== -1) {
|
||||||
state.messages.items.push(action.payload);
|
// 确保chat有messages数组
|
||||||
|
if (!state.chats.items[chatIndex].messages) {
|
||||||
|
state.chats.items[chatIndex].messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查消息是否已存在
|
||||||
|
const messageExists = state.chats.items[chatIndex].messages.some(
|
||||||
|
(msg) => msg.content === action.payload.content && msg.role === action.payload.role
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!messageExists) {
|
||||||
|
state.chats.items[chatIndex].messages.push(action.payload);
|
||||||
|
state.chats.items[chatIndex].last_message = action.payload.content;
|
||||||
|
state.chats.items[chatIndex].message_count =
|
||||||
|
(state.chats.items[chatIndex].message_count || 0) + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCase(sendMessage.rejected, (state, action) => {
|
.addCase(sendMessage.rejected, (state, action) => {
|
||||||
state.sendMessage.status = 'failed';
|
state.messageOperation.status = 'failed';
|
||||||
state.sendMessage.error = action.error.message;
|
state.messageOperation.error = action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理创建聊天记录
|
// 处理创建聊天记录
|
||||||
.addCase(createChatRecord.pending, (state) => {
|
.addCase(createChatRecord.pending, (state) => {
|
||||||
state.sendMessage.status = 'loading';
|
state.messageOperation.status = 'loading';
|
||||||
state.sendMessage.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(createChatRecord.fulfilled, (state, action) => {
|
.addCase(createChatRecord.fulfilled, (state, action) => {
|
||||||
// 更新状态以反映聊天已创建
|
// 聊天创建成功,但消息状态由addMessage和updateMessage处理
|
||||||
if (action.payload.conversation_id && !state.currentChat.data) {
|
state.activeConversationId = action.payload.conversation_id;
|
||||||
// 设置当前聊天的会话ID
|
|
||||||
state.currentChat.data = {
|
|
||||||
conversation_id: action.payload.conversation_id,
|
|
||||||
// 其他信息将由流式更新填充
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 不再在这里添加消息,因为消息已经在thunk函数中添加
|
|
||||||
})
|
})
|
||||||
.addCase(createChatRecord.rejected, (state, action) => {
|
.addCase(createChatRecord.rejected, (state, action) => {
|
||||||
state.sendMessage.status = 'failed';
|
state.messageOperation.status = 'failed';
|
||||||
state.sendMessage.error = action.error.message;
|
state.messageOperation.error = action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理创建会话
|
// 处理创建会话
|
||||||
.addCase(createConversation.pending, (state) => {
|
.addCase(createConversation.pending, (state) => {
|
||||||
state.createSession.status = 'loading';
|
state.chatOperation.status = 'loading';
|
||||||
state.createSession.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(createConversation.fulfilled, (state, action) => {
|
.addCase(createConversation.fulfilled, (state, action) => {
|
||||||
state.createSession.status = 'succeeded';
|
state.chatOperation.status = 'succeeded';
|
||||||
state.createSession.sessionId = action.payload.conversation_id;
|
state.activeConversationId = action.payload.conversation_id;
|
||||||
|
|
||||||
// 当前聊天设置 - 使用与fetchConversationDetail相同的数据结构
|
// 在执行createConversation时,已经通过dispatch添加了新聊天到列表
|
||||||
state.currentChat.data = {
|
// 所以这里只需确保当前激活的聊天ID已设置
|
||||||
conversation_id: action.payload.conversation_id,
|
|
||||||
datasets: action.payload.datasets || [],
|
|
||||||
// 添加其他必要的字段,确保与fetchConversationDetail返回的数据结构兼容
|
|
||||||
messages: [],
|
|
||||||
create_time: new Date().toISOString(),
|
|
||||||
update_time: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
state.currentChat.status = 'succeeded';
|
|
||||||
state.currentChat.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(createConversation.rejected, (state, action) => {
|
.addCase(createConversation.rejected, (state, action) => {
|
||||||
state.createSession.status = 'failed';
|
state.chatOperation.status = 'failed';
|
||||||
state.createSession.error = action.payload || action.error.message;
|
state.chatOperation.error = action.payload || action.error.message;
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理获取可用知识库
|
// 处理获取可用知识库
|
||||||
.addCase(fetchAvailableDatasets.pending, (state) => {
|
.addCase(fetchAvailableDatasets.pending, (state) => {
|
||||||
state.availableDatasets.status = 'loading';
|
state.availableDatasets.status = 'loading';
|
||||||
state.availableDatasets.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(fetchAvailableDatasets.fulfilled, (state, action) => {
|
.addCase(fetchAvailableDatasets.fulfilled, (state, action) => {
|
||||||
state.availableDatasets.status = 'succeeded';
|
state.availableDatasets.status = 'succeeded';
|
||||||
state.availableDatasets.items = action.payload || [];
|
state.availableDatasets.items = action.payload || [];
|
||||||
state.availableDatasets.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(fetchAvailableDatasets.rejected, (state, action) => {
|
.addCase(fetchAvailableDatasets.rejected, (state, action) => {
|
||||||
state.availableDatasets.status = 'failed';
|
state.availableDatasets.status = 'failed';
|
||||||
@ -320,35 +318,38 @@ const chatSlice = createSlice({
|
|||||||
|
|
||||||
// 获取会话详情
|
// 获取会话详情
|
||||||
.addCase(fetchConversationDetail.pending, (state) => {
|
.addCase(fetchConversationDetail.pending, (state) => {
|
||||||
state.currentChat.status = 'loading';
|
// 设置加载状态
|
||||||
state.currentChat.error = null;
|
|
||||||
})
|
})
|
||||||
.addCase(fetchConversationDetail.fulfilled, (state, action) => {
|
.addCase(fetchConversationDetail.fulfilled, (state, action) => {
|
||||||
|
// 如果有返回数据
|
||||||
if (action.payload) {
|
if (action.payload) {
|
||||||
state.currentChat.status = 'succeeded';
|
const conversationId = action.payload.conversation_id;
|
||||||
state.currentChat.data = action.payload;
|
const chatIndex = findChatIndex(state, conversationId);
|
||||||
} else {
|
|
||||||
state.currentChat.status = 'idle';
|
if (chatIndex !== -1) {
|
||||||
state.currentChat.data = null;
|
// 更新现有聊天
|
||||||
|
state.chats.items[chatIndex] = {
|
||||||
|
...state.chats.items[chatIndex],
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 添加新聊天
|
||||||
|
state.chats.items.push(action.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置为当前活跃聊天
|
||||||
|
state.activeConversationId = conversationId;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCase(fetchConversationDetail.rejected, (state, action) => {
|
.addCase(fetchConversationDetail.rejected, (state, action) => {
|
||||||
state.currentChat.status = 'failed';
|
// 仅在操作失败时设置错误状态
|
||||||
state.currentChat.error = action.payload || action.error.message;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 导出 actions
|
// 导出 actions
|
||||||
export const {
|
export const { resetChatOperation, setActiveChat, resetMessageOperation, addMessage, updateMessage } =
|
||||||
resetOperationStatus,
|
chatSlice.actions;
|
||||||
resetCurrentChat,
|
|
||||||
setCurrentChat,
|
|
||||||
resetMessages,
|
|
||||||
resetSendMessageStatus,
|
|
||||||
addMessage,
|
|
||||||
updateMessage,
|
|
||||||
} = chatSlice.actions;
|
|
||||||
|
|
||||||
// 导出 reducer
|
// 导出 reducer
|
||||||
export default chatSlice.reducer;
|
export default chatSlice.reducer;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { get, post, put, del, streamRequest } from '../../services/api';
|
import { get, post, put, del, streamRequest } from '../../services/api';
|
||||||
import { showNotification } from '../notification.slice';
|
import { showNotification } from '../notification.slice';
|
||||||
import { addMessage, updateMessage, setCurrentChat } from './chat.slice';
|
import { addMessage, updateMessage, setActiveChat } from './chat.slice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取聊天列表
|
* 获取聊天列表
|
||||||
@ -30,29 +30,9 @@ export const fetchChats = createAsyncThunk('chat/fetchChats', async (params = {}
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新聊天
|
|
||||||
* @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?.message || 'Failed to create chat');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新聊天
|
* 更新聊天
|
||||||
|
* 更新已经发送出去的聊天,获得新的回复,相当于编辑以往的聊天记录。暂时未使用
|
||||||
* @param {Object} params - 更新参数
|
* @param {Object} params - 更新参数
|
||||||
* @param {string} params.id - 聊天ID
|
* @param {string} params.id - 聊天ID
|
||||||
* @param {Object} params.data - 更新数据
|
* @param {Object} params.data - 更新数据
|
||||||
@ -114,7 +94,8 @@ export const fetchAvailableDatasets = createAsyncThunk(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建聊天记录
|
* 创建/继续聊天
|
||||||
|
* 创建新会话,或者继续一个已有的会话,根据conversation_id来更新
|
||||||
* @param {Object} params - 聊天参数
|
* @param {Object} params - 聊天参数
|
||||||
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
||||||
* @param {string} params.question - 用户问题
|
* @param {string} params.question - 用户问题
|
||||||
@ -134,24 +115,36 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
|
|
||||||
// 先添加用户消息到聊天窗口
|
// 先添加用户消息到聊天窗口
|
||||||
const userMessageId = Date.now().toString();
|
const userMessageId = Date.now().toString();
|
||||||
|
const userMessage = {
|
||||||
|
id: userMessageId,
|
||||||
|
role: 'user',
|
||||||
|
content: question,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加用户消息
|
||||||
dispatch(
|
dispatch(
|
||||||
addMessage({
|
addMessage({
|
||||||
id: userMessageId,
|
conversationId: conversation_id,
|
||||||
role: 'user',
|
message: userMessage,
|
||||||
content: question,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 添加临时的助手消息(流式传输期间显示)
|
// 添加临时的助手消息(流式传输期间显示)
|
||||||
const assistantMessageId = (Date.now() + 1).toString();
|
const assistantMessageId = (Date.now() + 1).toString();
|
||||||
|
const assistantMessage = {
|
||||||
|
id: assistantMessageId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
is_streaming: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加助手消息
|
||||||
dispatch(
|
dispatch(
|
||||||
addMessage({
|
addMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversation_id,
|
||||||
role: 'assistant',
|
message: assistantMessage,
|
||||||
content: '',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
is_streaming: true,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -185,11 +178,16 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
finalMessage += data.data.content;
|
finalMessage += data.data.content;
|
||||||
console.log('累加内容:', finalMessage);
|
console.log('累加内容:', finalMessage);
|
||||||
|
|
||||||
|
// 获取服务器消息ID (如果存在)
|
||||||
|
const serverMessageId = data.data.id;
|
||||||
|
|
||||||
// 更新消息内容
|
// 更新消息内容
|
||||||
dispatch(
|
dispatch(
|
||||||
updateMessage({
|
updateMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversationId,
|
||||||
content: finalMessage,
|
messageId: assistantMessageId,
|
||||||
|
serverMessageId: serverMessageId,
|
||||||
|
updates: { content: finalMessage },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -197,10 +195,16 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
// 处理结束标志
|
// 处理结束标志
|
||||||
if (data.data.is_end) {
|
if (data.data.is_end) {
|
||||||
console.log('检测到消息结束标志');
|
console.log('检测到消息结束标志');
|
||||||
|
|
||||||
|
// 获取服务器消息ID (如果存在)
|
||||||
|
const serverMessageId = data.data.id;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateMessage({
|
updateMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversationId,
|
||||||
is_streaming: false,
|
messageId: assistantMessageId,
|
||||||
|
serverMessageId: serverMessageId,
|
||||||
|
updates: { is_streaming: false },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -217,10 +221,15 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
messageType === '结束流式传输'
|
messageType === '结束流式传输'
|
||||||
) {
|
) {
|
||||||
console.log('收到完成消息');
|
console.log('收到完成消息');
|
||||||
|
// 获取服务器消息ID (如果存在)
|
||||||
|
const serverMessageId = data.data?.id;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateMessage({
|
updateMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversationId,
|
||||||
is_streaming: false,
|
messageId: assistantMessageId,
|
||||||
|
serverMessageId: serverMessageId,
|
||||||
|
updates: { is_streaming: false },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -230,10 +239,15 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
// 如果有content字段,也尝试更新
|
// 如果有content字段,也尝试更新
|
||||||
if (data.data && data.data.content !== undefined) {
|
if (data.data && data.data.content !== undefined) {
|
||||||
finalMessage += data.data.content;
|
finalMessage += data.data.content;
|
||||||
|
// 获取服务器消息ID (如果存在)
|
||||||
|
const serverMessageId = data.data.id;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateMessage({
|
updateMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversationId,
|
||||||
content: finalMessage,
|
messageId: assistantMessageId,
|
||||||
|
serverMessageId: serverMessageId,
|
||||||
|
updates: { content: finalMessage },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -250,9 +264,12 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
console.error('流式请求错误:', error);
|
console.error('流式请求错误:', error);
|
||||||
dispatch(
|
dispatch(
|
||||||
updateMessage({
|
updateMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversationId,
|
||||||
content: `错误: ${error.message || '请求失败'}`,
|
messageId: assistantMessageId,
|
||||||
is_streaming: false,
|
updates: {
|
||||||
|
content: `错误: ${error.message || '请求失败'}`,
|
||||||
|
is_streaming: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -261,8 +278,9 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
// 确保流式传输结束后标记消息已完成
|
// 确保流式传输结束后标记消息已完成
|
||||||
dispatch(
|
dispatch(
|
||||||
updateMessage({
|
updateMessage({
|
||||||
id: assistantMessageId,
|
conversationId: conversationId,
|
||||||
is_streaming: false,
|
messageId: assistantMessageId,
|
||||||
|
updates: { is_streaming: false },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -277,7 +295,7 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
// 获取知识库信息
|
// 获取知识库信息
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const availableDatasets = state.chat.availableDatasets.items || [];
|
const availableDatasets = state.chat.availableDatasets.items || [];
|
||||||
const existingChats = state.chat.history.items || [];
|
const existingChats = state.chat.chats.items || [];
|
||||||
|
|
||||||
// 检查是否已存在此会话ID的记录
|
// 检查是否已存在此会话ID的记录
|
||||||
const existingChat = existingChats.find((chat) => chat.conversation_id === chatInfo.conversation_id);
|
const existingChat = existingChats.find((chat) => chat.conversation_id === chatInfo.conversation_id);
|
||||||
@ -303,6 +321,7 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
create_time: new Date().toISOString(),
|
create_time: new Date().toISOString(),
|
||||||
last_message: question,
|
last_message: question,
|
||||||
message_count: 2, // 用户问题和助手回复
|
message_count: 2, // 用户问题和助手回复
|
||||||
|
messages: [userMessage, assistantMessage], // 添加消息到聊天记录
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新当前聊天
|
// 更新当前聊天
|
||||||
@ -319,23 +338,7 @@ export const createChatRecord = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置为当前聊天
|
// 设置为当前聊天
|
||||||
dispatch(
|
dispatch(setActiveChat(chatInfo.conversation_id));
|
||||||
setCurrentChat({
|
|
||||||
conversation_id: chatInfo.conversation_id,
|
|
||||||
datasets: existingChat
|
|
||||||
? existingChat.datasets
|
|
||||||
: dataset_id_list.map((id) => {
|
|
||||||
const formattedId = id.includes('-')
|
|
||||||
? id
|
|
||||||
: id.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5');
|
|
||||||
const dataset = availableDatasets.find((ds) => ds.id === formattedId);
|
|
||||||
return {
|
|
||||||
id: formattedId,
|
|
||||||
name: dataset?.name || '新知识库对话',
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return chatInfo;
|
return chatInfo;
|
||||||
@ -363,34 +366,11 @@ export const fetchConversationDetail = createAsyncThunk(
|
|||||||
'chat/fetchConversationDetail',
|
'chat/fetchConversationDetail',
|
||||||
async (conversationId, { rejectWithValue, dispatch, getState }) => {
|
async (conversationId, { rejectWithValue, dispatch, getState }) => {
|
||||||
try {
|
try {
|
||||||
// 先检查是否是刚创建的会话
|
|
||||||
const state = getState();
|
|
||||||
const createSession = state.chat.createSession || {};
|
|
||||||
const currentChat = state.chat.currentChat.data;
|
|
||||||
|
|
||||||
// 如果是刚创建成功的会话,且会话ID匹配,则直接返回现有会话数据
|
|
||||||
if (
|
|
||||||
createSession.status === 'succeeded' &&
|
|
||||||
createSession.sessionId === conversationId &&
|
|
||||||
currentChat?.conversation_id === conversationId
|
|
||||||
) {
|
|
||||||
console.log('使用新创建的会话数据,跳过详情请求:', conversationId);
|
|
||||||
return currentChat;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await get('/chat-history/conversation_detail', {
|
const response = await get('/chat-history/conversation_detail', {
|
||||||
params: { conversation_id: conversationId },
|
params: { conversation_id: conversationId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && response.code === 200) {
|
if (response && response.code === 200) {
|
||||||
// 如果存在消息,更新Redux状态
|
|
||||||
if (response.data.messages) {
|
|
||||||
dispatch({
|
|
||||||
type: 'chat/fetchMessages/fulfilled',
|
|
||||||
payload: response.data.messages,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,7 +391,7 @@ export const fetchConversationDetail = createAsyncThunk(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新会话(仅获取会话ID,不发送消息)
|
* 创建新会话(仅获取会话ID,相当于一个会话凭证,不发送消息)
|
||||||
* @param {Object} params - 参数
|
* @param {Object} params - 参数
|
||||||
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
* @param {string[]} params.dataset_id_list - 知识库ID列表
|
||||||
*/
|
*/
|
||||||
@ -450,6 +430,7 @@ export const createConversation = createAsyncThunk(
|
|||||||
create_time: new Date().toISOString(),
|
create_time: new Date().toISOString(),
|
||||||
last_message: '',
|
last_message: '',
|
||||||
message_count: 0,
|
message_count: 0,
|
||||||
|
messages: [], // 初始化空消息数组
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新聊天历史列表
|
// 更新聊天历史列表
|
||||||
@ -463,12 +444,7 @@ export const createConversation = createAsyncThunk(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 设置为当前聊天
|
// 设置为当前聊天
|
||||||
dispatch(
|
dispatch(setActiveChat(conversationData.conversation_id));
|
||||||
setCurrentChat({
|
|
||||||
conversation_id: conversationData.conversation_id,
|
|
||||||
datasets: newChatEntry.datasets,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return conversationData;
|
return conversationData;
|
||||||
}
|
}
|
||||||
|
101
src/store/gmailChat/gmailChat.slice.js
Normal file
101
src/store/gmailChat/gmailChat.slice.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { refreshGmailWatch, setupGmailChat, checkGmailAuth } from './gmailChat.thunks';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
|
error: null,
|
||||||
|
data: {
|
||||||
|
historyId: null,
|
||||||
|
expiration: null,
|
||||||
|
wasExpired: null,
|
||||||
|
gmailEmail: null,
|
||||||
|
},
|
||||||
|
setup: {
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
knowledgeBaseId: null,
|
||||||
|
conversationId: null,
|
||||||
|
message: null,
|
||||||
|
noEmailsWarning: null,
|
||||||
|
troubleshooting: null,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
needsSetup: false,
|
||||||
|
watchExpired: false,
|
||||||
|
lastHistoryId: null,
|
||||||
|
watchExpiration: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const gmailChatSlice = createSlice({
|
||||||
|
name: 'gmailChat',
|
||||||
|
initialState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(refreshGmailWatch.pending, (state) => {
|
||||||
|
state.status = 'loading';
|
||||||
|
})
|
||||||
|
.addCase(refreshGmailWatch.fulfilled, (state, action) => {
|
||||||
|
state.status = 'succeeded';
|
||||||
|
state.data = {
|
||||||
|
historyId: action.payload.data.history_id,
|
||||||
|
expiration: action.payload.data.expiration,
|
||||||
|
wasExpired: action.payload.data.was_expired,
|
||||||
|
gmailEmail: action.payload.data.gmail_email,
|
||||||
|
};
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(refreshGmailWatch.rejected, (state, action) => {
|
||||||
|
state.status = 'failed';
|
||||||
|
state.error = action.error.message;
|
||||||
|
})
|
||||||
|
.addCase(setupGmailChat.pending, (state) => {
|
||||||
|
state.setup.status = 'loading';
|
||||||
|
state.setup.noEmailsWarning = null;
|
||||||
|
state.setup.troubleshooting = null;
|
||||||
|
})
|
||||||
|
.addCase(setupGmailChat.fulfilled, (state, action) => {
|
||||||
|
state.setup.status = 'succeeded';
|
||||||
|
|
||||||
|
if (action.payload.message && action.payload.message.includes('没有找到')) {
|
||||||
|
state.setup.noEmailsWarning = action.payload.message;
|
||||||
|
state.setup.troubleshooting = action.payload.troubleshooting || null;
|
||||||
|
} else {
|
||||||
|
state.setup.noEmailsWarning = null;
|
||||||
|
state.setup.troubleshooting = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.setup.knowledgeBaseId =
|
||||||
|
action.payload.knowledge_base_id || action.payload.data?.knowledge_base_id;
|
||||||
|
state.setup.conversationId = action.payload.conversation_id || action.payload.data?.conversation_id;
|
||||||
|
state.setup.message = action.payload.message;
|
||||||
|
state.setup.error = null;
|
||||||
|
})
|
||||||
|
.addCase(setupGmailChat.rejected, (state, action) => {
|
||||||
|
state.setup.status = 'failed';
|
||||||
|
state.setup.error = action.error.message;
|
||||||
|
})
|
||||||
|
.addCase(checkGmailAuth.pending, (state) => {
|
||||||
|
state.auth.status = 'loading';
|
||||||
|
})
|
||||||
|
.addCase(checkGmailAuth.fulfilled, (state, action) => {
|
||||||
|
state.auth.status = 'succeeded';
|
||||||
|
state.auth.isAuthenticated = action.payload.data.authenticated;
|
||||||
|
state.auth.needsSetup = action.payload.data.needs_setup;
|
||||||
|
state.auth.watchExpired = action.payload.data.watch_expired;
|
||||||
|
state.auth.lastHistoryId = action.payload.data.last_history_id;
|
||||||
|
state.auth.watchExpiration = action.payload.data.watch_expiration;
|
||||||
|
state.auth.error = null;
|
||||||
|
})
|
||||||
|
.addCase(checkGmailAuth.rejected, (state, action) => {
|
||||||
|
state.auth.status = 'failed';
|
||||||
|
state.auth.error = action.error.message;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default gmailChatSlice.reducer;
|
76
src/store/gmailChat/gmailChat.thunks.js
Normal file
76
src/store/gmailChat/gmailChat.thunks.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { post } from '../../services/api';
|
||||||
|
|
||||||
|
export const refreshGmailWatch = createAsyncThunk('gmailChat/refreshGmailWatch', async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
use_proxy: true,
|
||||||
|
proxy_url: 'http://127.0.0.1:7890',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await post('/gmail/refresh-watch/', requestData);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || 'Failed to refresh Gmail watch';
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupGmailChat = createAsyncThunk('gmailChat/setupGmailChat', async (payload, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
let requestData;
|
||||||
|
|
||||||
|
// 处理两种不同的调用情况:只传入邮箱字符串 或 传入包含auth_code的对象
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
// 第一次调用,只传入达人邮箱
|
||||||
|
requestData = {
|
||||||
|
client_secret_json: {
|
||||||
|
installed: {
|
||||||
|
client_id: '266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com',
|
||||||
|
project_id: 'knowledge-454905',
|
||||||
|
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||||
|
token_uri: 'https://oauth2.googleapis.com/token',
|
||||||
|
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||||
|
client_secret: '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',
|
||||||
|
redirect_uris: ['http://localhost'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
talent_gmail: payload,
|
||||||
|
auth_code: "",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 第二次调用,包含授权码
|
||||||
|
requestData = {
|
||||||
|
client_secret_json: {
|
||||||
|
installed: {
|
||||||
|
client_id: '266164728215-v84lngbp3vgr4ulql01sqkg5vaigf4a5.apps.googleusercontent.com',
|
||||||
|
project_id: 'knowledge-454905',
|
||||||
|
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||||
|
token_uri: 'https://oauth2.googleapis.com/token',
|
||||||
|
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||||
|
client_secret: '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',
|
||||||
|
redirect_uris: ['http://localhost'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
talent_gmail: payload.talent_gmail,
|
||||||
|
auth_code: payload.auth_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await post('/gmail/setup/', requestData);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error.response?.data?.message || error.message || 'Failed to setup Gmail chat');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const checkGmailAuth = createAsyncThunk('gmailChat/checkAuth', async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/gmail/check-auth/');
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || 'Failed to check Gmail authentication';
|
||||||
|
return rejectWithValue(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
@ -7,6 +7,8 @@ import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js';
|
|||||||
import chatReducer from './chat/chat.slice.js';
|
import chatReducer from './chat/chat.slice.js';
|
||||||
import permissionsReducer from './permissions/permissions.slice.js';
|
import permissionsReducer from './permissions/permissions.slice.js';
|
||||||
import notificationCenterReducer from './notificationCenter/notificationCenter.slice.js';
|
import notificationCenterReducer from './notificationCenter/notificationCenter.slice.js';
|
||||||
|
import gmailChatReducer from './gmailChat/gmailChat.slice.js';
|
||||||
|
import talentChatReducer from './talentChat/talentChat.slice.js';
|
||||||
|
|
||||||
const rootRducer = combineReducers({
|
const rootRducer = combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
@ -15,6 +17,8 @@ const rootRducer = combineReducers({
|
|||||||
chat: chatReducer,
|
chat: chatReducer,
|
||||||
permissions: permissionsReducer,
|
permissions: permissionsReducer,
|
||||||
notificationCenter: notificationCenterReducer,
|
notificationCenter: notificationCenterReducer,
|
||||||
|
gmailChat: gmailChatReducer,
|
||||||
|
talentChat: talentChatReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistConfig = {
|
const persistConfig = {
|
||||||
|
84
src/store/talentChat/talentChat.slice.js
Normal file
84
src/store/talentChat/talentChat.slice.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { setUserGoal, getConversationSummary, getRecommendedReply } from './talentChat.thunks';
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
const initialState = {
|
||||||
|
userGoal: {
|
||||||
|
data: null,
|
||||||
|
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
conversationSummary: {
|
||||||
|
data: null,
|
||||||
|
status: 'idle',
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
recommendedReply: {
|
||||||
|
data: null,
|
||||||
|
status: 'idle',
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建 slice
|
||||||
|
const talentChatSlice = createSlice({
|
||||||
|
name: 'talentChat',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
resetTalentChat: (state) => {
|
||||||
|
state.userGoal = { data: null, status: 'idle', error: null };
|
||||||
|
state.conversationSummary = { data: null, status: 'idle', error: null };
|
||||||
|
state.recommendedReply = { data: null, status: 'idle', error: null };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
// 设置用户目标
|
||||||
|
builder
|
||||||
|
.addCase(setUserGoal.pending, (state) => {
|
||||||
|
state.userGoal.status = 'loading';
|
||||||
|
state.userGoal.error = null;
|
||||||
|
})
|
||||||
|
.addCase(setUserGoal.fulfilled, (state, action) => {
|
||||||
|
state.userGoal.status = 'succeeded';
|
||||||
|
state.userGoal.data = action.payload?.data || action.payload;
|
||||||
|
state.userGoal.error = null;
|
||||||
|
})
|
||||||
|
.addCase(setUserGoal.rejected, (state, action) => {
|
||||||
|
state.userGoal.status = 'failed';
|
||||||
|
state.userGoal.error = action.payload || action.error.message;
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取会话摘要
|
||||||
|
.addCase(getConversationSummary.pending, (state) => {
|
||||||
|
state.conversationSummary.status = 'loading';
|
||||||
|
state.conversationSummary.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getConversationSummary.fulfilled, (state, action) => {
|
||||||
|
state.conversationSummary.status = 'succeeded';
|
||||||
|
state.conversationSummary.data = action.payload || null;
|
||||||
|
state.conversationSummary.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getConversationSummary.rejected, (state, action) => {
|
||||||
|
state.conversationSummary.status = 'failed';
|
||||||
|
state.conversationSummary.error = action.payload || action.error.message;
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取推荐回复
|
||||||
|
.addCase(getRecommendedReply.pending, (state) => {
|
||||||
|
state.recommendedReply.status = 'loading';
|
||||||
|
state.recommendedReply.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getRecommendedReply.fulfilled, (state, action) => {
|
||||||
|
state.recommendedReply.status = 'succeeded';
|
||||||
|
state.recommendedReply.data = action.payload?.data || action.payload;
|
||||||
|
state.recommendedReply.error = null;
|
||||||
|
})
|
||||||
|
.addCase(getRecommendedReply.rejected, (state, action) => {
|
||||||
|
state.recommendedReply.status = 'failed';
|
||||||
|
state.recommendedReply.error = action.payload || action.error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { resetTalentChat } = talentChatSlice.actions;
|
||||||
|
export default talentChatSlice.reducer;
|
51
src/store/talentChat/talentChat.thunks.js
Normal file
51
src/store/talentChat/talentChat.thunks.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { post, get } from '../../services/api';
|
||||||
|
|
||||||
|
export const setUserGoal = createAsyncThunk('talentChat/setUserGoal', async (content, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await post('/user-goal/', requestData);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error.response?.data?.message || error.message || 'Failed to set user goal');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getConversationSummary = createAsyncThunk(
|
||||||
|
'talentChat/getConversationSummary',
|
||||||
|
async (talentEmail, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
talent_email: talentEmail,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { summary } = await get('/conversation-summary/', { params });
|
||||||
|
return summary;
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(
|
||||||
|
error.response?.data?.message || error.message || 'Failed to get conversation summary'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRecommendedReply = createAsyncThunk(
|
||||||
|
'talentChat/getRecommendedReply',
|
||||||
|
async ({ conversation_id, talent_email }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const requestData = {
|
||||||
|
conversation_id,
|
||||||
|
talent_email,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await post('/recommended-reply/', requestData);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
return rejectWithValue(error.response?.data?.message || error.message || 'Failed to get recommended reply');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user