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(
|
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) })
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
/** 浮动待办面板,绝对定位在消息区域上方 */
|
/** 浮动待办面板,绝对定位在消息区域上方 */
|
||||||
|
|||||||
@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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: [],
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user