更新AI聊天页面样式

This commit is contained in:
pingchuan
2025-06-06 15:07:57 +08:00
parent d4ad36c3f2
commit 1b79f973da
17 changed files with 11257 additions and 2813 deletions
+16 -17
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Input, Button, Upload, Tooltip, message as antdMessage, Space } from 'antd';
import { SendOutlined, StopOutlined, PaperClipOutlined, PictureOutlined } from '@ant-design/icons';
import { StopOutlined, PictureOutlined, CommentOutlined } from '@ant-design/icons';
import type { VisionFile } from '../../types/dify_chat';
import '../../styles/components/chat-with-llm/chat-input.css';
@@ -184,7 +184,8 @@ export default function ChatInput({
icon={<PictureOutlined />}
size="small"
disabled={isDisabled}
className="text-gray-500 hover:text-blue-500"
className="chat-upload-button"
shape="circle"
/>
</Tooltip>
</Upload>
@@ -192,21 +193,19 @@ export default function ChatInput({
};
return (
<div className="border-t border-gray-200 bg-white p-4">
<div className="bg-white p-4">
<div className="max-w-4xl mx-auto">
{/* 文件预览 */}
{renderFilePreview()}
{/* 输入区域 */}
<div className="relative">
<div className="flex items-end gap-2">
{/* 输入区域 - 方块容器 */}
<div className="chat-input-box">
<div className="chat-input-content">
{/* 上传按钮 */}
<div className="flex items-end pb-1">
{renderUploadButton()}
</div>
{/* 文本输入 */}
<div className="flex-1">
<div className="chat-input-textarea-wrapper">
<TextArea
ref={textareaRef}
value={message}
@@ -216,13 +215,12 @@ export default function ChatInput({
placeholder={placeholder}
disabled={disabled}
autoSize={{ minRows: 1, maxRows: 6 }}
className="resize-none"
style={{ paddingRight: '50px' }}
className="chat-input-textarea"
/>
</div>
{/* 发送/停止按钮 */}
<div className="flex items-end pb-1">
<div className="chat-input-button">
{isResponding ? (
<Tooltip title="停止生成">
<Button
@@ -231,24 +229,25 @@ export default function ChatInput({
icon={<StopOutlined />}
onClick={handleStop}
disabled={disabled}
size="large"
size="small"
shape="circle"
/>
</Tooltip>
) : (
<Tooltip title="发送消息 (Enter)">
<Button
type="primary"
icon={<SendOutlined />}
icon={<CommentOutlined style={{ fontSize: '30px' }} />}
onClick={handleSubmit}
disabled={disabled || !message.trim()}
size="large"
size="small"
shape="circle"
className='chat-input-button-send'
/>
</Tooltip>
)}
</div>
</div>
{/* 字符计数和提示 */}
</div>
</div>
</div>
+1 -30
View File
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { Button, Tooltip, Avatar, Card, Image, Spin } from 'antd';
import { AntDesignOutlined, SmileTwoTone, RobotOutlined } from '@ant-design/icons';
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';
@@ -146,12 +145,6 @@ export default function ChatMessage({
<div className="chat-message">
<Card className="message-card assistant" size="small">
<div className="flex items-start gap-3">
{/* <Avatar icon={<RobotOutlined />} className="flex-shrink-0" />
*/}
<Avatar
size={{ xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 }}
icon={<RobotOutlined />}
/>
<div className="flex-1">
<Markdown content={content} />
{renderSuggestedQuestions()}
@@ -168,15 +161,6 @@ export default function ChatMessage({
<div className={`message-card ${isAnswer ? 'assistant' : 'user'}`}>
<Card size="small" className="shadow-sm">
<div className="flex items-start gap-2">
{/* 头像 */}
<Avatar
icon={isAnswer ? <RobotOutlined /> : <SmileTwoTone />}
className="flex-shrink-0"
style={{
backgroundColor: isAnswer ? '#1890ff' : '#52c41a'
}}
/>
{/* 消息内容 */}
<div className="flex-1 min-w-0">
{isAnswer ? renderAnswerContent() : (
@@ -185,19 +169,6 @@ export default function ChatMessage({
{/* {renderImages(message_files)} */}
</div>
)}
{/* 建议问题 */}
{/* {isAnswer && renderSuggestedQuestions()} */}
{/* 消息元信息 */}
{/* {renderMessageMeta()} */}
{/* 反馈按钮 */}
{/* {isAnswer && (
<div className="feedback-buttons">
{renderFeedbackButtons()}
</div>
)} */}
</div>
</div>
</Card>
+5 -4
View File
@@ -504,7 +504,7 @@ export default function Chat() {
const conversationIntroduction = currConversationInfo?.introduction || '';
return (
<Layout style={{ height: '90vh' }}>
<Layout style={{ height: '100%', minHeight: '100%' }}>
{/* 侧边栏 */}
<ChatSidebar
ref={sidebarRef}
@@ -526,10 +526,11 @@ export default function Chat() {
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: '100%',
}}
>
{/* 聊天区域 */}
<div className="flex-[0.85] overflow-hidden">
<div className="flex-1 overflow-hidden">
<div
ref={chatListDomRef}
className="h-full overflow-y-auto px-4 py-4 space-y-2"
@@ -555,11 +556,11 @@ export default function Chat() {
</div>
{/* 输入区域 */}
<div className="flex-[0.16] border-t border-gray-200 bg-white">
<div className="flex-shrink-0 bg-white" style={{ minHeight: '120px' }}>
<ChatInput
onSendMessage={handleSendMessage}
disabled={isResponding}
placeholder="请输入您的问题..."
placeholder="有什么我能帮您的吗?"
onStop={stopResponding}
isResponding={isResponding}
/>
+68 -221
View File
@@ -1,9 +1,12 @@
import React from 'react';
import { Typography } from 'antd';
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';
const { Paragraph, Text, Title } = Typography;
interface MarkdownProps {
content: string;
className?: string;
@@ -11,232 +14,76 @@ interface MarkdownProps {
/**
* Markdown 渲染组件
* 简化版本,支持基本的 Markdown 语法
* 使用 react-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(
<Paragraph key={elements.length} className={className}>
{parseInlineElements(paragraphText)}
</Paragraph>
);
currentParagraph = [];
}
};
const flushCodeBlock = () => {
if (codeBlockContent.length > 0) {
elements.push(
<pre
key={elements.length}
className="bg-gray-800 text-white p-3 rounded-md overflow-x-auto my-2"
>
<code className={codeBlockLanguage ? `language-${codeBlockLanguage}` : ''}>
{codeBlockContent.join('\n')}
</code>
</pre>
);
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(
<Title key={elements.length} level={1} className={className}>
{parseInlineElements(line.slice(2))}
</Title>
);
return;
}
if (line.startsWith('## ')) {
flushParagraph();
elements.push(
<Title key={elements.length} level={2} className={className}>
{parseInlineElements(line.slice(3))}
</Title>
);
return;
}
if (line.startsWith('### ')) {
flushParagraph();
elements.push(
<Title key={elements.length} level={3} className={className}>
{parseInlineElements(line.slice(4))}
</Title>
);
return;
}
// 处理列表
if (line.startsWith('- ') || line.startsWith('* ')) {
flushParagraph();
elements.push(
<ul key={elements.length} className="list-disc list-inside my-2">
<li>{parseInlineElements(line.slice(2))}</li>
</ul>
);
return;
}
if (/^\d+\.\s/.test(line)) {
flushParagraph();
const match = line.match(/^\d+\.\s(.*)$/);
if (match) {
elements.push(
<ol key={elements.length} className="list-decimal list-inside my-2">
<li>{parseInlineElements(match[1])}</li>
</ol>
);
}
return;
}
// 处理空行
if (line.trim() === '') {
flushParagraph();
return;
}
// 普通段落
currentParagraph.push(line);
console.log('🎨 [Markdown] 渲染组件:', {
contentLength: content?.length || 0,
contentPreview: content?.substring(0, 100) + (content && content.length > 100 ? '...' : ''),
className,
hasContent: !!content
});
// 处理剩余内容
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(
<Text key={`code-${key}`} code className="bg-gray-100 px-1 rounded">
{code}
</Text>
);
key++;
return placeholder;
});
// 处理粗体
currentText = currentText.replace(/\*\*([^*]+)\*\*/g, (match, bold) => {
const placeholder = `__BOLD_${key}__`;
parts.push(
<Text key={`bold-${key}`} strong>
{bold}
</Text>
);
key++;
return placeholder;
});
// 处理斜体
currentText = currentText.replace(/\*([^*]+)\*/g, (match, italic) => {
const placeholder = `__ITALIC_${key}__`;
parts.push(
<Text key={`italic-${key}`} italic>
{italic}
</Text>
);
key++;
return placeholder;
});
// 处理链接
currentText = currentText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
const placeholder = `__LINK_${key}__`;
parts.push(
<a
key={`link-${key}`}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700 underline"
>
{linkText}
</a>
);
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);
if (!content) {
console.log('⚠️ [Markdown] 内容为空,返回null');
return null;
}
} else if (part) {
finalParts.push(part);
}
});
return finalParts.length === 1 ? finalParts[0] : finalParts;
};
return (
<div className={`markdown-content ${className}`}>
{parseMarkdown(content)}
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[RehypeKatex]}
components={{
code({ className, children, ...props }: any) {
const match = /language-(\w+)/.exec(className || '');
const isCodeBlock = match;
if (isCodeBlock) {
// 代码块
return (
<pre style={{
backgroundColor: '#f6f8fa',
padding: '16px',
borderRadius: '6px',
overflow: 'auto',
fontSize: '14px',
lineHeight: '1.45',
margin: '1em 0'
}}>
<code style={{
backgroundColor: 'transparent',
padding: '0',
fontSize: 'inherit',
fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}>
{String(children).replace(/\n$/, '')}
</code>
</pre>
);
} else {
// 内联代码
return (
<code
className={className}
style={{
backgroundColor: 'rgba(175, 184, 193, 0.2)',
padding: '0.2em 0.4em',
borderRadius: '3px',
fontSize: '85%',
fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}
{...props}
>
{children}
</code>
);
}
},
}}
>
{content}
</ReactMarkdown>
</div>
);
}
+4 -3
View File
@@ -202,6 +202,7 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
icon={<MoreOutlined />}
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
// style={{ backgroundColor: '#00684A' }}
/>
</Dropdown>
)}
@@ -336,9 +337,9 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
</div>
{/* 侧边栏底部 - 固定在底部 */}
{!collapsed && (
<div className="p-4 border-t border-gray-100 flex-shrink-0">
<div className="text-xs text-gray-500 text-center">
{!collapsed && conversations.length > 0 && (
<div className="sidebar-footer">
<div className="stats-text">
{conversations.length}
</div>
</div>
+66 -18
View File
@@ -61,39 +61,45 @@ export default function useChatMessage({
questionItem: ChatItem,
originalResponseId?: string
) => {
// console.log('🔄 更新聊天列表:', {
// responseItemId: responseItem.id,
// responseContent: responseItem.content,
// originalResponseId,
// questionId,
// placeholderAnswerId
// });
console.log('🔄 [useChatMessage] 更新聊天列表:', {
responseItemId: responseItem.id,
responseContentLength: responseItem.content.length,
responsePreview: responseItem.content.substring(0, 50) + (responseItem.content.length > 50 ? '...' : ''),
originalResponseId,
questionId,
placeholderAnswerId
});
setChatList(produce(getChatList(), (draft) => {
// console.log('📝 当前聊天列表:', draft.map(item => ({ id: item.id, content: item.content.substring(0, 20), isAnswer: item.isAnswer })));
console.log('📝 [useChatMessage] 当前聊天列表:', draft.map(item => ({
id: item.id,
contentLength: item.content.length,
contentPreview: item.content.substring(0, 20) + (item.content.length > 20 ? '...' : ''),
isAnswer: item.isAnswer
})));
// 移除占位符
const placeholderIndex = draft.findIndex(item => item.id === placeholderAnswerId);
if (placeholderIndex !== -1) {
// console.log('🗑️ 移除占位符:', placeholderAnswerId);
console.log('🗑️ [useChatMessage] 移除占位符:', placeholderAnswerId, 'at index:', placeholderIndex);
draft.splice(placeholderIndex, 1);
}
// 确保问题存在
const questionIndex = draft.findIndex(item => item.id === questionId);
if (questionIndex === -1) {
// console.log(' 添加问题:', questionId);
console.log(' [useChatMessage] 添加问题:', questionId);
draft.push({ ...questionItem });
}
// 更新或添加响应 - 考虑ID可能已经改变的情况
let responseIndex = draft.findIndex(item => item.id === responseItem.id);
// console.log('🔍 查找响应索引 (当前ID):', { responseItemId: responseItem.id, responseIndex });
console.log('🔍 [useChatMessage] 查找响应索引 (当前ID):', { responseItemId: responseItem.id, responseIndex });
// 如果找不到当前ID的响应,尝试查找原始ID
if (responseIndex === -1 && originalResponseId) {
responseIndex = draft.findIndex(item => item.id === originalResponseId);
// console.log('🔍 查找响应索引 (原始ID):', { originalResponseId, responseIndex });
console.log('🔍 [useChatMessage] 查找响应索引 (原始ID):', { originalResponseId, responseIndex });
}
// 如果找不到任何匹配的响应,查找最后一个AI回答
@@ -102,18 +108,30 @@ export default function useChatMessage({
item.isAnswer &&
index > draft.findIndex(q => q.id === questionId)
);
// console.log('🔍 查找响应索引 (最后AI回答):', { responseIndex });
console.log('🔍 [useChatMessage] 查找响应索引 (最后AI回答):', { responseIndex });
}
if (responseIndex !== -1) {
// console.log('✏️ 更新现有响应:', { responseIndex, newContent: responseItem.content.substring(0, 20) });
console.log('✏️ [useChatMessage] 更新现有响应:', {
responseIndex,
oldContentLength: draft[responseIndex].content.length,
newContentLength: responseItem.content.length
});
draft[responseIndex] = { ...responseItem };
} else {
// console.log(' 添加新响应:', { responseId: responseItem.id, content: responseItem.content.substring(0, 20) });
console.log(' [useChatMessage] 添加新响应:', {
responseId: responseItem.id,
contentLength: responseItem.content.length
});
draft.push({ ...responseItem });
}
// console.log('📝 更新后聊天列表:', draft.map(item => ({ id: item.id, content: item.content.substring(0, 20), isAnswer: item.isAnswer })));
console.log('📝 [useChatMessage] 更新后聊天列表:', draft.map(item => ({
id: item.id,
contentLength: item.content.length,
contentPreview: item.content.substring(0, 20) + (item.content.length > 20 ? '...' : ''),
isAnswer: item.isAnswer
})));
}));
}, [getChatList, setChatList]);
@@ -251,16 +269,35 @@ export default function useChatMessage({
// 发送消息
await sendChatMessage(data, {
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }) => {
// console.log('📨 收到流式数据:', { message, isFirstMessage, messageId, newConversationId });
console.log('📨 [useChatMessage] 收到流式数据:', {
messageLength: message.length,
message: message.substring(0, 100) + (message.length > 100 ? '...' : ''),
isFirstMessage,
messageId,
newConversationId,
taskId,
isAgentMode,
currentContentLength: responseItem.content.length
});
if (!isAgentMode) {
// 累积消息内容
const oldContent = responseItem.content;
responseItem.content = responseItem.content + message;
// console.log('📝 累积消息内容:', { currentContent: responseItem.content });
console.log('📝 [useChatMessage] 累积消息内容:', {
oldLength: oldContent.length,
newLength: responseItem.content.length,
addedLength: message.length,
preview: responseItem.content.substring(0, 50) + '...'
});
} else {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1];
if (lastThought) {
lastThought.thought = (lastThought.thought || '') + message;
console.log('🤔 [useChatMessage] 累积思考内容:', {
thoughtLength: lastThought.thought.length,
addedLength: message.length
});
}
}
@@ -303,6 +340,17 @@ export default function useChatMessage({
// originalResponseId
// });
console.log('🔄 [useChatMessage] 准备调用updateCurrentQA:', {
responseItemId: responseItem.id,
responseContentLength: responseItem.content.length,
responsePreview: responseItem.content.substring(0, 100) + (responseItem.content.length > 100 ? '...' : ''),
questionId,
placeholderAnswerId,
originalResponseId,
isAgentMode,
agentThoughtsCount: responseItem.agent_thoughts?.length || 0
});
// 更新当前问答(使用防抖)
updateCurrentQA({
responseItem: { ...responseItem }, // 创建副本避免引用问题
+24 -2
View File
@@ -19,7 +19,16 @@ export async function action({ request }: ActionFunctionArgs) {
response_mode: responseMode,
} = body;
// ('🚀 Chat Messages API - User:', user, 'Query:', query?.substring(0, 100));
console.log('🚀 [API] Chat Messages API - 收到请求:', {
user,
queryLength: query?.length || 0,
queryPreview: query?.substring(0, 100) + (query?.length > 100 ? '...' : ''),
conversationId,
responseMode,
hasInputs: !!inputs,
hasFiles: !!files && files.length > 0,
filesCount: files?.length || 0
});
const response = await difyClient.createChatMessage(
inputs,
@@ -30,8 +39,16 @@ export async function action({ request }: ActionFunctionArgs) {
files
);
console.log('📡 [API] Dify响应状态:', {
status: response.status,
statusText: response.statusText,
hasBody: !!response.body,
headers: Object.fromEntries(response.headers.entries())
});
// 对于流式响应,直接返回流
if (responseMode === 'streaming') {
console.log('🌊 [API] 返回流式响应');
return new Response(response.body, {
status: response.status,
headers: {
@@ -46,6 +63,7 @@ export async function action({ request }: ActionFunctionArgs) {
}
// 对于非流式响应,返回JSON
console.log('📄 [API] 返回JSON响应');
return new Response(JSON.stringify(response), {
status: 200,
headers: {
@@ -54,7 +72,11 @@ export async function action({ request }: ActionFunctionArgs) {
});
} catch (error: any) {
// console.error('❌ Chat Messages API - Error:', error);
console.error('❌ [API] Chat Messages API - Error:', {
message: error.message,
stack: error.stack,
name: error.name
});
return new Response(
JSON.stringify({ error: error.message || 'Failed to send message' }),
{
+8 -1
View File
@@ -35,7 +35,14 @@ export const meta: MetaFunction = () => {
*/
export default function ChatWithLLMIndex() {
return (
<div className="h-full chat-container">
<div className="flex-1 chat-container" style={{
height: 'calc(100vh - 80px)',
borderRadius: '0.5rem',
marginTop: '20px',
marginBottom: '-20px',
minHeight: '990px',
maxHeight: '89vh'
}}>
<Chat />
</div>
);
+86 -10
View File
@@ -122,7 +122,14 @@ const handleStream = (
onNodeFinished?: IOnNodeFinished,
onError?: IOnError,
) => {
console.log('🌊 [handleStream] 开始处理流式响应:', {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
});
if (!response.ok) {
console.error('❌ [handleStream] 响应错误:', response.status, response.statusText);
onError?.('网络响应错误');
throw new Error('网络响应错误');
}
@@ -132,25 +139,57 @@ const handleStream = (
let buffer = '';
let bufferObj: Record<string, any>;
let isFirstMessage = true;
let messageCount = 0;
console.log('📖 [handleStream] 获取reader:', !!reader);
function read() {
let hasError = false;
reader?.read().then((result: any) => {
console.log('📨 [handleStream] 读取数据块:', {
done: result.done,
valueLength: result.value?.length,
messageCount: ++messageCount
});
if (result.done) {
console.log('✅ [handleStream] 流式响应完成, 总消息数:', messageCount);
onCompleted && onCompleted();
return;
}
buffer += decoder.decode(result.value, { stream: true });
const chunk = decoder.decode(result.value, { stream: true });
buffer += chunk;
const lines = buffer.split('\n');
console.log('🔍 [handleStream] 处理数据块:', {
chunkLength: chunk.length,
bufferLength: buffer.length,
linesCount: lines.length,
chunk: chunk.substring(0, 100) + (chunk.length > 100 ? '...' : '')
});
try {
lines.forEach((message) => {
lines.forEach((message, index) => {
if (message.startsWith('data: ')) {
const jsonStr = message.substring(6);
console.log(`📋 [handleStream] 解析消息 ${index}:`, {
jsonLength: jsonStr.length,
preview: jsonStr.substring(0, 200) + (jsonStr.length > 200 ? '...' : '')
});
try {
bufferObj = JSON.parse(message.substring(6)) as Record<string, any>;
bufferObj = JSON.parse(jsonStr) as Record<string, any>;
console.log('✨ [handleStream] JSON解析成功:', {
event: bufferObj.event,
hasAnswer: !!bufferObj.answer,
answerLength: bufferObj.answer?.length || 0,
conversationId: bufferObj.conversation_id,
messageId: bufferObj.id || bufferObj.message_id
});
}
catch (e) {
console.warn('⚠️ [handleStream] JSON解析失败:', e, 'JSON:', jsonStr);
// 处理消息截断
onData('', isFirstMessage, {
conversationId: bufferObj?.conversation_id,
@@ -160,6 +199,12 @@ const handleStream = (
}
if (bufferObj.status === 400 || !bufferObj.event) {
console.error('❌ [handleStream] 错误响应:', {
status: bufferObj.status,
event: bufferObj.event,
message: bufferObj.message,
code: bufferObj.code
});
onData('', false, {
conversationId: undefined,
messageId: '',
@@ -172,37 +217,64 @@ const handleStream = (
}
if (bufferObj.event === 'message' || bufferObj.event === 'agent_message') {
onData(unicodeToChar(bufferObj.answer), isFirstMessage, {
const answer = unicodeToChar(bufferObj.answer);
console.log('💬 [handleStream] 处理消息事件:', {
event: bufferObj.event,
isFirstMessage,
answerLength: answer.length,
answer: answer.substring(0, 50) + (answer.length > 50 ? '...' : ''),
conversationId: bufferObj.conversation_id,
messageId: bufferObj.id || bufferObj.message_id
});
onData(answer, isFirstMessage, {
conversationId: bufferObj.conversation_id,
messageId: bufferObj.id || bufferObj.message_id,
taskId: bufferObj.task_id,
});
isFirstMessage = false;
} else if (bufferObj.event === 'agent_thought' && onThought) {
console.log('🤔 [handleStream] 处理思考事件:', bufferObj.event);
onThought(bufferObj as ThoughtItem);
} else if (bufferObj.event === 'message_file' && onFile) {
console.log('📁 [handleStream] 处理文件事件:', bufferObj.event);
onFile(bufferObj as VisionFile);
} else if (bufferObj.event === 'message_end' && onMessageEnd) {
console.log('🏁 [handleStream] 处理消息结束事件:', bufferObj.event);
onMessageEnd(bufferObj as MessageEnd);
} else if (bufferObj.event === 'message_replace' && onMessageReplace) {
console.log('🔄 [handleStream] 处理消息替换事件:', bufferObj.event);
onMessageReplace(bufferObj as MessageReplace);
} else if (bufferObj.event === 'workflow_started' && onWorkflowStarted) {
console.log('🚀 [handleStream] 处理工作流开始事件:', bufferObj.event);
onWorkflowStarted(bufferObj as WorkflowStartedResponse);
} else if (bufferObj.event === 'workflow_finished' && onWorkflowFinished) {
console.log('🎯 [handleStream] 处理工作流完成事件:', bufferObj.event);
onWorkflowFinished(bufferObj as WorkflowFinishedResponse);
} else if (bufferObj.event === 'node_started' && onNodeStarted) {
console.log('🔗 [handleStream] 处理节点开始事件:', bufferObj.event);
onNodeStarted(bufferObj as NodeStartedResponse);
} else if (bufferObj.event === 'node_finished' && onNodeFinished) {
console.log('✅ [handleStream] 处理节点完成事件:', bufferObj.event);
onNodeFinished(bufferObj as NodeFinishedResponse);
} else {
console.log('❓ [handleStream] 未知事件类型:', bufferObj.event);
}
} else if (message.trim()) {
console.log('📝 [handleStream] 非data消息:', message.substring(0, 100));
}
});
// 保留最后一行(可能是不完整的消息)
buffer = lines[lines.length - 1];
const lastLine = lines[lines.length - 1];
buffer = lastLine;
console.log('💾 [handleStream] 保留缓冲区:', {
lastLineLength: lastLine.length,
preview: lastLine.substring(0, 50)
});
}
catch (err) {
console.error('解析响应时出错:', err);
console.error('❌ [handleStream] 解析响应时出错:', err);
onData('', false, {
conversationId: undefined,
messageId: '',
@@ -213,10 +285,14 @@ const handleStream = (
return;
}
if (!hasError)
if (!hasError) {
console.log('🔄 [handleStream] 继续读取下一块...');
read();
} else {
console.log('🛑 [handleStream] 因错误停止读取');
}
}).catch(err => {
console.error('读取流时出错:', err);
console.error('❌ [handleStream] 读取流时出错:', err);
onError?.(err.message);
});
}
@@ -272,7 +348,7 @@ const baseFetch = (url: string, fetchOptions: any, needAllResponseContent: boole
.then((res: Response) => {
const resClone = res.clone();
('📥 API Response:', {
console.log('📥 API Response:', {
status: res.status,
statusText: res.statusText,
url: urlWithPrefix
@@ -380,7 +456,7 @@ export const ssePost = (
return fetch(urlWithPrefix, options)
.then((res: Response) => {
('📡 SSE Response:', {
console.log('📡 SSE Response:', {
status: res.status,
statusText: res.statusText,
url: urlWithPrefix
+22
View File
@@ -122,16 +122,38 @@ export const difyClient = {
files: files || [],
};
console.log('🌐 [DifyClient] 发送聊天消息:', {
queryLength: query.length,
queryPreview: query.substring(0, 100) + (query.length > 100 ? '...' : ''),
user,
responseMode,
conversationId,
hasInputs: !!inputs && Object.keys(inputs).length > 0,
inputsKeys: inputs ? Object.keys(inputs) : [],
hasFiles: !!files && files.length > 0,
filesCount: files?.length || 0
});
const response = await difyFetch('chat-messages', {
method: 'POST',
body: JSON.stringify(body),
});
console.log('📡 [DifyClient] Dify API响应:', {
status: response.status,
statusText: response.statusText,
hasBody: !!response.body,
contentType: response.headers.get('Content-Type'),
responseMode
});
// 对于流式响应,直接返回Response对象
if (responseMode === 'streaming') {
console.log('🌊 [DifyClient] 返回流式响应对象');
return response;
}
console.log('📄 [DifyClient] 解析JSON响应');
return response.json();
},
@@ -1,8 +1,7 @@
/* 聊天输入区域 */
.chat-input-container {
flex-shrink: 0;
background: #fff;
border-top: 1px solid #e5e7eb;
background: #F9FAFB;
padding: 16px 20px;
z-index: 10;
}
@@ -13,15 +12,69 @@
position: relative;
}
/* 新的输入框容器样式 */
.chat-input-box {
border: 1px solid #d1d5db;
/* 圆角 */
border-radius: 30px;
background: #F9FAFB;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.chat-input-content {
display: flex;
align-items: center;
padding: 12px;
}
.chat-input-textarea-wrapper {
flex: 1;
height: 10vh;
margin: 0 8px;
}
.chat-input-textarea {
resize: none !important;
border: 0 !important;
box-shadow: none !important;
padding: 8px 0 !important;
background: transparent !important;
}
.chat-input-button {
flex-shrink: 0;
}
.chat-upload-button {
color: #6b7280;
flex-shrink: 0;
}
.chat-upload-button:hover {
color: #3b82f6;
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-input-container {
padding: 12px 16px;
}
.chat-input-content {
padding: 10px;
}
}
@media (max-width: 480px) {
.chat-input-container {
padding: 8px 12px;
}
.chat-input-content {
padding: 8px;
}
}
.chat-input-button-send {
margin-top: 80px;
}
@@ -14,10 +14,41 @@
.message-card.user {
margin-left: auto;
margin-right: 15px;
/* 用户消息离右边远一点 */
}
.message-card.assistant {
margin-right: auto;
margin-left: 15px;
/* AI响应离左边远一点 */
}
.message-card.assistant .ant-card {
background-color: #a4e2ad;
}
.message-card.assistant .ant-card .ant-card-body {
background-color: #a4e2ad;
}
/* Card组件圆角调整 */
.message-card .ant-card {
border-radius: 20px !important;
/* 增加圆角度 */
}
.message-card .ant-card .ant-card-body {
border-radius: 20px !important;
/* 确保body也有圆角 */
min-height: 20px !important;
/* 设置最小高度 */
padding: 12px 16px !important;
/* 调整内边距 - 减少上下内边距 */
display: block !important;
/* 改为block布局,移除flex */
line-height: 1.4 !important;
/* 减少行高 */
}
/* 流式文本效果 */
@@ -123,10 +154,30 @@
.message-card {
max-width: 95%;
}
.message-card.user {
margin-right: 30px;
/* 平板上减少右边距 */
}
.message-card.assistant {
margin-left: 30px;
/* 平板上减少左边距 */
}
}
@media (max-width: 480px) {
.message-card {
max-width: 100%;
}
.message-card.user {
margin-right: 15px;
/* 手机上进一步减少右边距 */
}
.message-card.assistant {
margin-left: 15px;
/* 手机上进一步减少左边距 */
}
}
@@ -2,7 +2,8 @@
/* 聊天容器 - 自适应布局 */
.chat-container {
height: 400px;
height: 100%;
min-height: 100%;
width: 100%;
flex-direction: column;
background: #fff;
@@ -11,6 +12,7 @@
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin: 20px auto;
display: flex;
}
/* 聊天头部 */
+194 -29
View File
@@ -4,60 +4,97 @@
line-height: 1.6;
color: #374151;
overflow-wrap: break-word;
text-align: left;
/* 左对齐 */
width: 100%;
box-sizing: border-box;
margin: 0;
/* 移除外边距 */
padding: 0;
/* 移除内边距 */
}
/* 标题样式 */
/* 移除Ant Design Typography组件的居中样式 */
.markdown-content .ant-typography {
margin: 0 !important;
text-align: left !important;
width: 100% !important;
}
/* 段落样式 - 减少边距 */
.markdown-content p,
.markdown-content .markdown-paragraph {
margin: 0 !important;
/* 移除段落边距 */
text-align: left;
line-height: 1.4;
/* 减少行高 */
padding: 0;
/* 移除内边距 */
}
/* 标题样式 - 减少边距 */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
.markdown-content h6,
.markdown-content .markdown-heading {
margin: 0.5em 0 0.25em 0 !important;
/* 减少标题边距 */
font-weight: 600;
line-height: 1.25;
line-height: 1.2;
/* 减少行高 */
text-align: left;
}
.markdown-content h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
.markdown-content h2 {
.markdown-content h1,
.markdown-content .markdown-h1 {
font-size: 1.5em;
/* 减小字体 */
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
padding-bottom: 0.2em;
}
.markdown-content h3 {
font-size: 1.25em;
.markdown-content h2,
.markdown-content .markdown-h2 {
font-size: 1.3em;
/* 减小字体 */
border-bottom: 1px solid #eaecef;
padding-bottom: 0.2em;
}
.markdown-content h4 {
.markdown-content h3,
.markdown-content .markdown-h3 {
font-size: 1.1em;
/* 减小字体 */
}
.markdown-content h4,
.markdown-content .markdown-h4 {
font-size: 1em;
}
/* 段落样式 */
.markdown-content p {
margin: 0.75em 0;
}
/* 列表样式 */
.markdown-content ul,
.markdown-content ol {
.markdown-content ol,
.markdown-content .markdown-list {
margin-top: 0.5em;
margin-bottom: 0.5em;
padding-left: 1.5em;
text-align: left;
}
.markdown-content li {
.markdown-content li,
.markdown-content .markdown-list-item {
margin: 0.3em 0;
text-align: left;
}
/* 代码样式 */
.markdown-content code {
.markdown-content code,
.markdown-content .inline-code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
@@ -66,7 +103,8 @@
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
}
.markdown-content pre {
.markdown-content pre,
.markdown-content .code-block {
padding: 16px;
overflow: auto;
font-size: 85%;
@@ -74,6 +112,7 @@
background-color: #f6f8fa;
border-radius: 6px;
margin: 1em 0;
text-align: left;
}
.markdown-content pre code {
@@ -86,7 +125,8 @@
}
/* 表格样式 */
.markdown-content table {
.markdown-content table,
.markdown-content .markdown-table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
@@ -97,6 +137,7 @@
.markdown-content table td {
padding: 8px 16px;
border: 1px solid #dfe2e5;
text-align: left;
}
.markdown-content table th {
@@ -109,17 +150,20 @@
}
/* 链接样式 */
.markdown-content a {
.markdown-content a,
.markdown-content .markdown-link {
color: #0969da;
text-decoration: none;
}
.markdown-content a:hover {
.markdown-content a:hover,
.markdown-content .markdown-link:hover {
text-decoration: underline;
}
/* 引用样式 */
.markdown-content blockquote {
.markdown-content blockquote,
.markdown-content .markdown-blockquote {
margin: 1em 0;
padding: 0 1em;
color: #6a737d;
@@ -134,3 +178,124 @@
background-color: #e1e4e8;
border: 0;
}
/* 确保父容器Card的body支持左对齐并减少高度 */
.ant-card-body {
display: block !important;
text-align: left !important;
min-height: 20px !important;
padding: 12px 16px !important;
line-height: 1.4 !important;
}
/* 流式文本效果 */
.streaming-text {
position: relative;
}
.streaming-text::after {
content: '';
animation: blink 1s infinite;
color: #1890ff;
margin-left: 2px;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
/* 响应指示器 */
.responding-indicator {
display: flex;
align-items: center;
gap: 8px;
color: #6b7280;
font-size: 14px;
}
/* 建议问题 */
.suggested-questions {
margin-top: 16px;
}
.question-button {
margin-bottom: 8px;
margin-right: 8px;
white-space: normal;
height: auto;
padding: 8px 12px;
text-align: left;
border: 1px solid #d1d5db;
background: #f9fafb;
color: #374151;
transition: all 0.2s ease;
}
.question-button:hover {
border-color: #1890ff;
background: #f0f9ff;
color: #1890ff;
}
/* 消息时间戳 */
.message-timestamp {
font-size: 12px;
color: #9ca3af;
margin-top: 8px;
display: flex;
gap: 12px;
}
/* 消息图片 */
.message-images {
margin-top: 12px;
}
.message-images .ant-image {
border-radius: 8px;
overflow: hidden;
}
/* 响应式设计 */
@media (max-width: 768px) {
.message-card {
max-width: 95%;
}
.message-card.user {
margin-right: 30px;
}
.message-card.assistant {
margin-left: 30px;
}
}
@media (max-width: 480px) {
.message-card {
max-width: 100%;
}
.message-card.user {
margin-right: 15px;
}
.message-card.assistant {
margin-left: 15px;
}
}
/* AI消息中的Markdown内容背景色 */
.message-card.assistant .markdown-content {
background-color: #a4e2ad;
max-width: 65vh;
}
+129 -8
View File
@@ -1,74 +1,195 @@
/* 聊天侧边栏样式 */
/* ========== 聊天侧边栏样式 ========== */
/* 会话菜单项基础样式 - 作用域:每个会话项的容器 */
.chat-sidebar-menu .ant-menu-item {
margin: 4px 0;
/* 会话项之间的垂直间距 */
border-radius: 6px;
/* 会话项圆角 */
height: auto;
/* 自适应高度 */
line-height: 1.4;
/* 行高 */
padding: 8px 12px;
/* 内边距 */
color: #374151;
/* 会话文字颜色 - 默认状态 */
}
/* 会话项悬停状态 - 作用域:鼠标悬停在会话项上时 */
.chat-sidebar-menu .ant-menu-item:hover {
background-color: #f5f5f5;
/* 悬停时的背景色 */
/* color: #1f2937; */
/* 悬停时的文字颜色 */
}
/* 会话项选中状态 - 作用域:当前选中的会话项 */
.chat-sidebar-menu .ant-menu-item-selected {
background-color: rgba(0, 104, 74, 0.1);
border-color: rgb(0, 104, 74);
/* 选中时的背景色(绿色半透明) */
border-color: #00684A;
/* 选中时的边框色 */
color: #00684A;
/* 选中时的文字颜色(绿色) */
font-weight: 500;
/* 选中时的文字粗细 */
}
/* 会话项选中状态的右侧指示条 - 作用域:选中会话项的右侧边框 */
.chat-sidebar-menu .ant-menu-item-selected::after {
border-right: 3px solid rgb(0, 104, 74);
border-right: 3px solid #00684A;
/* 选中时右侧的绿色指示条 */
}
/* 会话项样式 */
/* 会话项图标样式 - 作用域:会话项前面的消息图标 */
.chat-sidebar-menu .ant-menu-item .anticon {
color: inherit;
/* 图标颜色继承文字颜色 */
font-size: 14px;
/* 图标大小 */
}
/* 会话项标题内容容器 - 作用域:会话名称和操作按钮的容器 */
.chat-sidebar-menu .ant-menu-item .ant-menu-title-content {
width: 100%;
/* 占满整个宽度 */
}
/* 响应式设计 */
/* 会话名称文本样式 - 作用域:会话名称文字 */
.chat-sidebar-menu .ant-menu-item .ant-menu-title-content span {
color: inherit;
/* 继承父元素的文字颜色 */
font-size: 14px;
/* 文字大小 */
font-weight: inherit;
/* 继承父元素的文字粗细 */
}
/* ========== 响应式设计 ========== */
/* 平板和手机端侧边栏 - 作用域:屏幕宽度小于768px时的侧边栏 */
@media (max-width: 768px) {
.ant-layout-sider {
position: fixed !important;
/* 固定定位 */
left: 0;
/* 贴左边 */
top: 0;
/* 贴顶部 */
bottom: 0;
/* 贴底部 */
z-index: 1000;
/* 层级 */
}
/* 折叠状态的侧边栏 - 作用域:手机端折叠时隐藏侧边栏 */
.ant-layout-sider.ant-layout-sider-collapsed {
left: -200px;
/* 向左移出屏幕 */
}
}
/* 滚动条样式 */
/* ========== 滚动条样式 ========== */
/* Webkit浏览器滚动条宽度 - 作用域:会话列表的滚动条 */
.chat-sidebar-menu::-webkit-scrollbar {
width: 4px;
/* 滚动条宽度 */
}
/* Webkit浏览器滚动条轨道 - 作用域:滚动条背景轨道 */
.chat-sidebar-menu::-webkit-scrollbar-track {
background: #f1f1f1;
/* 轨道背景色 */
border-radius: 2px;
/* 轨道圆角 */
}
/* Webkit浏览器滚动条滑块 - 作用域:滚动条可拖拽部分 */
.chat-sidebar-menu::-webkit-scrollbar-thumb {
background: #c1c1c1;
/* 滑块背景色 */
border-radius: 2px;
/* 滑块圆角 */
}
/* Webkit浏览器滚动条滑块悬停 - 作用域:鼠标悬停在滚动条滑块上时 */
.chat-sidebar-menu::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
/* 悬停时滑块颜色 */
}
/* 确保侧边栏布局正确 */
/* ========== 侧边栏布局样式 ========== */
/* 侧边栏容器布局 - 作用域:整个侧边栏容器 */
.ant-layout-sider {
display: flex !important;
/* 弹性布局 */
flex-direction: column !important;
/* 垂直方向排列 */
height: 100% !important;
/* 占满父容器高度 */
min-height: 100% !important;
/* 最小高度也是100% */
}
/* 侧边栏内容区域样式 */
/* 侧边栏内容区域 - 作用域:侧边栏内部所有内容的容器 */
.ant-layout-sider .ant-layout-sider-children {
display: flex;
/* 弹性布局 */
flex-direction: column;
/* 垂直方向排列 */
height: 100%;
/* 占满高度 */
min-height: 100%;
/* 最小高度 */
overflow: hidden;
/* 隐藏溢出内容 */
}
/* ========== 操作按钮样式 ========== */
/* 更多操作按钮 - 作用域:会话项右侧的三点菜单按钮 */
.chat-sidebar-menu .ant-menu-item .ant-dropdown-trigger {
color: #6b7280;
/* 默认颜色(灰色) */
}
/* 更多操作按钮悬停 - 作用域:鼠标悬停在三点菜单按钮上时 */
.chat-sidebar-menu .ant-menu-item .ant-dropdown-trigger:hover {
color: #00684A;
/* 悬停时颜色(绿色) */
}
/* ========== 侧边栏底部统计信息 ========== */
/* 侧边栏底部容器 - 作用域:显示对话数量统计的底部区域 */
.ant-layout-sider .sidebar-footer {
background-color: #f9fafb;
/* 底部背景色 */
border-top: 1px solid #e5e7eb;
/* 顶部分割线 */
padding: 12px 16px;
/* 内边距 */
flex-shrink: 0;
/* 不允许收缩 */
/* 距离顶部位置 */
margin-top: 20px;
}
/* 侧边栏底部文本 - 作用域:对话数量统计文本 */
.ant-layout-sider .sidebar-footer .stats-text {
font-size: 12px;
/* 文字大小 */
color: #6b7280;
/* 文字颜色 */
text-align: center;
/* 居中对齐 */
font-weight: 500;
/* 文字粗细 */
line-height: 1.4;
/* 行高 */
margin: 0;
/* 移除外边距 */
}
+8085 -34
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -26,10 +26,12 @@
"dayjs": "^1.11.13",
"diff": "^7.0.0",
"docx-preview": "^0.3.5",
"highlight.js": "^11.11.1",
"html-docx-js": "^0.3.1",
"immer": "^10.1.1",
"isbot": "^4.1.0",
"jszip": "^3.10.1",
"katex": "^0.16.22",
"mammoth": "^1.9.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
@@ -37,7 +39,12 @@
"prismjs": "^1.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-pdf": "^5.7.2",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remixicon": "^4.6.0",
"tslib": "^2.8.1",
"uuid": "^11.1.0"