@@ -4,21 +4,25 @@ import { getAnswerPresentation } from "../answer-ui/answer-presentation";
import { Disclosure } from "@/shared/ui/disclosure" ;
import {
Activity ,
ArrowUpRight ,
CheckCircle2 ,
ChevronDown ,
ChevronsUpDown ,
ChevronLeft ,
DatabaseZap ,
Folder ,
FolderPlus ,
History ,
LoaderCircle ,
MapPinned ,
Mic ,
Pause ,
Play ,
Plus ,
Quote ,
SendHorizontal ,
Shield ,
ShieldCheck ,
Square ,
UserRound ,
Volume2 ,
X
} from "lucide-react" ;
@@ -27,6 +31,7 @@ import {
useCallback ,
useEffect ,
useId ,
useLayoutEffect ,
useMemo ,
useRef ,
useState ,
@@ -58,12 +63,15 @@ import {
DropdownMenuContent ,
DropdownMenuItem ,
DropdownMenuLabel ,
DropdownMenuRadioGroup ,
DropdownMenuRadioItem ,
DropdownMenuSeparator ,
DropdownMenuTrigger
} from "@/shared/ui/dropdown-menu" ;
import { Button } from "@/shared/ui/button" ;
import { cn } from "@/shared/ui/cn" ;
import { StatusBadge , StatusDot , type StatusTone } from "@/shared/ui/status" ;
import type { AgentChatSessionSummary } from "../api/client" ;
import type { AgentChatSessionSummary , AgentInputDelivery , AgentResponseInput } from "../api/client" ;
import { AgentPersona } from "./agent-persona" ;
import {
agentPanelItemVariants ,
@@ -71,6 +79,8 @@ import {
agentPanelSectionVariants
} from "./agent-motion" ;
import { AgentHistoryPanel } from "./agent-history-panel" ;
import { AgentMessageInbox } from "./agent-message-inbox" ;
import type { ComponentProps } from "react" ;
import { AgentMessageDetails , AgentMessageProgress } from "./agent-message-details" ;
import { AgentUiEnvelopeRenderer } from "./agent-ui-envelope-renderer" ;
import { useAgentSpeechRecognition , useAgentSpeechSynthesis } from "../hooks/use-agent-voice" ;
@@ -101,6 +111,11 @@ export type AgentWorkspaceContextItem = {
tone ? : "neutral" | "info" | "warning" ;
} ;
export type AgentConversationProjectOption = {
id : string ;
name : string ;
} ;
type AgentCommandPanelProps = {
presentation? : AgentCommandPanelPresentation ;
collapsing? : boolean ;
@@ -108,20 +123,37 @@ type AgentCommandPanelProps = {
sessionTitle? : string ;
sessionHistory? : AgentChatSessionSummary [ ] ;
sessionHistoryLoading? : boolean ;
promptSuggestions? : Array < { question : string ; sourceLabel? : string } > ;
promptSuggestionsLoading? : boolean ;
promptSuggestionsUnavailable? : boolean ;
activeSessionId? : string | null ;
qaOnly? : boolean ;
statusLabel? : string ;
connectionStatus? : AgentConnectionStatus ;
streaming? : boolean ;
inputDelivery? : AgentInputDelivery ;
pendingInputs? : AgentResponseInput [ ] ;
onInputDeliveryChange ? : ( delivery : AgentInputDelivery ) = > void ;
onPendingInputDeliveryChange ? : ( id : string , delivery : AgentInputDelivery ) = > Promise < void > | void ;
messages? : AgentChatMessage [ ] ;
streamRenderState? : AgentStreamRenderState ;
modelOptions? : AgentModelOption [ ] ;
selectedModel? : string ;
conversationScope ? : "personal" | "project" ;
onConversationScopeChange ? : ( scope : "personal" | "project" ) = > Promise < void > | void ;
conversationProjects? : AgentConversationProjectOption [ ] ;
selectedConversationProjectId? : string | null ;
onConversationProjectChange ? : ( projectId : string ) = > Promise < void > | void ;
currentProjectName? : string | null ;
onOpenProjectCreator ? : ( ) = > void ;
approvalMode? : AgentApprovalMode ;
uiResults? : AgentUiResult [ ] ;
reviewPanel? : ReactNode ;
messageInbox? : ComponentProps < typeof AgentMessageInbox > ;
workspaceContext? : AgentWorkspaceContextItem [ ] ;
prompt : string ;
promptFocusRequest? : number ;
promptReference ? : { title : string ; detail? : string ; placeholder? : string } ;
onCollapse : ( ) = > void ;
onPromptChange : Dispatch < SetStateAction < string > > ;
onRefreshHistory ? : ( ) = > Promise < void > | void ;
@@ -142,22 +174,6 @@ type AgentCommandPanelProps = {
onRejectQuestion ? : ( request : AgentQuestionRequest ) = > Promise < void > | void ;
} ;
const AGENT_PROMPT_SUGGESTIONS = [
"分析当前管网中的水力瓶颈管道,并给出改造建议。" ,
"供水服务分区分析。" ,
"帮我分析当前管网压力异常点,并按风险等级排序。" ,
"帮我生成一份今日运行简报,包含问题、原因和建议。" ,
"查询关键 SCADA 点位最近 24 小时的异常波动。" ,
"排查当前管网爆管风险,并说明优先处置建议。"
] as const ;
const AGENT_CAPABILITIES = [
{ icon : DatabaseZap , label : "水力瓶颈诊断" } ,
{ icon : Activity , label : "压力异常研判" } ,
{ icon : ShieldCheck , label : "爆管风险排查" } ,
{ icon : MapPinned , label : "GIS 地图联动" }
] as const ;
export function AgentCommandPanel ( {
presentation = "desktop-dock" ,
collapsing = false ,
@@ -165,18 +181,35 @@ export function AgentCommandPanel({
sessionTitle = "新对话" ,
sessionHistory = [ ] ,
sessionHistoryLoading = false ,
promptSuggestions = [ ] ,
promptSuggestionsLoading = false ,
promptSuggestionsUnavailable = false ,
activeSessionId ,
qaOnly = false ,
connectionStatus = "connecting" ,
streaming = false ,
inputDelivery = "steer" ,
pendingInputs = [ ] ,
onInputDeliveryChange ,
onPendingInputDeliveryChange ,
messages = [ ] ,
streamRenderState = { } ,
modelOptions = [ ] ,
selectedModel = "" ,
conversationScope = "project" ,
onConversationScopeChange ,
conversationProjects = [ ] ,
selectedConversationProjectId ,
onConversationProjectChange ,
currentProjectName ,
onOpenProjectCreator ,
approvalMode = "request" ,
uiResults = [ ] ,
reviewPanel ,
messageInbox ,
prompt ,
promptFocusRequest = 0 ,
promptReference ,
onCollapse ,
onPromptChange ,
onRefreshHistory ,
@@ -200,11 +233,31 @@ export function AgentCommandPanel({
const historyTriggerRef = useRef < HTMLButtonElement | null > ( null ) ;
const historyPanelRef = useRef < HTMLDivElement | null > ( null ) ;
const promptRestoreGenerationRef = useRef ( 0 ) ;
const composerRef = useRef < HTMLDivElement | null > ( null ) ;
const promptTextareaRef = useRef < HTMLTextAreaElement | null > ( null ) ;
const composerStartRectRef = useRef < DOMRect | null > ( null ) ;
const composerTransitionRef = useRef < Animation | null > ( null ) ;
const mountedRef = useRef ( true ) ;
const desktopFloating = presentation === "desktop-floating" ;
const floatingPresentation = desktopFloating || presentation === "mobile-sheet" ;
const hasConversation = messages . length > 0 || streaming ;
const hasConversation = messages . length > 0 || streaming || Boolean ( messageInbox ? . messages . length ) ;
const centeredComposer = ! hasConversation && ( presentation === "desktop-dock" || Boolean ( promptReference ) ) ;
const showStartSuggestions = centeredComposer && ! qaOnly && ! promptReference ;
const prefersReducedMotion = useReducedMotion ( ) ;
const trimmedPrompt = prompt . trim ( ) ;
useEffect ( ( ) = > {
if ( ! promptFocusRequest ) return ;
const frame = requestAnimationFrame ( ( ) = > {
const textarea = promptTextareaRef . current ;
textarea ? . focus ( ) ;
textarea ? . setSelectionRange ( textarea . value . length , textarea . value . length ) ;
} ) ;
return ( ) = > cancelAnimationFrame ( frame ) ;
} , [ promptFocusRequest ] ) ;
const selectedConversationProject = conversationProjects . find (
( project ) = > project . id === selectedConversationProjectId
) ;
const selectedProjectName = selectedConversationProject ? . name ? ? currentProjectName ? ? "当前项目" ;
const {
speechState ,
speakingMessageId ,
@@ -296,12 +349,31 @@ export function AgentCommandPanel({
if ( streaming ) stopListening ( ) ;
} , [ stopListening , streaming ] ) ;
useLayoutEffect ( ( ) = > {
if ( centeredComposer || ! composerStartRectRef . current || ! composerRef . current ) return ;
const start = composerStartRectRef . current ;
composerStartRectRef . current = null ;
const end = composerRef . current . getBoundingClientRect ( ) ;
if ( prefersReducedMotion || ( start . x === end . x && start . y === end . y ) ) return ;
composerTransitionRef . current ? . cancel ( ) ;
composerTransitionRef . current = composerRef . current . animate (
[
{ transform : ` translate( ${ start . x - end . x } px, ${ start . y - end . y } px) ` } ,
{ transform : "translate(0, 0)" }
] ,
{ duration : 280 , easing : "cubic-bezier(0.22, 1, 0.36, 1)" }
) ;
} , [ centeredComposer , prefersReducedMotion ] ) ;
const submitPrompt = ( nextPrompt : string ) = > {
if ( ! nextPrompt || streaming ) {
if ( ! nextPrompt ) {
return ;
}
const restoreGeneration = promptRestoreGenerationRef . current + 1 ;
promptRestoreGenerationRef . current = restoreGeneration ;
if ( centeredComposer && ! prefersReducedMotion && composerRef . current ) {
composerStartRectRef . current = composerRef . current . getBoundingClientRect ( ) ;
}
onPromptChange ( "" ) ;
stopListening ( ) ;
setScrollRequestId ( ( requestId ) = > requestId + 1 ) ;
@@ -335,6 +407,7 @@ export function AgentCommandPanel({
< aside
aria-label = "Agent 命令面板"
data-agent-presentation = { presentation }
data-agent-starting = { centeredComposer }
className = { cn (
"pointer-events-auto flex h-full min-h-0 w-full flex-col" ,
"agent-panel-shell" ,
@@ -361,12 +434,12 @@ export function AgentCommandPanel({
historyOpen && "agent-panel-header-expanded"
) }
>
< div className = { cn ( "flex items-center justify-between gap-2 px-3" , presentation === "desktop-dock" ? "min- h-14 py-1.5 " : "min-h-16 py-2" ) } >
< div className = { cn ( "flex items-center justify-between gap-2 px-3" , presentation === "desktop-dock" ? "h-14 py-1" : "min-h-16 py-2" ) } >
< div className = "flex min-w-0 flex-1 items-center gap-2.5 text-slate-900" >
< AgentPersona className = { presentation === "desktop-dock" ? "h-10 w-10" : "h-12 w-12" } state = { personaState } / >
< div className = "min-w-0 flex-1" >
< div className = "min-w-0" >
< div className = "flex min-w-0 items-center gap-2 " >
< div className = "min-w-0 space-y-0.5 " >
< div className = "flex min-w-0 items-center gap-3 " >
< p className = "shrink-0 text-xs font-semibold uppercase text-[var(--ink-muted)]" >
对 话 主 题
< / p >
@@ -382,12 +455,90 @@ export function AgentCommandPanel({
{ agentConnectionMetadata [ connectionStatus ] . label }
< / StatusBadge >
< / div >
< p
className = "truncate text-sm font-semibold text-slate-950"
title = { sessionTitle }
>
{ sessionTitle }
< / p >
< div className = "flex min-w-0 items-center gap-2" >
< p
className = "min-w-0 shrink truncate text-sm font-semibold text-slate-950"
title = { sessionTitle }
>
{ sessionTitle }
< / p >
{ onConversationScopeChange && ! qaOnly ? (
< DropdownMenu >
< DropdownMenuTrigger asChild >
< Button type = "button" variant = "ghost" size = "sm" className = "agent-project-trigger h-7 max-w-36 shrink-0 gap-1.5 rounded-xl px-2.5 text-xs font-medium text-slate-600 focus-visible:ring-1 focus-visible:ring-offset-0"
aria-label = "选择对话归属" disabled = { streaming } >
{ conversationScope === "personal"
? < UserRound size = { 13 } aria-hidden = "true" / >
: < Folder size = { 13 } aria-hidden = "true" / > }
< span className = "truncate" >
{ conversationScope === "personal" ? "个人会话" : selectedProjectName }
< / span >
< ChevronDown size = { 12 } className = "opacity-60" aria-hidden = "true" / >
< / Button >
< / DropdownMenuTrigger >
< DropdownMenuContent data-agent-presentation = { presentation } align = "start" sideOffset = { 8 } className = "agent-panel-overlay w-60 p-2" >
< DropdownMenuLabel className = "px-3 pb-1.5 pt-1 text-xs font-medium text-[var(--ink-muted)]" >
对 话 归 属
< / DropdownMenuLabel >
< DropdownMenuItem
className = { cn (
"min-h-10 rounded-lg px-3 text-sm" ,
conversationScope === "personal" && "bg-[var(--action-selection-soft)] text-slate-900"
) }
onSelect = { ( ) = > void onConversationScopeChange ( "personal" ) }
>
< UserRound size = { 14 } className = "text-[var(--ink-muted)]" aria-hidden = "true" / >
< span className = "flex-1" > 个 人 会 话 < / span >
{ conversationScope === "personal" ? < CheckCircle2 size = { 14 } className = "text-[var(--action-blue-hover)]" aria-hidden = "true" / > : null }
< / DropdownMenuItem >
{ conversationProjects . length > 0 ? conversationProjects . map ( ( project ) = > {
const selected = conversationScope === "project"
&& project . id === selectedConversationProjectId ;
return (
< DropdownMenuItem
key = { project . id }
className = { cn (
"min-h-10 rounded-lg px-3 text-sm" ,
selected && "bg-[var(--action-selection-soft)] text-slate-900"
) }
onSelect = { ( ) = > {
if ( onConversationProjectChange ) {
void onConversationProjectChange ( project . id ) ;
return ;
}
void onConversationScopeChange ( "project" ) ;
} }
>
< Folder size = { 14 } className = "text-[var(--ink-muted)]" aria-hidden = "true" / >
< span className = "min-w-0 flex-1 truncate" > { project . name } < / span >
{ selected ? < CheckCircle2 size = { 14 } className = "text-[var(--action-blue-hover)]" aria-hidden = "true" / > : null }
< / DropdownMenuItem >
) ;
} ) : (
< DropdownMenuItem
className = { cn (
"min-h-10 rounded-lg px-3 text-sm" ,
conversationScope === "project" && "bg-[var(--action-selection-soft)] text-slate-900"
) }
onSelect = { ( ) = > void onConversationScopeChange ( "project" ) }
>
< Folder size = { 14 } className = "text-[var(--ink-muted)]" aria-hidden = "true" / >
< span className = "min-w-0 flex-1 truncate" > { selectedProjectName } < / span >
{ conversationScope === "project" ? < CheckCircle2 size = { 14 } className = "text-[var(--action-blue-hover)]" aria-hidden = "true" / > : null }
< / DropdownMenuItem >
) }
< DropdownMenuSeparator className = "my-1" / >
< DropdownMenuItem
className = "min-h-10 rounded-lg px-3 text-sm font-medium text-[var(--action-blue-hover)] focus:bg-[var(--action-soft)] focus:text-[var(--action-blue-hover)]"
onSelect = { onOpenProjectCreator }
>
< FolderPlus size = { 14 } aria-hidden = "true" / >
新 建 项 目
< / DropdownMenuItem >
< / DropdownMenuContent >
< / DropdownMenu >
) : null }
< / div >
< / div >
< / div >
< / div >
@@ -481,7 +632,7 @@ export function AgentCommandPanel({
) }
>
< AnimatePresence mode = "popLayout" initial = { false } >
{ messages . length > 0 ? (
{ messages . length > 0 || Boolean ( messageInbox ? . messages . length ) ? (
< BackendMessageList
key = "messages"
presentation = { presentation }
@@ -502,27 +653,12 @@ export function AgentCommandPanel({
onLocateAnswerFeatures = { onLocateAnswerFeatures }
onSubmitAnswerForm = { onSubmitPrompt }
/ >
) : (
< motion.div
key = "empty"
className = { cn (
"flex flex-1 px-4" ,
presentation === "mobile-sheet"
? "items-start justify-center pb-0 pt-5"
: "items-center justify-center py-8"
) }
initial = "initial"
animate = "animate"
exit = "exit"
variants = { agentPanelItemVariants }
>
< AgentEmptyState compact = { presentation === "mobile-sheet" } / >
< / motion.div >
) }
) : null }
< / AnimatePresence >
{ ! qaOnly ? < AgentUiEnvelopeRenderer results = { uiResults } / > : null }
{ reviewPanel }
{ messageInbox ? < AgentMessageInbox { ...messageInbox } / > : null }
< / ConversationContent >
< AgentConversationScrollManager
activeSessionId = { activeSessionId }
@@ -543,16 +679,20 @@ export function AgentCommandPanel({
< div
className = { cn (
"agent-panel-band relative" ,
centeredComposer && "agent-panel-start-composer" ,
floatingPresentation
? "form-field agent-panel-floating-acrylic agent-panel-composer absolute bottom-3 z-20 rounded-2xl p-1"
: "border-t border-[var(--surface-divider)] p-3"
) }
>
{ ! hasConversation ? (
{ showStartSuggestions ? (
< h2 className = "agent-panel-start-heading" > 我 能 帮 你 处 理 什 么 ? < / h2 >
) : null }
{ ! hasConversation && ! centeredComposer && ! qaOnly ? (
< div
className = { cn (
"agent-panel-conversa tion pointer-events-none absolute inset-x-0 bottom-full z-10 px-3 pb-2 pt-2" ,
floatingPresentation && "bg-transparent"
"agent-panel-sugges tions pointer-events-none absolute inset-x-0 bottom-full z-10 px-3 pb-2 pt-2" ,
floatingPresentation && "agent-panel-conversation bg-transparent"
) }
>
< Suggestions
@@ -560,78 +700,142 @@ export function AgentCommandPanel({
role = "group"
className = "pointer-events-auto px-0.5"
>
{ AGENT_PROMPT_SUGGESTIONS . map ( ( suggestion ) = > (
{ promptSuggestions . map ( ( { question } ) = > (
< Suggestion
key = { suggestion }
suggestion = { suggestion }
key = { question }
suggestion = { question }
onClick = { submitPrompt }
className = "surface-reading max-w-full border border-[var(--surface-divider)] text-slate-600 hover:bg-[var(--action-soft)] hover:text-[var(--action-blue-hover)]"
>
< span className = "truncate" > { suggestion } < / span >
< span className = "truncate" > { question } < / span >
< / Suggestion >
) ) }
{ promptSuggestionsLoading ? < span className = "px-2 text-xs text-[var(--ink-muted)]" > 正 在 整 理 推 荐 问 题 … < / span > : null }
< / Suggestions >
< / div >
) : null }
< PromptInput
className = "form-field agent-panel-control overflow-hidden rounded-2xl shadow-xs [&>[data-slot=input-group]]:rounded-[inherit] [&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:shadow-none"
onSubmit = { ( message ) = > {
const nextPrompt = message . text . trim ( ) ;
if ( nextPrompt ) {
submitPrompt ( nextPrompt ) ;
}
} }
< div
ref = { composerRef }
className = "agent-panel-composer-motion"
>
< PromptInputBody >
< PromptInputTextarea
value = { prompt }
onChange = { ( event ) = > {
promptRestoreGenerationRef . current += 1 ;
onPromptChange ( event . currentTarget . value ) ;
} }
className = { cn ( "text-sm" , floatingPresentation ? "min-h-20" : "min-h-14" ) }
placeholder = "输入调度问题,Agent 将通过后端会话流式响应"
/ >
< / PromptInputBody >
< PromptInputFooter className = "justify-between gap-2" >
< div className = "flex min-w-0 items-center gap-2" >
{ ! qaOnly ? < ApprovalModeSelect presentation = { presentation } value = { approvalMode } disabled = { streaming } onValueChange = { onApprovalModeChange } / > : < span className = "px-1 text-[11px] text-[var(--ink-muted)]" > 任 务 协 作 问 答 < / span > }
{ streaming ? (
< div className = "mb-2 flex items-center justify-between gap-2 px-1 text-xs text-[var(--ink-muted)]" >
< span > 追 加 消 息 < / span >
< InputDeliverySelect presentation = { presentation } label = "运行中消息发送方式"
value = { inputDelivery } onValueChange = { onInputDeliveryChange } / >
< / div >
< div className = "flex shrink-0 items-center gap-2" >
< AgentModelSelect
presentation = { presentation }
models = { modelOptions }
value = { selectedModel }
onValueChange = { onSelectModel }
/ >
{ isRecognitionSupported ? (
< VoiceInputButton
isListening = { isListening }
disabled = { streaming }
onClick = {
isListening ? stopListening : ( ) = > startListening ( showRecognitionError )
}
/ >
) : null }
{ pendingInputs . length ? (
< ul aria-label = "待处理消息" className = "mb-2 max-h-32 space-y-1 overflow-y-auto text-xs text-[var(--ink-muted)]" >
{ pendingInputs . map ( ( input ) = > (
< li key = { input . id } className = "flex min-w-0 items-center gap-2 rounded-md bg-[var(--surface-subtle)] px-2 py-1.5" >
< span className = "min-w-0 flex-1 truncate" title = { input . text } > { input . text } < / span >
< InputDeliverySelect presentation = { presentation } label = { ` 消息发送方式: ${ input . text } ` }
value = { input . delivery } onValueChange = { onPendingInputDeliveryChange
? ( delivery ) = > { void onPendingInputDeliveryChange ( input . id , delivery ) ; }
: undefined } / >
< / li >
) ) }
< / ul >
) : null }
< PromptInput
className = { cn (
"agent-panel-control overflow-hidden rounded-2xl [&>[data-slot=input-group]]:rounded-[inherit] [&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:shadow-none" ,
presentation === "desktop-dock" ? "agent-panel-input-form" : "form-field shadow-xs"
) }
onSubmit = { ( message ) = > {
const nextPrompt = message . text . trim ( ) ;
if ( nextPrompt ) {
submitPrompt ( nextPrompt ) ;
}
} }
>
< PromptInputBody className = { presentation === "desktop-dock" ? "agent-panel-input-body form-field" : undefined } >
{ promptReference ? (
< div role = "group" aria-label = "对话引用" className = "mx-3 mt-3 flex min-w-0 items-start gap-2 rounded-lg bg-[var(--surface-subtle)] px-3 py-2 text-[var(--ink-muted)]" >
< Quote aria-hidden = "true" className = "mt-0.5 size-4 shrink-0" / >
< div className = "min-w-0 text-xs leading-relaxed" >
< p className = "break-words font-medium text-[var(--ink)]" > { promptReference . title } < / p >
{ promptReference . detail ? < p className = "break-words" > { promptReference . detail } < / p > : null }
< / div >
< / div >
) : null }
< PromptInputSubmit
aria-label = { streaming ? "停止 Agent 指令" : "发送 Agent 指令" }
title = { streaming ? "停止 Agent 指令" : "发送 Agent 指令" }
className = "agent-panel-primary-icon"
disabled = { ! streaming && ! trimmedPrompt }
status = { streaming ? "streaming" : "ready" }
onStop = { onStopPrompt }
>
{ streaming ? undefined : (
< SendHorizontal
size = { 15 }
style = { { transform : "rotate(-45deg)" } }
aria-hidden = "true"
< PromptInputTextarea
ref = { promptTextareaRef }
value = { prompt }
onChange = { ( event ) = > {
promptRestoreGenerationRef . current += 1 ;
onPromptChange ( event . currentTarget . value ) ;
} }
className = { cn ( "agent-panel-prompt-textarea text-sm" , floatingPresentation ? "min-h-20" : "min-h-14" ) }
placeholder = { promptReference ? . placeholder ? ? "询问供水问题,或描述需要处理的任务" }
/ >
< / PromptInputBody >
< PromptInputFooter className = "agent-panel-input-tools justify-between gap-2" >
< div className = "flex min-w-0 items-center gap-2" >
{ ! qaOnly ? < ApprovalModeSelect presentation = { presentation } value = { approvalMode } disabled = { streaming } onValueChange = { onApprovalModeChange } / > : < span className = "px-1 text-[11px] text-[var(--ink-muted)]" > 任 务 协 作 问 答 < / span > }
< / div >
< div className = "flex shrink-0 items-center gap-2" >
< AgentModelSelect
presentation = { presentation }
models = { modelOptions }
value = { selectedModel }
onValueChange = { streaming ? undefined : onSelectModel }
/ >
{ isRecognitionSupported ? (
< VoiceInputButton
isListening = { isListening }
disabled = { streaming }
onClick = {
isListening ? stopListening : ( ) = > startListening ( showRecognitionError )
}
/ >
) }
< / PromptInputSubmit >
< / div >
< / PromptInputFooter >
< / PromptInput >
) : null }
{ streaming ? < Button type = "button" variant = "ghost" size = "icon" aria-label = "停止 Agent 指令" title = "停止 Agent 指令"
className = "agent-panel-icon-button shrink-0" onClick = { onStopPrompt } > < Square size = { 14 } aria-hidden = "true" / > < / Button > : null }
< PromptInputSubmit
aria-label = { streaming ? inputDelivery === "steer" ? "引导当前 Agent 任务" : "将消息加入 Agent 队列" : "发送 Agent 指令" }
title = { streaming ? inputDelivery === "steer" ? "引导当前 Agent 任务" : "将消息加入 Agent 队列" : "发送 Agent 指令" }
className = "agent-panel-primary-icon"
disabled = { ! trimmedPrompt }
status = "ready"
>
{ (
< SendHorizontal
size = { 15 }
style = { { transform : "rotate(-45deg)" } }
aria-hidden = "true"
/ >
) }
< / PromptInputSubmit >
< / div >
< / PromptInputFooter >
< / PromptInput >
< / div >
{ showStartSuggestions ? (
< div className = "agent-panel-start-suggestions" role = "group" aria-label = "推荐问题" aria-live = "polite" aria-busy = { promptSuggestionsLoading } >
{ promptSuggestionsLoading ? (
< p className = "agent-panel-suggestion-status" > 正 在 根 据 最 近 活 动 整 理 问 题 … < / p >
) : promptSuggestions . length ? promptSuggestions . map ( ( suggestion ) = > (
< button
key = { suggestion . question }
type = "button"
className = "agent-panel-start-suggestion group"
title = { suggestion . question }
onClick = { ( ) = > submitPrompt ( suggestion . question ) }
>
< Activity size = { 17 } className = "shrink-0 text-[var(--action-blue-hover)]" aria-hidden = "true" / >
< span className = "min-w-0 flex-1" >
< span className = "line-clamp-2 block text-sm leading-5" > { suggestion . question } < / span >
{ suggestion . sourceLabel ? < span className = "mt-1 block text-[11px] leading-4 text-[var(--ink-muted)]" > { suggestion . sourceLabel } < / span > : null }
< / span >
< ArrowUpRight size = { 14 } className = "shrink-0 text-[var(--ink-muted)]" aria-hidden = "true" / >
< / button >
) ) : (
< p className = "agent-panel-suggestion-status" > { promptSuggestionsUnavailable ? "推荐问题暂不可用,可直接输入任务。" : "描述调查目标,即可开始分析。" } < / p >
) }
< / div >
) : null }
< / div >
< / Agent >
< / aside >
@@ -666,14 +870,14 @@ function VoiceInputButton({
aria-pressed = { isListening }
title = { label }
className = { cn (
"group relative grid h-8 w-8 shrink-0 place-items-center overflow-visible rounded-lg border after:absolute after:-inset-1 after:content-['']" ,
"border-transparent bg-transparent shadow-none" ,
"group relative grid h-8 w-8 shrink-0 place-items-center overflow-visible rounded-lg border-0 after:absolute after:-inset-1 after:content-['']" ,
"bg-transparent shadow-none" ,
"transition-[color,background-color,border-color,box-shadow,transform] duration-200" ,
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] focus-visible:ring-offset-1 " ,
"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-[var(--action-focus-ring)] focus-visible:ring-offset-0 " ,
"disabled:cursor-not-allowed disabled:opacity-45" ,
isListening
? "status-tone-danger bg-[var(--status-soft)] text-[var(--status-foreground)] [@media(hover:hover)]:hover:border-[var(--status-border)] [@media(hover:hover)]:hover:brightness-95"
: "text-slate-600 [@media(hover:hover)]:hover:border-[var(--action-border-hover)] [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)]"
: "text-slate-600 [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)] active:bg-[var(--action-soft-pressed)] "
) }
disabled = { disabled }
onClick = { onClick }
@@ -762,6 +966,38 @@ function VoiceInputButton({
const AGENT_SELECT_ITEM_CLASS_NAME =
"rounded-lg border border-transparent px-2.5 py-2 focus:border-[var(--action-border-hover)] focus:bg-[var(--action-selection-soft-hover)] data-[highlighted]:border-[var(--action-border-hover)] data-[highlighted]:bg-[var(--action-selection-soft-hover)] motion-reduce:transition-none" ;
function InputDeliverySelect ( { presentation , label , value , onValueChange } : {
presentation : AgentCommandPanelPresentation ;
label : string ;
value : AgentInputDelivery ;
onValueChange ? : ( delivery : AgentInputDelivery ) = > void ;
} ) {
return < DropdownMenu >
< DropdownMenuTrigger asChild >
< Button type = "button" variant = "outline" size = "sm" aria-label = { label }
className = "agent-panel-control h-10 shrink-0 gap-2 px-2.5 text-xs font-medium"
disabled = { ! onValueChange } >
{ value === "queue" ? "排队处理" : "引导当前任务" }
< ChevronDown className = "size-3 opacity-60" aria-hidden = "true" / >
< / Button >
< / DropdownMenuTrigger >
< DropdownMenuContent data-agent-presentation = { presentation } side = "top" align = "end" sideOffset = { 6 }
collisionPadding = { 12 } className = "agent-panel-overlay agent-panel-control w-44 rounded-xl p-1.5 shadow-lg" >
< DropdownMenuRadioGroup aria-label = { label } value = { value } onValueChange = { ( delivery ) = > {
if ( delivery === "steer" || delivery === "queue" ) onValueChange ? . ( delivery ) ;
} } >
{ ( [ [ "steer" , "引导当前任务" ] , [ "queue" , "排队处理" ] ] as const ) . map ( ( [ delivery , text ] ) = > (
< DropdownMenuRadioItem key = { delivery } value = { delivery }
className = { cn ( AGENT_SELECT_ITEM_CLASS_NAME , "min-h-10 pl-8 pr-2.5 text-xs" ,
value === delivery && "border-[var(--surface-border)] bg-[var(--action-soft)] text-[var(--action-blue-hover)]" ) } >
{ text }
< / DropdownMenuRadioItem >
) ) }
< / DropdownMenuRadioGroup >
< / DropdownMenuContent >
< / DropdownMenu > ;
}
function ApprovalModeSelect ( {
presentation ,
value ,
@@ -780,9 +1016,9 @@ function ApprovalModeSelect({
< DropdownMenuTrigger asChild >
< Button
type = "button"
variant = "outline "
variant = "ghost "
size = "sm"
className = "shrink-0 gap-1.5 border-transparent bg-transparent px-1.5 text-xs text-slate-600 shadow-none hover:bg-[var(--surface-reading)] "
className = "agent-panel-toolbar-trigger shrink-0 gap-1.5 border-0 bg-transparent px-1.5 text-xs text-slate-600 shadow-none data-[state=open]:bg-[var(--action-soft)] data-[state=open]:text-[var(--action-blue-hover)] focus-visible:ring-1 focus-visible:ring-offset-0 "
disabled = { disabled || ! onValueChange }
aria-label = "权限批准模式"
>
@@ -816,7 +1052,7 @@ function ApprovalModeSelect({
< Shield className = "mt-0.5 text-[var(--action-blue-hover)]" aria-hidden = "true" / >
< span >
< span className = "block text-sm font-semibold text-slate-800" > 请 求 批 准 < / span >
< span className = "block text-xs leading-5 text-[var(--ink-muted)]" > 工 具 权 限 逐 次 确 认 < / span >
< span className = "block text-xs leading-5 text-[var(--ink-muted)]" > 需 要 审 批 的 请 求 逐 次 确 认 ; 只 读 白 名 单 直 接 执 行 < / span >
< / span >
{ requestApproval ? (
< CheckCircle2 className = "ml-auto mt-0.5 text-[var(--action-blue-hover)]" aria-hidden = "true" / >
@@ -837,7 +1073,7 @@ function ApprovalModeSelect({
/ >
< span >
< span className = "block text-sm font-semibold text-slate-800" > 始 终 允 许 < / span >
< span className = "block text-xs leading-5 text-[var(--ink-muted)]" > 自 动 允 许 本 轮 权 限 请 求 < / span >
< span className = "block text-xs leading-5 text-[var(--ink-muted)]" > 自 动 批 准 本 轮 工 具 权 限 请 求 , 不 改 变 业 务 权 限 或 安 全 限 制 < / span >
< / span >
{ ! requestApproval ? (
< CheckCircle2
@@ -951,7 +1187,7 @@ function AgentModelSelect({
value : string ;
onValueChange ? : ( model : string ) = > void ;
} ) {
const selectedModel = models . find ( ( model ) = > model . id === value ) ? ? models [ 0 ] ;
const selectedModel = models . find ( ( model ) = > model . id === value ) ;
if ( ! models . length ) {
return (
@@ -964,9 +1200,9 @@ function AgentModelSelect({
< DropdownMenuTrigger asChild >
< Button
type = "button"
variant = "outline "
variant = "ghost "
size = "sm"
className = "w-12 shrink-0 justify-between gap-1 border-transparent bg-transparent px-1 text-xs text-slate-600 shadow-none hover:bg-[var(--surface-reading)] sm:w-24 sm:gap-1.5 sm:px-1.5"
className = "agent-panel-toolbar-trigger w-12 shrink-0 justify-between gap-1 border-0 bg-transparent px-1 text-xs text-slate-600 shadow-none data-[state=open]:bg-[var(--action-soft)] data-[state=open]:text-[var(--action-blue-hover)] focus-visible:ring-1 focus-visible:ring-offset-0 sm:w-auto sm:min-w-[72px] sm:gap-1.5 sm:px-1.5"
disabled = { ! onValueChange }
aria-label = "选择 Agent 模型"
aria-description = { ` 当前模型: ${ selectedModel ? . label ? ? "模型" } ` }
@@ -1041,7 +1277,7 @@ function FastModelIcon({ size }: { size: number }) {
height = { size }
viewBox = "0 0 24 24"
fill = "none"
className = "text-sky-600 "
className = "text-[var(--action-blue-hover)] "
aria-hidden = "true"
>
< path
@@ -1070,111 +1306,6 @@ function ExpertModelIcon({ size }: { size: number }) {
) ;
}
function AgentReadyMark() {
return (
< svg viewBox = "0 0 1024 1024" className = "h-12 w-12 overflow-visible" aria-hidden = "true" >
< g className = "agent-ready-orbit" >
< path
d = "M384.1536 952.1664a38.4 38.4 0 0 1-49.3568 22.528 498.3808 498.3808 0 0 1-284.928-273.92 38.4 38.4 0 0 1 70.8608-29.6448 421.5808 421.5808 0 0 0 240.896 231.6288 38.4 38.4 0 0 1 22.528 49.408zM952.1152 384.9728a38.4 38.4 0 0 1-49.4592-22.528 421.5296 421.5296 0 0 0-234.1376-241.5104 38.4 38.4 0 0 1 29.184-71.0656 498.3296 498.3296 0 0 1 276.8896 285.696 38.4 38.4 0 0 1-22.528 49.408z"
fill = "var(--palette-brand)"
/ >
< path
d = "M981.248 768.0512a42.6496 42.6496 0 1 1-85.2992 0 42.6496 42.6496 0 0 1 85.2992 0zM127.9488 256.0512a42.6496 42.6496 0 1 1-85.3504 0 42.6496 42.6496 0 0 1 85.3504 0z"
fill = "var(--palette-champagne)"
/ >
< path
d = "M810.496 938.8544a42.6496 42.6496 0 1 1-85.2992 0 42.6496 42.6496 0 0 1 85.3504 0zM298.496 85.504a42.6496 42.6496 0 1 1-85.2992 0 42.6496 42.6496 0 0 1 85.3504 0z"
fill = "var(--palette-sand)"
/ >
< / g >
< path
className = "agent-ready-core"
d = "M511.9488 276.736l-27.8528 114.7392A126.0544 126.0544 0 0 1 391.3216 484.352l-114.7904 27.8528 114.7904 27.8016a126.0544 126.0544 0 0 1 92.7744 92.8256L512 747.52l27.8016-114.7392a126.0544 126.0544 0 0 1 92.8256-92.8256l114.7392-27.8016-114.7392-27.8528a126.0544 126.0544 0 0 1-92.8256-92.8256L512 276.736z m55.6544-62.1568c-14.1312-58.368-97.1776-58.368-111.36 0L417.28 375.296a57.344 57.344 0 0 1-42.1888 42.1888l-160.6656 38.912c-58.4192 14.1824-58.4192 97.28 0 111.4112l160.6656 38.9632c20.8384 5.12 37.12 21.3504 42.1888 42.1888l38.9632 160.7168c14.1824 58.368 97.2288 58.368 111.36 0l38.9632-160.7168a57.344 57.344 0 0 1 42.1888-42.1888l160.7168-38.912c58.368-14.1824 58.368-97.28 0-111.4112l-160.7168-38.9632a57.344 57.344 0 0 1-42.1888-42.1888l-38.912-160.7168z"
fill = "var(--action-selection-soft)"
/ >
< / svg >
) ;
}
function AgentEmptyState ( { compact = false } : { compact? : boolean } ) {
if ( compact ) {
return (
< section
aria-labelledby = "agent-empty-state-title"
className = "mx-auto flex w-full max-w-[360px] items-center gap-3 px-1 py-1"
lang = "zh-CN"
>
< div className = "surface-control grid size-9 shrink-0 place-items-center rounded-lg shadow-[inset_0_0_0_1px_oklch(0.84_0.025_260)]" >
< AgentReadyMark / >
< / div >
< div className = "min-w-0 flex-1 text-left" >
< h2
id = "agent-empty-state-title"
className = "text-sm font-semibold leading-5 text-slate-950"
>
我 已 就 绪 , 请 描 述 任 务
< / h2 >
< p className = "mt-0.5 text-xs leading-5 text-slate-600" >
描 述 调 查 目 标 , 我 会 整 理 证 据 并 带 回 地 图 。
< / p >
< / div >
< / section >
) ;
}
return (
< section
aria-labelledby = "agent-empty-state-title"
className = "agent-empty-state mx-auto w-full max-w-[360px]"
lang = "zh-CN"
>
< div className = "px-2 py-4" >
< div className = "flex items-center gap-4" >
< div className = "agent-empty-mark surface-control grid h-[72px] w-[72px] shrink-0 place-items-center rounded-xl shadow-[inset_0_0_0_1px_oklch(0.84_0.025_260)]" >
< AgentReadyMark / >
< / div >
< div className = "min-w-0 flex-1 text-left" >
< h2
id = "agent-empty-state-title"
className = "text-balance text-lg font-semibold leading-7 text-slate-950"
>
我 已 就 绪 , 请 描 述 任 务
< / h2 >
< / div >
< / div >
< p className = "mt-5 text-pretty text-left text-sm leading-6 text-[#334155]" >
直 接 说 出 你 想 调 查 的 问 题 , 我 会 整 理 监 测 与 空 间 证 据 , 并 将 分 析 结 果 带 回 地 图 。
< / p >
< / div >
< ul
className = { cn (
"agent-capabilities surface-control grid grid-cols-2 overflow-hidden rounded-xl shadow-[inset_0_0_0_1px_rgba(148,163,184,0.32)]" ,
compact && "hidden"
) }
aria-label = "Agent 分析能力"
>
{ AGENT_CAPABILITIES . map ( ( { icon : Icon , label } , index ) = > (
< li
key = { label }
className = { cn (
"agent-capability flex min-h-20 items-center gap-3 px-3.5 py-3 text-left" ,
index % 2 === 0 && "border-r border-[var(--surface-border)]/80" ,
index < 2 && "border-b border-[var(--surface-border)]/80"
) }
>
< span className = "agent-capability-icon grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[#f9fbfe] text-[#334155] shadow-[inset_0_0_0_1px_rgba(148,163,184,0.28)]" >
< Icon size = { 16 } strokeWidth = { 1.8 } aria-hidden = "true" / >
< / span >
< span className = "text-sm font-semibold leading-5 text-[#1e293b]" > { label } < / span >
< / li >
) ) }
< / ul >
< / section >
) ;
}
function BackendMessageList ( {
presentation ,
messages ,