feat(chat): 支持子智能体导航添加子智能体类型参数
- 扩展 enterSubAgentView 方法,新增 subagentType 可选参数 - 更新相关回调 onNavigateToSubAgent,添加 subagentType 参数支持 - 调整 MessageBubble 组件触发子智能体导航时传递 subagentType - 优化 MessageList 组件显示新消息计数及底部导航按钮交互 - 美化底部浮动导航按钮样式,增加新消息数字提示和动画 - TodoPanel 添加状态点样式,消息内容排版更紧凑 - 维护滚动位置状态,改进滚动时新消息计数逻辑
This commit is contained in:
parent
8684ff9549
commit
301506a3b1
@ -324,8 +324,8 @@ function App() {
|
||||
)
|
||||
|
||||
const handleNavigateToSubAgent = useCallback(
|
||||
(taskId: string, description: string) => {
|
||||
const cmd = enterSubAgentView(taskId, description)
|
||||
(taskId: string, description: string, subagentType?: string) => {
|
||||
const cmd = enterSubAgentView(taskId, description, subagentType)
|
||||
handleCommand(cmd)
|
||||
sendMessage({ type: 'command', payload: JSON.stringify(cmd) })
|
||||
},
|
||||
|
||||
@ -8,7 +8,7 @@ interface ChatContainerProps {
|
||||
isReadOnly?: boolean
|
||||
channelName?: string
|
||||
onSendMessage: (content: string, attachments: Attachment[]) => void
|
||||
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
||||
onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
|
||||
onStop?: () => void
|
||||
showThinking?: boolean
|
||||
/** 浮动待办面板,绝对定位在消息区域上方 */
|
||||
|
||||
@ -58,7 +58,7 @@ function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pen
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: ChatMessage
|
||||
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
||||
onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
|
||||
showThinking?: boolean
|
||||
}
|
||||
|
||||
@ -477,7 +477,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
||||
<button
|
||||
onClick={(e) => {
|
||||
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"
|
||||
>
|
||||
@ -491,7 +491,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
||||
<button
|
||||
onClick={(e) => {
|
||||
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"
|
||||
>
|
||||
@ -551,7 +551,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
||||
<button
|
||||
onClick={(e) => {
|
||||
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"
|
||||
>
|
||||
@ -581,7 +581,7 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr
|
||||
<button
|
||||
onClick={(e) => {
|
||||
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"
|
||||
>
|
||||
|
||||
@ -5,7 +5,7 @@ import { Sparkles, ArrowDown, ArrowUp } from 'lucide-react'
|
||||
|
||||
interface MessageListProps {
|
||||
messages: ChatMessage[]
|
||||
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
||||
onNavigateToSubAgent?: (taskId: string, description: string, subagentType?: string) => void
|
||||
showThinking?: boolean
|
||||
/** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */
|
||||
viewKey?: string
|
||||
@ -16,7 +16,6 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isAtBottomRef = useRef(true)
|
||||
const prevShowBottomRef = useRef(false)
|
||||
const prevShowTopRef = useRef(false)
|
||||
const prevViewKeyRef = useRef(viewKey)
|
||||
const viewKeyRef = useRef(viewKey)
|
||||
viewKeyRef.current = viewKey
|
||||
@ -25,15 +24,14 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
const scrollPositionsRef = useRef<Map<string, number>>(new Map())
|
||||
|
||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
||||
const [showScrollToTop, setShowScrollToTop] = useState(false)
|
||||
const [hasNewMessage, setHasNewMessage] = useState(false)
|
||||
const [newMessageCount, setNewMessageCount] = useState(0)
|
||||
|
||||
// ---- scroll helpers ----
|
||||
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
|
||||
isAtBottomRef.current = true
|
||||
setShowScrollToBottom(false)
|
||||
setHasNewMessage(false)
|
||||
setNewMessageCount(0)
|
||||
bottomRef.current?.scrollIntoView({ behavior })
|
||||
}, [])
|
||||
|
||||
@ -58,22 +56,18 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
|
||||
isAtBottomRef.current = nearBottom
|
||||
|
||||
const shouldShowTop = el.scrollTop > el.clientHeight * 0.8
|
||||
if (shouldShowTop !== prevShowTopRef.current) {
|
||||
prevShowTopRef.current = shouldShowTop
|
||||
setShowScrollToTop(shouldShowTop)
|
||||
}
|
||||
|
||||
const shouldShowBottom = !nearBottom
|
||||
// 回到底部:距底部 > 200px 时显示(同时显示回到顶部)
|
||||
const shouldShowBottom = distanceFromBottom > 200
|
||||
if (shouldShowBottom !== prevShowBottomRef.current) {
|
||||
prevShowBottomRef.current = shouldShowBottom
|
||||
setShowScrollToBottom(shouldShowBottom)
|
||||
}
|
||||
|
||||
if (nearBottom && hasNewMessage) {
|
||||
setHasNewMessage(false)
|
||||
// 滚回底部时清除新消息计数
|
||||
if (nearBottom) {
|
||||
setNewMessageCount(0)
|
||||
}
|
||||
}, [hasNewMessage])
|
||||
}, [])
|
||||
|
||||
// ---- 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) {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'instant' })
|
||||
} else {
|
||||
setHasNewMessage(true)
|
||||
setNewMessageCount((prev) => prev + 1)
|
||||
}
|
||||
}, [messages, viewKey])
|
||||
|
||||
@ -158,14 +152,14 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
{/* 浮动按钮 */}
|
||||
<div className="absolute right-4 bottom-4 z-10 flex flex-col items-center gap-2 pointer-events-none">
|
||||
{showScrollToTop && (
|
||||
{/* 浮动导航按钮 — 底部居中并排 */}
|
||||
{showScrollToBottom && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-2">
|
||||
{/* 回到顶部 */}
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className="pointer-events-auto group relative flex items-center justify-center
|
||||
w-9 h-9 rounded-full
|
||||
bg-[var(--bg-tertiary)]/80 backdrop-blur-md
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-full
|
||||
bg-[var(--bg-tertiary)]/90 backdrop-blur-md
|
||||
border border-[var(--border-color)]
|
||||
text-[var(--text-muted)] hover:text-[var(--accent-cyan)]
|
||||
hover:border-[var(--accent-cyan)]/30
|
||||
@ -174,16 +168,15 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
animate-fade-in"
|
||||
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>
|
||||
)}
|
||||
|
||||
{showScrollToBottom && (
|
||||
{/* 回到底部 */}
|
||||
<button
|
||||
onClick={() => scrollToBottom('smooth')}
|
||||
className="pointer-events-auto group relative flex items-center justify-center
|
||||
w-9 h-9 rounded-full
|
||||
bg-[var(--bg-tertiary)]/80 backdrop-blur-md
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full
|
||||
bg-[var(--bg-tertiary)]/90 backdrop-blur-md
|
||||
border border-[var(--border-color)]
|
||||
text-[var(--text-muted)] hover:text-[var(--accent-cyan)]
|
||||
hover:border-[var(--accent-cyan)]/30
|
||||
@ -192,17 +185,23 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru
|
||||
animate-fade-in"
|
||||
aria-label="回到底部"
|
||||
>
|
||||
<ArrowDown className={`h-4 w-4 transition-transform duration-300 group-hover:translate-y-0.5 ${hasNewMessage ? 'animate-bounce' : ''}`} />
|
||||
|
||||
{hasNewMessage && (
|
||||
<span className="absolute -top-0.5 -right-0.5 flex h-2.5 w-2.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent-cyan)]/60"></span>
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[var(--accent-cyan)]"></span>
|
||||
<ArrowDown className={`h-4 w-4 transition-transform duration-300 ${newMessageCount > 0 ? 'animate-bounce' : ''}`} />
|
||||
{newMessageCount > 0 ? (
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{newMessageCount} 条新消息
|
||||
</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>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -236,7 +236,8 @@ export function TodoPanel({ todos, requestTodoList, sendCommand }: TodoPanelProp
|
||||
{!isCollapsed && (
|
||||
<div className="ml-3 border-l border-[var(--border-color)]/30 pl-2 mt-0.5 space-y-px">
|
||||
{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">
|
||||
{item.content}
|
||||
</span>
|
||||
|
||||
@ -85,7 +85,7 @@ interface UseChatReturn {
|
||||
selectSession: (sessionId: string) => void
|
||||
|
||||
// 子智能体导航方法
|
||||
enterSubAgentView: (taskId: string, description: string) => Command
|
||||
enterSubAgentView: (taskId: string, description: string, subagentType?: string) => Command
|
||||
exitSubAgentView: () => void
|
||||
navigateToSubAgentLevel: (index: number) => void
|
||||
|
||||
@ -857,11 +857,11 @@ export function useChat(): UseChatReturn {
|
||||
selectedTopicRef.current = selectedTopic
|
||||
}, [selectedTopic])
|
||||
|
||||
const enterSubAgentView = useCallback((taskId: string, description: string): Command => {
|
||||
const enterSubAgentView = useCallback((taskId: string, description: string, subagentType?: string): Command => {
|
||||
const newView: SubAgentView = {
|
||||
taskId,
|
||||
description,
|
||||
subagentType: '',
|
||||
subagentType: subagentType || '',
|
||||
status: 'loading',
|
||||
messages: [],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user