+ {/* 输入区域 - 方块容器 */}
+
+
{/* 上传按钮 */}
-
- {renderUploadButton()}
-
+ {renderUploadButton()}
{/* 文本输入 */}
-
+
{/* 发送/停止按钮 */}
-
+
{isResponding ? (
}
onClick={handleStop}
disabled={disabled}
- size="large"
+ size="small"
+ shape="circle"
/>
) : (
}
+ icon={}
onClick={handleSubmit}
disabled={disabled || !message.trim()}
- size="large"
+ size="small"
+ shape="circle"
+ className='chat-input-button-send'
/>
)}
-
- {/* 字符计数和提示 */}
diff --git a/app/components/chat/chat-message.tsx b/app/components/chat/chat-message.tsx
index 44d63c8..6426859 100644
--- a/app/components/chat/chat-message.tsx
+++ b/app/components/chat/chat-message.tsx
@@ -1,208 +1,179 @@
-import React, { useState } from 'react';
-import { Button, Tooltip, Avatar, Card, Image, Spin } from 'antd';
-import { AntDesignOutlined, SmileTwoTone, RobotOutlined } from '@ant-design/icons';
-import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../types/dify_chat';
-import { CHAT_CONFIG } from '../../config/chat';
-import Markdown from './markdown';
-import ThoughtProcess from './thought-process';
-import { Dayjs } from 'dayjs';
-import '../../styles/components/chat-with-llm/chat-message.css';
-
-interface ChatMessageProps {
- message: ChatItem;
- onFeedback?: (messageId: string, feedback: Feedbacktype) => void;
- isResponding?: boolean;
- onRegenerate?: (messageId: string) => void;
-}
-
-/**
- * 聊天消息组件
- */
-export default function ChatMessage({
- message,
- onFeedback,
- isResponding = false,
- onRegenerate
-}: ChatMessageProps) {
- const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
- message.feedback?.rating || null
- );
-
- const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more } = message;
- const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
-
- /**
- * 处理反馈
- */
- const handleFeedback = async (type: 'like' | 'dislike') => {
- if (!id || id.startsWith('placeholder-') || id.startsWith('question-') || id.startsWith('opening-'))
- return;
-
- // 如果已经选择了相同的反馈,则取消选择
- const newFeedback = feedback === type ? null : type;
- setFeedback(newFeedback);
-
- await onFeedback?.(id, {
- rating: newFeedback,
- });
- };
-
- /**
- * 渲染AI回答内容
- */
- const renderAnswerContent = () => {
- // console.log('🎨 渲染AI回答内容:', { content, isResponding, isAgentMode });
-
- // 如果正在响应且没有内容
- if (isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) {
- return (
-
-
- AI 正在思考...
-
- );
- }
-
- // Agent模式(有思考过程)
- if (isAgentMode) {
- return (
-
- {agent_thoughts?.map((thought, index) => (
-
- {thought.thought && (
-
-
-
- )}
- {thought.tool && (
-
- )}
-
- ))}
-
- );
- }
-
- // 普通模式 - 恢复Markdown渲染
- return (
-
- );
- };
-
- /**
- * 渲染建议问题
- */
- const renderSuggestedQuestions = () => {
- if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
-
- return (
-
-
建议问题:
-
- {suggestedQuestions.map((question, index) => (
-
- ))}
-
-
- );
- };
-
- /**
- * 渲染消息元信息
- */
- const renderMessageMeta = () => {
- if (!more) return null;
-
- return (
-
- {more.time && 时间: {more.time}}
- {more.tokens && Token: {more.tokens}}
- {more.latency && 延迟: {more.latency}}
-
- );
- };
-
- // 如果是开场白,特殊处理
- if (isOpeningStatement) {
- return (
-
-
-
- {/*
} className="flex-shrink-0" />
- */}
-
}
- />
-
-
- {renderSuggestedQuestions()}
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- {/* 头像 */}
-
:
}
- className="flex-shrink-0"
- style={{
- backgroundColor: isAnswer ? '#1890ff' : '#52c41a'
- }}
- />
-
- {/* 消息内容 */}
-
- {isAnswer ? renderAnswerContent() : (
-
-
- {/* {renderImages(message_files)} */}
-
- )}
-
- {/* 建议问题 */}
- {/* {isAnswer && renderSuggestedQuestions()} */}
-
- {/* 消息元信息 */}
- {/* {renderMessageMeta()} */}
-
- {/* 反馈按钮 */}
- {/* {isAnswer && (
-
- {renderFeedbackButtons()}
-
- )} */}
-
-
-
-
-
-
- );
+import React, { useState } from 'react';
+import { Button, Card, Spin } from 'antd';
+import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../types/dify_chat';
+import { CHAT_CONFIG } from '../../config/chat';
+import Markdown from './markdown';
+import ThoughtProcess from './thought-process';
+import { Dayjs } from 'dayjs';
+import '../../styles/components/chat-with-llm/chat-message.css';
+
+interface ChatMessageProps {
+ message: ChatItem;
+ onFeedback?: (messageId: string, feedback: Feedbacktype) => void;
+ isResponding?: boolean;
+ onRegenerate?: (messageId: string) => void;
+}
+
+/**
+ * 聊天消息组件
+ */
+export default function ChatMessage({
+ message,
+ onFeedback,
+ isResponding = false,
+ onRegenerate
+}: ChatMessageProps) {
+ const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
+ message.feedback?.rating || null
+ );
+
+ const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more } = message;
+ const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
+
+ /**
+ * 处理反馈
+ */
+ const handleFeedback = async (type: 'like' | 'dislike') => {
+ if (!id || id.startsWith('placeholder-') || id.startsWith('question-') || id.startsWith('opening-'))
+ return;
+
+ // 如果已经选择了相同的反馈,则取消选择
+ const newFeedback = feedback === type ? null : type;
+ setFeedback(newFeedback);
+
+ await onFeedback?.(id, {
+ rating: newFeedback,
+ });
+ };
+
+ /**
+ * 渲染AI回答内容
+ */
+ const renderAnswerContent = () => {
+ // console.log('🎨 渲染AI回答内容:', { content, isResponding, isAgentMode });
+
+ // 如果正在响应且没有内容
+ if (isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) {
+ return (
+
+
+ AI 正在思考...
+
+ );
+ }
+
+ // Agent模式(有思考过程)
+ if (isAgentMode) {
+ return (
+
+ {agent_thoughts?.map((thought, index) => (
+
+ {thought.thought && (
+
+
+
+ )}
+ {thought.tool && (
+
+ )}
+
+ ))}
+
+ );
+ }
+
+ // 普通模式 - 恢复Markdown渲染
+ return (
+
+ );
+ };
+
+ /**
+ * 渲染建议问题
+ */
+ const renderSuggestedQuestions = () => {
+ if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
+
+ return (
+
+
建议问题:
+
+ {suggestedQuestions.map((question, index) => (
+
+ ))}
+
+
+ );
+ };
+
+ /**
+ * 渲染消息元信息
+ */
+ const renderMessageMeta = () => {
+ if (!more) return null;
+
+ return (
+
+ {more.time && 时间: {more.time}}
+ {more.tokens && Token: {more.tokens}}
+ {more.latency && 延迟: {more.latency}}
+
+ );
+ };
+
+ // 如果是开场白,特殊处理
+ if (isOpeningStatement) {
+ return (
+
+
+
+
+
+ {renderSuggestedQuestions()}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {/* 消息内容 */}
+
+ {isAnswer ? renderAnswerContent() : (
+
+
+ {/* {renderImages(message_files)} */}
+
+ )}
+
+
+
+
+
+
+ );
}
\ No newline at end of file
diff --git a/app/components/chat/index.tsx b/app/components/chat/index.tsx
index 45a2600..68a7b64 100644
--- a/app/components/chat/index.tsx
+++ b/app/components/chat/index.tsx
@@ -1,571 +1,572 @@
-import { useState, useEffect, useRef } from 'react';
-import { produce } from 'immer';
-import { useBoolean, useGetState } from 'ahooks';
-import { Layout, theme } from 'antd';
-import ChatMessage from './chat-message';
-import ChatInput from './chat-input';
-import ChatSidebar, { type ChatSidebarRef } from './sidebar';
-// import Header from '../layout/Header';
-import useConversation from '../../hooks/use-conversation';
-import useChatMessage from '../../hooks/use-chat-message';
-import type { ChatItem, ConversationItem } from '../../types/dify_chat';
-import { CHAT_CONFIG } from '../../config/chat';
-import { fetchConversations, fetchAppParams, fetchChatList } from '../../services/api.client';
-import '../../styles/components/chat-with-llm/index.css';
-
-const { Content } = Layout;
-
-/**
- * 主聊天组件
- * 实现单页面应用模式,参考webapp-conversation的初始化逻辑
- */
-export default function Chat() {
- // 侧边栏状态
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
- const {
- token: { colorBgContainer, borderRadiusLG },
- } = theme.useToken();
-
- // 会话管理
- 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('🔄 收到会话ID变更通知:', conversationId);
-
- // 设置当前会话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 [inited, setInited] = useState(false);
- const [promptConfig, setPromptConfig] = useState(null);
-
- // 会话状态管理
- 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());
-
- // 检查应用配置
- const hasSetAppConfig = CHAT_CONFIG.APP_ID && CHAT_CONFIG.API_KEY;
-
- /**
- * 处理开始聊天
- */
- 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') || [],
- });
- });
-
- // 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('📤 发送消息:', { message, conversationId: currConversationId });
-
- try {
- // 准备输入数据
- const toServerInputs: Record = {};
- if (currInputs) {
- Object.keys(currInputs).forEach((key) => {
- toServerInputs[key] = currInputs[key];
- });
- }
-
- // 使用 useChatMessage 钩子的 handleSend 方法
- await handleSend(
- message,
- isNewConversation ? null : currConversationId,
- files,
- toServerInputs
- );
-
- } catch (error) {
- console.error('发送消息失败:', error);
- }
- };
-
- /**
- * 处理侧边栏切换
- */
- const handleSidebarToggle = () => {
- setSidebarCollapsed(!sidebarCollapsed);
- };
-
- /**
- * 处理会话选择
- */
- const handleConversationSelect = (conversationId: string) => {
- if (conversationId !== currConversationId) {
- setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID);
- }
- };
-
- /**
- * 处理新建会话
- */
- const handleNewConversation = () => {
- setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false);
- createNewChat();
- };
-
- /**
- * 处理会话删除后的状态更新
- */
- 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);
- };
-
- /**
- * 组件初始化 - 参考webapp-conversation的逻辑
- */
- useEffect(() => {
- if (!hasSetAppConfig) {
- console.error('应用配置不完整');
- setAppUnavailable(true);
- return;
- }
-
- (async () => {
- try {
- console.log('🚀 开始初始化聊天应用...');
-
- // 并行获取会话列表和应用参数
- const [conversationData, appParams] = await Promise.all([
- fetchConversations(),
- fetchAppParams()
- ]);
-
- // console.log('📋 获取到的数据:', { conversationData, appParams });
-
- // 处理会话数据
- const conversations = (conversationData as any).data || [];
- if ((conversationData as any).error) {
- console.error('获取会话列表失败:', (conversationData as any).error);
- throw new Error((conversationData as any).error);
- }
-
- // 处理当前会话ID
- const _conversationId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID);
- const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId);
-
- // console.log('💾 本地存储的会话ID:', _conversationId);
- // console.log('🔍 是否为已存在的会话:', isNotNewConversation);
-
- // 获取新会话信息
- 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('🎯 设置当前会话ID:', _conversationId);
- setCurrConversationId(_conversationId, CHAT_CONFIG.APP_ID, false);
- } else {
- // 如果localStorage为空或会话不存在,自动创建新会话
- console.log('🆕 localStorage为空或会话不存在,创建新会话');
- setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false);
- }
-
- setInited(true);
- console.log('✅ 聊天应用初始化完成');
- } catch (e: any) {
- console.error('❌ 初始化失败:', e);
- if (e.status === 404) {
- setAppUnavailable(true);
- } else {
- setIsUnknownReason(true);
- setAppUnavailable(true);
- }
- }
- })();
- }, []);
-
- // 监听会话切换
- useEffect(() => {
- handleConversationSwitch();
- }, [currConversationId, inited]);
-
- // 自动滚动到底部
- useEffect(() => {
- if (chatListDomRef.current) {
- chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight;
- }
- }, [chatList]);
-
- // 如果应用不可用,显示错误页面
- if (appUnavailable) {
- return (
-
-
-
应用暂时不可用
-
- {isUnknownReason ? '发生了未知错误,请稍后重试' : '应用配置不正确,请检查配置'}
-
-
-
- );
- }
-
- // 如果未初始化完成,显示加载状态
- if (!inited) {
- return (
-
- );
- }
-
- // 判断是否已设置输入
- const hasSetInputs = (() => {
- if (!isNewConversation) {
- return true;
- }
- return isChatStarted;
- })();
-
- const conversationName = currConversationInfo?.name || '新对话';
- const conversationIntroduction = currConversationInfo?.introduction || '';
-
- return (
-
- {/* 侧边栏 */}
-
-
- {/* 主内容区域 */}
-
-
- {/* 聊天区域 */}
-
-
- {/* 如果是新会话且未开始聊天,显示欢迎信息 */}
- {isNewConversation && !isChatStarted && (
-
- )}
-
- {/* 聊天消息列表 */}
- {chatList.map((item) => (
-
- ))}
-
-
-
- {/* 输入区域 */}
-
-
-
-
-
-
- );
+import { useState, useEffect, useRef } from 'react';
+import { produce } from 'immer';
+import { useBoolean, useGetState } from 'ahooks';
+import { Layout, theme } from 'antd';
+import ChatMessage from './chat-message';
+import ChatInput from './chat-input';
+import ChatSidebar, { type ChatSidebarRef } from './sidebar';
+// import Header from '../layout/Header';
+import useConversation from '../../hooks/use-conversation';
+import useChatMessage from '../../hooks/use-chat-message';
+import type { ChatItem, ConversationItem } from '../../types/dify_chat';
+import { CHAT_CONFIG } from '../../config/chat';
+import { fetchConversations, fetchAppParams, fetchChatList } from '../../services/api.client';
+import '../../styles/components/chat-with-llm/index.css';
+
+const { Content } = Layout;
+
+/**
+ * 主聊天组件
+ * 实现单页面应用模式,参考webapp-conversation的初始化逻辑
+ */
+export default function Chat() {
+ // 侧边栏状态
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const {
+ token: { colorBgContainer, borderRadiusLG },
+ } = theme.useToken();
+
+ // 会话管理
+ 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('🔄 收到会话ID变更通知:', conversationId);
+
+ // 设置当前会话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 [inited, setInited] = useState(false);
+ const [promptConfig, setPromptConfig] = useState(null);
+
+ // 会话状态管理
+ 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());
+
+ // 检查应用配置
+ const hasSetAppConfig = CHAT_CONFIG.APP_ID && CHAT_CONFIG.API_KEY;
+
+ /**
+ * 处理开始聊天
+ */
+ 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') || [],
+ });
+ });
+
+ // 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('📤 发送消息:', { message, conversationId: currConversationId });
+
+ try {
+ // 准备输入数据
+ const toServerInputs: Record = {};
+ if (currInputs) {
+ Object.keys(currInputs).forEach((key) => {
+ toServerInputs[key] = currInputs[key];
+ });
+ }
+
+ // 使用 useChatMessage 钩子的 handleSend 方法
+ await handleSend(
+ message,
+ isNewConversation ? null : currConversationId,
+ files,
+ toServerInputs
+ );
+
+ } catch (error) {
+ console.error('发送消息失败:', error);
+ }
+ };
+
+ /**
+ * 处理侧边栏切换
+ */
+ const handleSidebarToggle = () => {
+ setSidebarCollapsed(!sidebarCollapsed);
+ };
+
+ /**
+ * 处理会话选择
+ */
+ const handleConversationSelect = (conversationId: string) => {
+ if (conversationId !== currConversationId) {
+ setCurrConversationId(conversationId, CHAT_CONFIG.APP_ID);
+ }
+ };
+
+ /**
+ * 处理新建会话
+ */
+ const handleNewConversation = () => {
+ setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false);
+ createNewChat();
+ };
+
+ /**
+ * 处理会话删除后的状态更新
+ */
+ 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);
+ };
+
+ /**
+ * 组件初始化 - 参考webapp-conversation的逻辑
+ */
+ useEffect(() => {
+ if (!hasSetAppConfig) {
+ console.error('应用配置不完整');
+ setAppUnavailable(true);
+ return;
+ }
+
+ (async () => {
+ try {
+ console.log('🚀 开始初始化聊天应用...');
+
+ // 并行获取会话列表和应用参数
+ const [conversationData, appParams] = await Promise.all([
+ fetchConversations(),
+ fetchAppParams()
+ ]);
+
+ // console.log('📋 获取到的数据:', { conversationData, appParams });
+
+ // 处理会话数据
+ const conversations = (conversationData as any).data || [];
+ if ((conversationData as any).error) {
+ console.error('获取会话列表失败:', (conversationData as any).error);
+ throw new Error((conversationData as any).error);
+ }
+
+ // 处理当前会话ID
+ const _conversationId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID);
+ const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId);
+
+ // console.log('💾 本地存储的会话ID:', _conversationId);
+ // console.log('🔍 是否为已存在的会话:', isNotNewConversation);
+
+ // 获取新会话信息
+ 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('🎯 设置当前会话ID:', _conversationId);
+ setCurrConversationId(_conversationId, CHAT_CONFIG.APP_ID, false);
+ } else {
+ // 如果localStorage为空或会话不存在,自动创建新会话
+ console.log('🆕 localStorage为空或会话不存在,创建新会话');
+ setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false);
+ }
+
+ setInited(true);
+ console.log('✅ 聊天应用初始化完成');
+ } catch (e: any) {
+ console.error('❌ 初始化失败:', e);
+ if (e.status === 404) {
+ setAppUnavailable(true);
+ } else {
+ setIsUnknownReason(true);
+ setAppUnavailable(true);
+ }
+ }
+ })();
+ }, []);
+
+ // 监听会话切换
+ useEffect(() => {
+ handleConversationSwitch();
+ }, [currConversationId, inited]);
+
+ // 自动滚动到底部
+ useEffect(() => {
+ if (chatListDomRef.current) {
+ chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight;
+ }
+ }, [chatList]);
+
+ // 如果应用不可用,显示错误页面
+ if (appUnavailable) {
+ return (
+
+
+
应用暂时不可用
+
+ {isUnknownReason ? '发生了未知错误,请稍后重试' : '应用配置不正确,请检查配置'}
+
+
+
+ );
+ }
+
+ // 如果未初始化完成,显示加载状态
+ if (!inited) {
+ return (
+
+ );
+ }
+
+ // 判断是否已设置输入
+ const hasSetInputs = (() => {
+ if (!isNewConversation) {
+ return true;
+ }
+ return isChatStarted;
+ })();
+
+ const conversationName = currConversationInfo?.name || '新对话';
+ const conversationIntroduction = currConversationInfo?.introduction || '';
+
+ return (
+
+ {/* 侧边栏 */}
+
+
+ {/* 主内容区域 */}
+
+
+ {/* 聊天区域 */}
+
+
+ {/* 如果是新会话且未开始聊天,显示欢迎信息 */}
+ {isNewConversation && !isChatStarted && (
+
+ )}
+
+ {/* 聊天消息列表 */}
+ {chatList.map((item) => (
+
+ ))}
+
+
+
+ {/* 输入区域 */}
+
+
+
+
+
+
+ );
}
\ No newline at end of file
diff --git a/app/components/chat/markdown.tsx b/app/components/chat/markdown.tsx
index bfa8454..412635b 100644
--- a/app/components/chat/markdown.tsx
+++ b/app/components/chat/markdown.tsx
@@ -1,242 +1,89 @@
-import React from 'react';
-import { Typography } from 'antd';
-import '../../styles/components/chat-with-llm/markdown.css';
-
-const { Paragraph, Text, Title } = Typography;
-
-interface MarkdownProps {
- content: string;
- className?: string;
-}
-
-/**
- * Markdown 渲染组件
- * 简化版本,支持基本的 Markdown 语法
- */
-export default function Markdown({ content, className = '' }: MarkdownProps) {
- if (!content) return null;
-
- /**
- * 简单的 Markdown 解析器
- */
- const parseMarkdown = (text: string): React.ReactNode => {
- // 分割成行
- const lines = text.split('\n');
- const elements: React.ReactNode[] = [];
- let currentParagraph: string[] = [];
- let inCodeBlock = false;
- let codeBlockContent: string[] = [];
- let codeBlockLanguage = '';
-
- const flushParagraph = () => {
- if (currentParagraph.length > 0) {
- const paragraphText = currentParagraph.join('\n');
- elements.push(
-
- {parseInlineElements(paragraphText)}
-
- );
- currentParagraph = [];
- }
- };
-
- const flushCodeBlock = () => {
- if (codeBlockContent.length > 0) {
- elements.push(
-
-
- {codeBlockContent.join('\n')}
-
-
- );
- codeBlockContent = [];
- codeBlockLanguage = '';
- }
- };
-
- lines.forEach((line, index) => {
- // 处理代码块
- if (line.startsWith('```')) {
- if (inCodeBlock) {
- // 结束代码块
- flushCodeBlock();
- inCodeBlock = false;
- } else {
- // 开始代码块
- flushParagraph();
- inCodeBlock = true;
- codeBlockLanguage = line.slice(3).trim();
- }
- return;
- }
-
- if (inCodeBlock) {
- codeBlockContent.push(line);
- return;
- }
-
- // 处理标题
- if (line.startsWith('# ')) {
- flushParagraph();
- elements.push(
-
- {parseInlineElements(line.slice(2))}
-
- );
- return;
- }
-
- if (line.startsWith('## ')) {
- flushParagraph();
- elements.push(
-
- {parseInlineElements(line.slice(3))}
-
- );
- return;
- }
-
- if (line.startsWith('### ')) {
- flushParagraph();
- elements.push(
-
- {parseInlineElements(line.slice(4))}
-
- );
- return;
- }
-
- // 处理列表
- if (line.startsWith('- ') || line.startsWith('* ')) {
- flushParagraph();
- elements.push(
-
- - {parseInlineElements(line.slice(2))}
-
- );
- return;
- }
-
- if (/^\d+\.\s/.test(line)) {
- flushParagraph();
- const match = line.match(/^\d+\.\s(.*)$/);
- if (match) {
- elements.push(
-
- - {parseInlineElements(match[1])}
-
- );
- }
- return;
- }
-
- // 处理空行
- if (line.trim() === '') {
- flushParagraph();
- return;
- }
-
- // 普通段落
- currentParagraph.push(line);
- });
-
- // 处理剩余内容
- flushParagraph();
- flushCodeBlock();
-
- return elements;
- };
-
- /**
- * 解析行内元素(粗体、斜体、代码、链接等)
- */
- const parseInlineElements = (text: string): React.ReactNode => {
- const parts: React.ReactNode[] = [];
- let currentText = text;
- let key = 0;
-
- // 处理行内代码
- currentText = currentText.replace(/`([^`]+)`/g, (match, code) => {
- const placeholder = `__CODE_${key}__`;
- parts.push(
-
- {code}
-
- );
- key++;
- return placeholder;
- });
-
- // 处理粗体
- currentText = currentText.replace(/\*\*([^*]+)\*\*/g, (match, bold) => {
- const placeholder = `__BOLD_${key}__`;
- parts.push(
-
- {bold}
-
- );
- key++;
- return placeholder;
- });
-
- // 处理斜体
- currentText = currentText.replace(/\*([^*]+)\*/g, (match, italic) => {
- const placeholder = `__ITALIC_${key}__`;
- parts.push(
-
- {italic}
-
- );
- key++;
- return placeholder;
- });
-
- // 处理链接
- currentText = currentText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
- const placeholder = `__LINK_${key}__`;
- parts.push(
-
- {linkText}
-
- );
- key++;
- return placeholder;
- });
-
- // 重新组装文本
- const finalParts: React.ReactNode[] = [];
- const textParts = currentText.split(/(__(?:CODE|BOLD|ITALIC|LINK)_\d+__)/);
-
- textParts.forEach((part, index) => {
- const match = part.match(/^__(\w+)_(\d+)__$/);
- if (match) {
- const [, type, partKey] = match;
- const component = parts.find((p: any) =>
- p.key === `${type.toLowerCase()}-${partKey}`
- );
- if (component) {
- finalParts.push(component);
- }
- } else if (part) {
- finalParts.push(part);
- }
- });
-
- return finalParts.length === 1 ? finalParts[0] : finalParts;
- };
-
- return (
-
- {parseMarkdown(content)}
-
- );
+import React from 'react';
+import ReactMarkdown from 'react-markdown';
+import 'katex/dist/katex.min.css';
+import RemarkMath from 'remark-math';
+import RemarkBreaks from 'remark-breaks';
+import RehypeKatex from 'rehype-katex';
+import RemarkGfm from 'remark-gfm';
+import '../../styles/components/chat-with-llm/markdown.css';
+
+interface MarkdownProps {
+ content: string;
+ className?: string;
+}
+
+/**
+ * Markdown 渲染组件
+ * 使用 react-markdown 库进行标准 Markdown 解析,支持流式渲染
+ */
+export default function Markdown({ content, className = '' }: MarkdownProps) {
+ console.log('🎨 [Markdown] 渲染组件:', {
+ contentLength: content?.length || 0,
+ contentPreview: content?.substring(0, 100) + (content && content.length > 100 ? '...' : ''),
+ className,
+ hasContent: !!content
+ });
+
+ if (!content) {
+ console.log('⚠️ [Markdown] 内容为空,返回null');
+ return null;
+ }
+
+ return (
+
+
+
+ {String(children).replace(/\n$/, '')}
+
+
+ );
+ } else {
+ // 内联代码
+ return (
+
+ {children}
+
+ );
+ }
+ },
+ }}
+ >
+ {content}
+
+
+ );
}
\ No newline at end of file
diff --git a/app/components/chat/sidebar.tsx b/app/components/chat/sidebar.tsx
index 41e72f7..eef8abf 100644
--- a/app/components/chat/sidebar.tsx
+++ b/app/components/chat/sidebar.tsx
@@ -202,6 +202,7 @@ const ChatSidebar = forwardRef(({
icon={}
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
+ // style={{ backgroundColor: '#00684A' }}
/>
)}
@@ -336,9 +337,9 @@ const ChatSidebar = forwardRef(({
{/* 侧边栏底部 - 固定在底部 */}
- {!collapsed && (
-