793 lines
30 KiB
TypeScript
793 lines
30 KiB
TypeScript
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('rag: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<boolean>(false);
|
|
const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false);
|
|
const [conversationPermissionDenied, setConversationPermissionDenied] = useState<boolean>(false);
|
|
const [inited, setInited] = useState<boolean>(false);
|
|
const [promptConfig, setPromptConfig] = useState<any>(null);
|
|
// 防止重复初始化
|
|
const [initializing, setInitializing] = useState<boolean>(false);
|
|
|
|
// 会话状态管理
|
|
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false);
|
|
const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false);
|
|
|
|
// 聊天列表容器引用
|
|
const chatListDomRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 侧边栏组件引用
|
|
const sidebarRef = useRef<ChatSidebarRef>(null);
|
|
|
|
// 跟踪新对话是否已发送第一条消息
|
|
const [newConversationFirstMessageSent, setNewConversationFirstMessageSent] = useState<Set<string>>(new Set());
|
|
|
|
// 检查应用配置 - 现在客户端通过Remix API routes调用,不需要APP_KEY
|
|
// 只检查API_URL是否配置
|
|
const hasSetAppConfig = !!CHAT_CONFIG.API_URL;
|
|
|
|
/**
|
|
* 处理开始聊天
|
|
*/
|
|
const handleStartChat = (inputs: Record<string, any>) => {
|
|
createNewChat();
|
|
setConversationIdChangeBecauseOfNew(true);
|
|
setCurrInputs(inputs);
|
|
setChatStarted();
|
|
// 解析变量并生成开场白
|
|
setChatList(generateNewChatListWithOpenStatement('', inputs));
|
|
};
|
|
|
|
/**
|
|
* 创建新聊天
|
|
*/
|
|
const createNewChat = () => {
|
|
setChatList([]);
|
|
setChatNotStarted();
|
|
resetNewConversationInputs();
|
|
};
|
|
|
|
/**
|
|
* 生成带开场白的新聊天列表
|
|
*/
|
|
const generateNewChatListWithOpenStatement = (introduction?: string, inputs?: Record<string, any> | 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<string, any> | 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<string, any> = {};
|
|
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 (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">应用暂时不可用</h2>
|
|
<p className="text-gray-600">
|
|
{isUnknownReason ? '发生了未知错误,请稍后重试' : '应用配置不正确,请检查配置'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 如果未初始化完成,显示加载状态
|
|
if (!inited) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
|
<p className="text-gray-600">正在加载...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 判断是否已设置输入
|
|
const hasSetInputs = (() => {
|
|
if (!isNewConversation) {
|
|
return true;
|
|
}
|
|
return isChatStarted;
|
|
})();
|
|
|
|
const conversationName = currConversationInfo?.name || '新对话';
|
|
const conversationIntroduction = currConversationInfo?.introduction || '';
|
|
|
|
// SSR 兼容:等客户端挂载后再渲染 Ant Design 组件
|
|
if (!mounted) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
|
<p className="text-gray-600">正在加载...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
|
|
{/* 移动端遮罩层 - 点击可收起侧边栏 */}
|
|
<div
|
|
className={`chat-sidebar-overlay ${!sidebarCollapsed && isMobile ? 'visible' : ''}`}
|
|
onClick={handleSidebarToggle}
|
|
/>
|
|
|
|
{/* ChatSidebar 隐藏时显示的展开按钮 */}
|
|
{sidebarCollapsed && (
|
|
<button
|
|
onClick={handleSidebarToggle}
|
|
className="fixed left-0 top-1/2 -translate-y-1/2 z-[998] bg-white hover:bg-gray-100 shadow-lg rounded-r-lg px-2 py-4 transition-all duration-200 border border-l-0 border-gray-200"
|
|
style={{
|
|
width: '32px',
|
|
height: '48px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
}}
|
|
aria-label="展开对话列表"
|
|
>
|
|
<i className="ri-menu-unfold-line text-lg text-gray-600"></i>
|
|
</button>
|
|
)}
|
|
|
|
{/* 侧边栏 */}
|
|
<ChatSidebar
|
|
ref={sidebarRef}
|
|
collapsed={sidebarCollapsed}
|
|
onToggle={handleSidebarToggle}
|
|
conversations={conversationList}
|
|
currentConversationId={currConversationId}
|
|
onConversationSelect={handleConversationSelect}
|
|
onNewConversation={handleNewConversation}
|
|
onConversationDeleted={handleConversationDeleted}
|
|
onConversationRenamed={handleConversationRenamed}
|
|
chatApps={chatApps}
|
|
loadingChatApps={loadingChatApps}
|
|
currentChatApp={currentChatApp}
|
|
onChatAppChange={handleChatAppChange}
|
|
conversationReadOnly={conversationPermissionDenied}
|
|
/>
|
|
|
|
{/* 主内容区域 */}
|
|
<Layout style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
<Content
|
|
style={{
|
|
background: colorBgContainer,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
flex: 1,
|
|
minHeight: 0,
|
|
}}
|
|
>
|
|
{/* 聊天区域 */}
|
|
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
|
<div
|
|
ref={chatListDomRef}
|
|
className="h-full overflow-y-auto px-4 py-4 space-y-2"
|
|
>
|
|
{/* 如果是新会话且未开始聊天,显示欢迎信息 */}
|
|
{isNewConversation && !isChatStarted && (
|
|
<div className="text-center py-8">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">开始新的对话</h3>
|
|
<p className="text-gray-600">请在下方输入您的问题</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 聊天消息列表 */}
|
|
{chatList.map((item) => (
|
|
<ChatMessage
|
|
key={item.id}
|
|
message={item}
|
|
isResponding={isResponding && item.id === chatList[chatList.length - 1]?.id}
|
|
onFeedback={handleFeedback}
|
|
onSuggestedQuestionClick={(question) => handleSendMessage(question)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 输入区域 */}
|
|
<div className="flex-shrink-0 bg-white">
|
|
<ChatInput
|
|
onSendMessage={handleSendMessage}
|
|
disabled={isResponding || !canChat}
|
|
placeholder={canChat ? "有什么我能帮您的吗?" : "您没有发送消息的权限"}
|
|
onStop={stopResponding}
|
|
isResponding={isResponding}
|
|
/>
|
|
</div>
|
|
</Content>
|
|
</Layout>
|
|
</Layout>
|
|
);
|
|
}
|