feat(chat): 支持子智能体导航添加子智能体类型参数

- 扩展 enterSubAgentView 方法,新增 subagentType 可选参数
- 更新相关回调 onNavigateToSubAgent,添加 subagentType 参数支持
- 调整 MessageBubble 组件触发子智能体导航时传递 subagentType
- 优化 MessageList 组件显示新消息计数及底部导航按钮交互
- 美化底部浮动导航按钮样式,增加新消息数字提示和动画
- TodoPanel 添加状态点样式,消息内容排版更紧凑
- 维护滚动位置状态,改进滚动时新消息计数逻辑
This commit is contained in:
oudecheng 2026-06-18 15:34:27 +08:00
parent 8684ff9549
commit 301506a3b1
6 changed files with 48 additions and 48 deletions

View File

@ -324,8 +324,8 @@ function App() {
) )
const handleNavigateToSubAgent = useCallback( const handleNavigateToSubAgent = useCallback(
(taskId: string, description: string) => { (taskId: string, description: string, subagentType?: string) => {
const cmd = enterSubAgentView(taskId, description) const cmd = enterSubAgentView(taskId, description, subagentType)
handleCommand(cmd) handleCommand(cmd)
sendMessage({ type: 'command', payload: JSON.stringify(cmd) }) sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
}, },

View File

@ -8,7 +8,7 @@ interface ChatContainerProps {
isReadOnly?: boolean isReadOnly?: boolean
channelName?: string channelName?: string
onSendMessage: (content: string, attachments: Attachment[]) => void onSendMessage: (content: string, attachments: Attachment[]) => void
onNavigateToSubAgent?: (taskId: string, description: string) => void onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
onStop?: () => void onStop?: () => void
showThinking?: boolean showThinking?: boolean
/** 浮动待办面板,绝对定位在消息区域上方 */ /** 浮动待办面板,绝对定位在消息区域上方 */

View File

@ -58,7 +58,7 @@ function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pen
interface MessageBubbleProps { interface MessageBubbleProps {
message: ChatMessage message: ChatMessage
onNavigateToSubAgent?: (taskId: string, description: string) => void onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
showThinking?: boolean showThinking?: boolean
} }
@ -477,7 +477,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务') onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务', subagentType)
}} }}
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1" className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
> >
@ -491,7 +491,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务') onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务', subagentType)
}} }}
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1" className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
> >
@ -551,7 +551,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务') onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务', subagentType)
}} }}
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1" className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
> >
@ -581,7 +581,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务') onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务', subagentType)
}} }}
className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1" className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1"
> >

View File

