优化agent选择列表样式

This commit is contained in:
xintaofei
2026-03-24 23:25:42 +08:00
parent 4a47ec217e
commit 9e0eb98f3d

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { acpListAgents } from "@/lib/tauri" import { acpListAgents } from "@/lib/tauri"
import { disposeTauriListener } from "@/lib/tauri-listener" import { disposeTauriListener } from "@/lib/tauri-listener"
@@ -35,6 +35,59 @@ export function AgentSelector({
const onSelectRef = useRef(onSelect) const onSelectRef = useRef(onSelect)
const onAgentsLoadedRef = useRef(onAgentsLoaded) const onAgentsLoadedRef = useRef(onAgentsLoaded)
// Sliding indicator state
const containerRef = useRef<HTMLDivElement>(null)
const itemRefs = useRef<Map<AgentType, HTMLButtonElement>>(new Map())
const [indicator, setIndicator] = useState<{
left: number
width: number
} | null>(null)
// Use ResizeObserver to track button size changes during CSS transitions
useEffect(() => {
const container = containerRef.current
if (!container) return
const measure = () => {
if (!selected) {
setIndicator(null)
return
}
const btn = itemRefs.current.get(selected)
if (!btn || !container) {
setIndicator(null)
return
}
const containerRect = container.getBoundingClientRect()
const btnRect = btn.getBoundingClientRect()
setIndicator({
left: btnRect.left - containerRect.left,
width: btnRect.width,
})
}
const ro = new ResizeObserver(() => {
measure()
})
// Observe all button elements so indicator updates as they resize
for (const btn of itemRefs.current.values()) {
ro.observe(btn)
}
ro.observe(container)
// Initial measurement
measure()
const onResize = () => measure()
window.addEventListener("resize", onResize)
return () => {
ro.disconnect()
window.removeEventListener("resize", onResize)
}
}, [selected, agents])
useEffect(() => { useEffect(() => {
onSelectRef.current = onSelect onSelectRef.current = onSelect
}, [onSelect]) }, [onSelect])
@@ -120,6 +173,17 @@ export function AgentSelector({
onSelect(agentType) onSelect(agentType)
} }
const setItemRef = useCallback(
(agentType: AgentType) => (el: HTMLButtonElement | null) => {
if (el) {
itemRefs.current.set(agentType, el)
} else {
itemRefs.current.delete(agentType)
}
},
[]
)
if (agents.length === 0) { if (agents.length === 0) {
return ( return (
<div className="rounded-lg border border-dashed bg-muted/30 px-4 py-3 text-center text-sm text-muted-foreground"> <div className="rounded-lg border border-dashed bg-muted/30 px-4 py-3 text-center text-sm text-muted-foreground">
@@ -138,27 +202,57 @@ export function AgentSelector({
} }
return ( return (
<div className="flex flex-wrap items-center justify-center gap-2"> <div
{agents.map((agent) => ( ref={containerRef}
<button className="relative inline-flex items-center self-center rounded-full bg-muted/50 p-1 border border-border/50"
key={agent.agent_type} >
disabled={disabled || !agent.available} {/* Sliding droplet indicator */}
onClick={() => handleSelect(agent.agent_type)} {indicator && (
className={cn( <div
"inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors", className="absolute top-1 bottom-1 rounded-full bg-background shadow-sm ring-1 ring-border/50 transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]"
"border", style={{
disabled || !agent.available left: indicator.left,
? "cursor-not-allowed opacity-40" width: indicator.width,
: "cursor-pointer hover:bg-accent", }}
selected === agent.agent_type />
? "border-primary bg-primary/10 text-primary" )}
: "border-border text-muted-foreground" {agents.map((agent) => {
)} const isSelected = selected === agent.agent_type
> return (
<AgentIcon agentType={agent.agent_type} className="w-3.5 h-3.5" /> <button
{AGENT_LABELS[agent.agent_type]} key={agent.agent_type}
</button> ref={setItemRef(agent.agent_type)}
))} title={!isSelected ? AGENT_LABELS[agent.agent_type] : undefined}
disabled={disabled || !agent.available}
onClick={() => handleSelect(agent.agent_type)}
className={cn(
"relative z-10 inline-flex items-center justify-center gap-1.5 rounded-full text-xs font-medium transition-all duration-300",
isSelected ? "px-3 py-2" : "px-2 py-2",
disabled || !agent.available
? "cursor-not-allowed opacity-40"
: "cursor-pointer",
isSelected
? "text-foreground"
: "text-muted-foreground hover:text-foreground/70"
)}
>
<AgentIcon
agentType={agent.agent_type}
className="w-4 h-4 shrink-0"
/>
<span
className={cn(
"overflow-hidden whitespace-nowrap transition-all duration-300",
isSelected
? "max-w-[80px] opacity-100"
: "max-w-0 opacity-0"
)}
>
{AGENT_LABELS[agent.agent_type]}
</span>
</button>
)
})}
</div> </div>
) )
} }