import { useBoolean, useGetState } from 'ahooks'; import { Layout } from 'antd'; import { useEffect, useRef, useState } from 'react'; import ChatInput from './chat-input'; import ChatMessage from './chat-message'; import ChatSidebar, { type ChatSidebarRef } from './sidebar'; // import Header from '../layout/Header'; import type { ChatItem, ConversationItem } from '~/api/dify-chat'; import { fetchAppParams, fetchChatList, fetchConversations } from '~/api/dify-chat'; import { CHAT_CONFIG } from '../../config/chat'; import { usePermission } from '~/hooks/usePermission'; import useChatMessage from '../../hooks/use-chat-message'; import useConversation from '../../hooks/use-conversation'; import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps'; import '../../styles/components/chat-with-llm/index.css'; const { Content } = Layout; // 扩展 Window 接口以支持自定义属性 declare global { interface Window { hasSetInitialSidebarState?: boolean; } } interface ChatTheme { colorBgContainer: string; borderRadiusLG: number; } /** * 主题配置常量 * 避免 SSR 环境下调用 theme.useToken() 导致 CSS-in-JS 注入报错 */ const CHAT_THEME: ChatTheme = { colorBgContainer: '#ffffff', borderRadiusLG: 8, }; /** * 主聊天组件 * 实现单页面应用模式,参考webapp-conversation的初始化逻辑 */ export default function Chat() { // SSR 兼容:Ant Design 的 CSS-in-JS 在 Remix SSR 环境下 // hydration 时会因找不到样式容器而报错,需要客户端渲染 const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // 权限检查 const { hasPermission: checkPerm } = usePermission(); const canChat = checkPerm('dify:chat:use'); // 侧边栏状态 const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(false); // 主题配置 const { colorBgContainer, borderRadiusLG } = CHAT_THEME; // 对话应用管理 const { chatApps, loadingChatApps, currentChatApp, handleChatAppChange: originalHandleChatAppChange, } = useChatApps(); // 调试日志 - 监听chatApps和currentChatApp变化 useEffect(() => { console.log('[Chat] chatApps 更新:', { count: chatApps.length, apps: chatApps.map(app => ({ app_id: app.app_id, app_name: app.app_name, is_default: app.is_default })) }); }, [chatApps]); useEffect(() => { console.log('[Chat] currentChatApp 更新:', currentChatApp ? { app_id: currentChatApp.app_id, app_name: currentChatApp.app_name, is_default: currentChatApp.is_default } : 'null'); }, [currentChatApp]); // 会话管理 const { conversationList, setConversationList, currConversationId, getCurrConversationId, setCurrConversationId, getConversationIdFromStorage, isNewConversation, currInputs, newConversationInputs, resetNewConversationInputs, setCurrInputs, currConversationInfo, setNewConversationInfo, setExistConversationInfo, addConversationToList, updateConversationInList, removeConversationFromList, } = useConversation(); // 消息管理 const { chatList, setChatList, getChatList, isResponding, handleSend, stopResponding, handleFeedback, } = useChatMessage({ onUpdateConversationList: updateConversationInList, onConversationIdChange: async (conversationId: string) => { console.log('🔄 [Chat] 收到会话ID变更通知:', { oldConversationId: currConversationId, newConversationId: conversationId, willUpdateLocalStorage: true }); // 设置当前会话ID(这会触发localStorage更新) setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID); // 如果是新会话,添加到会话列表 const existingConversation = conversationList.find(item => item.id === conversationId); if (!existingConversation) { // console.log('🆕 添加新会话到列表:', conversationId); const newConversation = { id: conversationId, name: '新对话', inputs: currInputs || {}, introduction: '', }; addConversationToList(newConversation); // 检查是否需要自动重命名(新对话的第一条消息) if (!newConversationFirstMessageSent.has(conversationId)) { // console.log('🏷️ 新对话第一条消息,准备自动重命名:', conversationId); // 标记该对话已发送第一条消息 setNewConversationFirstMessageSent(prev => new Set(prev).add(conversationId)); // 延迟一下确保组件状态已更新 setTimeout(async () => { try { if (sidebarRef.current) { await sidebarRef.current.autoRename(conversationId); // console.log('✅ 新对话自动重命名完成:', conversationId); } else { console.warn('⚠️ 侧边栏引用不可用,无法自动重命名'); } } catch (error) { console.error('❌ 新对话自动重命名失败:', error); } }, 1000); } } }, }); // 应用状态 const [appUnavailable, setAppUnavailable] = useState(false); const [isUnknownReason, setIsUnknownReason] = useState(false); const [conversationPermissionDenied, setConversationPermissionDenied] = useState(false); const [inited, setInited] = useState(false); const [promptConfig, setPromptConfig] = useState(null); // 防止重复初始化 const [initializing, setInitializing] = useState(false); // 会话状态管理 const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false); const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false); // 聊天列表容器引用 const chatListDomRef = useRef(null); // 侧边栏组件引用 const sidebarRef = useRef(null); // 跟踪新对话是否已发送第一条消息 const [newConversationFirstMessageSent, setNewConversationFirstMessageSent] = useState>(new Set()); // 检查应用配置 - 现在客户端通过Remix API routes调用,不需要APP_KEY // 只检查API_URL是否配置 const hasSetAppConfig = !!CHAT_CONFIG.API_URL; /** * 处理开始聊天 */ const handleStartChat = (inputs: Record) => { createNewChat(); setConversationIdChangeBecauseOfNew(true); setCurrInputs(inputs); setChatStarted(); // 解析变量并生成开场白 setChatList(generateNewChatListWithOpenStatement('', inputs)); }; /** * 创建新聊天 */ const createNewChat = () => { setChatList([]); setChatNotStarted(); resetNewConversationInputs(); }; /** * 生成带开场白的新聊天列表 */ const generateNewChatListWithOpenStatement = (introduction?: string, inputs?: Record | null): ChatItem[] => { const newChatList: ChatItem[] = []; if (introduction) { newChatList.push({ id: `opening-statement-${Date.now()}`, content: introduction, isAnswer: true, isOpeningStatement: true, }); } return newChatList; }; /** * 处理会话切换 */ const handleConversationSwitch = async () => { if (!inited) { return; } // console.log('🔄 处理会话切换:', { currConversationId, isNewConversation }); // 更新当前会话的输入 let notSyncToStateIntroduction = ''; let notSyncToStateInputs: Record | undefined | null = {}; if (!isNewConversation) { const item = conversationList.find(item => item.id === currConversationId); notSyncToStateInputs = item?.inputs || {}; setCurrInputs(notSyncToStateInputs as any); notSyncToStateIntroduction = item?.introduction || ''; setExistConversationInfo({ name: item?.name || '', introduction: notSyncToStateIntroduction, }); } else { notSyncToStateInputs = newConversationInputs; setCurrInputs(notSyncToStateInputs); } // 更新当前会话的聊天列表 if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponding) { try { // console.log('📨 获取会话历史消息:', currConversationId); // 调用API获取历史消息 const response = await fetchChatList(currConversationId); // console.log('📋 历史消息响应:', response); if (response && (response as any).data) { const { data: historyMessages } = response as any; // 生成新的聊天列表,包含开场白 const newChatList: ChatItem[] = generateNewChatListWithOpenStatement(notSyncToStateIntroduction, notSyncToStateInputs); // 添加历史消息 historyMessages.forEach((item: any) => { // 添加用户问题 newChatList.push({ id: `question-${item.id}`, content: item.query, isAnswer: false, message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [], }); // 添加AI回答(包含检索资源) newChatList.push({ id: item.id, content: item.answer, agent_thoughts: item.agent_thoughts || [], feedback: item.feedback, isAnswer: true, message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [], retriever_resources: item.retriever_resources || [], }); }); // console.log('✅ 设置历史聊天列表:', newChatList.length, '条消息'); setChatList(newChatList); } else { console.warn('⚠️ 获取历史消息失败或无数据'); // 如果获取失败,至少显示开场白 const newChatList: ChatItem[] = generateNewChatListWithOpenStatement(notSyncToStateIntroduction, notSyncToStateInputs); setChatList(newChatList); } } catch (error) { console.error('❌ 获取历史消息失败:', error); // 如果获取失败,至少显示开场白 const newChatList: ChatItem[] = generateNewChatListWithOpenStatement(notSyncToStateIntroduction, notSyncToStateInputs); setChatList(newChatList); } } if (isNewConversation && isChatStarted) { setChatList(generateNewChatListWithOpenStatement()); } }; /** * 处理会话ID变化 */ const handleConversationIdChange = (id: string) => { // console.log('🔄 会话ID变化:', { id, currentId: currConversationId }); if (id === '-1') { createNewChat(); setConversationIdChangeBecauseOfNew(true); } else { setConversationIdChangeBecauseOfNew(false); } // 触发会话切换 setCurrConversationId(id, CHAT_CONFIG.APP_ID); }; /** * 检查是否可以发送消息 */ const checkCanSend = () => { if (currConversationId !== '-1') { return true; } if (!currInputs || !promptConfig?.prompt_variables) { return true; } const inputLens = Object.values(currInputs).length; const promptVariablesLens = promptConfig.prompt_variables.length; const emptyInput = inputLens < promptVariablesLens || Object.values(currInputs).find(v => !v); if (emptyInput) { console.error('必填变量值不能为空'); return false; } return true; }; /** * 处理发送消息 */ const handleSendMessage = async (message: string, files?: any[]) => { if (isResponding) { console.log('正在响应中,请等待...'); return; } if (!checkCanSend()) { return; } console.log('📤 [Chat] 发送消息:', { message: message.substring(0, 50) + (message.length > 50 ? '...' : ''), currConversationId, isNewConversation, willSendConversationId: isNewConversation ? null : currConversationId, appId: currentChatApp?.app_id }); try { // 准备输入数据 const toServerInputs: Record = {}; if (currInputs) { Object.keys(currInputs).forEach((key) => { toServerInputs[key] = currInputs[key]; }); } // 使用 useChatMessage 钩子的 handleSend 方法,传递当前选中的应用 ID await handleSend( message, isNewConversation ? null : currConversationId, files, toServerInputs, currentChatApp?.app_id // 传递对话应用 ID ); } catch (error) { console.error('发送消息失败:', error); } }; /** * 处理侧边栏切换 */ const handleSidebarToggle = () => { setSidebarCollapsed(!sidebarCollapsed); }; /** * 处理会话选择 */ const handleConversationSelect = (conversationId: string) => { if (conversationId !== currConversationId) { setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID); } // 移动端选中对话后自动隐藏侧边栏 if (isMobile && !sidebarCollapsed) { setSidebarCollapsed(true); } }; /** * 处理新建会话 */ const handleNewConversation = () => { setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false); createNewChat(); }; /** * 处理对话应用切换 * 切换应用后刷新加载对应的会话列表 */ const handleChatAppChange = async (appId: string) => { console.log('🔄 [Chat] 用户点击切换对话应用,目标ID:', appId); console.log('🔄 [Chat] 当前可用应用列表:', chatApps.map(app => ({ app_id: app.app_id, app_name: app.app_name }))); // 查找目标应用 const targetApp = chatApps.find(app => app.app_id === appId); if (!targetApp) { console.error('❌ [Chat] 未找到目标应用 ID:', appId); return; } console.log('🔄 [Chat] 找到目标应用:', targetApp.app_name); // 调用原始的切换方法 originalHandleChatAppChange(appId, async (app) => { console.log('✅ [Chat] originalHandleChatAppChange回调触发,传入应用:', app.app_name); try { // 重新获取会话列表,传入新的应用ID获取该应用的会话 console.log('📋 [Chat] 开始获取新应用的会话列表...'); const conversationData = await fetchConversations(app.app_id); const conversations = (conversationData as any).data || []; console.log('📋 [Chat] 切换应用后获取到会话列表:', conversations.length, '条'); // 更新会话列表 setConversationList(conversations); // 清空当前聊天,创建新会话 setChatList([]); setChatNotStarted(); // 如果有会话,选择第一个;否则创建新会话 if (conversations.length > 0) { const firstConversation = conversations[0]; setCurrConversationId(firstConversation.id, app.app_id, false); console.log('🎯 [Chat] 自动选择第一个会话:', firstConversation.id); } else { setCurrConversationId('-1', app.app_id, false); console.log('🆕 [Chat] 无会话,创建新会话'); } } catch (error) { console.error('❌ [Chat] 切换应用后刷新会话列表失败:', error); // 即使刷新失败,也清空当前状态 setConversationList([]); setChatList([]); setCurrConversationId('-1', appId, false); } }); }; /** * 处理会话删除后的状态更新 */ const handleConversationDeleted = (conversationId: string) => { // console.log('🗑️ 处理会话删除后的状态更新:', conversationId); // 如果删除的是当前会话,切换到新会话 if (conversationId === currConversationId) { handleNewConversation(); } // 从列表中移除会话 removeConversationFromList(conversationId); // console.log('✅ 会话删除状态更新完成:', conversationId); }; /** * 处理会话重命名后的状态更新 */ const handleConversationRenamed = (conversationId: string, newName: string) => { // console.log('✏️ 处理会话重命名后的状态更新:', { conversationId, newName }); // 更新本地会话列表中的名称 updateConversationInList(conversationId, { name: newName }); // console.log('✅ 会话重命名状态更新完成:', conversationId, '->', newName); }; /** * 组件初始化 - 等待 currentChatApp 就绪后再获取会话列表 * 确保按当前应用过滤会话,避免不同应用的会话混在一起 */ useEffect(() => { if (!hasSetAppConfig) { console.error('应用配置不完整'); setAppUnavailable(true); return; } // 必须等 currentChatApp 就绪,否则不知道该获取哪个应用的会话 if (!currentChatApp || initializing || inited) { return; } setInitializing(true); (async () => { try { console.log('🚀 [Chat] 开始初始化,当前应用:', currentChatApp.app_name, currentChatApp.app_id); // 用当前应用的 appId 获取会话列表(失败时降级为空列表,不阻塞初始化) const [conversationData, appParams] = await Promise.all([ fetchConversations(currentChatApp.app_id).catch(err => { console.warn('⚠️ [Chat] 获取会话列表失败(权限不足或网络问题),降级为空列表:', err.message); setConversationPermissionDenied(true); return { data: [] }; }), fetchAppParams().catch(err => { console.warn('⚠️ [Chat] 获取应用参数失败,使用默认值:', err.message); return { data: { user_input_form: [], opening_statement: '' } }; }), ]); console.log('📋 [Chat] 获取到的数据:', { conversationData, appParams }); // 处理会话数据 const conversations = (conversationData as any).data || []; console.log('📋 [Chat] 会话列表:', conversations); // 处理当前会话ID const _conversationId = getConversationIdFromStorage(currentChatApp.app_id); const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId); console.log('💾 [Chat] 初始化 - 本地存储的会话ID:', { conversationId: _conversationId, isNotNewConversation, conversationsCount: conversations.length, conversationIds: conversations.map((c: ConversationItem) => c.id) }); // 获取新会话信息 const { user_input_form, opening_statement: introduction } = (appParams as any).data || {}; setNewConversationInfo({ name: '新对话', introduction: introduction || '', }); // 设置提示配置 setPromptConfig({ prompt_template: '', prompt_variables: user_input_form || [], }); // 设置会话列表 setConversationList(conversations); // 如果存在有效的会话ID,则设置为当前会话 if (isNotNewConversation) { console.log('🎯 [Chat] 初始化 - 设置当前会话ID:', _conversationId); setCurrConversationId(_conversationId, currentChatApp.app_id, false); } else { // 如果localStorage为空或会话不存在,自动创建新会话 console.log('🆕 [Chat] 初始化 - localStorage为空或会话不存在,创建新会话'); setCurrConversationId('-1', currentChatApp.app_id, false); } setInited(true); console.log('✅ [Chat] 聊天应用初始化完成'); } catch (e: any) { console.error('❌ 初始化失败:', e); if (e.status === 404) { setAppUnavailable(true); } else { setIsUnknownReason(true); setAppUnavailable(true); } } })(); }, [currentChatApp]); // 监听会话切换 useEffect(() => { handleConversationSwitch(); }, [currConversationId, inited]); // 自动滚动到底部 useEffect(() => { if (chatListDomRef.current) { chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight; } }, [chatList]); // 检查屏幕尺寸 useEffect(() => { const checkScreenSize = () => { const isMobileDevice = window.innerWidth < 992; setIsMobile(isMobileDevice); // 移动端默认隐藏侧边栏,桌面端默认显示 // 只在初次加载时设置,避免影响用户的手动切换 if (!window.hasSetInitialSidebarState) { setSidebarCollapsed(isMobileDevice); window.hasSetInitialSidebarState = true; } }; // 初始检查 checkScreenSize(); // 监听窗口大小变化 window.addEventListener('resize', checkScreenSize); return () => { window.removeEventListener('resize', checkScreenSize); }; }, []); // 如果应用不可用,显示错误页面 if (appUnavailable) { return (

应用暂时不可用

{isUnknownReason ? '发生了未知错误,请稍后重试' : '应用配置不正确,请检查配置'}

); } // 如果未初始化完成,显示加载状态 if (!inited) { return (

正在加载...

); } // 判断是否已设置输入 const hasSetInputs = (() => { if (!isNewConversation) { return true; } return isChatStarted; })(); const conversationName = currConversationInfo?.name || '新对话'; const conversationIntroduction = currConversationInfo?.introduction || ''; // SSR 兼容:等客户端挂载后再渲染 Ant Design 组件 if (!mounted) { return (

正在加载...

); } return ( {/* 移动端遮罩层 - 点击可收起侧边栏 */}
{/* ChatSidebar 隐藏时显示的展开按钮 */} {sidebarCollapsed && ( )} {/* 侧边栏 */} {/* 主内容区域 */} {/* 聊天区域 */}
{/* 如果是新会话且未开始聊天,显示欢迎信息 */} {isNewConversation && !isChatStarted && (

开始新的对话

请在下方输入您的问题

)} {/* 聊天消息列表 */} {chatList.map((item) => ( handleSendMessage(question)} /> ))}
{/* 输入区域 */}
); }