更新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
+17 -18
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from 'react'; import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Input, Button, Upload, Tooltip, message as antdMessage, Space } from 'antd'; 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 type { VisionFile } from '../../types/dify_chat';
import '../../styles/components/chat-with-llm/chat-input.css'; import '../../styles/components/chat-with-llm/chat-input.css';
@@ -184,7 +184,8 @@ export default function ChatInput({
icon={<PictureOutlined />} icon={<PictureOutlined />}
size="small" size="small"
disabled={isDisabled} disabled={isDisabled}
className="text-gray-500 hover:text-blue-500" className="chat-upload-button"
shape="circle"
/> />
</Tooltip> </Tooltip>
</Upload> </Upload>
@@ -192,21 +193,19 @@ export default function ChatInput({
}; };
return ( 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"> <div className="max-w-4xl mx-auto">
{/* 文件预览 */} {/* 文件预览 */}
{renderFilePreview()} {renderFilePreview()}
{/* 输入区域 */} {/* 输入区域 - 方块容器 */}
<div className="relative"> <div className="chat-input-box">
<div className="flex items-end gap-2"> <div className="chat-input-content">
{/* 上传按钮 */} {/* 上传按钮 */}
<div className="flex items-end pb-1"> {renderUploadButton()}
{renderUploadButton()}
</div>
{/* 文本输入 */} {/* 文本输入 */}
<div className="flex-1"> <div className="chat-input-textarea-wrapper">
<TextArea <TextArea
ref={textareaRef} ref={textareaRef}
value={message} value={message}
@@ -216,13 +215,12 @@ export default function ChatInput({
placeholder={placeholder} placeholder={placeholder}
disabled={disabled} disabled={disabled}
autoSize={{ minRows: 1, maxRows: 6 }} autoSize={{ minRows: 1, maxRows: 6 }}
className="resize-none" className="chat-input-textarea"
style={{ paddingRight: '50px' }}
/> />
</div> </div>
{/* 发送/停止按钮 */} {/* 发送/停止按钮 */}
<div className="flex items-end pb-1"> <div className="chat-input-button">
{isResponding ? ( {isResponding ? (
<Tooltip title="停止生成"> <Tooltip title="停止生成">
<Button <Button
@@ -231,24 +229,25 @@ export default function ChatInput({
icon={<StopOutlined />} icon={<StopOutlined />}
onClick={handleStop} onClick={handleStop}
disabled={disabled} disabled={disabled}
size="large" size="small"
shape="circle"
/> />
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip title="发送消息 (Enter)"> <Tooltip title="发送消息 (Enter)">
<Button <Button
type="primary" type="primary"
icon={<SendOutlined />} icon={<CommentOutlined style={{ fontSize: '30px' }} />}
onClick={handleSubmit} onClick={handleSubmit}
disabled={disabled || !message.trim()} disabled={disabled || !message.trim()}
size="large" size="small"
shape="circle"
className='chat-input-button-send'
/> />
</Tooltip> </Tooltip>
)} )}
</div> </div>
</div> </div>
{/* 字符计数和提示 */}
</div> </div>
</div> </div>
</div> </div>
+178 -207
View File
@@ -1,208 +1,179 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button, Tooltip, Avatar, Card, Image, Spin } from 'antd'; import { Button, Card, Spin } from 'antd';
import { AntDesignOutlined, SmileTwoTone, RobotOutlined } from '@ant-design/icons'; import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../types/dify_chat';
import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../types/dify_chat'; import { CHAT_CONFIG } from '../../config/chat';
import { CHAT_CONFIG } from '../../config/chat'; import Markdown from './markdown';
import Markdown from './markdown'; import ThoughtProcess from './thought-process';
import ThoughtProcess from './thought-process'; import { Dayjs } from 'dayjs';
import { Dayjs } from 'dayjs'; import '../../styles/components/chat-with-llm/chat-message.css';
import '../../styles/components/chat-with-llm/chat-message.css';
interface ChatMessageProps {
interface ChatMessageProps { message: ChatItem;
message: ChatItem; onFeedback?: (messageId: string, feedback: Feedbacktype) => void;
onFeedback?: (messageId: string, feedback: Feedbacktype) => void; isResponding?: boolean;
isResponding?: boolean; onRegenerate?: (messageId: string) => void;
onRegenerate?: (messageId: string) => void; }
}
/**
/** * 聊天消息组件
* 聊天消息组件 */
*/ export default function ChatMessage({
export default function ChatMessage({ message,
message, onFeedback,
onFeedback, isResponding = false,
isResponding = false, onRegenerate
onRegenerate }: ChatMessageProps) {
}: ChatMessageProps) { const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
const [feedback, setFeedback] = useState<'like' | 'dislike' | null>( message.feedback?.rating || null
message.feedback?.rating || null );
);
const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more } = message;
const { id, content, isAnswer, agent_thoughts, message_files, isOpeningStatement, suggestedQuestions, more } = message; const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0;
/**
/** * 处理反馈
* 处理反馈 */
*/ const handleFeedback = async (type: 'like' | 'dislike') => {
const handleFeedback = async (type: 'like' | 'dislike') => { if (!id || id.startsWith('placeholder-') || id.startsWith('question-') || id.startsWith('opening-'))
if (!id || id.startsWith('placeholder-') || id.startsWith('question-') || id.startsWith('opening-')) return;
return;
// 如果已经选择了相同的反馈,则取消选择
// 如果已经选择了相同的反馈,则取消选择 const newFeedback = feedback === type ? null : type;
const newFeedback = feedback === type ? null : type; setFeedback(newFeedback);
setFeedback(newFeedback);
await onFeedback?.(id, {
await onFeedback?.(id, { rating: newFeedback,
rating: newFeedback, });
}); };
};
/**
/** * 渲染AI回答内容
* 渲染AI回答内容 */
*/ const renderAnswerContent = () => {
const renderAnswerContent = () => { // console.log('🎨 渲染AI回答内容:', { content, isResponding, isAgentMode });
// console.log('🎨 渲染AI回答内容:', { content, isResponding, isAgentMode });
// 如果正在响应且没有内容
// 如果正在响应且没有内容 if (isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) {
if (isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content)) { return (
return ( <div className="responding-indicator">
<div className="responding-indicator"> <Spin size="small" />
<Spin size="small" /> <span>AI ...</span>
<span>AI ...</span> </div>
</div> );
); }
}
// Agent模式(有思考过程)
// Agent模式(有思考过程) if (isAgentMode) {
if (isAgentMode) { return (
return ( <div className="space-y-3">
<div className="space-y-3"> {agent_thoughts?.map((thought, index) => (
{agent_thoughts?.map((thought, index) => ( <div key={index}>
<div key={index}> {thought.thought && (
{thought.thought && ( <div className={isResponding && index === agent_thoughts.length - 1 ? 'streaming-text' : ''}>
<div className={isResponding && index === agent_thoughts.length - 1 ? 'streaming-text' : ''}> <Markdown content={thought.thought} />
<Markdown content={thought.thought} /> </div>
</div> )}
)} {thought.tool && (
{thought.tool && ( <ThoughtProcess
<ThoughtProcess thought={thought}
thought={thought} isFinished={!!thought.observation || !isResponding}
isFinished={!!thought.observation || !isResponding} />
/> )}
)} </div>
</div> ))}
))} </div>
</div> );
); }
}
// 普通模式 - 恢复Markdown渲染
// 普通模式 - 恢复Markdown渲染 return (
return ( <div>
<div> <div className={isResponding ? 'streaming-text' : ''}>
<div className={isResponding ? 'streaming-text' : ''}> <Markdown content={content} />
<Markdown content={content} /> </div>
</div> </div>
</div> );
); };
};
/**
/** * 渲染建议问题
* 渲染建议问题 */
*/ const renderSuggestedQuestions = () => {
const renderSuggestedQuestions = () => { if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
return (
return ( <div className="suggested-questions">
<div className="suggested-questions"> <div className="text-sm text-gray-500 mb-2"></div>
<div className="text-sm text-gray-500 mb-2"></div> <div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2"> {suggestedQuestions.map((question, index) => (
{suggestedQuestions.map((question, index) => ( <Button
<Button key={index}
key={index} size="small"
size="small" type="dashed"
type="dashed" className="question-button text-left"
className="question-button text-left" onClick={() => {
onClick={() => { // 这里可以添加点击建议问题的处理逻辑
// 这里可以添加点击建议问题的处理逻辑 // console.log('Suggested question clicked:', question);
// console.log('Suggested question clicked:', question); }}
}} >
> {question}
{question} </Button>
</Button> ))}
))} </div>
</div> </div>
</div> );
); };
};
/**
/** * 渲染消息元信息
* 渲染消息元信息 */
*/ const renderMessageMeta = () => {
const renderMessageMeta = () => { if (!more) return null;
if (!more) return null;
return (
return ( <div className="message-timestamp">
<div className="message-timestamp"> {more.time && <span>: {more.time}</span>}
{more.time && <span>: {more.time}</span>} {more.tokens && <span className="ml-2">Token: {more.tokens}</span>}
{more.tokens && <span className="ml-2">Token: {more.tokens}</span>} {more.latency && <span className="ml-2">: {more.latency}</span>}
{more.latency && <span className="ml-2">: {more.latency}</span>} </div>
</div> );
); };
};
// 如果是开场白,特殊处理
// 如果是开场白,特殊处理 if (isOpeningStatement) {
if (isOpeningStatement) { return (
return ( <div className="chat-message">
<div className="chat-message"> <Card className="message-card assistant" size="small">
<Card className="message-card assistant" size="small"> <div className="flex items-start gap-3">
<div className="flex items-start gap-3"> <div className="flex-1">
{/* <Avatar icon={<RobotOutlined />} className="flex-shrink-0" /> <Markdown content={content} />
*/} {renderSuggestedQuestions()}
<Avatar </div>
size={{ xs: 24, sm: 32, md: 40, lg: 64, xl: 80, xxl: 100 }} </div>
icon={<RobotOutlined />} </Card>
/> </div>
<div className="flex-1"> );
<Markdown content={content} /> }
{renderSuggestedQuestions()}
</div> return (
</div> <div className="chat-message">
</Card> <div className={`flex ${isAnswer ? 'justify-start' : 'justify-end'} mb-2`}>
</div> <div className={`message-card ${isAnswer ? 'assistant' : 'user'}`}>
); <Card size="small" className="shadow-sm">
} <div className="flex items-start gap-2">
{/* 消息内容 */}
return ( <div className="flex-1 min-w-0">
<div className="chat-message"> {isAnswer ? renderAnswerContent() : (
<div className={`flex ${isAnswer ? 'justify-start' : 'justify-end'} mb-2`}> <div>
<div className={`message-card ${isAnswer ? 'assistant' : 'user'}`}> <Markdown content={content} />
<Card size="small" className="shadow-sm"> {/* {renderImages(message_files)} */}
<div className="flex items-start gap-2"> </div>
{/* 头像 */} )}
<Avatar </div>
icon={isAnswer ? <RobotOutlined /> : <SmileTwoTone />} </div>
className="flex-shrink-0" </Card>
style={{ </div>
backgroundColor: isAnswer ? '#1890ff' : '#52c41a' </div>
}} </div>
/> );
{/* 消息内容 */}
<div className="flex-1 min-w-0">
{isAnswer ? renderAnswerContent() : (
<div>
<Markdown content={content} />
{/* {renderImages(message_files)} */}
</div>
)}
{/* 建议问题 */}
{/* {isAnswer && renderSuggestedQuestions()} */}
{/* 消息元信息 */}
{/* {renderMessageMeta()} */}
{/* 反馈按钮 */}
{/* {isAnswer && (
<div className="feedback-buttons">
{renderFeedbackButtons()}
</div>
)} */}
</div>
</div>
</Card>
</div>
</div>
</div>
);
} }
File diff suppressed because it is too large Load Diff
+88 -241
View File
@@ -1,242 +1,89 @@
import React from 'react'; import React from 'react';
import { Typography } from 'antd'; import ReactMarkdown from 'react-markdown';
import '../../styles/components/chat-with-llm/markdown.css'; import 'katex/dist/katex.min.css';
import RemarkMath from 'remark-math';
const { Paragraph, Text, Title } = Typography; import RemarkBreaks from 'remark-breaks';
import RehypeKatex from 'rehype-katex';
interface MarkdownProps { import RemarkGfm from 'remark-gfm';
content: string; import '../../styles/components/chat-with-llm/markdown.css';
className?: string;
} interface MarkdownProps {
content: string;
/** className?: string;
* Markdown 渲染组件 }
* 简化版本,支持基本的 Markdown 语法
*/ /**
export default function Markdown({ content, className = '' }: MarkdownProps) { * Markdown 渲染组件
if (!content) return null; * 使用 react-markdown 库进行标准 Markdown 解析,支持流式渲染
*/
/** export default function Markdown({ content, className = '' }: MarkdownProps) {
* 简单的 Markdown 解析器 console.log('🎨 [Markdown] 渲染组件:', {
*/ contentLength: content?.length || 0,
const parseMarkdown = (text: string): React.ReactNode => { contentPreview: content?.substring(0, 100) + (content && content.length > 100 ? '...' : ''),
// 分割成行 className,
const lines = text.split('\n'); hasContent: !!content
const elements: React.ReactNode[] = []; });
let currentParagraph: string[] = [];
let inCodeBlock = false; if (!content) {
let codeBlockContent: string[] = []; console.log('⚠️ [Markdown] 内容为空,返回null');
let codeBlockLanguage = ''; return null;
}
const flushParagraph = () => {
if (currentParagraph.length > 0) { return (
const paragraphText = currentParagraph.join('\n'); <div className={`markdown-content ${className}`}>
elements.push( <ReactMarkdown
<Paragraph key={elements.length} className={className}> remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
{parseInlineElements(paragraphText)} rehypePlugins={[RehypeKatex]}
</Paragraph> components={{
); code({ className, children, ...props }: any) {
currentParagraph = []; const match = /language-(\w+)/.exec(className || '');
} const isCodeBlock = match;
};
if (isCodeBlock) {
const flushCodeBlock = () => { // 代码块
if (codeBlockContent.length > 0) { return (
elements.push( <pre style={{
<pre backgroundColor: '#f6f8fa',
key={elements.length} padding: '16px',
className="bg-gray-800 text-white p-3 rounded-md overflow-x-auto my-2" borderRadius: '6px',
> overflow: 'auto',
<code className={codeBlockLanguage ? `language-${codeBlockLanguage}` : ''}> fontSize: '14px',
{codeBlockContent.join('\n')} lineHeight: '1.45',
</code> margin: '1em 0'
</pre> }}>
); <code style={{
codeBlockContent = []; backgroundColor: 'transparent',
codeBlockLanguage = ''; padding: '0',
} fontSize: 'inherit',
}; fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}>
lines.forEach((line, index) => { {String(children).replace(/\n$/, '')}
// 处理代码块 </code>
if (line.startsWith('```')) { </pre>
if (inCodeBlock) { );
// 结束代码块 } else {
flushCodeBlock(); // 内联代码
inCodeBlock = false; return (
} else { <code
// 开始代码块 className={className}
flushParagraph(); style={{
inCodeBlock = true; backgroundColor: 'rgba(175, 184, 193, 0.2)',
codeBlockLanguage = line.slice(3).trim(); padding: '0.2em 0.4em',
} borderRadius: '3px',
return; fontSize: '85%',
} fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
}}
if (inCodeBlock) { {...props}
codeBlockContent.push(line); >
return; {children}
} </code>
);
// 处理标题 }
if (line.startsWith('# ')) { },
flushParagraph(); }}
elements.push( >
<Title key={elements.length} level={1} className={className}> {content}
{parseInlineElements(line.slice(2))} </ReactMarkdown>
</Title> </div>
); );
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);
});
// 处理剩余内容
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);
}
} else if (part) {
finalParts.push(part);
}
});
return finalParts.length === 1 ? finalParts[0] : finalParts;
};
return (
<div className={`markdown-content ${className}`}>
{parseMarkdown(content)}
</div>
);
} }
+4 -3
View File
@@ -202,6 +202,7 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
icon={<MoreOutlined />} icon={<MoreOutlined />}
className="opacity-0 group-hover:opacity-100 transition-opacity" className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
// style={{ backgroundColor: '#00684A' }}
/> />
</Dropdown> </Dropdown>
)} )}
@@ -336,9 +337,9 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
</div> </div>
{/* 侧边栏底部 - 固定在底部 */} {/* 侧边栏底部 - 固定在底部 */}
{!collapsed && ( {!collapsed && conversations.length > 0 && (
<div className="p-4 border-t border-gray-100 flex-shrink-0"> <div className="sidebar-footer">
<div className="text-xs text-gray-500 text-center"> <div className="stats-text">
{conversations.length} {conversations.length}
</div> </div>
</div> </div>
File diff suppressed because it is too large Load Diff
+24 -2
View File
@@ -19,7 +19,16 @@ export async function action({ request }: ActionFunctionArgs) {
response_mode: responseMode, response_mode: responseMode,
} = body; } = 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( const response = await difyClient.createChatMessage(
inputs, inputs,
@@ -30,8 +39,16 @@ export async function action({ request }: ActionFunctionArgs) {
files files
); );
console.log('📡 [API] Dify响应状态:', {
status: response.status,
statusText: response.statusText,
hasBody: !!response.body,
headers: Object.fromEntries(response.headers.entries())
});
// 对于流式响应,直接返回流 // 对于流式响应,直接返回流
if (responseMode === 'streaming') { if (responseMode === 'streaming') {
console.log('🌊 [API] 返回流式响应');
return new Response(response.body, { return new Response(response.body, {
status: response.status, status: response.status,
headers: { headers: {
@@ -46,6 +63,7 @@ export async function action({ request }: ActionFunctionArgs) {
} }
// 对于非流式响应,返回JSON // 对于非流式响应,返回JSON
console.log('📄 [API] 返回JSON响应');
return new Response(JSON.stringify(response), { return new Response(JSON.stringify(response), {
status: 200, status: 200,
headers: { headers: {
@@ -54,7 +72,11 @@ export async function action({ request }: ActionFunctionArgs) {
}); });
} catch (error: any) { } 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( return new Response(
JSON.stringify({ error: error.message || 'Failed to send message' }), JSON.stringify({ error: error.message || 'Failed to send message' }),
{ {
+48 -41
View File
@@ -1,42 +1,49 @@
import { json } from "@remix-run/node"; import { json } from "@remix-run/node";
import { type MetaFunction } from "@remix-run/node"; import { type MetaFunction } from "@remix-run/node";
import Chat from "~/components/chat"; import Chat from "~/components/chat";
import chatIndexStyles from "~/styles/components/chat-with-llm/index.css?url"; import chatIndexStyles from "~/styles/components/chat-with-llm/index.css?url";
import chatMessageStyles from "~/styles/components/chat-with-llm/chat-message.css?url"; import chatMessageStyles from "~/styles/components/chat-with-llm/chat-message.css?url";
import chatInputStyles from "~/styles/components/chat-with-llm/chat-input.css?url"; import chatInputStyles from "~/styles/components/chat-with-llm/chat-input.css?url";
import chatSidebarStyles from "~/styles/components/chat-with-llm/sidebar.css?url"; import chatSidebarStyles from "~/styles/components/chat-with-llm/sidebar.css?url";
import chatThoughtProcessStyles from "~/styles/components/chat-with-llm/thought-process.css?url"; import chatThoughtProcessStyles from "~/styles/components/chat-with-llm/thought-process.css?url";
import chatMarkdownStyles from "~/styles/components/chat-with-llm/markdown.css?url"; import chatMarkdownStyles from "~/styles/components/chat-with-llm/markdown.css?url";
export function links() { export function links() {
return [ return [
{ rel: "stylesheet", href: chatIndexStyles }, { rel: "stylesheet", href: chatIndexStyles },
{ rel: "stylesheet", href: chatMessageStyles }, { rel: "stylesheet", href: chatMessageStyles },
{ rel: "stylesheet", href: chatInputStyles }, { rel: "stylesheet", href: chatInputStyles },
{ rel: "stylesheet", href: chatSidebarStyles }, { rel: "stylesheet", href: chatSidebarStyles },
{ rel: "stylesheet", href: chatThoughtProcessStyles }, { rel: "stylesheet", href: chatThoughtProcessStyles },
{ rel: "stylesheet", href: chatMarkdownStyles } { rel: "stylesheet", href: chatMarkdownStyles }
]; ];
} }
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
{ title: "AI对话 - 中国烟草AI合同及卷宗审核系统" }, { title: "AI对话 - 中国烟草AI合同及卷宗审核系统" },
{ {
name: "description", name: "description",
content: "与AI助手进行智能对话,获取专业的法务建议和文档分析" content: "与AI助手进行智能对话,获取专业的法务建议和文档分析"
} }
]; ];
}; };
/** /**
* 聊天主页面 * 聊天主页面
* 实现单页面应用模式,所有会话切换都在同一页面内完成 * 实现单页面应用模式,所有会话切换都在同一页面内完成
*/ */
export default function ChatWithLLMIndex() { export default function ChatWithLLMIndex() {
return ( return (
<div className="h-full chat-container"> <div className="flex-1 chat-container" style={{
<Chat /> height: 'calc(100vh - 80px)',
</div> borderRadius: '0.5rem',
); marginTop: '20px',
marginBottom: '-20px',
minHeight: '990px',
maxHeight: '89vh'
}}>
<Chat />
</div>
);
} }
+655 -579
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -122,16 +122,38 @@ export const difyClient = {
files: files || [], 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', { const response = await difyFetch('chat-messages', {
method: 'POST', method: 'POST',
body: JSON.stringify(body), 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对象 // 对于流式响应,直接返回Response对象
if (responseMode === 'streaming') { if (responseMode === 'streaming') {
console.log('🌊 [DifyClient] 返回流式响应对象');
return response; return response;
} }
console.log('📄 [DifyClient] 解析JSON响应');
return response.json(); return response.json();
}, },
@@ -1,27 +1,80 @@
/* 聊天输入区域 */ /* 聊天输入区域 */
.chat-input-container { .chat-input-container {
flex-shrink: 0; flex-shrink: 0;
background: #fff; background: #F9FAFB;
border-top: 1px solid #e5e7eb; padding: 16px 20px;
padding: 16px 20px; z-index: 10;
z-index: 10; }
}
.chat-input-wrapper {
.chat-input-wrapper { max-width: 100%;
max-width: 100%; margin: 0 auto;
margin: 0 auto; position: relative;
position: relative; }
}
/* 新的输入框容器样式 */
/* 响应式设计 */ .chat-input-box {
@media (max-width: 768px) { border: 1px solid #d1d5db;
.chat-input-container { /* 圆角 */
padding: 12px 16px; border-radius: 30px;
} background: #F9FAFB;
} box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
@media (max-width: 480px) {
.chat-input-container { .chat-input-content {
padding: 8px 12px; 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;
} }
@@ -1,132 +1,183 @@
/* 消息项样式 */ /* 消息项样式 */
.chat-message { .chat-message {
margin-bottom: 20px; margin-bottom: 20px;
animation: fadeInUp 0.3s ease-out; animation: fadeInUp 0.3s ease-out;
max-width: 100%; max-width: 100%;
} }
/* 消息卡片 */ /* 消息卡片 */
.message-card { .message-card {
max-width: 85%; max-width: 85%;
word-wrap: break-word; word-wrap: break-word;
word-break: break-word; word-break: break-word;
} }
.message-card.user { .message-card.user {
margin-left: auto; margin-left: auto;
} margin-right: 15px;
/* 用户消息离右边远一点 */
.message-card.assistant { }
margin-right: auto;
} .message-card.assistant {
margin-right: auto;
/* 流式文本效果 */ margin-left: 15px;
.streaming-text { /* AI响应离左边远一点 */
position: relative; }
}
.message-card.assistant .ant-card {
.streaming-text::after { background-color: #a4e2ad;
content: ''; }
/* 移除光标 */
animation: blink 1s infinite; .message-card.assistant .ant-card .ant-card-body {
color: #1890ff; background-color: #a4e2ad;
margin-left: 2px; }
}
/* Card组件圆角调整 */
@keyframes blink { .message-card .ant-card {
border-radius: 20px !important;
0%, /* 增加圆角度 */
50% { }
opacity: 1;
} .message-card .ant-card .ant-card-body {
border-radius: 20px !important;
51%, /* 确保body也有圆角 */
100% { min-height: 20px !important;
opacity: 0; /* 设置最小高度 */
} padding: 12px 16px !important;
} /* 调整内边距 - 减少上下内边距 */
display: block !important;
/* 消息动画 */ /* 改为block布局,移除flex */
@keyframes fadeInUp { line-height: 1.4 !important;
from { /* 减少行高 */
opacity: 0; }
transform: translateY(20px);
} /* 流式文本效果 */
.streaming-text {
to { position: relative;
opacity: 1; }
transform: translateY(0);
} .streaming-text::after {
} content: '';
/* 移除光标 */
/* 响应指示器 */ animation: blink 1s infinite;
.responding-indicator { color: #1890ff;
display: flex; margin-left: 2px;
align-items: center; }
gap: 8px;
color: #6b7280; @keyframes blink {
font-size: 14px;
} 0%,
50% {
/* 反馈按钮容器 */ opacity: 1;
.feedback-buttons { }
margin-top: 12px;
display: flex; 51%,
gap: 8px; 100% {
} opacity: 0;
}
/* 建议问题 */ }
.suggested-questions {
margin-top: 16px; /* 消息动画 */
} @keyframes fadeInUp {
from {
.question-button { opacity: 0;
margin-bottom: 8px; transform: translateY(20px);
margin-right: 8px; }
white-space: normal;
height: auto; to {
padding: 8px 12px; opacity: 1;
text-align: left; transform: translateY(0);
border: 1px solid #d1d5db; }
background: #f9fafb; }
color: #374151;
transition: all 0.2s ease; /* 响应指示器 */
} .responding-indicator {
display: flex;
.question-button:hover { align-items: center;
border-color: #1890ff; gap: 8px;
background: #f0f9ff; color: #6b7280;
color: #1890ff; font-size: 14px;
} }
/* 消息时间戳 */ /* 反馈按钮容器 */
.message-timestamp { .feedback-buttons {
font-size: 12px; margin-top: 12px;
color: #9ca3af; display: flex;
margin-top: 8px; gap: 8px;
display: flex; }
gap: 12px;
} /* 建议问题 */
.suggested-questions {
/* 消息图片 */ margin-top: 16px;
.message-images { }
margin-top: 12px;
} .question-button {
margin-bottom: 8px;
.message-images .ant-image { margin-right: 8px;
border-radius: 8px; white-space: normal;
overflow: hidden; height: auto;
} padding: 8px 12px;
text-align: left;
/* 响应式设计 */ border: 1px solid #d1d5db;
@media (max-width: 768px) { background: #f9fafb;
.message-card { color: #374151;
max-width: 95%; transition: all 0.2s ease;
} }
}
.question-button:hover {
@media (max-width: 480px) { border-color: #1890ff;
.message-card { background: #f0f9ff;
max-width: 100%; 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;
/* 手机上进一步减少左边距 */
}
} }
+247 -245
View File
@@ -1,246 +1,248 @@
/* 聊天布局样式 */ /* 聊天布局样式 */
/* 聊天容器 - 自适应布局 */ /* 聊天容器 - 自适应布局 */
.chat-container { .chat-container {
height: 400px; height: 100%;
width: 100%; min-height: 100%;
flex-direction: column; width: 100%;
background: #fff; flex-direction: column;
overflow: hidden; background: #fff;
position: relative; overflow: hidden;
border-radius: 8px; position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); border-radius: 8px;
margin: 20px auto; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} margin: 20px auto;
display: flex;
/* 聊天头部 */ }
.chat-header {
flex-shrink: 0; /* 聊天头部 */
height: 60px; .chat-header {
background: #fff; flex-shrink: 0;
border-bottom: 1px solid #e5e7eb; height: 60px;
display: flex; background: #fff;
align-items: center; border-bottom: 1px solid #e5e7eb;
justify-content: space-between; display: flex;
padding: 0 20px; align-items: center;
z-index: 10; justify-content: space-between;
} padding: 0 20px;
z-index: 10;
.chat-header h1 { }
font-size: 18px;
font-weight: 600; .chat-header h1 {
color: #1f2937; font-size: 18px;
margin: 0; font-weight: 600;
} color: #1f2937;
margin: 0;
/* 聊天消息列表容器 */ }
.chat-messages {
flex: 1; /* 聊天消息列表容器 */
overflow-y: auto; .chat-messages {
overflow-x: hidden; flex: 1;
padding: 20px; overflow-y: auto;
scroll-behavior: smooth; overflow-x: hidden;
background: #f9fafb; padding: 20px;
position: relative; scroll-behavior: smooth;
} background: #f9fafb;
position: relative;
/* 自定义滚动条 */ }
.chat-messages::-webkit-scrollbar {
width: 6px; /* 自定义滚动条 */
} .chat-messages::-webkit-scrollbar {
width: 6px;
.chat-messages::-webkit-scrollbar-track { }
background: transparent;
} .chat-messages::-webkit-scrollbar-track {
background: transparent;
.chat-messages::-webkit-scrollbar-thumb { }
background: #d1d5db;
border-radius: 3px; .chat-messages::-webkit-scrollbar-thumb {
} background: #d1d5db;
border-radius: 3px;
.chat-messages::-webkit-scrollbar-thumb:hover { }
background: #9ca3af;
} .chat-messages::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
/* 新对话欢迎界面 */ }
.chat-welcome {
display: flex; /* 新对话欢迎界面 */
flex-direction: column; .chat-welcome {
align-items: center; display: flex;
justify-content: center; flex-direction: column;
height: 100%; align-items: center;
text-align: center; justify-content: center;
padding: 40px 20px; height: 100%;
} text-align: center;
padding: 40px 20px;
.chat-welcome h3 { }
font-size: 24px;
font-weight: 600; .chat-welcome h3 {
color: #1f2937; font-size: 24px;
margin-bottom: 12px; font-weight: 600;
} color: #1f2937;
margin-bottom: 12px;
.chat-welcome p { }
font-size: 16px;
color: #6b7280; .chat-welcome p {
margin-bottom: 0; font-size: 16px;
} color: #6b7280;
margin-bottom: 0;
/* 加载状态 */ }
.chat-loading {
display: flex; /* 加载状态 */
flex-direction: column; .chat-loading {
align-items: center; display: flex;
justify-content: center; flex-direction: column;
height: 100%; align-items: center;
gap: 16px; justify-content: center;
} height: 100%;
gap: 16px;
.chat-loading .ant-spin { }
font-size: 24px;
} .chat-loading .ant-spin {
font-size: 24px;
/* 错误状态 */ }
.chat-error {
display: flex; /* 错误状态 */
flex-direction: column; .chat-error {
align-items: center; display: flex;
justify-content: center; flex-direction: column;
height: 100%; align-items: center;
text-align: center; justify-content: center;
padding: 40px 20px; height: 100%;
} text-align: center;
padding: 40px 20px;
.chat-error h2 { }
font-size: 20px;
font-weight: 600; .chat-error h2 {
color: #dc2626; font-size: 20px;
margin-bottom: 12px; font-weight: 600;
} color: #dc2626;
margin-bottom: 12px;
.chat-error p { }
font-size: 14px;
color: #6b7280; .chat-error p {
margin-bottom: 0; font-size: 14px;
} color: #6b7280;
margin-bottom: 0;
/* 确保聊天容器在主内容区域中占满全部空间 */ }
.main-content .chat-container {
height: calc(89vh - 0px); /* 确保聊天容器在主内容区域中占满全部空间 */
/* 减去任何顶部导航栏的高度 */ .main-content .chat-container {
} height: calc(89vh - 0px);
/* 减去任何顶部导航栏的高度 */
/* 如果有面包屑导航,需要调整高度 */ }
.main-content .breadcrumb+.chat-container {
height: calc(100vh - 60px); /* 如果有面包屑导航,需要调整高度 */
/* 减去面包屑的高度 */ .main-content .breadcrumb+.chat-container {
} height: calc(100vh - 60px);
/* 减去面包屑的高度 */
/* 侧边栏滚动区域样式 */ }
.h-full.overflow-y-auto::-webkit-scrollbar {
width: 6px; /* 侧边栏滚动区域样式 */
} .h-full.overflow-y-auto::-webkit-scrollbar {
width: 6px;
.h-full.overflow-y-auto::-webkit-scrollbar-track { }
background: #f8f9fa;
border-radius: 3px; .h-full.overflow-y-auto::-webkit-scrollbar-track {
} background: #f8f9fa;
border-radius: 3px;
.h-full.overflow-y-auto::-webkit-scrollbar-thumb { }
background: #d1d5db;
border-radius: 3px; .h-full.overflow-y-auto::-webkit-scrollbar-thumb {
transition: background-color 0.2s ease; background: #d1d5db;
} border-radius: 3px;
transition: background-color 0.2s ease;
.h-full.overflow-y-auto::-webkit-scrollbar-thumb:hover { }
background: #9ca3af;
} .h-full.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
/* Firefox 滚动条样式 */ }
.h-full.overflow-y-auto {
scrollbar-width: thin; /* Firefox 滚动条样式 */
scrollbar-color: #d1d5db #f8f9fa; .h-full.overflow-y-auto {
} scrollbar-width: thin;
scrollbar-color: #d1d5db #f8f9fa;
/* 响应式设计 */ }
@media (max-width: 768px) {
.chat-container { /* 响应式设计 */
height: 100vh; @media (max-width: 768px) {
} .chat-container {
height: 100vh;
.chat-messages { }
padding: 16px;
} .chat-messages {
padding: 16px;
.chat-header { }
padding: 0 16px;
height: 56px; .chat-header {
} padding: 0 16px;
height: 56px;
.chat-header h1 { }
font-size: 16px;
} .chat-header h1 {
} font-size: 16px;
}
@media (max-width: 480px) { }
.chat-messages {
padding: 12px; @media (max-width: 480px) {
} .chat-messages {
padding: 12px;
.chat-welcome { }
padding: 20px 16px;
} .chat-welcome {
padding: 20px 16px;
.chat-welcome h3 { }
font-size: 20px;
} .chat-welcome h3 {
font-size: 20px;
.chat-welcome p { }
font-size: 14px;
} .chat-welcome p {
} font-size: 14px;
}
/* 全局按钮主题色统一 */ }
.ant-btn-primary {
background-color: rgb(0, 104, 74) !important; /* 全局按钮主题色统一 */
border-color: rgb(0, 104, 74) !important; .ant-btn-primary {
} background-color: rgb(0, 104, 74) !important;
border-color: rgb(0, 104, 74) !important;
.ant-btn-primary:hover { }
background-color: rgba(0, 104, 74, 0.8) !important;
border-color: rgba(0, 104, 74, 0.8) !important; .ant-btn-primary:hover {
} background-color: rgba(0, 104, 74, 0.8) !important;
border-color: rgba(0, 104, 74, 0.8) !important;
.ant-btn-primary:focus { }
background-color: rgb(0, 104, 74) !important;
border-color: rgb(0, 104, 74) !important; .ant-btn-primary:focus {
} background-color: rgb(0, 104, 74) !important;
border-color: rgb(0, 104, 74) !important;
.ant-btn-primary:active { }
background-color: rgba(0, 104, 74, 0.9) !important;
border-color: rgba(0, 104, 74, 0.9) !important; .ant-btn-primary:active {
} background-color: rgba(0, 104, 74, 0.9) !important;
border-color: rgba(0, 104, 74, 0.9) !important;
/* 禁用状态保持原样 */ }
.ant-btn-primary:disabled {
background-color: rgba(0, 0, 0, 0.04) !important; /* 禁用状态保持原样 */
border-color: #d9d9d9 !important; .ant-btn-primary:disabled {
color: rgba(0, 0, 0, 0.25) !important; background-color: rgba(0, 0, 0, 0.04) !important;
} border-color: #d9d9d9 !important;
color: rgba(0, 0, 0, 0.25) !important;
/* 链接按钮主题色 */ }
.ant-btn-link {
color: rgb(0, 104, 74) !important; /* 链接按钮主题色 */
} .ant-btn-link {
color: rgb(0, 104, 74) !important;
.ant-btn-link:hover { }
color: rgba(0, 104, 74, 0.8) !important;
} .ant-btn-link:hover {
color: rgba(0, 104, 74, 0.8) !important;
.ant-btn-link:focus { }
color: rgb(0, 104, 74) !important;
} .ant-btn-link:focus {
color: rgb(0, 104, 74) !important;
.ant-btn-link:active { }
color: rgba(0, 104, 74, 0.9) !important;
.ant-btn-link:active {
color: rgba(0, 104, 74, 0.9) !important;
} }
+300 -135
View File
@@ -1,136 +1,301 @@
/* Markdown 样式 */ /* Markdown 样式 */
.markdown-content { .markdown-content {
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #374151; color: #374151;
overflow-wrap: break-word; overflow-wrap: break-word;
} text-align: left;
/* 左对齐 */
/* 标题样式 */ width: 100%;
.markdown-content h1, box-sizing: border-box;
.markdown-content h2, margin: 0;
.markdown-content h3, /* 移除外边距 */
.markdown-content h4, padding: 0;
.markdown-content h5, /* 移除内边距 */
.markdown-content h6 { }
margin-top: 1.5em;
margin-bottom: 0.5em; /* 移除Ant Design Typography组件的居中样式 */
font-weight: 600; .markdown-content .ant-typography {
line-height: 1.25; margin: 0 !important;
} text-align: left !important;
width: 100% !important;
.markdown-content h1 { }
font-size: 2em;
border-bottom: 1px solid #eaecef; /* 段落样式 - 减少边距 */
padding-bottom: 0.3em; .markdown-content p,
} .markdown-content .markdown-paragraph {
margin: 0 !important;
.markdown-content h2 { /* 移除段落边距 */
font-size: 1.5em; text-align: left;
border-bottom: 1px solid #eaecef; line-height: 1.4;
padding-bottom: 0.3em; /* 减少行高 */
} padding: 0;
/* 移除内边距 */
.markdown-content h3 { }
font-size: 1.25em;
} /* 标题样式 - 减少边距 */
.markdown-content h1,
.markdown-content h4 { .markdown-content h2,
font-size: 1em; .markdown-content h3,
} .markdown-content h4,
.markdown-content h5,
/* 段落样式 */ .markdown-content h6,
.markdown-content p { .markdown-content .markdown-heading {
margin: 0.75em 0; margin: 0.5em 0 0.25em 0 !important;
} /* 减少标题边距 */
font-weight: 600;
/* 列表样式 */ line-height: 1.2;
.markdown-content ul, /* 减少行高 */
.markdown-content ol { text-align: left;
margin-top: 0.5em; }
margin-bottom: 0.5em;
padding-left: 1.5em; .markdown-content h1,
} .markdown-content .markdown-h1 {
font-size: 1.5em;
.markdown-content li { /* 减小字体 */
margin: 0.3em 0; border-bottom: 1px solid #eaecef;
} padding-bottom: 0.2em;
}
/* 代码样式 */
.markdown-content code { .markdown-content h2,
padding: 0.2em 0.4em; .markdown-content .markdown-h2 {
margin: 0; font-size: 1.3em;
font-size: 85%; /* 减小字体 */
background-color: rgba(175, 184, 193, 0.2); border-bottom: 1px solid #eaecef;
border-radius: 3px; padding-bottom: 0.2em;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; }
}
.markdown-content h3,
.markdown-content pre { .markdown-content .markdown-h3 {
padding: 16px; font-size: 1.1em;
overflow: auto; /* 减小字体 */
font-size: 85%; }
line-height: 1.45;
background-color: #f6f8fa; .markdown-content h4,
border-radius: 6px; .markdown-content .markdown-h4 {
margin: 1em 0; font-size: 1em;
} }
.markdown-content pre code { /* 列表样式 */
padding: 0; .markdown-content ul,
margin: 0; .markdown-content ol,
background-color: transparent; .markdown-content .markdown-list {
border: 0; margin-top: 0.5em;
word-break: normal; margin-bottom: 0.5em;
white-space: pre; padding-left: 1.5em;
} text-align: left;
}
/* 表格样式 */
.markdown-content table { .markdown-content li,
border-collapse: collapse; .markdown-content .markdown-list-item {
width: 100%; margin: 0.3em 0;
margin: 1em 0; text-align: left;
overflow: auto; }
}
/* 代码样式 */
.markdown-content table th, .markdown-content code,
.markdown-content table td { .markdown-content .inline-code {
padding: 8px 16px; padding: 0.2em 0.4em;
border: 1px solid #dfe2e5; margin: 0;
} font-size: 85%;
background-color: rgba(175, 184, 193, 0.2);
.markdown-content table th { border-radius: 3px;
font-weight: 600; font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
background-color: #f6f8fa; }
}
.markdown-content pre,
.markdown-content table tr:nth-child(2n) { .markdown-content .code-block {
background-color: #f6f8fa; padding: 16px;
} overflow: auto;
font-size: 85%;
/* 链接样式 */ line-height: 1.45;
.markdown-content a { background-color: #f6f8fa;
color: #0969da; border-radius: 6px;
text-decoration: none; margin: 1em 0;
} text-align: left;
}
.markdown-content a:hover {
text-decoration: underline; .markdown-content pre code {
} padding: 0;
margin: 0;
/* 引用样式 */ background-color: transparent;
.markdown-content blockquote { border: 0;
margin: 1em 0; word-break: normal;
padding: 0 1em; white-space: pre;
color: #6a737d; }
border-left: 0.25em solid #dfe2e5;
} /* 表格样式 */
.markdown-content table,
/* 水平线样式 */ .markdown-content .markdown-table {
.markdown-content hr { border-collapse: collapse;
height: 0.25em; width: 100%;
padding: 0; margin: 1em 0;
margin: 24px 0; overflow: auto;
background-color: #e1e4e8; }
border: 0;
.markdown-content table th,
.markdown-content table td {
padding: 8px 16px;
border: 1px solid #dfe2e5;
text-align: left;
}
.markdown-content table th {
font-weight: 600;
background-color: #f6f8fa;
}
.markdown-content table tr:nth-child(2n) {
background-color: #f6f8fa;
}
/* 链接样式 */
.markdown-content a,
.markdown-content .markdown-link {
color: #0969da;
text-decoration: none;
}
.markdown-content a:hover,
.markdown-content .markdown-link:hover {
text-decoration: underline;
}
/* 引用样式 */
.markdown-content blockquote,
.markdown-content .markdown-blockquote {
margin: 1em 0;
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
/* 水平线样式 */
.markdown-content hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
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 { .chat-sidebar-menu .ant-menu-item {
margin: 4px 0; margin: 4px 0;
/* 会话项之间的垂直间距 */
border-radius: 6px; border-radius: 6px;
/* 会话项圆角 */
height: auto; height: auto;
/* 自适应高度 */
line-height: 1.4; line-height: 1.4;
/* 行高 */
padding: 8px 12px; padding: 8px 12px;
/* 内边距 */
color: #374151;
/* 会话文字颜色 - 默认状态 */
} }
/* 会话项悬停状态 - 作用域:鼠标悬停在会话项上时 */
.chat-sidebar-menu .ant-menu-item:hover { .chat-sidebar-menu .ant-menu-item:hover {
background-color: #f5f5f5; background-color: #f5f5f5;
/* 悬停时的背景色 */
/* color: #1f2937; */
/* 悬停时的文字颜色 */
} }
/* 会话项选中状态 - 作用域:当前选中的会话项 */
.chat-sidebar-menu .ant-menu-item-selected { .chat-sidebar-menu .ant-menu-item-selected {
background-color: rgba(0, 104, 74, 0.1); 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 { .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 { .chat-sidebar-menu .ant-menu-item .ant-menu-title-content {
width: 100%; 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) { @media (max-width: 768px) {
.ant-layout-sider { .ant-layout-sider {
position: fixed !important; position: fixed !important;
/* 固定定位 */
left: 0; left: 0;
/* 贴左边 */
top: 0; top: 0;
/* 贴顶部 */
bottom: 0; bottom: 0;
/* 贴底部 */
z-index: 1000; z-index: 1000;
/* 层级 */
} }
/* 折叠状态的侧边栏 - 作用域:手机端折叠时隐藏侧边栏 */
.ant-layout-sider.ant-layout-sider-collapsed { .ant-layout-sider.ant-layout-sider-collapsed {
left: -200px; left: -200px;
/* 向左移出屏幕 */
} }
} }
/* 滚动条样式 */ /* ========== 滚动条样式 ========== */
/* Webkit浏览器滚动条宽度 - 作用域:会话列表的滚动条 */
.chat-sidebar-menu::-webkit-scrollbar { .chat-sidebar-menu::-webkit-scrollbar {
width: 4px; width: 4px;
/* 滚动条宽度 */
} }
/* Webkit浏览器滚动条轨道 - 作用域:滚动条背景轨道 */
.chat-sidebar-menu::-webkit-scrollbar-track { .chat-sidebar-menu::-webkit-scrollbar-track {
background: #f1f1f1; background: #f1f1f1;
/* 轨道背景色 */
border-radius: 2px; border-radius: 2px;
/* 轨道圆角 */
} }
/* Webkit浏览器滚动条滑块 - 作用域:滚动条可拖拽部分 */
.chat-sidebar-menu::-webkit-scrollbar-thumb { .chat-sidebar-menu::-webkit-scrollbar-thumb {
background: #c1c1c1; background: #c1c1c1;
/* 滑块背景色 */
border-radius: 2px; border-radius: 2px;
/* 滑块圆角 */
} }
/* Webkit浏览器滚动条滑块悬停 - 作用域:鼠标悬停在滚动条滑块上时 */
.chat-sidebar-menu::-webkit-scrollbar-thumb:hover { .chat-sidebar-menu::-webkit-scrollbar-thumb:hover {
background: #a8a8a8; background: #a8a8a8;
/* 悬停时滑块颜色 */
} }
/* 确保侧边栏布局正确 */ /* ========== 侧边栏布局样式 ========== */
/* 侧边栏容器布局 - 作用域:整个侧边栏容器 */
.ant-layout-sider { .ant-layout-sider {
display: flex !important; display: flex !important;
/* 弹性布局 */
flex-direction: column !important; flex-direction: column !important;
/* 垂直方向排列 */
height: 100% !important;
/* 占满父容器高度 */
min-height: 100% !important;
/* 最小高度也是100% */
} }
/* 侧边栏内容区域样式 */ /* 侧边栏内容区域 - 作用域:侧边栏内部所有内容的容器 */
.ant-layout-sider .ant-layout-sider-children { .ant-layout-sider .ant-layout-sider-children {
display: flex; display: flex;
/* 弹性布局 */
flex-direction: column; flex-direction: column;
/* 垂直方向排列 */
height: 100%; height: 100%;
/* 占满高度 */
min-height: 100%;
/* 最小高度 */
overflow: hidden; 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", "dayjs": "^1.11.13",
"diff": "^7.0.0", "diff": "^7.0.0",
"docx-preview": "^0.3.5", "docx-preview": "^0.3.5",
"highlight.js": "^11.11.1",
"html-docx-js": "^0.3.1", "html-docx-js": "^0.3.1",
"immer": "^10.1.1", "immer": "^10.1.1",
"isbot": "^4.1.0", "isbot": "^4.1.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"katex": "^0.16.22",
"mammoth": "^1.9.0", "mammoth": "^1.9.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
@@ -37,7 +39,12 @@
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-pdf": "^5.7.2", "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", "remixicon": "^4.6.0",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"uuid": "^11.1.0" "uuid": "^11.1.0"