2026-03-21 10:13:35 +08:00
< template >
< div class = "agent-view scanlines" >
2026-03-25 11:27:16 +08:00
<!-- Background -- >
2026-03-21 10:13:35 +08:00
< div class = "bg-grid" > < / div >
< div class = "bg-glow" > < / div >
< div class = "bg-particles" >
< span v-for = "p in bgParticles" :key="p.id" class="bg-particle" :style="p.style" > < / span >
< / div >
2026-03-25 11:27:16 +08:00
<!-- Header -- >
2026-03-21 10:13:35 +08:00
< div class = "view-header" >
< div class = "header-title" >
< span class = "title-bracket" > [ < / span >
2026-03-25 11:27:16 +08:00
< span class = "title-text" > AGENT COMMAND CENTER < / span >
2026-03-21 10:13:35 +08:00
< span class = "title-bracket" > ] < / span >
< / div >
< div class = "header-actions" >
< button class = "btn-icon" @click ="refreshStats" : class = "{ spinning: loading }" title = "刷新状态" >
< RefreshCw :size = "14" / >
< / button >
2026-03-25 11:27:16 +08:00
< button class = "btn-add" @click ="addModalOpen = true" >
< Plus :size = "14" / >
< span > 新增智能体 < / span >
< / button >
2026-03-21 10:13:35 +08:00
< div class = "status-bar" >
< span class = "status-dot" :class = "connectionStatus" > < / span >
< span class = "status-label" > { { connectionLabel } } < / span >
< / div >
< / div >
< / div >
2026-03-25 11:27:16 +08:00
<!-- Nodes Canvas -- >
< div
class = "nodes-canvas"
ref = "canvasRef"
: class = "{ panning: isPanning }"
@ mousedown = "startPan"
@ wheel . prevent = "handleWheel"
>
< div class = "canvas-aura" > < / div >
< div class = "canvas-scan" > < / div >
< div class = "nodes-viewport" :style = "viewportStyle" >
< div class = "nodes-stage" :style = "stageStyle" >
<!-- SVG for connection lines ( dynamically sized to canvas ) -- >
< svg class = "conn-svg" ref = "svgRef" >
< defs >
< filter id = "lineGlow" >
< feGaussianBlur stdDeviation = "3" result = "blur" / >
< feMerge > < feMergeNode in = "blur" / > < feMergeNode in = "SourceGraphic" / > < / feMerge >
< / filter >
< / defs >
< path
v - for = "sub in subAgents"
: key = "`line-${sub.id}`"
: d = "getLinePath(sub.id)"
class = "conn-path"
: class = "{ active: activeLine === sub.id }"
/ >
< circle r = "5" class = "pulse-dot" >
< animateMotion
v - if = "firingLine"
: dur = "pulseDuration + 'ms'"
: path = "getLinePath(firingLine)"
fill = "freeze"
@ end = "onPulseEnd"
/ >
< / circle >
< / svg >
<!-- Master node -- >
< div
ref = "masterCardRef"
class = "node-card node-master"
: class = "{ selected: selectedAgentId === 'master' }"
: style = "masterNodeStyle"
@ click = "selectAgent('master')"
>
< div class = "node-inner" >
< div class = "node-corner tl" > < / div >
< div class = "node-corner tr" > < / div >
< div class = "node-corner bl" > < / div >
< div class = "node-corner br" > < / div >
< div class = "node-status" :class = "getStatusClass('master')" >
< span class = "status-ring" > < / span >
< / div >
< div class = "node-label" > MASTER CORE < / div >
< div class = "node-name" > { { getAgentName ( 'master' ) } } < / div >
< div class = "node-role" > { { getAgentRole ( 'master' ) } } < / div >
< div class = "node-desc" > { { getAgentDesc ( 'master' ) } } < / div >
< div class = "node-footer" >
< span class = "node-stat" >
< span class = "stat-label" > 调用 < / span >
< span class = "stat-val" > { { agentData [ 'master' ] ? . callCount || 0 } } < / span >
< / span >
< span v-if = "agentData['master']?.currentTask" class="node-task-tag" >
{ { agentData [ 'master' ] . currentTask } }
< / span >
2026-03-24 21:42:01 +08:00
< / div >
< / div >
2026-03-21 10:13:35 +08:00
< / div >
2026-03-24 21:42:01 +08:00
2026-03-25 11:27:16 +08:00
<!-- Sub - agent nodes -- >
< div
v - for = "sub in subAgents"
: key = "sub.id"
: ref = "el => setSubRef(sub.id, el as HTMLElement)"
class = "node-card node-sub"
: class = "{ selected: selectedAgentId === sub.id, disabled: !localAgents[sub.id]?.enabled }"
: style = "getSubNodeStyle(sub)"
@ click = "selectAgent(sub.id)"
>
< div class = "node-inner" >
< div class = "node-corner tl" > < / div >
< div class = "node-corner tr" > < / div >
< div class = "node-corner bl" > < / div >
< div class = "node-corner br" > < / div >
< div class = "node-status" :class = "getStatusClass(sub.id)" >
< span class = "status-ring" > < / span >
2026-03-24 21:42:01 +08:00
< / div >
2026-03-25 11:27:16 +08:00
< div class = "node-label" > { { sub . name } } < / div >
< div class = "node-role" > { { sub . role } } < / div >
< div class = "node-desc" > { { sub . description } } < / div >
< div class = "node-footer" >
< span class = "node-stat" >
< span class = "stat-label" > 调用 < / span >
< span class = "stat-val" > { { agentData [ sub . id ] ? . callCount || 0 } } < / span >
< / span >
< span v-if = "agentData[sub.id]?.currentTask" class="node-task-tag" >
{ { agentData [ sub . id ] . currentTask } }
< / span >
< span v-else class = "node-idle" > 待机中 < / span >
2026-03-24 21:42:01 +08:00
< / div >
2026-03-25 11:27:16 +08:00
< div class = "rel-label" > { { sub . relLabel } } < / div >
< / div >
2026-03-21 10:13:35 +08:00
< / div >
2026-03-25 11:27:16 +08:00
< / div >
< / div >
< div class = "canvas-controls" >
< button class = "control-chip zoom-chip" @click ="zoomOut" title = "缩小视图" >
< span class = "chip-symbol" > − < / span >
< / button >
< button class = "control-chip zoom-readout" @click ="resetView" title = "重置视图" >
< span class = "chip-value" > { { zoomPercent } } < / span >
< / button >
< button class = "control-chip zoom-chip" @click ="zoomIn" title = "放大视图" >
< span class = "chip-symbol" > + < / span >
< / button >
2026-03-21 10:13:35 +08:00
< / div >
< / div >
2026-03-25 11:27:16 +08:00
<!-- Right Drawer -- >
2026-03-21 10:13:35 +08:00
< Transition :css = "false" @enter ="animateIn" @leave ="animateOut" >
< div v-if = "drawerOpen" class="config-drawer" >
< div class = "drawer-header" >
< span class = "drawer-title" > // AGENT CONFIGURATION</span>
< button class = "btn-close" @click ="drawerOpen = false" > < X :size = "16" / > < / button >
< / div >
< div class = "drawer-body" v-if = "editAgent" >
< div class = "form-group" >
< label class = "form-label" > // AGENT NAME</label>
< input v-model = "editAgent.name" type="text" class="form-input" / >
< / div >
< div class = "form-group" >
< label class = "form-label" > // ROLE</label>
< input v-model = "editAgent.role" type="text" class="form-input" / >
< / div >
< div class = "form-group" >
< label class = "form-label" > // DESCRIPTION</label>
< textarea v-model = "editAgent.description" class="form-textarea" rows="2" > < / textarea >
< / div >
< div class = "form-group flex-1" >
< label class = "form-label" > // SYSTEM PROMPT</label>
< textarea v-model = "editAgent.systemPrompt" class="form-textarea code-textarea" rows="10" > < / textarea >
< / div >
< div class = "form-group" >
< label class = "form-label" > // STATUS</label>
< div class = "toggle-row" >
< span class = "toggle-label" : class = "{ dim: editAgent.enabled }" > DISABLED < / span >
< button class = "toggle-btn" : class = "{ active: editAgent.enabled }" @click ="editAgent.enabled = !editAgent.enabled" >
< span class = "toggle-knob" > < / span >
< / button >
< span class = "toggle-label" : class = "{ dim: !editAgent.enabled }" > ENABLED < / span >
< / div >
< / div >
< div class = "drawer-actions" >
< button class = "btn-secondary" @click ="resetConfig" > 重置 < / button >
< button class = "btn-primary" @click ="saveConfig" :disabled = "saving" >
< span v-if = "saving" class="btn-loader" > < / span >
{ { saving ? '保存中...' : '保存配置' } }
< / button >
< / div >
< / div >
< / div >
< / Transition >
2026-03-25 11:27:16 +08:00
<!-- Add Modal -- >
< Transition :css = "false" @enter ="animateIn" @leave ="animateOut" >
< div v-if = "addModalOpen" class="modal-overlay" @click.self="addModalOpen = false" >
< div class = "modal-card" >
< div class = "modal-header" >
< span class = "modal-title" > // ADD NEW AGENT</span>
< button class = "btn-close" @click ="addModalOpen = false" > < X :size = "16" / > < / button >
< / div >
< div class = "modal-body" >
< div class = "form-group" >
< label class = "form-label" > // AGENT NAME</label>
< input v-model = "newAgent.name" type="text" class="form-input" placeholder="例如: CODER" / >
< / div >
< div class = "form-group" >
< label class = "form-label" > // ROLE KEY (英文唯一标识)</label>
< input v-model = "newAgent.roleKey" type="text" class="form-input" placeholder="例如: coder" / >
< / div >
< div class = "form-group" >
< label class = "form-label" > // ROLE</label>
< input v-model = "newAgent.role" type="text" class="form-input" placeholder="中文角色名" / >
< / div >
< div class = "form-group" >
< label class = "form-label" > // DESCRIPTION</label>
< textarea v-model = "newAgent.description" class="form-textarea" rows="2" placeholder="描述此 Agent 的职责..." > < / textarea >
< / div >
< div class = "form-group flex-1" >
< label class = "form-label" > // SYSTEM PROMPT</label>
< textarea v-model = "newAgent.systemPrompt" class="form-textarea code-textarea" rows="6" placeholder="输入系统提示词..." > < / textarea >
< / div >
< / div >
< div class = "modal-footer" >
< button class = "btn-secondary" @click ="addModalOpen = false" > 取消 < / button >
< button class = "btn-primary" @click ="addAgent" : disabled = "!newAgent.name || !newAgent.roleKey" > 创建智能体 < / button >
< / div >
< / div >
< / div >
< / Transition >
2026-03-21 10:13:35 +08:00
< Transition :css = "false" @enter ="fadeIn" @leave ="fadeOut" >
< div v-if = "drawerOpen" class="drawer-backdrop" @click="drawerOpen = false" > < / div >
< / Transition >
< / div >
< / template >
< script setup lang = "ts" >
2026-03-25 11:27:16 +08:00
import { ref , reactive , computed , onMounted , onUnmounted } from 'vue'
import { RefreshCw , X , Plus } from 'lucide-vue-next'
import { DEFAULT _AGENTS , RELATION _LABELS } from '@/data/agents'
import type { Agent } from '@/data/agents'
import { agentApi } from '@/api/agent'
// ── Node dimensions ──────────────────────────────────────────────
const NODE _W = 200
const NODE _H = 170
const MASTER _TOP = 48 // px from top
const SUB _TOP = 350 // px from top
// Horizontal percentages: master=50%, subs=12.5/37.5/62.5/87.5%
const SUB _XS = [ 12.5 , 37.5 , 62.5 , 87.5 ]
// ── Sub-agent static data ────────────────────────────────────────
interface SubAgentCard {
id : string
name : string
role : string
description : string
relLabel : string
}
const subAgents : SubAgentCard [ ] = [
{ id : 'planner' , name : 'PLANNER' , role : '规划者' , description : '制定任务计划,拆解复杂目标为可执行步骤' , relLabel : RELATION _LABELS [ 'master-planner' ] } ,
{ id : 'executor' , name : 'EXECUTOR' , role : '执行者' , description : '调用工具执行具体操作,创建/更新/删除系统资源' , relLabel : RELATION _LABELS [ 'master-executor' ] } ,
{ id : 'librarian' , name : 'LIBRARIAN' , role : '知识官' , description : '管理知识库和知识图谱,检索相关信息,更新记忆' , relLabel : RELATION _LABELS [ 'master-librarian' ] } ,
{ id : 'analyst' , name : 'ANALYST' , role : '分析师' , description : '分析工作数据,生成统计报告,提供洞察建议' , relLabel : RELATION _LABELS [ 'master-analyst' ] } ,
]
type PlaybackHandle = ReturnType < typeof window.setTimeout >
// ── Refs ────────────────────────────────────────────────────────
const canvasRef = ref < HTMLElement | null > ( null )
const svgRef = ref < SVGElement | null > ( null )
const masterCardRef = ref < HTMLElement | null > ( null )
const subRefs : Record < string , HTMLElement > = { }
const cleanupFns : Array < ( ) => void > = [ ]
const hoverResetTimers : Record < string , PlaybackHandle | null > = { }
// Background particles
2026-03-21 10:13:35 +08:00
const bgParticles = Array . from ( { length : 60 } , ( _ , i ) => {
const d = 3 + Math . random ( ) * 5
const delay = Math . random ( ) * 4
const o = 0.25 + Math . random ( ) * 0.5
const size = 1 + Math . random ( ) * 2.5
return {
id : i ,
style : {
left : ` ${ Math . random ( ) * 98 } % ` ,
top : ` ${ Math . random ( ) * 95 } % ` ,
width : ` ${ size } px ` ,
height : ` ${ size } px ` ,
'--d' : ` ${ d } s ` ,
'--delay' : ` ${ delay } s ` ,
'--o' : String ( o ) ,
opacity : o ,
} ,
}
} )
2026-03-25 11:27:16 +08:00
let resizeObserver : ResizeObserver | null = null
2026-03-21 10:13:35 +08:00
const selectedAgentId = ref < string | null > ( null )
const drawerOpen = ref ( false )
2026-03-25 11:27:16 +08:00
const addModalOpen = ref ( false )
2026-03-21 10:13:35 +08:00
const editAgent = ref < { name : string ; role : string ; description : string ; systemPrompt : string ; enabled : boolean } | null > ( null )
2026-03-25 11:27:16 +08:00
const newAgent = reactive ( { name : '' , roleKey : '' , role : '' , description : '' , systemPrompt : '' } )
2026-03-21 10:13:35 +08:00
const saving = ref ( false )
const loading = ref ( false )
2026-03-25 11:27:16 +08:00
const zoom = ref ( 1 )
const pan = reactive ( { x : 0 , y : 0 } )
const basePan = reactive ( { x : 0 , y : 0 } )
const isPanning = ref ( false )
const panStart = reactive ( { x : 0 , y : 0 } )
const panOrigin = reactive ( { x : 0 , y : 0 } )
2026-03-21 10:13:35 +08:00
const connectionStatus = ref < 'connected' | 'disconnected' > ( 'disconnected' )
const connectionLabel = computed ( ( ) => connectionStatus . value === 'connected' ? '实时同步' : '离线模式' )
2026-03-25 11:27:16 +08:00
const zoomPercent = computed ( ( ) => ` ${ Math . round ( zoom . value * 100 ) } % ` )
function roundPx ( value : number ) {
return Math . round ( value )
}
const viewportStyle = computed ( ( ) => ( {
transform : ` translate( ${ roundPx ( basePan . x + pan . x ) } px, ${ roundPx ( basePan . y + pan . y ) } px) ` ,
} ) )
const stageStyle = computed ( ( ) => ( {
width : '100%' ,
left : '0px' ,
'--node-scale' : String ( zoom . value ) ,
} ) )
const motionEnabled = window . matchMedia ( '(prefers-reduced-motion: no-preference)' ) . matches
const MIN _ZOOM = 0.8
const MAX _ZOOM = 1.6
const ZOOM _STEP = 0.1
const agentData = reactive < Record < string , { callCount : number ; currentTask : string | null ; status : string } > > ( { } )
const activeLine = ref < string | null > ( null )
const firingLine = ref < string | null > ( null )
const pulseDuration = 600
const localAgents = reactive < Record < string , Agent > > (
Object . fromEntries ( DEFAULT _AGENTS . map ( a => [ a . id , { ... a } ] ) )
)
2026-03-21 10:13:35 +08:00
let pollInterval : ReturnType < typeof setInterval > | null = null
2026-03-25 11:27:16 +08:00
// ── Layout ─────────────────────────────────────────────────────
function pxToSvg ( pctX : number , top : number ) {
const canvas = canvasRef . value
if ( ! canvas ) return { x : 0 , y : top }
return {
x : ( pctX / 100 ) * canvas . clientWidth ,
y : top ,
}
}
function getCanvasMetrics ( ) {
const canvas = canvasRef . value
if ( ! canvas ) return { width : 0 , height : 0 }
return {
width : canvas . clientWidth ,
height : canvas . clientHeight ,
}
}
function updateBasePan ( ) {
const { width } = getCanvasMetrics ( )
const scaledWidth = width * zoom . value
basePan . x = width ? roundPx ( ( width - scaledWidth ) / 2 ) : 0
basePan . y = 0
}
function getNodeMetrics ( ) {
return {
width : roundPx ( NODE _W * zoom . value ) ,
height : roundPx ( NODE _H * zoom . value ) ,
paddingX : roundPx ( 16 * zoom . value ) ,
paddingY : roundPx ( 14 * zoom . value ) ,
corner : Math . max ( 6 , roundPx ( 10 * zoom . value ) ) ,
status : Math . max ( 8 , roundPx ( 10 * zoom . value ) ) ,
statusRing : Math . max ( 6 , roundPx ( 8 * zoom . value ) ) ,
relOffset : roundPx ( - 20 * zoom . value ) ,
}
}
const masterNodeStyle = computed ( ( ) => {
const { x } = pxToSvg ( 50 , MASTER _TOP )
const metrics = getNodeMetrics ( )
return {
left : ` ${ roundPx ( x - metrics . width / 2 ) } px ` ,
top : ` ${ roundPx ( MASTER _TOP ) } px ` ,
width : ` ${ metrics . width } px ` ,
height : ` ${ metrics . height } px ` ,
'--node-padding-x' : ` ${ metrics . paddingX } px ` ,
'--node-padding-y' : ` ${ metrics . paddingY } px ` ,
'--node-corner-size' : ` ${ metrics . corner } px ` ,
'--node-status-size' : ` ${ metrics . status } px ` ,
'--node-status-ring-size' : ` ${ metrics . statusRing } px ` ,
'--node-rel-offset' : ` ${ metrics . relOffset } px ` ,
}
} )
function getSubNodeStyle ( sub : SubAgentCard ) {
const idx = subAgents . findIndex ( s => s . id === sub . id )
const pct = SUB _XS [ idx ] ? ? 50
const { x } = pxToSvg ( pct , SUB _TOP )
const metrics = getNodeMetrics ( )
return {
left : ` ${ roundPx ( x - metrics . width / 2 ) } px ` ,
top : ` ${ roundPx ( SUB _TOP ) } px ` ,
width : ` ${ metrics . width } px ` ,
height : ` ${ metrics . height } px ` ,
'--node-padding-x' : ` ${ metrics . paddingX } px ` ,
'--node-padding-y' : ` ${ metrics . paddingY } px ` ,
'--node-corner-size' : ` ${ metrics . corner } px ` ,
'--node-status-size' : ` ${ metrics . status } px ` ,
'--node-status-ring-size' : ` ${ metrics . statusRing } px ` ,
'--node-rel-offset' : ` ${ metrics . relOffset } px ` ,
}
}
function getLinePath ( subId : string ) {
const canvas = canvasRef . value
if ( ! canvas ) return ''
const w = canvas . clientWidth
const idx = subAgents . findIndex ( s => s . id === subId )
const subPct = SUB _XS [ idx ] ? ? 50
const metrics = getNodeMetrics ( )
const masterX = ( 50 / 100 ) * w
const masterY = MASTER _TOP + metrics . height / 2
const subX = ( subPct / 100 ) * w
const subY = SUB _TOP + metrics . height / 2
const midY = ( masterY + subY ) / 2
return ` M ${ roundPx ( masterX ) } , ${ roundPx ( masterY ) } C ${ roundPx ( masterX ) } , ${ roundPx ( midY ) } ${ roundPx ( subX ) } , ${ roundPx ( midY ) } ${ roundPx ( subX ) } , ${ roundPx ( subY ) } `
}
// ── SVG sizing ──────────────────────────────────────────────────
function updateSvgSize ( ) {
const canvas = canvasRef . value
const svg = svgRef . value
if ( ! canvas || ! svg ) return
svg . setAttribute ( 'width' , String ( canvas . clientWidth ) )
svg . setAttribute ( 'height' , String ( canvas . clientHeight ) )
}
// ── Ref helpers ──────────────────────────────────────────────────
function setSubRef ( id : string , el : HTMLElement | null ) {
if ( el ) subRefs [ id ] = el
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
// ── Status / data ────────────────────────────────────────────────
2026-03-24 21:42:01 +08:00
function getStatusClass ( agentId : string ) {
const data = agentData [ agentId ]
const agent = localAgents [ agentId ]
if ( ! agent ? . enabled ) return 'disabled'
if ( ! data ) return 'idle'
return data . status === 'active' ? 'active' : 'idle'
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
function getAgentName ( id : string ) { return localAgents [ id ] ? . name || DEFAULT _AGENTS . find ( a => a . id === id ) ? . name || id . toUpperCase ( ) }
function getAgentRole ( id : string ) { return localAgents [ id ] ? . role || DEFAULT _AGENTS . find ( a => a . id === id ) ? . role || '' }
function getAgentDesc ( id : string ) { return localAgents [ id ] ? . description || DEFAULT _AGENTS . find ( a => a . id === id ) ? . description || '' }
function clampZoom ( value : number ) {
return Math . min ( MAX _ZOOM , Math . max ( MIN _ZOOM , Number ( value . toFixed ( 2 ) ) ) )
}
2026-03-21 10:13:35 +08:00
2026-03-25 11:27:16 +08:00
function zoomIn ( ) {
zoom . value = clampZoom ( zoom . value + ZOOM _STEP )
updateBasePan ( )
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
function zoomOut ( ) {
zoom . value = clampZoom ( zoom . value - ZOOM _STEP )
updateBasePan ( )
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
function resetView ( ) {
zoom . value = 1
pan . x = 0
pan . y = 0
updateBasePan ( )
requestAnimationFrame ( ( ) => {
pan . x = 0
pan . y = 0
} )
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
function handleWheel ( event : WheelEvent ) {
const delta = event . deltaY < 0 ? ZOOM _STEP : - ZOOM _STEP
zoom . value = clampZoom ( zoom . value + delta )
updateBasePan ( )
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
function startPan ( event : MouseEvent ) {
const target = event . target as HTMLElement | null
if ( ! target || target . closest ( '.node-card' ) || target . closest ( '.canvas-controls' ) ) return
isPanning . value = true
panStart . x = event . clientX
panStart . y = event . clientY
panOrigin . x = pan . x
panOrigin . y = pan . y
window . addEventListener ( 'mousemove' , movePan )
window . addEventListener ( 'mouseup' , endPan , { once : true } )
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
function movePan ( event : MouseEvent ) {
if ( ! isPanning . value ) return
pan . x = panOrigin . x + event . clientX - panStart . x
pan . y = panOrigin . y + event . clientY - panStart . y
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
function endPan ( ) {
isPanning . value = false
window . removeEventListener ( 'mousemove' , movePan )
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
// ── Actions ───────────────────────────────────────────────────────
2026-03-21 10:13:35 +08:00
function selectAgent ( id : string ) {
const agent = localAgents [ id ]
if ( ! agent ) return
selectedAgentId . value = id
2026-03-25 11:27:16 +08:00
editAgent . value = { name : agent . name , role : agent . role , description : agent . description , systemPrompt : agent . systemPrompt , enabled : agent . enabled }
2026-03-21 10:13:35 +08:00
drawerOpen . value = true
}
function resetConfig ( ) {
2026-03-25 11:27:16 +08:00
const original = DEFAULT _AGENTS . find ( a => a . id === selectedAgentId . value )
if ( original && editAgent . value ) Object . assign ( editAgent . value , { name : original . name , role : original . role , description : original . description , systemPrompt : original . systemPrompt , enabled : original . enabled } )
2026-03-21 10:13:35 +08:00
}
async function saveConfig ( ) {
if ( ! editAgent . value || ! selectedAgentId . value ) return
saving . value = true
try {
if ( localAgents [ selectedAgentId . value ] ) Object . assign ( localAgents [ selectedAgentId . value ] , editAgent . value )
try {
2026-03-25 11:27:16 +08:00
await agentApi . updateConfig ( selectedAgentId . value , { name : editAgent . value ! . name , description : editAgent . value ! . description , system _prompt : editAgent . value ! . systemPrompt , enabled : editAgent . value ! . enabled } )
} catch { /* local only */ }
2026-03-21 10:13:35 +08:00
drawerOpen . value = false
2026-03-25 11:27:16 +08:00
} finally { saving . value = false }
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
function addAgent ( ) {
if ( ! newAgent . name || ! newAgent . roleKey ) return
const id = newAgent . roleKey . toLowerCase ( ) . replace ( /\s+/g , '_' )
if ( localAgents [ id ] ) return
localAgents [ id ] = { id , name : newAgent . name . toUpperCase ( ) , role : newAgent . role , roleKey : id , description : newAgent . description , systemPrompt : newAgent . systemPrompt , enabled : true }
addModalOpen . value = false
2026-03-21 10:13:35 +08:00
}
async function refreshStats ( ) {
loading . value = true
try {
2026-03-25 11:27:16 +08:00
const stats = await agentApi . getStats ( )
for ( const s of stats ) {
agentData [ s . agent _id ] = { callCount : s . call _count , currentTask : s . current _task , status : s . status }
if ( s . status === 'active' && s . agent _id !== 'master' ) { firingLine . value = s . agent _id ; activeLine . value = s . agent _id }
}
2026-03-21 10:13:35 +08:00
connectionStatus . value = 'connected'
} catch {
connectionStatus . value = 'disconnected'
2026-03-25 11:27:16 +08:00
agentData [ 'master' ] = { callCount : 47 , currentTask : '协调子Agent工作' , status : 'active' }
agentData [ 'planner' ] = { callCount : 12 , currentTask : null , status : 'idle' }
agentData [ 'executor' ] = { callCount : 8 , currentTask : '创建文档' , status : 'active' }
agentData [ 'librarian' ] = { callCount : 5 , currentTask : null , status : 'idle' }
agentData [ 'analyst' ] = { callCount : 3 , currentTask : null , status : 'idle' }
firingLine . value = 'executor' ; activeLine . value = 'executor'
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
loading . value = false
}
function onPulseEnd ( ) { firingLine . value = null ; activeLine . value = null }
function stopTimer ( timer : PlaybackHandle | null ) {
if ( timer ) window . clearTimeout ( timer )
2026-03-21 22:13:12 +08:00
}
function runTransition (
el : Element ,
keyframes : Keyframe [ ] ,
options : KeyframeAnimationOptions ,
done ? : ( ) => void ,
) {
2026-03-25 11:27:16 +08:00
const animation = ( el as HTMLElement ) . animate ( keyframes , {
fill : 'forwards' ,
... options ,
} )
2026-03-21 22:13:12 +08:00
const finish = ( ) => done ? . ( )
animation . addEventListener ( 'finish' , finish , { once : true } )
cleanupFns . push ( ( ) => animation . cancel ( ) )
return animation
}
2026-03-25 11:27:16 +08:00
// ── Motion helpers ───────────────────────────────────────────────
2026-03-21 22:13:12 +08:00
function animateIn ( el : Element , done : ( ) => void ) {
runTransition (
el ,
2026-03-25 11:27:16 +08:00
[
{ opacity : 0 , transform : 'translateX(80px)' } ,
{ opacity : 1 , transform : 'translateX(0)' } ,
] ,
2026-03-21 22:13:12 +08:00
{ duration : 350 , easing : 'cubic-bezier(0.4, 0, 0.2, 1)' } ,
done ,
)
}
function animateOut ( el : Element , done : ( ) => void ) {
runTransition (
el ,
2026-03-25 11:27:16 +08:00
[
{ opacity : 1 , transform : 'translateX(0)' } ,
{ opacity : 0 , transform : 'translateX(80px)' } ,
] ,
2026-03-21 22:13:12 +08:00
{ duration : 250 , easing : 'cubic-bezier(0.4, 0, 1, 1)' } ,
done ,
)
}
function fadeIn ( el : Element , done : ( ) => void ) {
runTransition ( el , [ { opacity : 0 } , { opacity : 1 } ] , { duration : 250 } , done )
}
function fadeOut ( el : Element , done : ( ) => void ) {
runTransition ( el , [ { opacity : 1 } , { opacity : 0 } ] , { duration : 200 } , done )
}
2026-03-21 10:13:35 +08:00
2026-03-25 11:27:16 +08:00
function playEntranceAnimations ( ) {
if ( ! motionEnabled ) return
if ( masterCardRef . value ) {
runTransition (
masterCardRef . value ,
[
{ opacity : 0 , transform : 'translateY(14px) scale(0.99)' , filter : 'brightness(0.86)' } ,
{ opacity : 1 , transform : 'translateY(0) scale(1)' , filter : 'brightness(1)' } ,
] ,
{ duration : 680 , easing : 'cubic-bezier(0.16, 1, 0.3, 1)' } ,
)
}
subAgents . forEach ( ( sub , idx ) => {
const el = subRefs [ sub . id ]
if ( ! el ) return
runTransition (
el ,
[
{ opacity : 0 , transform : 'translateY(12px) scale(0.99)' , filter : 'brightness(0.82)' } ,
{ opacity : 1 , transform : 'translateY(0) scale(1)' , filter : 'brightness(1)' } ,
] ,
{
duration : 500 ,
delay : 210 + idx * 85 ,
easing : 'cubic-bezier(0.16, 1, 0.3, 1)' ,
} ,
)
const handleMouseEnter = ( ) => {
if ( ! localAgents [ sub . id ] ? . enabled ) return
stopTimer ( hoverResetTimers [ sub . id ] ? ? null )
el . style . transform = 'translateY(-2px)'
}
const handleMouseLeave = ( ) => {
stopTimer ( hoverResetTimers [ sub . id ] ? ? null )
hoverResetTimers [ sub . id ] = window . setTimeout ( ( ) => {
el . style . transform = ''
hoverResetTimers [ sub . id ] = null
} , 180 )
}
el . addEventListener ( 'mouseenter' , handleMouseEnter )
el . addEventListener ( 'mouseleave' , handleMouseLeave )
cleanupFns . push ( ( ) => {
stopTimer ( hoverResetTimers [ sub . id ] ? ? null )
el . removeEventListener ( 'mouseenter' , handleMouseEnter )
el . removeEventListener ( 'mouseleave' , handleMouseLeave )
} )
} )
}
// ── Lifecycle ────────────────────────────────────────────────────
2026-03-21 10:13:35 +08:00
onMounted ( async ( ) => {
await refreshStats ( )
pollInterval = setInterval ( refreshStats , 5000 )
2026-03-25 11:27:16 +08:00
requestAnimationFrame ( ( ) => {
updateBasePan ( )
updateSvgSize ( )
playEntranceAnimations ( )
} )
resizeObserver = new ResizeObserver ( ( ) => {
updateBasePan ( )
updateSvgSize ( )
} )
if ( canvasRef . value ) resizeObserver . observe ( canvasRef . value )
2026-03-21 10:13:35 +08:00
} )
onUnmounted ( ( ) => {
if ( pollInterval ) clearInterval ( pollInterval )
2026-03-25 11:27:16 +08:00
resizeObserver ? . disconnect ( )
window . removeEventListener ( 'mousemove' , movePan )
window . removeEventListener ( 'mouseup' , endPan )
cleanupFns . forEach ( cleanup => cleanup ( ) )
2026-03-21 10:13:35 +08:00
} )
< / script >
< style scoped >
. agent - view {
height : 100 % ;
display : flex ;
flex - direction : column ;
2026-03-25 11:27:16 +08:00
overflow : hidden ;
2026-03-21 10:13:35 +08:00
position : relative ;
2026-03-25 11:27:16 +08:00
background : var ( -- bg - void ) ;
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
. bg - grid {
2026-03-21 10:13:35 +08:00
position : absolute ;
inset : 0 ;
background - image :
2026-03-25 11:27:16 +08:00
linear - gradient ( rgba ( 0 , 245 , 212 , 0.04 ) 1 px , transparent 1 px ) ,
linear - gradient ( 90 deg , rgba ( 0 , 245 , 212 , 0.04 ) 1 px , transparent 1 px ) ;
2026-03-21 10:13:35 +08:00
background - size : 40 px 40 px ;
2026-03-25 11:27:16 +08:00
pointer - events : none ;
z - index : 0 ;
2026-03-21 10:13:35 +08:00
}
. bg - glow {
2026-03-25 11:27:16 +08:00
position : absolute ;
inset : 0 ;
background : radial - gradient ( ellipse 80 % 60 % at 50 % 40 % , rgba ( 0 , 245 , 212 , 0.05 ) 0 % , transparent 70 % ) ;
pointer - events : none ;
z - index : 0 ;
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
. bg - particles {
position : absolute ;
inset : 0 ;
pointer - events : none ;
z - index : 0 ;
overflow : hidden ;
2026-03-21 10:13:35 +08:00
}
. bg - particle {
position : absolute ;
border - radius : 50 % ;
background : var ( -- accent - cyan ) ;
box - shadow : 0 0 4 px rgba ( 0 , 245 , 212 , 0.6 ) , 0 0 8 px rgba ( 0 , 245 , 212 , 0.2 ) ;
animation : star - twinkle var ( -- d , 4 s ) ease - in - out infinite var ( -- delay , 0 s ) ;
}
@ keyframes star - twinkle {
0 % , 100 % { opacity : var ( -- o , 0.4 ) ; transform : scale ( 1 ) ; }
2026-03-25 11:27:16 +08:00
50 % { opacity : calc ( var ( -- o , 0.4 ) * 0.3 ) ; transform : scale ( 0.5 ) ; }
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
/* Header */
2026-03-21 10:13:35 +08:00
. view - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 14 px 24 px ;
border - bottom : 1 px solid var ( -- border - dim ) ;
2026-03-25 11:27:16 +08:00
position : relative ;
2026-03-21 10:13:35 +08:00
z - index : 10 ;
2026-03-25 11:27:16 +08:00
background : rgba ( 5 , 8 , 16 , 0.6 ) ;
2026-03-21 10:13:35 +08:00
backdrop - filter : blur ( 8 px ) ;
}
2026-03-25 11:27:16 +08:00
. header - title { font - family : var ( -- font - display ) ; font - size : 13 px ; letter - spacing : 0.2 em ; color : var ( -- text - primary ) ; }
. title - bracket { color : var ( -- accent - cyan ) ; opacity : 0.6 ; }
. header - actions { display : flex ; align - items : center ; gap : 12 px ; }
2026-03-24 21:42:01 +08:00
2026-03-25 11:27:16 +08:00
. btn - icon {
width : 32 px ; height : 32 px ; display : flex ; align - items : center ; justify - content : center ;
background : var ( -- bg - card ) ; border : 1 px solid var ( -- border - mid ) ; border - radius : var ( -- radius - sm ) ;
color : var ( -- text - secondary ) ; cursor : pointer ; transition : all var ( -- transition - fast ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. btn - icon : hover { border - color : var ( -- accent - cyan ) ; color : var ( -- accent - cyan ) ; box - shadow : var ( -- glow - cyan ) ; }
. btn - icon . spinning svg { animation : spin 0.8 s linear infinite ; }
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
2026-03-24 21:42:01 +08:00
2026-03-25 11:27:16 +08:00
. btn - add {
display : flex ; align - items : center ; gap : 6 px ; padding : 6 px 14 px ;
background : rgba ( 0 , 245 , 212 , 0.08 ) ; border : 1 px solid rgba ( 0 , 245 , 212 , 0.3 ) ;
border - radius : var ( -- radius - sm ) ; color : var ( -- accent - cyan ) ; font - family : var ( -- font - mono ) ;
font - size : 11 px ; letter - spacing : 0.1 em ; cursor : pointer ; transition : all var ( -- transition - fast ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. btn - add : hover { background : rgba ( 0 , 245 , 212 , 0.15 ) ; border - color : var ( -- accent - cyan ) ; box - shadow : var ( -- glow - cyan ) ; }
2026-03-21 10:13:35 +08:00
2026-03-25 11:27:16 +08:00
. status - bar { display : flex ; align - items : center ; gap : 6 px ; font - size : 10 px ; color : var ( -- text - dim ) ; letter - spacing : 0.1 em ; }
. status - dot { width : 6 px ; height : 6 px ; border - radius : 50 % ; }
. status - dot . connected { background : var ( -- accent - cyan ) ; box - shadow : 0 0 6 px var ( -- accent - cyan ) ; animation : status - pulse - soft 2.6 s ease - in - out infinite ; }
. status - dot . disconnected { background : var ( -- text - dim ) ; }
@ keyframes status - pulse - soft {
0 % , 100 % { opacity : 1 ; transform : scale ( 1 ) ; }
50 % { opacity : 0.55 ; transform : scale ( 0.82 ) ; }
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
/* Canvas */
. nodes - canvas {
flex : 1 ;
position : relative ;
overflow : hidden ;
isolation : isolate ;
cursor : grab ;
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
. nodes - canvas . panning {
cursor : grabbing ;
user - select : none ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. canvas - controls {
position : absolute ;
right : 20 px ;
bottom : 18 px ;
z - index : 12 ;
2026-03-24 21:42:01 +08:00
display : flex ;
align - items : center ;
gap : 8 px ;
2026-03-25 11:27:16 +08:00
padding : 8 px ;
border : 1 px solid rgba ( 0 , 245 , 212 , 0.12 ) ;
border - radius : 22 px ;
background : linear - gradient ( 180 deg , rgba ( 8 , 13 , 22 , 0.92 ) , rgba ( 5 , 9 , 18 , 0.86 ) ) ;
backdrop - filter : blur ( 14 px ) ;
box - shadow : 0 16 px 36 px rgba ( 0 , 0 , 0 , 0.32 ) , inset 0 1 px 0 rgba ( 255 , 255 , 255 , 0.04 ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. control - chip {
height : 36 px ;
border : 1 px solid rgba ( 0 , 245 , 212 , 0.12 ) ;
background : rgba ( 9 , 16 , 28 , 0.9 ) ;
color : var ( -- text - secondary ) ;
border - radius : 14 px ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
transition : all 0.18 s ease ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. control - chip : hover {
border - color : rgba ( 0 , 245 , 212 , 0.3 ) ;
color : var ( -- accent - cyan ) ;
box - shadow : 0 0 18 px rgba ( 0 , 245 , 212 , 0.08 ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. zoom - chip {
width : 36 px ;
flex - shrink : 0 ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. chip - symbol {
font - family : var ( -- font - display ) ;
font - size : 18 px ;
line - height : 1 ;
}
. zoom - readout {
min - width : 72 px ;
padding : 0 14 px ;
}
. chip - value {
font - family : var ( -- font - display ) ;
2026-03-24 21:42:01 +08:00
font - size : 11 px ;
letter - spacing : 0.08 em ;
2026-03-25 11:27:16 +08:00
color : var ( -- text - primary ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. nodes - viewport {
position : absolute ;
inset : 0 ;
2026-03-24 21:42:01 +08:00
z - index : 1 ;
2026-03-25 11:27:16 +08:00
will - change : transform ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. nodes - stage {
position : absolute ;
inset : 0 ;
will - change : auto ;
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
. canvas - aura ,
. canvas - scan {
2026-03-21 10:13:35 +08:00
position : absolute ;
2026-03-25 11:27:16 +08:00
inset : 0 ;
2026-03-21 10:13:35 +08:00
pointer - events : none ;
2026-03-25 11:27:16 +08:00
z - index : 0 ;
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
. canvas - aura {
background :
radial - gradient ( circle at 50 % 18 % , rgba ( 0 , 245 , 212 , 0.1 ) 0 % , rgba ( 0 , 245 , 212 , 0.05 ) 20 % , transparent 46 % ) ,
radial - gradient ( circle at 50 % 62 % , rgba ( 0 , 245 , 212 , 0.035 ) 0 % , transparent 54 % ) ;
2026-03-24 21:42:01 +08:00
filter : blur ( 12 px ) ;
2026-03-25 11:27:16 +08:00
opacity : 0.72 ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. canvas - scan {
inset : - 20 % 0 ;
background : linear - gradient ( 180 deg , transparent 0 % , rgba ( 0 , 245 , 212 , 0.018 ) 42 % , rgba ( 0 , 245 , 212 , 0.045 ) 50 % , rgba ( 0 , 245 , 212 , 0.018 ) 58 % , transparent 100 % ) ;
animation : canvas - scan 11 s linear infinite ;
opacity : 0.5 ;
}
@ keyframes canvas - scan {
from { transform : translateY ( - 18 % ) ; }
to { transform : translateY ( 18 % ) ; }
}
. conn - svg {
position : absolute ;
inset : 0 ;
width : 100 % ;
height : 100 % ;
pointer - events : none ;
z - index : 1 ;
overflow : visible ;
}
. conn - path {
fill : none ;
stroke : rgba ( 0 , 245 , 212 , 0.22 ) ;
stroke - width : 1.5 ;
stroke - dasharray : 5 7 ;
stroke - linecap : round ;
filter : drop - shadow ( 0 0 6 px rgba ( 0 , 245 , 212 , 0.06 ) ) ;
animation : dash - flow 5.5 s linear infinite ;
}
@ keyframes dash - flow { to { stroke - dashoffset : - 48 ; } }
. conn - path . active {
stroke : color - mix ( in srgb , var ( -- accent - cyan ) 68 % , var ( -- accent - amber ) 32 % ) ;
stroke - opacity : 0.72 ;
stroke - width : 1.9 ;
stroke - dasharray : none ;
filter : url ( # lineGlow ) drop - shadow ( 0 0 8 px rgba ( 0 , 245 , 212 , 0.16 ) ) ;
animation : line - flare 1.8 s ease - in - out infinite alternate ;
}
@ keyframes line - flare {
from { stroke - opacity : 0.42 ; }
to { stroke - opacity : 0.78 ; }
}
. pulse - dot { fill : var ( -- accent - amber ) ; filter : drop - shadow ( 0 0 8 px var ( -- accent - amber ) ) ; }
/* Node Cards */
. node - card {
2026-03-21 10:13:35 +08:00
position : absolute ;
2026-03-25 11:27:16 +08:00
z - index : 2 ;
cursor : pointer ;
transition : transform 0.22 s ease ;
2026-03-21 10:13:35 +08:00
}
2026-03-25 11:27:16 +08:00
. node - sub . disabled { opacity : 0.35 ; cursor : not - allowed ; }
2026-03-21 10:13:35 +08:00
2026-03-25 11:27:16 +08:00
. node - inner {
width : 100 % ;
height : 100 % ;
background : rgba ( 13 , 21 , 37 , 0.92 ) ;
border : 1 px solid rgba ( 0 , 245 , 212 , 0.2 ) ;
border - radius : var ( -- radius - md ) ;
padding : var ( -- node - padding - y , 14 px ) var ( -- node - padding - x , 16 px ) ;
2026-03-21 10:13:35 +08:00
display : flex ;
flex - direction : column ;
2026-03-25 11:27:16 +08:00
gap : calc ( 3 px * var ( -- node - scale , 1 ) ) ;
position : relative ;
overflow : hidden ;
backdrop - filter : blur ( 12 px ) ;
transition : border - color 0.2 s , box - shadow 0.2 s , background 0.25 s ease ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - inner : : before {
content : '' ;
position : absolute ;
inset : 0 ;
background : linear - gradient ( 115 deg , transparent 20 % , rgba ( 255 , 255 , 255 , 0.045 ) 32 % , transparent 44 % ) ;
transform : translateX ( - 150 % ) ;
opacity : 0 ;
pointer - events : none ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - master . node - inner : : after {
content : '' ;
position : absolute ;
inset : - 18 % ;
background : radial - gradient ( circle , rgba ( 0 , 245 , 212 , 0.1 ) 0 % , rgba ( 0 , 245 , 212 , 0.045 ) 28 % , transparent 62 % ) ;
opacity : 0.72 ;
animation : core - breathe 5.6 s ease - in - out infinite ;
pointer - events : none ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - master . node - inner {
background : linear - gradient ( 135 deg , rgba ( 0 , 245 , 212 , 0.06 ) 0 % , rgba ( 13 , 21 , 37 , 0.95 ) 100 % ) ;
border - color : rgba ( 0 , 245 , 212 , 0.3 ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - card : hover . node - inner {
border - color : rgba ( 0 , 245 , 212 , 0.42 ) ;
box - shadow : 0 8 px 28 px rgba ( 0 , 245 , 212 , 0.11 ) , 0 0 0 1 px rgba ( 0 , 245 , 212 , 0.08 ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - card : hover . node - inner : : before {
opacity : 0.9 ;
animation : node - sheen 1.45 s ease ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - card . selected . node - inner {
border - color : var ( -- accent - cyan ) ;
box - shadow : 0 0 0 1 px rgba ( 0 , 245 , 212 , 0.26 ) , 0 0 18 px rgba ( 0 , 245 , 212 , 0.14 ) ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
@ keyframes node - sheen {
0 % { transform : translateX ( - 150 % ) ; }
100 % { transform : translateX ( 150 % ) ; }
}
@ keyframes core - breathe {
0 % , 100 % { opacity : 0.46 ; transform : scale ( 0.98 ) ; }
50 % { opacity : 0.8 ; transform : scale ( 1.01 ) ; }
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - corner { position : absolute ; width : var ( -- node - corner - size , 10 px ) ; height : var ( -- node - corner - size , 10 px ) ; opacity : 0.6 ; }
. node - corner . tl { top : calc ( 6 px * var ( -- node - scale , 1 ) ) ; left : calc ( 6 px * var ( -- node - scale , 1 ) ) ; border - top : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; border - left : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; }
. node - corner . tr { top : calc ( 6 px * var ( -- node - scale , 1 ) ) ; right : calc ( 6 px * var ( -- node - scale , 1 ) ) ; border - top : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; border - right : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; }
. node - corner . bl { bottom : calc ( 6 px * var ( -- node - scale , 1 ) ) ; left : calc ( 6 px * var ( -- node - scale , 1 ) ) ; border - bottom : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; border - left : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; }
. node - corner . br { bottom : calc ( 6 px * var ( -- node - scale , 1 ) ) ; right : calc ( 6 px * var ( -- node - scale , 1 ) ) ; border - bottom : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; border - right : calc ( 1.5 px * var ( -- node - scale , 1 ) ) solid var ( -- accent - cyan ) ; }
2026-03-24 21:42:01 +08:00
2026-03-25 11:27:16 +08:00
. node - status { position : absolute ; top : calc ( 10 px * var ( -- node - scale , 1 ) ) ; right : calc ( 10 px * var ( -- node - scale , 1 ) ) ; width : var ( -- node - status - size , 10 px ) ; height : var ( -- node - status - size , 10 px ) ; border - radius : 50 % ; display : flex ; align - items : center ; justify - content : center ; }
. status - ring { width : var ( -- node - status - ring - size , 8 px ) ; height : var ( -- node - status - ring - size , 8 px ) ; border - radius : 50 % ; }
. node - status . active : : before {
2026-03-24 21:42:01 +08:00
content : '' ;
position : absolute ;
2026-03-25 11:27:16 +08:00
inset : - 6 px ;
border : 1 px solid rgba ( 0 , 245 , 212 , 0.22 ) ;
2026-03-24 21:42:01 +08:00
border - radius : 999 px ;
2026-03-25 11:27:16 +08:00
animation : status - orbit 2.3 s ease - out infinite ;
}
. node - status . active . status - ring { background : var ( -- accent - cyan ) ; box - shadow : 0 0 8 px var ( -- accent - cyan ) ; animation : status - pulse 1.8 s ease - in - out infinite ; }
. node - status . idle . status - ring { background : var ( -- text - secondary ) ; }
. node - status . disabled . status - ring { background : var ( -- text - dim ) ; opacity : 0.4 ; }
@ keyframes status - pulse { 0 % , 100 % { opacity : 1 } 50 % { opacity : 0.4 } }
@ keyframes status - orbit {
0 % { transform : scale ( 0.5 ) ; opacity : 0.65 ; }
100 % { transform : scale ( 1.3 ) ; opacity : 0 ; }
}
. node - label { font - family : var ( -- font - display ) ; font - size : calc ( 8 px * var ( -- node - scale , 1 ) ) ; letter - spacing : 0.2 em ; color : var ( -- text - dim ) ; margin - bottom : 1 px ; }
. node - master . node - label { color : rgba ( 0 , 245 , 212 , 0.5 ) ; }
. node - name { font - family : var ( -- font - display ) ; font - size : calc ( 15 px * var ( -- node - scale , 1 ) ) ; font - weight : 700 ; letter - spacing : 0.08 em ; color : var ( -- accent - cyan ) ; line - height : 1.2 ; }
. node - master . node - name { font - size : calc ( 18 px * var ( -- node - scale , 1 ) ) ; }
. node - role { font - family : var ( -- font - mono ) ; font - size : calc ( 10 px * var ( -- node - scale , 1 ) ) ; color : var ( -- accent - amber ) ; letter - spacing : 0.05 em ; }
. node - desc {
font - family : var ( -- font - mono ) ; font - size : calc ( 10 px * var ( -- node - scale , 1 ) ) ; color : var ( -- text - secondary ) ;
line - height : 1.5 ; flex : 1 ; overflow : hidden ; display : - webkit - box ;
- webkit - line - clamp : 3 ; - webkit - box - orient : vertical ; text - overflow : ellipsis ;
}
. node - footer { display : flex ; align - items : center ; gap : calc ( 8 px * var ( -- node - scale , 1 ) ) ; flex - wrap : wrap ; margin - top : 2 px ; }
. node - stat { display : flex ; align - items : center ; gap : calc ( 4 px * var ( -- node - scale , 1 ) ) ; font - family : var ( -- font - mono ) ; font - size : calc ( 9 px * var ( -- node - scale , 1 ) ) ; }
2026-03-24 21:42:01 +08:00
. stat - label { color : var ( -- text - dim ) ; }
2026-03-25 11:27:16 +08:00
. stat - val { color : var ( -- accent - cyan ) ; font - weight : 600 ; }
. node - task - tag {
font - family : var ( -- font - mono ) ; font - size : calc ( 9 px * var ( -- node - scale , 1 ) ) ; color : var ( -- accent - amber ) ;
background : rgba ( 249 , 168 , 37 , 0.08 ) ; border : 1 px solid rgba ( 249 , 168 , 37 , 0.18 ) ;
border - radius : 3 px ; padding : calc ( 1 px * var ( -- node - scale , 1 ) ) calc ( 6 px * var ( -- node - scale , 1 ) ) ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; max - width : calc ( 120 px * var ( -- node - scale , 1 ) ) ;
box - shadow : 0 0 10 px rgba ( 249 , 168 , 37 , 0.05 ) ;
animation : task - tag - glow 3.4 s ease - in - out infinite ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. node - idle { font - family : var ( -- font - mono ) ; font - size : calc ( 9 px * var ( -- node - scale , 1 ) ) ; color : var ( -- text - dim ) ; font - style : italic ; }
. rel - label {
position : absolute ; font - family : var ( -- font - mono ) ; font - size : calc ( 8 px * var ( -- node - scale , 1 ) ) ; color : var ( -- text - dim ) ;
letter - spacing : 0.05 em ; pointer - events : none ; left : 50 % ; transform : translateX ( - 50 % ) ;
bottom : var ( -- node - rel - offset , - 20 px ) ; white - space : nowrap ;
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
. particle { position : absolute ; border - radius : 50 % ; background : var ( -- accent - cyan ) ; pointer - events : none ; }
2026-03-24 21:42:01 +08:00
2026-03-25 11:27:16 +08:00
@ keyframes task - tag - glow {
0 % , 100 % { box - shadow : 0 0 8 px rgba ( 249 , 168 , 37 , 0.04 ) ; }
50 % { box - shadow : 0 0 14 px rgba ( 249 , 168 , 37 , 0.1 ) ; }
2026-03-24 21:42:01 +08:00
}
2026-03-25 11:27:16 +08:00
/* Drawer */
2026-03-24 21:42:01 +08:00
. config - drawer {
2026-03-25 11:27:16 +08:00
position : fixed ; top : 0 ; right : 0 ; width : 420 px ; height : 100 % ;
background : rgba ( 5 , 8 , 16 , 0.97 ) ; border - left : 1 px solid var ( -- border - mid ) ;
backdrop - filter : blur ( 20 px ) ; z - index : 100 ; display : flex ; flex - direction : column ;
box - shadow : - 10 px 0 40 px rgba ( 0 , 0 , 0 , 0.5 ) ;
}
. drawer - backdrop { position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , 0.5 ) ; z - index : 99 ; }
. drawer - header { display : flex ; align - items : center ; justify - content : space - between ; padding : 16 px 20 px ; border - bottom : 1 px solid var ( -- border - dim ) ; }
. drawer - title { font - family : var ( -- font - display ) ; font - size : 11 px ; letter - spacing : 0.15 em ; color : var ( -- accent - cyan ) ; }
2026-03-24 21:42:01 +08:00
. btn - close {
2026-03-25 11:27:16 +08:00
width : 28 px ; height : 28 px ; display : flex ; align - items : center ; justify - content : center ;
background : transparent ; border : 1 px solid var ( -- border - dim ) ; border - radius : var ( -- radius - sm ) ;
color : var ( -- text - dim ) ; cursor : pointer ; transition : all var ( -- transition - fast ) ;
}
. btn - close : hover { border - color : var ( -- accent - red ) ; color : var ( -- accent - red ) ; }
. drawer - body { flex : 1 ; overflow - y : auto ; padding : 20 px ; display : flex ; flex - direction : column ; gap : 16 px ; }
. drawer - body : : - webkit - scrollbar { width : 4 px ; }
. drawer - body : : - webkit - scrollbar - thumb { background : var ( -- border - mid ) ; border - radius : 2 px ; }
. form - group { display : flex ; flex - direction : column ; gap : 6 px ; }
. form - group . flex - 1 { flex : 1 ; display : flex ; flex - direction : column ; }
. form - label { font - family : var ( -- font - mono ) ; font - size : 9 px ; letter - spacing : 0.15 em ; color : var ( -- text - dim ) ; }
. form - input {
background : var ( -- bg - card ) ; border : 1 px solid var ( -- border - mid ) ; border - radius : var ( -- radius - sm ) ;
padding : 10 px 12 px ; color : var ( -- text - primary ) ; font - family : var ( -- font - mono ) ; font - size : 12 px ; outline : none ;
transition : border - color var ( -- transition - fast ) ;
}
. form - input : focus { border - color : var ( -- accent - cyan ) ; box - shadow : 0 0 0 1 px rgba ( 0 , 245 , 212 , .1 ) ; }
2026-03-24 21:42:01 +08:00
. form - textarea {
2026-03-25 11:27:16 +08:00
background : var ( -- bg - card ) ; border : 1 px solid var ( -- border - mid ) ; border - radius : var ( -- radius - sm ) ;
padding : 10 px 12 px ; color : var ( -- text - primary ) ; font - family : var ( -- font - mono ) ; font - size : 11 px ;
outline : none ; resize : none ; line - height : 1.5 ; transition : border - color var ( -- transition - fast ) ;
}
. form - textarea : focus { border - color : var ( -- accent - cyan ) ; box - shadow : 0 0 0 1 px rgba ( 0 , 245 , 212 , .1 ) ; }
. code - textarea { font - size : 10 px ; flex : 1 ; }
. toggle - row { display : flex ; align - items : center ; gap : 12 px ; }
. toggle - label { font - family : var ( -- font - mono ) ; font - size : 10 px ; letter - spacing : 0.1 em ; color : var ( -- accent - cyan ) ; transition : color .2 s ; }
. toggle - label . dim { color : var ( -- text - dim ) ; }
. toggle - btn { width : 44 px ; height : 22 px ; background : var ( -- bg - card ) ; border : 1 px solid var ( -- border - mid ) ; border - radius : 11 px ; padding : 2 px ; cursor : pointer ; transition : all .25 s ; }
. toggle - btn . active { background : rgba ( 0 , 245 , 212 , .15 ) ; border - color : var ( -- accent - cyan ) ; }
. toggle - knob { display : block ; width : 16 px ; height : 16 px ; border - radius : 50 % ; background : var ( -- text - dim ) ; transition : all .25 s ; }
. toggle - btn . active . toggle - knob { background : var ( -- accent - cyan ) ; box - shadow : 0 0 8 px var ( -- accent - cyan ) ; transform : translateX ( 22 px ) ; }
. drawer - actions { display : flex ; gap : 12 px ; padding - top : 8 px ; }
. btn - secondary , . btn - primary {
flex : 1 ; padding : 10 px 16 px ; border - radius : var ( -- radius - sm ) ; font - family : var ( -- font - mono ) ;
font - size : 11 px ; letter - spacing : 0.1 em ; cursor : pointer ; transition : all var ( -- transition - fast ) ;
display : flex ; align - items : center ; justify - content : center ; gap : 6 px ;
}
. btn - secondary { background : transparent ; border : 1 px solid var ( -- border - mid ) ; color : var ( -- text - secondary ) ; }
. btn - secondary : hover { border - color : var ( -- accent - cyan ) ; color : var ( -- accent - cyan ) ; }
. btn - primary { background : rgba ( 0 , 245 , 212 , .1 ) ; border : 1 px solid var ( -- accent - cyan ) ; color : var ( -- accent - cyan ) ; }
. btn - primary : hover { background : rgba ( 0 , 245 , 212 , .2 ) ; box - shadow : var ( -- glow - cyan ) ; }
. btn - primary : disabled { opacity : 0.4 ; cursor : not - allowed ; }
. btn - loader { width : 12 px ; height : 12 px ; border : 1.5 px solid transparent ; border - top - color : var ( -- accent - cyan ) ; border - radius : 50 % ; animation : spin .6 s linear infinite ; }
/* Modal */
. modal - overlay {
position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .7 ) ; backdrop - filter : blur ( 4 px ) ;
z - index : 200 ; display : flex ; align - items : center ; justify - content : center ;
}
. modal - card {
width : 480 px ; max - height : 80 vh ; background : rgba ( 10 , 15 , 26 , .98 ) ; border : 1 px solid var ( -- border - mid ) ;
border - radius : var ( -- radius - lg ) ; display : flex ; flex - direction : column ;
box - shadow : 0 20 px 60 px rgba ( 0 , 0 , 0 , .6 ) , 0 0 0 1 px rgba ( 0 , 245 , 212 , .05 ) ;
}
. modal - header { display : flex ; align - items : center ; justify - content : space - between ; padding : 16 px 20 px ; border - bottom : 1 px solid var ( -- border - dim ) ; }
. modal - title { font - family : var ( -- font - display ) ; font - size : 11 px ; letter - spacing : 0.15 em ; color : var ( -- accent - cyan ) ; }
. modal - body { flex : 1 ; overflow - y : auto ; padding : 20 px ; display : flex ; flex - direction : column ; gap : 14 px ; }
. modal - body : : - webkit - scrollbar { width : 4 px ; }
. modal - body : : - webkit - scrollbar - thumb { background : var ( -- border - mid ) ; border - radius : 2 px ; }
. modal - footer { display : flex ; gap : 12 px ; padding : 16 px 20 px ; border - top : 1 px solid var ( -- border - dim ) ; }
2026-03-21 10:13:35 +08:00
< / style >