@ -5,7 +5,7 @@ import { Sparkles, ArrowDown, ArrowUp } from 'lucide-react'
interface MessageListProps { interface MessageListProps {
messages: ChatMessage[] messages: ChatMessage[]
onNavigateToSubAgent?: (taskId: string, description: string) => void onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
showThinking?: boolean showThinking?: boolean
/** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */ /** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */
viewKey?: string viewKey?: string
@ -16,7 +16,6 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const isAtBottomRef = useRef(true) const isAtBottomRef = useRef(true)
const prevShowBottomRef = useRef(false) const prevShowBottomRef = useRef(false)
const prevShowTopRef = useRef(false)
const prevViewKeyRef = useRef(viewKey) const prevViewKeyRef = useRef(viewKey)
const viewKeyRef = useRef(viewKey) const viewKeyRef = useRef(viewKey)
viewKeyRef.current = viewKey viewKeyRef.current = viewKey
@ -25,15 +24,14 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
const scrollPositionsRef = useRef<Map<string, number>>(new Map()) const scrollPositionsRef = useRef<Map<string, number>>(new Map())
const [showScrollToBottom, setShowScrollToBottom] = useState(false) const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const [showScrollToTop, setShowScrollToTop] = useState(false) const [newMessageCount, setNewMessageCount] = useState(0)
const [hasNewMessage, setHasNewMessage] = useState(false)
// ---- scroll helpers ---- // ---- scroll helpers ----
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => { const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
isAtBottomRef.current = true isAtBottomRef.current = true
setShowScrollToBottom(false) setShowScrollToBottom(false)
setHasNewMessage(false) setNewMessageCount(0)
bottomRef.current?.scrollIntoView({ behavior }) bottomRef.current?.scrollIntoView({ behavior })
}, []) }, [])
@ -58,22 +56,18 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
isAtBottomRef.current = nearBottom isAtBottomRef.current = nearBottom
const shouldShowTop = el.scrollTop > el.clientHeight * 0.8 // 回到底部:距底部 > 200px 时显示(同时显示回到顶部)
if (shouldShowTop !== prevShowTopRef.current) { const shouldShowBottom = distanceFromBottom > 200
prevShowTopRef.current = shouldShowTop
setShowScrollToTop(shouldShowTop)
}
const shouldShowBottom = !nearBottom
if (shouldShowBottom !== prevShowBottomRef.current) { if (shouldShowBottom !== prevShowBottomRef.current) {
prevShowBottomRef.current = shouldShowBottom prevShowBottomRef.current = shouldShowBottom
setShowScrollToBottom(shouldShowBottom) setShowScrollToBottom(shouldShowBottom)
} }
if (nearBottom && hasNewMessage) { // 滚回底部时清除新消息计数
setHasNewMessage(false) if (nearBottom) {
setNewMessageCount(0)
} }
}, [hasNewMessage]) }, [])
// ---- auto-scroll: handle view switches and message updates ---- // ---- auto-scroll: handle view switches and message updates ----
@ -110,7 +104,7 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
if (lastMessage.role === 'user' || isAtBottomRef.current) { if (lastMessage.role === 'user' || isAtBottomRef.current) {
bottomRef.current?.scrollIntoView({ behavior: 'instant' }) bottomRef.current?.scrollIntoView({ behavior: 'instant' })
} else { } else {
setHasNewMessage(true) setNewMessageCount((prev) => prev + 1)
} }
}, [messages, viewKey]) }, [messages, viewKey])
@ -158,14 +152,14 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
<div ref={bottomRef} /> <div ref={bottomRef} />
</div> </div>
{/* 浮动按钮 */} {/* 浮动导航按钮 — 底部居中并排 */}
<div className="absolute right-4 bottom-4 z-10 flex flex-col items-center gap-2 pointer-events-none"> {showScrollToBottom && (
{showScrollToTop && ( <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2">
{/* 回到顶部 */}
<button <button
onClick={scrollToTop} onClick={scrollToTop}
className="pointer-events-auto group relative flex items-center justify-center className="flex items-center gap-1.5 px-3 py-2 rounded-full
w-9 h-9 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-md
bg-[var(--bg-tertiary)]/80 backdrop-blur-md
border border-[var(--border-color)] border border-[var(--border-color)]
text-[var(--text-muted)] hover:text-[var(--accent-cyan)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)]
hover:border-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/30
@ -174,16 +168,15 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
animate-fade-in" animate-fade-in"
aria-label="回到顶部" aria-label="回到顶部"
> >
<ArrowUp className="h-4 w-4 transition-transform duration-300 group-hover:-translate-y-0.5" /> <ArrowUp className="h-4 w-4" />
<span className="text-sm text-[var(--text-secondary)]"></span>
</button> </button>
)}
{showScrollToBottom && ( {/* 回到底部 */}
<button <button
onClick={() => scrollToBottom('smooth')} onClick={() => scrollToBottom('smooth')}
className="pointer-events-auto group relative flex items-center justify-center className="flex items-center gap-2 px-4 py-2 rounded-full
w-9 h-9 rounded-full bg-[var(--bg-tertiary)]/90 backdrop-blur-md
bg-[var(--bg-tertiary)]/80 backdrop-blur-md
border border-[var(--border-color)] border border-[var(--border-color)]
text-[var(--text-muted)] hover:text-[var(--accent-cyan)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)]
hover:border-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/30
@ -192,17 +185,23 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
animate-fade-in" animate-fade-in"
aria-label="回到底部" aria-label="回到底部"
> >
<ArrowDown className={`h-4 w-4 transition-transform duration-300 group-hover:translate-y-0.5 ${hasNewMessage ? 'animate-bounce' : ''}`} /> <ArrowDown className={`h-4 w-4 transition-transform duration-300 ${newMessageCount > 0 ? 'animate-bounce' : ''}`} />
{newMessageCount > 0 ? (
{hasNewMessage && ( <span className="text-sm font-medium text-[var(--text-primary)]">
<span className="absolute -top-0.5 -right-0.5 flex h-2.5 w-2.5"> {newMessageCount}
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent-cyan)]/60"></span> </span>
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[var(--accent-cyan)]"></span> ) : (
<span className="text-sm text-[var(--text-secondary)]"></span>
)}
{newMessageCount > 0 && (
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent-cyan)]/60" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--accent-cyan)]" />
</span> </span>
)} )}
</button> </button>
)}
</div> </div>
)}
</div> </div>
) )
} }

View File

@ -236,7 +236,8 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
{!isCollapsed && ( {!isCollapsed && (
<div className="ml-3 border-l border-[var(--border-color)]/30 pl-2 mt-0.5 space-y-px"> <div className="ml-3 border-l border-[var(--border-color)]/30 pl-2 mt-0.5 space-y-px">
{items.map(item => ( {items.map(item => (
<div key={item.id} className="py-[3px]"> <div key={item.id} className="py-[3px] flex items-start gap-1.5">
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot} shrink-0 mt-[5px]`} />
<span className="text-[12px] leading-snug text-[var(--text-primary)]/90 break-words"> <span className="text-[12px] leading-snug text-[var(--text-primary)]/90 break-words">
{item.content} {item.content}
</span> </span>

View File

@ -85,7 +85,7 @@ interface UseChatReturn {
selectSession: (sessionId: string) => void selectSession: (sessionId: string) => void
// 子智能体导航方法 // 子智能体导航方法
enterSubAgentView: (taskId: string, description: string) => Command enterSubAgentView: (taskId: string, description: string, subagentType?: string) => Command
exitSubAgentView: () => void exitSubAgentView: () => void
navigateToSubAgentLevel: (index: number) => void navigateToSubAgentLevel: (index: number) => void
@ -857,11 +857,11 @@ export function useChat(): UseChatReturn {
selectedTopicRef.current = selectedTopic selectedTopicRef.current = selectedTopic
}, [selectedTopic]) }, [selectedTopic])
const enterSubAgentView = useCallback((taskId: string, description: string): Command => { const enterSubAgentView = useCallback((taskId: string, description: string, subagentType?: string): Command => {
const newView: SubAgentView = { const newView: SubAgentView = {
taskId, taskId,
description, description,
subagentType: '', subagentType: subagentType || '',
status: 'loading', status: 'loading',
messages: [], messages: [],
} }