From c93a87a65efc1add70df855ede320a8165a5176f Mon Sep 17 00:00:00 2001 From: Wenyan Date: Thu, 30 Oct 2025 15:21:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=80=9D=E8=80=83=E6=A8=A1?= =?UTF-8?q?=E5=BC=8FAI=E5=9B=9E=E5=A4=8D=E7=9A=84UI=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E6=A0=87=E7=AD=BE=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: 1. 创建消息解析工具 message-parser.ts - 解析 标签,提取思考过程 - 分离思考内容和实际回复 2. 创建思考过程展示组件 thinking-block.tsx - 可折叠/展开的思考过程区域 - 参考 GPT-5 和 Claude 网页版设计 - 默认折叠,点击展开查看详细思考过程 3. 修改聊天消息组件 chat-message.tsx - 集成思考过程解析和展示 - 思考过程单独显示在顶部 - 实际回复正常显示在下方 4. 新增样式 thinking-block.css - 契合当前淡绿色(#a4e2ad)配色方案 - 渐变背景和流畅动画效果 - 灯泡图标标识思考过程 - 完整的响应式设计 UI效果: - 思考过程:淡绿色渐变背景,可折叠区域 - 实际回复:正常Markdown渲染 - 交互流畅:展开/折叠动画平滑 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/components/chat/chat-message.tsx | 20 ++- app/components/chat/thinking-block.tsx | 63 +++++++ .../chat-with-llm/thinking-block.css | 168 ++++++++++++++++++ app/utils/message-parser.ts | 79 ++++++++ 4 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 app/components/chat/thinking-block.tsx create mode 100644 app/styles/components/chat-with-llm/thinking-block.css create mode 100644 app/utils/message-parser.ts diff --git a/app/components/chat/chat-message.tsx b/app/components/chat/chat-message.tsx index 6426859..c08df7f 100644 --- a/app/components/chat/chat-message.tsx +++ b/app/components/chat/chat-message.tsx @@ -4,6 +4,8 @@ import type { ChatItem, Feedbacktype, ThoughtItem, VisionFile } from '../../type import { CHAT_CONFIG } from '../../config/chat'; import Markdown from './markdown'; import ThoughtProcess from './thought-process'; +import ThinkingBlock from './thinking-block'; +import { parseMessageContent } from '../../utils/message-parser'; import { Dayjs } from 'dayjs'; import '../../styles/components/chat-with-llm/chat-message.css'; @@ -85,12 +87,22 @@ export default function ChatMessage({ ); } - // 普通模式 - 恢复Markdown渲染 + // 普通模式 - 解析内容,检查是否包含思考过程 + const parsed = parseMessageContent(content); + return (
-
- -
+ {/* 思考过程区域 */} + {parsed.hasThinking && ( + + )} + + {/* 实际回复内容 */} + {parsed.response && ( +
+ +
+ )}
); }; diff --git a/app/components/chat/thinking-block.tsx b/app/components/chat/thinking-block.tsx new file mode 100644 index 0000000..48e43fa --- /dev/null +++ b/app/components/chat/thinking-block.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { DownOutlined, UpOutlined, BulbOutlined } from '@ant-design/icons'; +import '../../styles/components/chat-with-llm/thinking-block.css'; + +interface ThinkingBlockProps { + /** 思考过程内容 */ + content: string; + /** 是否默认展开 */ + defaultExpanded?: boolean; +} + +/** + * 思考过程展示组件 + * 参考 GPT-5 和 Claude 网页版的思考过程UI设计 + * 可折叠的思考过程区域,契合当前淡绿色配色方案 + */ +export default function ThinkingBlock({ content, defaultExpanded = false }: ThinkingBlockProps) { + const [expanded, setExpanded] = useState(defaultExpanded); + + if (!content) return null; + + return ( +
+ {/* 折叠/展开按钮 */} +
setExpanded(!expanded)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setExpanded(!expanded); + } + }} + > +
+ + 思考过程 +
+
+ {expanded ? ( + + ) : ( + + )} +
+
+ + {/* 思考内容 */} + {expanded && ( +
+
+ {content.split('\n').map((line, index) => ( +

+ {line || '\u00A0'} +

+ ))} +
+
+ )} +
+ ); +} diff --git a/app/styles/components/chat-with-llm/thinking-block.css b/app/styles/components/chat-with-llm/thinking-block.css new file mode 100644 index 0000000..d54fc9e --- /dev/null +++ b/app/styles/components/chat-with-llm/thinking-block.css @@ -0,0 +1,168 @@ +/* 思考过程区域 - 契合当前淡绿色配色方案 */ +.thinking-block { + margin-bottom: 12px; + border-radius: 12px; + background: linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%); + border: 1px solid #a4e2ad; + overflow: hidden; + transition: all 0.3s ease; +} + +.thinking-block:hover { + border-color: #8dd99a; + box-shadow: 0 2px 8px rgba(164, 226, 173, 0.2); +} + +/* 思考过程头部 */ +.thinking-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + cursor: pointer; + user-select: none; + background: rgba(164, 226, 173, 0.15); + transition: background 0.2s ease; +} + +.thinking-header:hover { + background: rgba(164, 226, 173, 0.25); +} + +.thinking-header:active { + background: rgba(164, 226, 173, 0.35); +} + +.thinking-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + color: #2e7d32; +} + +.thinking-icon { + font-size: 16px; + color: #66bb6a; +} + +.thinking-label { + font-weight: 500; +} + +.thinking-toggle { + display: flex; + align-items: center; +} + +.toggle-icon { + font-size: 12px; + color: #66bb6a; + transition: transform 0.3s ease; +} + +/* 思考内容区域 */ +.thinking-content { + padding: 12px 14px; + background: rgba(255, 255, 255, 0.5); + border-top: 1px solid rgba(164, 226, 173, 0.3); + animation: expandContent 0.3s ease-out; +} + +@keyframes expandContent { + from { + opacity: 0; + max-height: 0; + padding-top: 0; + padding-bottom: 0; + } + to { + opacity: 1; + max-height: 1000px; + padding-top: 12px; + padding-bottom: 12px; + } +} + +.thinking-text { + font-size: 13px; + line-height: 1.6; + color: #37474f; + white-space: pre-wrap; + word-wrap: break-word; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.thinking-paragraph { + margin: 0 0 8px 0; + padding: 0; +} + +.thinking-paragraph:last-child { + margin-bottom: 0; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .thinking-block { + border-radius: 10px; + margin-bottom: 10px; + } + + .thinking-header { + padding: 8px 12px; + } + + .thinking-title { + font-size: 13px; + } + + .thinking-icon { + font-size: 14px; + } + + .thinking-content { + padding: 10px 12px; + } + + .thinking-text { + font-size: 12px; + line-height: 1.5; + } +} + +@media (max-width: 480px) { + .thinking-block { + border-radius: 8px; + margin-bottom: 8px; + } + + .thinking-header { + padding: 6px 10px; + } + + .thinking-title { + font-size: 12px; + } + + .thinking-icon { + font-size: 13px; + } + + .toggle-icon { + font-size: 11px; + } + + .thinking-content { + padding: 8px 10px; + } + + .thinking-text { + font-size: 11px; + } + + .thinking-paragraph { + margin-bottom: 6px; + } +} diff --git a/app/utils/message-parser.ts b/app/utils/message-parser.ts new file mode 100644 index 0000000..54aba58 --- /dev/null +++ b/app/utils/message-parser.ts @@ -0,0 +1,79 @@ +/** + * 消息解析工具 + * 用于解析和处理AI消息中的特殊标签 + */ + +/** + * 解析后的消息内容 + */ +export interface ParsedMessage { + /** 是否包含思考过程 */ + hasThinking: boolean; + /** 思考过程内容 */ + thinking: string; + /** 实际回复内容 */ + response: string; +} + +/** + * 解析消息内容,提取 标签中的思考过程 + * + * @param content - 原始消息内容 + * @returns 解析后的消息对象 + * + * @example + * ```typescript + * const parsed = parseMessageContent(` + * 这是思考过程 + * 这是实际回复 + * `); + * // parsed.hasThinking === true + * // parsed.thinking === '这是思考过程' + * // parsed.response === '这是实际回复' + * ``` + */ +export function parseMessageContent(content: string): ParsedMessage { + if (!content) { + return { + hasThinking: false, + thinking: '', + response: content || '', + }; + } + + // 匹配 标签(支持多行) + const thinkRegex = /([\s\S]*?)<\/think>/i; + const match = content.match(thinkRegex); + + if (!match) { + // 没有思考标签,直接返回原内容 + return { + hasThinking: false, + thinking: '', + response: content, + }; + } + + // 提取思考内容 + const thinking = match[1].trim(); + + // 移除 标签,获取实际回复 + const response = content.replace(thinkRegex, '').trim(); + + return { + hasThinking: true, + thinking, + response, + }; +} + +/** + * 检查内容是否包含思考标签 + * + * @param content - 要检查的内容 + * @returns 是否包含思考标签 + */ +export function hasThinkingTag(content: string): boolean { + if (!content) return false; + return //i.test(content); +}