feat: 添加思考过程显示功能,允许用户选择是否展示助手的思考内容

This commit is contained in:
oudecheng 2026-06-11 17:15:03 +08:00
parent 6ff5907616
commit 4487f1a490
7 changed files with 47 additions and 18 deletions

View File

@ -585,6 +585,9 @@ impl OutboundMessage {
outbound.push(resp);
}
// AssistantResponse 已携带 reasoning 时ToolCall 不再重复;
// 只有 AssistantResponse 没发时ToolCall 才带 reasoning
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
outbound.extend(tool_calls.iter().map(|tool_call| {
let mut tc = Self::tool_call(
channel.to_string(),
@ -596,7 +599,7 @@ impl OutboundMessage {
reply_to.clone(),
metadata.clone(),
);
tc.reasoning_content = message.reasoning_content.clone();
tc.reasoning_content = tc_reasoning.clone();
tc
}));
outbound

View File

@ -102,6 +102,7 @@
- 回答应以帮助用户完成当前目标为中心。
- 在信息不足时先补关键前提,在信息充分时直接执行。
- Skill 不是工具名。看到可用 Skill 时,不能直接调用 Skill 名称;必须先调用 skill_activate并传入对应的 name。
- 调用工具的时候必须同时用简短的话告诉用户你调用工具是做什么
## 定时任务
@ -109,5 +110,3 @@
- 默认创建静默任务silent_agent_task在独立后台会话中执行不干扰主对话
- 静默模式下如需发送消息给用户prompt中需显式使用 send_session_message 工具
## 注意
- 不要通过一次调用写入一个很长的文件,请分段写入

View File

@ -29,6 +29,8 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
});
}
// AssistantResponse 已携带 reasoning 时ToolCall 不再重复
let tc_reasoning = if has_content_or_reasoning { None } else { message.reasoning_content.clone() };
outbound.extend(tool_calls.iter().map(|tool_call| WsOutbound::ToolCall {
id: message.id.clone(),
tool_call_id: tool_call.id.clone(),
@ -39,7 +41,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
subagent_task_id: None,
topic_id: None,
timestamp: None,
reasoning_content: message.reasoning_content.clone(),
reasoning_content: tc_reasoning.clone(),
}));
outbound
} else {

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X } from 'lucide-react'
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain } from 'lucide-react'
import { ChatContainer } from './components/Chat/ChatContainer'
import { TopicList } from './components/Sidebar/TopicList'
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
@ -120,6 +120,14 @@ function App() {
return () => clearTimeout(timer)
}, [theme])
const [showThinking, setShowThinking] = useState(() => {
return localStorage.getItem('picobot-show-thinking') !== 'false'
})
useEffect(() => {
localStorage.setItem('picobot-show-thinking', String(showThinking))
}, [showThinking])
// ---- WebSocket 初始化 ----
// Step 1: 连接建立后先请求通道列表
@ -443,6 +451,18 @@ function App() {
>
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button>
<button
onClick={() => setShowThinking(prev => !prev)}
className={`flex h-8 w-8 items-center justify-center rounded-lg transition-all ${
showThinking
? 'text-purple-400 hover:text-purple-300 bg-purple-500/10 hover:bg-purple-500/20'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
}`}
title={showThinking ? '隐藏思考过程' : '显示思考过程'}
aria-label="Toggle thinking display"
>
<Brain className="h-4 w-4" />
</button>
</div>
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
<ChannelSelector
@ -589,6 +609,7 @@ function App() {
onSendMessage={subAgentView || schedulerView ? () => {} : handleSendMessage}
onNavigateToSubAgent={handleNavigateToSubAgent}
onStop={handleStopExecution}
showThinking={showThinking}
/>
</div>
</div>

View File

@ -10,6 +10,7 @@ interface ChatContainerProps {
onSendMessage: (content: string, attachments: Attachment[]) => void
onNavigateToSubAgent?: (taskId: string, description: string) => void
onStop?: () => void
showThinking?: boolean
}
export function ChatContainer({
@ -20,11 +21,12 @@ export function ChatContainer({
onSendMessage,
onNavigateToSubAgent,
onStop,
showThinking = true,
}: ChatContainerProps) {
return (
<div className="flex h-full flex-col">
<div className="flex-1 overflow-hidden">
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} />
<MessageList messages={messages} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
</div>
<MessageInput
onSend={onSendMessage}

View File

@ -58,6 +58,7 @@ function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pen
interface MessageBubbleProps {
message: ChatMessage
onNavigateToSubAgent?: (taskId: string, description: string) => void
showThinking?: boolean
}
function getAttachmentIcon(mediaType: string) {
@ -240,13 +241,13 @@ function ThinkingSection({ content }: { content: string }) {
const [expanded, setExpanded] = useState(false)
return (
<div className="mb-3 rounded-lg border border-purple-500/20 bg-purple-500/5 overflow-hidden">
<div className="mb-3 rounded-lg border border-[var(--border-color)] bg-[var(--overlay-hover)] overflow-hidden">
<button
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded) }}
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-purple-500/5 transition-colors cursor-pointer select-none"
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-subtle)] transition-colors cursor-pointer select-none"
>
<Brain className="h-3.5 w-3.5 text-purple-400 flex-shrink-0" />
<span className="font-medium text-purple-300">Thinking</span>
<Brain className="h-3.5 w-3.5 text-[var(--accent-purple)] flex-shrink-0" />
<span className="font-medium text-[var(--accent-purple)]">Thinking</span>
{expanded ? (
<ChevronDown className="h-3 w-3 ml-auto flex-shrink-0" />
) : (
@ -259,8 +260,8 @@ function ThinkingSection({ content }: { content: string }) {
)}
</button>
{expanded && (
<div className="px-3 py-2 border-t border-purple-500/10 animate-thinking-reveal">
<div className="text-xs text-purple-200/80 whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto scrollbar-thin">
<div className="px-3 py-2 border-t border-[var(--border-color)] animate-thinking-reveal">
<div className="text-xs text-[var(--text-secondary)] whitespace-pre-wrap leading-relaxed max-h-64 overflow-y-auto scrollbar-thin">
{content}
</div>
</div>
@ -288,7 +289,7 @@ function parseTaskResult(content: string): TaskToolResult | null {
}
}
export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubbleProps) {
export function MessageBubble({ message, onNavigateToSubAgent, showThinking = true }: MessageBubbleProps) {
const isUser = message.role === 'user'
const isTool = message.role === 'tool'
const isMergedTool = message.type === 'merged_tool'
@ -438,7 +439,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
</div>
{/* 模型思考内容(工具调用时展示) */}
{message.reasoningContent && !toolExpanded && (
{showThinking && message.reasoningContent && !toolExpanded && (
<div className="px-3 pb-1">
<ThinkingSection content={message.reasoningContent} />
</div>
@ -502,7 +503,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
{toolExpanded && (
<div className="border-t border-[var(--border-color)] px-3 py-2 space-y-2">
{/* 模型思考内容(展开时也展示) */}
{message.reasoningContent && (
{showThinking && message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
{taskResult ? (
@ -653,7 +654,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
) : (
// 模型思考内容(仅助手消息,非工具消息)
<>
{!isTool && message.reasoningContent && (
{showThinking && !isTool && message.reasoningContent && (
<ThinkingSection content={message.reasoningContent} />
)}
{/* AI 和工具消息使用 Markdown 渲染 */}

View File

@ -6,9 +6,10 @@ import { Sparkles, ArrowDown, ArrowUp } from 'lucide-react'
interface MessageListProps {
messages: ChatMessage[]
onNavigateToSubAgent?: (taskId: string, description: string) => void
showThinking?: boolean
}
export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps) {
export function MessageList({ messages, onNavigateToSubAgent, showThinking = true }: MessageListProps) {
const bottomRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const isAtBottomRef = useRef(true)
@ -116,7 +117,7 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps
className="h-full overflow-y-auto p-6 space-y-6"
>
{messages.map((message) => (
<MessageBubble key={message.id} message={message} onNavigateToSubAgent={onNavigateToSubAgent} />
<MessageBubble key={message.id} message={message} onNavigateToSubAgent={onNavigateToSubAgent} showThinking={showThinking} />
))}
<div ref={bottomRef} />
</div>