更新AI聊天页面样式
This commit is contained in:
@@ -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>
|
||||
{renderUploadButton()}
|
||||
|
||||
{/* 文本输入 */}
|
||||
<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,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 (
|
||||
<div className="responding-indicator">
|
||||
<Spin size="small" />
|
||||
<span>AI 正在思考...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Agent模式(有思考过程)
|
||||
if (isAgentMode) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{agent_thoughts?.map((thought, index) => (
|
||||
<div key={index}>
|
||||
{thought.thought && (
|
||||
<div className={isResponding && index === agent_thoughts.length - 1 ? 'streaming-text' : ''}>
|
||||
<Markdown content={thought.thought} />
|
||||
</div>
|
||||
)}
|
||||
{thought.tool && (
|
||||
<ThoughtProcess
|
||||
thought={thought}
|
||||
isFinished={!!thought.observation || !isResponding}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 普通模式 - 恢复Markdown渲染
|
||||
return (
|
||||
<div>
|
||||
<div className={isResponding ? 'streaming-text' : ''}>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染建议问题
|
||||
*/
|
||||
const renderSuggestedQuestions = () => {
|
||||
if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="suggested-questions">
|
||||
<div className="text-sm text-gray-500 mb-2">建议问题:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
size="small"
|
||||
type="dashed"
|
||||
className="question-button text-left"
|
||||
onClick={() => {
|
||||
// 这里可以添加点击建议问题的处理逻辑
|
||||
// console.log('Suggested question clicked:', question);
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染消息元信息
|
||||
*/
|
||||
const renderMessageMeta = () => {
|
||||
if (!more) return null;
|
||||
|
||||
return (
|
||||
<div className="message-timestamp">
|
||||
{more.time && <span>时间: {more.time}</span>}
|
||||
{more.tokens && <span className="ml-2">Token: {more.tokens}</span>}
|
||||
{more.latency && <span className="ml-2">延迟: {more.latency}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 如果是开场白,特殊处理
|
||||
if (isOpeningStatement) {
|
||||
return (
|
||||
<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()}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-message">
|
||||
<div className={`flex ${isAnswer ? 'justify-start' : 'justify-end'} mb-2`}>
|
||||
<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() : (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
{/* {renderImages(message_files)} */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 建议问题 */}
|
||||
{/* {isAnswer && renderSuggestedQuestions()} */}
|
||||
|
||||
{/* 消息元信息 */}
|
||||
{/* {renderMessageMeta()} */}
|
||||
|
||||
{/* 反馈按钮 */}
|
||||
{/* {isAnswer && (
|
||||
<div className="feedback-buttons">
|
||||
{renderFeedbackButtons()}
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
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 (
|
||||
<div className="responding-indicator">
|
||||
<Spin size="small" />
|
||||
<span>AI 正在思考...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Agent模式(有思考过程)
|
||||
if (isAgentMode) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{agent_thoughts?.map((thought, index) => (
|
||||
<div key={index}>
|
||||
{thought.thought && (
|
||||
<div className={isResponding && index === agent_thoughts.length - 1 ? 'streaming-text' : ''}>
|
||||
<Markdown content={thought.thought} />
|
||||
</div>
|
||||
)}
|
||||
{thought.tool && (
|
||||
<ThoughtProcess
|
||||
thought={thought}
|
||||
isFinished={!!thought.observation || !isResponding}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 普通模式 - 恢复Markdown渲染
|
||||
return (
|
||||
<div>
|
||||
<div className={isResponding ? 'streaming-text' : ''}>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染建议问题
|
||||
*/
|
||||
const renderSuggestedQuestions = () => {
|
||||
if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="suggested-questions">
|
||||
<div className="text-sm text-gray-500 mb-2">建议问题:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
size="small"
|
||||
type="dashed"
|
||||
className="question-button text-left"
|
||||
onClick={() => {
|
||||
// 这里可以添加点击建议问题的处理逻辑
|
||||
// console.log('Suggested question clicked:', question);
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染消息元信息
|
||||
*/
|
||||
const renderMessageMeta = () => {
|
||||
if (!more) return null;
|
||||
|
||||
return (
|
||||
<div className="message-timestamp">
|
||||
{more.time && <span>时间: {more.time}</span>}
|
||||
{more.tokens && <span className="ml-2">Token: {more.tokens}</span>}
|
||||
{more.latency && <span className="ml-2">延迟: {more.latency}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 如果是开场白,特殊处理
|
||||
if (isOpeningStatement) {
|
||||
return (
|
||||
<div className="chat-message">
|
||||
<Card className="message-card assistant" size="small">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1">
|
||||
<Markdown content={content} />
|
||||
{renderSuggestedQuestions()}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-message">
|
||||
<div className={`flex ${isAnswer ? 'justify-start' : 'justify-end'} mb-2`}>
|
||||
<div className={`message-card ${isAnswer ? 'assistant' : 'user'}`}>
|
||||
<Card size="small" className="shadow-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
{/* 消息内容 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{isAnswer ? renderAnswerContent() : (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
{/* {renderImages(message_files)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+571
-570
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
<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);
|
||||
});
|
||||
|
||||
// 处理剩余内容
|
||||
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>
|
||||
);
|
||||
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 (
|
||||
<div className={`markdown-content ${className}`}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+621
-573
File diff suppressed because it is too large
Load Diff
@@ -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' }),
|
||||
{
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
import { json } from "@remix-run/node";
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
import Chat from "~/components/chat";
|
||||
import chatIndexStyles from "~/styles/components/chat-with-llm/index.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 chatSidebarStyles from "~/styles/components/chat-with-llm/sidebar.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";
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: chatIndexStyles },
|
||||
{ rel: "stylesheet", href: chatMessageStyles },
|
||||
{ rel: "stylesheet", href: chatInputStyles },
|
||||
{ rel: "stylesheet", href: chatSidebarStyles },
|
||||
{ rel: "stylesheet", href: chatThoughtProcessStyles },
|
||||
{ rel: "stylesheet", href: chatMarkdownStyles }
|
||||
];
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "AI对话 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{
|
||||
name: "description",
|
||||
content: "与AI助手进行智能对话,获取专业的法务建议和文档分析"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* 聊天主页面
|
||||
* 实现单页面应用模式,所有会话切换都在同一页面内完成
|
||||
*/
|
||||
export default function ChatWithLLMIndex() {
|
||||
return (
|
||||
<div className="h-full chat-container">
|
||||
<Chat />
|
||||
</div>
|
||||
);
|
||||
import { json } from "@remix-run/node";
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
import Chat from "~/components/chat";
|
||||
import chatIndexStyles from "~/styles/components/chat-with-llm/index.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 chatSidebarStyles from "~/styles/components/chat-with-llm/sidebar.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";
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: chatIndexStyles },
|
||||
{ rel: "stylesheet", href: chatMessageStyles },
|
||||
{ rel: "stylesheet", href: chatInputStyles },
|
||||
{ rel: "stylesheet", href: chatSidebarStyles },
|
||||
{ rel: "stylesheet", href: chatThoughtProcessStyles },
|
||||
{ rel: "stylesheet", href: chatMarkdownStyles }
|
||||
];
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "AI对话 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{
|
||||
name: "description",
|
||||
content: "与AI助手进行智能对话,获取专业的法务建议和文档分析"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* 聊天主页面
|
||||
* 实现单页面应用模式,所有会话切换都在同一页面内完成
|
||||
*/
|
||||
export default function ChatWithLLMIndex() {
|
||||
return (
|
||||
<div className="flex-1 chat-container" style={{
|
||||
height: 'calc(100vh - 80px)',
|
||||
borderRadius: '0.5rem',
|
||||
marginTop: '20px',
|
||||
marginBottom: '-20px',
|
||||
minHeight: '990px',
|
||||
maxHeight: '89vh'
|
||||
}}>
|
||||
<Chat />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+655
-579
File diff suppressed because it is too large
Load Diff
@@ -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,27 +1,80 @@
|
||||
/* 聊天输入区域 */
|
||||
.chat-input-container {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 16px 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-input-container {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-input-container {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
/* 聊天输入区域 */
|
||||
.chat-input-container {
|
||||
flex-shrink: 0;
|
||||
background: #F9FAFB;
|
||||
padding: 16px 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
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;
|
||||
}
|
||||
@@ -1,132 +1,183 @@
|
||||
/* 消息项样式 */
|
||||
.chat-message {
|
||||
margin-bottom: 20px;
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 消息卡片 */
|
||||
.message-card {
|
||||
max-width: 85%;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-card.user {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message-card.assistant {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* 流式文本效果 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 消息动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应指示器 */
|
||||
.responding-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 反馈按钮容器 */
|
||||
.feedback-buttons {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 建议问题 */
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.message-card {
|
||||
max-width: 100%;
|
||||
}
|
||||
/* 消息项样式 */
|
||||
.chat-message {
|
||||
margin-bottom: 20px;
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 消息卡片 */
|
||||
.message-card {
|
||||
max-width: 85%;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* 减少行高 */
|
||||
}
|
||||
|
||||
/* 流式文本效果 */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 消息动画 */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应指示器 */
|
||||
.responding-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 反馈按钮容器 */
|
||||
.feedback-buttons {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 建议问题 */
|
||||
.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;
|
||||
/* 手机上进一步减少左边距 */
|
||||
}
|
||||
}
|
||||
@@ -1,246 +1,248 @@
|
||||
/* 聊天布局样式 */
|
||||
|
||||
/* 聊天容器 - 自适应布局 */
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* 聊天头部 */
|
||||
.chat-header {
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 聊天消息列表容器 */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
background: #f9fafb;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* 新对话欢迎界面 */
|
||||
.chat-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.chat-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-loading .ant-spin {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.chat-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-error h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-error p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 确保聊天容器在主内容区域中占满全部空间 */
|
||||
.main-content .chat-container {
|
||||
height: calc(89vh - 0px);
|
||||
/* 减去任何顶部导航栏的高度 */
|
||||
}
|
||||
|
||||
/* 如果有面包屑导航,需要调整高度 */
|
||||
.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-track {
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* Firefox 滚动条样式 */
|
||||
.h-full.overflow-y-auto {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d1d5db #f8f9fa;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-messages {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.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: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: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;
|
||||
color: rgba(0, 0, 0, 0.25) !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:focus {
|
||||
color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:active {
|
||||
color: rgba(0, 104, 74, 0.9) !important;
|
||||
/* 聊天布局样式 */
|
||||
|
||||
/* 聊天容器 - 自适应布局 */
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 聊天头部 */
|
||||
.chat-header {
|
||||
flex-shrink: 0;
|
||||
height: 60px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 聊天消息列表容器 */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
background: #f9fafb;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* 新对话欢迎界面 */
|
||||
.chat-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome p {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.chat-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-loading .ant-spin {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.chat-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.chat-error h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-error p {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 确保聊天容器在主内容区域中占满全部空间 */
|
||||
.main-content .chat-container {
|
||||
height: calc(89vh - 0px);
|
||||
/* 减去任何顶部导航栏的高度 */
|
||||
}
|
||||
|
||||
/* 如果有面包屑导航,需要调整高度 */
|
||||
.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-track {
|
||||
background: #f8f9fa;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.h-full.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* Firefox 滚动条样式 */
|
||||
.h-full.overflow-y-auto {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d1d5db #f8f9fa;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-messages {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.chat-welcome {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.chat-welcome h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.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: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: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;
|
||||
color: rgba(0, 0, 0, 0.25) !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:focus {
|
||||
color: rgb(0, 104, 74) !important;
|
||||
}
|
||||
|
||||
.ant-btn-link:active {
|
||||
color: rgba(0, 104, 74, 0.9) !important;
|
||||
}
|
||||
@@ -1,136 +1,301 @@
|
||||
/* Markdown 样式 */
|
||||
.markdown-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.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;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-content h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.markdown-content h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* 段落样式 */
|
||||
.markdown-content p {
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin: 0.3em 0;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
.markdown-content code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 3px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-content table th,
|
||||
.markdown-content table td {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-content table th {
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-content table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
.markdown-content a {
|
||||
color: #0969da;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 引用样式 */
|
||||
.markdown-content 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;
|
||||
/* Markdown 样式 */
|
||||
.markdown-content {
|
||||
font-size: 14px;
|
||||
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,
|
||||
.markdown-content .markdown-heading {
|
||||
margin: 0.5em 0 0.25em 0 !important;
|
||||
/* 减少标题边距 */
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
/* 减少行高 */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content .markdown-h1 {
|
||||
font-size: 1.5em;
|
||||
/* 减小字体 */
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.markdown-content h2,
|
||||
.markdown-content .markdown-h2 {
|
||||
font-size: 1.3em;
|
||||
/* 减小字体 */
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.markdown-content h3,
|
||||
.markdown-content .markdown-h3 {
|
||||
font-size: 1.1em;
|
||||
/* 减小字体 */
|
||||
}
|
||||
|
||||
.markdown-content h4,
|
||||
.markdown-content .markdown-h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
.markdown-content ul,
|
||||
.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 .markdown-list-item {
|
||||
margin: 0.3em 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
.markdown-content code,
|
||||
.markdown-content .inline-code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 3px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
}
|
||||
|
||||
.markdown-content pre,
|
||||
.markdown-content .code-block {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.markdown-content table,
|
||||
.markdown-content .markdown-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -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;
|
||||
/* 移除外边距 */
|
||||
}
|
||||
Generated
+8085
-34
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user