Files
JARVIS/frontend/src/pages/agents/index.vue

1858 lines
50 KiB
Vue
Raw Normal View History

2026-03-21 10:13:35 +08:00
<template>
<div class="agent-view scanlines">
<div class="bg-grid"></div>
<div class="bg-glow"></div>
<div class="bg-circuit"></div>
2026-03-21 10:13:35 +08:00
<div class="bg-particles">
<span v-for="p in bgParticles" :key="p.id" class="bg-particle" :style="p.style"></span>
</div>
<div class="view-header">
<div class="header-title">
<span class="title-bracket">[</span>
<span class="title-text">ULTRON 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>
<div class="status-bar">
<span class="status-dot" :class="connectionStatus"></span>
<span class="status-label">{{ connectionLabel }}</span>
</div>
</div>
</div>
<div class="command-stage">
<div class="board-shell holo-card">
<div class="board-trace trace-1"></div>
<div class="board-trace trace-2"></div>
<div class="board-trace trace-3"></div>
<section class="topology-stage">
<div class="topology-core-lane">
<div class="stage-caption">
<span class="stage-caption-kicker">COMMAND CORE</span>
<span class="stage-caption-title">战术蓝图主控矩阵</span>
</div>
<div
class="master-chip chip-card topology-master"
data-testid="agent-chip-master"
:class="[
getStatusClass('master'),
{ selected: selectedAgentId === 'master', energized: isAgentEnergized('master') },
]"
@click="selectAgent('master')"
>
<div class="chip-shell master-chip-shell">
<span class="chip-pin pin-top"></span>
<span class="chip-pin pin-right"></span>
<span class="chip-pin pin-bottom"></span>
<span class="chip-pin pin-left"></span>
<div class="chip-overlay"></div>
<div class="chip-status-strip">
<span class="chip-state-dot" :class="getStatusClass('master')"></span>
<span class="chip-state-label">MASTER CORE</span>
</div>
<div class="chip-title-row">
<div>
<div class="chip-code">JRV-PRIME</div>
<div class="chip-name">{{ getAgentName('master') }}</div>
</div>
<div class="chip-runtime">
<span class="runtime-tag">{{ activeCommanderSkillId === 'skill_orchestration' ? 'ROUTING' : 'STANDBY' }}</span>
</div>
</div>
<div class="chip-role">{{ getAgentRole('master') }}</div>
<div class="chip-desc">{{ getAgentDesc('master') }}</div>
<div class="chip-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>
<span v-else class="node-idle">总控待命</span>
</div>
<div class="route-telemetry" data-testid="route-telemetry">
<div class="route-telemetry-header">
<span class="route-telemetry-label">ACTIVE ROUTE</span>
<span class="route-telemetry-state">{{ activeRouteTelemetry.state }}</span>
</div>
<div class="route-telemetry-path">
<span
v-for="segment in activeRouteTelemetry.segments"
:key="segment"
class="route-segment"
>
{{ segment }}
</span>
</div>
</div>
<div class="commander-skills embedded-commander-skills" data-testid="commander-skills" @click.stop>
<div class="skills-header embedded-skills-header">
<div>
<div class="skills-eyebrow">COMMANDER MATRIX</div>
<div class="skills-title">指挥官技能</div>
</div>
<div class="skills-badge">{{ activeCommanderSkillId ? 'ENGAGED' : 'STANDBY' }}</div>
</div>
<div class="skills-grid embedded-skills-grid">
<article
v-for="skill in commanderSkillsForDisplay"
:key="skill.id"
:data-testid="`commander-skill-${skill.id}`"
class="skill-chip embedded-skill-chip"
:class="{ active: skill.active, standby: !skill.active }"
>
<div class="skill-chip-top">
<span class="skill-label">{{ skill.label }}</span>
<span class="skill-state">{{ skill.active ? skill.stateLabel : 'STANDBY' }}</span>
</div>
<div class="skill-title">{{ skill.title }}</div>
<div class="skill-description">{{ skill.description }}</div>
</article>
</div>
</div>
</div>
</div>
2026-03-21 10:13:35 +08:00
</div>
<div class="topology-trunk" :class="{ energized: !!activeMainAgentId || activeCommanderSkillId === 'skill_orchestration' }">
<span class="topology-trunk-joint"></span>
<span class="bus-spine-label">PRIMARY TRUNK</span>
2026-03-21 10:13:35 +08:00
</div>
<div class="main-grid-shell">
<div class="stage-caption bus-caption">
<span class="stage-caption-kicker">BUS ARRAY</span>
<span class="stage-caption-title"> Agent 分层总线</span>
</div>
<div class="topology-main-grid">
<article
v-for="agentId in MAIN_AGENT_IDS"
:key="agentId"
class="topology-column"
:class="{ expanded: expandedAgentId === agentId }"
>
<div class="topology-drop" :class="{ energized: isBranchEnergized(agentId) }">
<span class="link-joint"></span>
</div>
<div
class="chip-card agent-chip topology-main-chip"
:data-testid="`agent-chip-${agentId}`"
:class="[
getStatusClass(agentId),
{ selected: selectedAgentId === agentId, disabled: !localAgents[agentId]?.enabled, energized: isAgentEnergized(agentId) },
]"
@click="selectAgent(agentId)"
>
<div class="chip-shell compact">
<span class="chip-pin pin-top"></span>
<span class="chip-pin pin-right"></span>
<span class="chip-pin pin-bottom"></span>
<span class="chip-pin pin-left"></span>
<div class="chip-overlay"></div>
<div class="chip-status-strip">
<span class="chip-state-dot" :class="getStatusClass(agentId)"></span>
<span class="chip-state-label">{{ getAgentName(agentId) }}</span>
</div>
<div class="chip-code">{{ getAgentRole(agentId) }}</div>
<div class="chip-desc">{{ getAgentDesc(agentId) }}</div>
<div class="chip-footer compact-footer">
<span class="node-stat">
<span class="stat-label">调用</span>
<span class="stat-val">{{ agentData[agentId]?.callCount || 0 }}</span>
</span>
<span v-if="agentData[agentId]?.currentTask" class="node-task-tag">{{ agentData[agentId]?.currentTask }}</span>
<span v-else class="node-idle">待机中</span>
</div>
</div>
</div>
<div
class="main-link topology-link-label"
:data-testid="`bus-link-${agentId}`"
:class="{ energized: isBranchEnergized(agentId) }"
>
<span class="main-link-label">{{ RELATION_LABELS[`master-${agentId}`] }}</span>
</div>
<button class="cluster-toggle" @click="toggleCluster(agentId)">
<span>{{ expandedAgentId === agentId ? '收起子指挥官' : '展开子指挥官' }}</span>
<span class="toggle-count">{{ getSubCommanders(agentId).length }}</span>
</button>
<Transition name="cluster-fade">
<div v-if="expandedAgentId === agentId" class="sub-cluster topology-sub-cluster">
<div
v-for="sub in getSubCommanders(agentId)"
:key="sub.id"
class="sub-node topology-sub-node"
:class="getSubCommanderStatus(sub.id)"
>
<div
class="sub-link topology-sub-link"
:data-testid="`sub-link-${sub.id}`"
:class="{ energized: isBranchEnergized(agentId, sub.id) }"
>
<span class="link-joint"></span>
<span class="sub-link-line"></span>
<span class="sub-link-label">{{ sub.relationLabel }}</span>
</div>
<div class="sub-node-card holo-card" :class="{ energized: isBranchEnergized(agentId, sub.id) }">
<div class="sub-node-topline">
<span class="sub-name">{{ sub.name }}</span>
<span class="sub-badge">{{ sub.toolScopeLabel }}</span>
</div>
<div class="sub-role">{{ sub.role }}</div>
<div class="sub-desc">{{ sub.description }}</div>
<div class="sub-footer">
<span class="sub-status-dot"></span>
<span class="sub-status-text">{{ getSubCommanderTask(sub.id) || getSubCommanderStatusText(sub.id) }}</span>
</div>
</div>
</div>
</div>
</Transition>
</article>
2026-03-21 10:13:35 +08:00
</div>
</div>
</section>
2026-03-21 10:13:35 +08:00
</div>
</div>
<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>
<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">
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { RefreshCw, X } from 'lucide-vue-next'
import {
COMMANDER_SKILLS,
DEFAULT_AGENTS,
MAIN_AGENT_ORDER,
RELATION_LABELS,
SUB_COMMANDERS_BY_PARENT,
type Agent,
type MainAgentId,
type SubCommander,
} from '@/data/agents'
import { agentApi, type AgentHierarchyStats } from '@/api/agent'
const MAIN_AGENT_IDS = MAIN_AGENT_ORDER as Exclude<MainAgentId, 'master'>[]
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,
},
}
})
type AgentRuntimeState = { callCount: number; currentTask: string | null; status: string }
type SubCommanderRuntimeState = AgentRuntimeState & { parentId: string }
const cleanupFns: Array<() => void> = []
2026-03-21 10:13:35 +08:00
const selectedAgentId = ref<string | null>(null)
const expandedAgentId = ref<Exclude<MainAgentId, 'master'> | null>('planner')
2026-03-21 10:13:35 +08:00
const drawerOpen = ref(false)
const editAgent = ref<{ name: string; role: string; description: string; systemPrompt: string; enabled: boolean } | null>(null)
const saving = ref(false)
const loading = ref(false)
const connectionStatus = ref<'connected' | 'disconnected'>('disconnected')
const connectionLabel = computed(() => connectionStatus.value === 'connected' ? '实时同步' : '离线模式')
const agentData = reactive<Record<string, AgentRuntimeState>>({})
const subCommanderData = reactive<Record<string, SubCommanderRuntimeState>>({})
const localAgents = reactive<Record<string, Agent>>(Object.fromEntries(DEFAULT_AGENTS.map((a) => [a.id, { ...a }])))
const demoMainAgentId = ref<Exclude<MainAgentId, 'master'> | null>(null)
const demoSubCommanderId = ref<string | null>(null)
2026-03-21 10:13:35 +08:00
let pollInterval: ReturnType<typeof setInterval> | null = null
let demoInterval: ReturnType<typeof setInterval> | null = null
2026-03-21 10:13:35 +08:00
function getSubCommanders(agentId: Exclude<MainAgentId, 'master'>): SubCommander[] {
return SUB_COMMANDERS_BY_PARENT[agentId]
2026-03-21 10:13:35 +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
}
function getAgentName(id: string) {
return localAgents[id]?.name || DEFAULT_AGENTS.find((a) => a.id === id)?.name || id.toUpperCase()
2026-03-21 10:13:35 +08:00
}
function getAgentRole(id: string) {
return localAgents[id]?.role || DEFAULT_AGENTS.find((a) => a.id === id)?.role || ''
2026-03-21 10:13:35 +08:00
}
function getAgentDesc(id: string) {
return localAgents[id]?.description || DEFAULT_AGENTS.find((a) => a.id === id)?.description || ''
2026-03-21 10:13:35 +08:00
}
function getSubCommanderStatus(subId: string) {
const data = subCommanderData[subId]
2026-03-21 10:13:35 +08:00
if (!data) return 'idle'
return data.status === 'active' ? 'active' : data.status === 'disabled' ? 'disabled' : 'idle'
}
function getSubCommanderTask(subId: string) {
return subCommanderData[subId]?.currentTask || null
}
function getSubCommanderStatusText(subId: string) {
const status = getSubCommanderStatus(subId)
if (status === 'active') return '子指挥官处理中'
if (status === 'disabled') return '当前停用'
return '待命中'
}
function toggleCluster(agentId: Exclude<MainAgentId, 'master'>) {
expandedAgentId.value = expandedAgentId.value === agentId ? null : agentId
2026-03-21 10:13:35 +08:00
}
function selectAgent(id: string) {
const agent = localAgents[id]
if (!agent) return
selectedAgentId.value = id
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() {
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 {
await agentApi.updateConfig(selectedAgentId.value, {
name: editAgent.value.name,
description: editAgent.value.description,
system_prompt: editAgent.value.systemPrompt,
enabled: editAgent.value.enabled,
})
} catch {
// fallback to local only
}
2026-03-21 10:13:35 +08:00
drawerOpen.value = false
} finally {
saving.value = false
}
}
function applyFlatStats() {
agentData.master = { callCount: 47, currentTask: '协调主角色与子指挥官', 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' }
subCommanderData.planner_scope = { parentId: 'planner', callCount: 6, currentTask: null, status: 'idle' }
subCommanderData.planner_steps = { parentId: 'planner', callCount: 9, currentTask: '拆解执行步骤', status: 'active' }
subCommanderData.executor_tasks = { parentId: 'executor', callCount: 8, currentTask: '处理任务动作', status: 'active' }
subCommanderData.executor_forum = { parentId: 'executor', callCount: 4, currentTask: null, status: 'idle' }
subCommanderData.librarian_retrieval = { parentId: 'librarian', callCount: 5, currentTask: null, status: 'idle' }
subCommanderData.librarian_graph = { parentId: 'librarian', callCount: 2, currentTask: null, status: 'idle' }
subCommanderData.analyst_progress = { parentId: 'analyst', callCount: 2, currentTask: null, status: 'idle' }
subCommanderData.analyst_insights = { parentId: 'analyst', callCount: 3, currentTask: '生成趋势判断', status: 'active' }
}
function applyHierarchyStats(stats: AgentHierarchyStats) {
agentData.master = { callCount: 47, currentTask: '协调主角色与子指挥官', status: 'active' }
for (const main of stats.main_agents) {
agentData[main.agent_id] = {
callCount: main.call_count,
currentTask: main.current_task,
status: main.status,
}
for (const child of main.sub_commanders) {
subCommanderData[child.agent_id] = {
parentId: main.agent_id,
callCount: child.call_count,
currentTask: child.current_task,
status: child.status,
}
}
}
}
function getDemoSequence() {
return MAIN_AGENT_IDS.flatMap((agentId) => {
const subs = getSubCommanders(agentId)
return subs.length > 0
? subs.map((sub) => ({ mainId: agentId, subId: sub.id }))
: [{ mainId: agentId, subId: null }]
})
}
function setDemoPath(mainId: Exclude<MainAgentId, 'master'> | null, subId: string | null) {
demoMainAgentId.value = mainId
demoSubCommanderId.value = subId
if (mainId) {
expandedAgentId.value = mainId
}
}
function startDemoCircuit() {
if (demoInterval) return
const sequence = getDemoSequence()
if (sequence.length === 0) return
let index = 0
const applyStep = () => {
const current = sequence[index % sequence.length]
setDemoPath(current.mainId, current.subId)
index += 1
}
applyStep()
demoInterval = setInterval(applyStep, 1600)
}
function stopDemoCircuit() {
if (demoInterval) {
clearInterval(demoInterval)
demoInterval = null
}
setDemoPath(null, null)
}
const activeSubCommanderId = computed(() => {
const realActive = Object.entries(subCommanderData).find(([, data]) => data.status === 'active')?.[0] ?? null
return connectionStatus.value === 'connected' ? realActive : (demoSubCommanderId.value ?? realActive)
})
const activeMainAgentId = computed<Exclude<MainAgentId, 'master'> | null>(() => {
if (connectionStatus.value !== 'connected' && demoMainAgentId.value) {
return demoMainAgentId.value
}
const activeMain = MAIN_AGENT_IDS.find((agentId) => agentData[agentId]?.status === 'active')
if (activeMain) return activeMain
if (activeSubCommanderId.value) {
const parentId = subCommanderData[activeSubCommanderId.value]?.parentId
if (parentId && MAIN_AGENT_IDS.includes(parentId as Exclude<MainAgentId, 'master'>)) {
return parentId as Exclude<MainAgentId, 'master'>
}
}
return agentData.master?.status === 'active' ? 'planner' : null
})
const activeCommanderSkillId = computed(() => {
if (activeSubCommanderId.value) {
return COMMANDER_SKILLS.find((skill) => skill.relatedAgentIds.includes(activeSubCommanderId.value as string))?.id ?? null
}
if (activeMainAgentId.value) {
return COMMANDER_SKILLS.find((skill) => skill.relatedAgentIds.includes(activeMainAgentId.value as string))?.id ?? null
}
return agentData.master?.status === 'active' ? 'skill_orchestration' : null
})
const commanderSkillsForDisplay = computed(() => COMMANDER_SKILLS.map((skill) => ({
...skill,
active: skill.id === activeCommanderSkillId.value,
})))
const activeRouteTelemetry = computed(() => {
const segments = ['JARVIS']
if (activeMainAgentId.value) {
segments.push(getAgentName(activeMainAgentId.value))
}
if (activeSubCommanderId.value) {
const activeSub = Object.values(SUB_COMMANDERS_BY_PARENT)
.flat()
.find((item) => item.id === activeSubCommanderId.value)
if (activeSub) {
segments.push(activeSub.name)
}
}
return {
segments,
state: activeCommanderSkillId.value ? 'ENGAGED' : 'STANDBY',
}
})
function isAgentEnergized(agentId: string) {
if (agentId === 'master') return agentData.master?.status === 'active' || !!activeMainAgentId.value || !!activeSubCommanderId.value
if (activeSubCommanderId.value) {
return agentId === activeMainAgentId.value || agentId === activeSubCommanderId.value
}
return agentId === activeMainAgentId.value
2026-03-21 10:13:35 +08:00
}
function isBranchEnergized(parentId: string, subId?: string) {
if (subId) return activeSubCommanderId.value === subId
return activeMainAgentId.value === parentId || (agentData.master?.status === 'active' && activeCommanderSkillId.value === 'skill_orchestration' && parentId === 'planner')
2026-03-21 10:13:35 +08:00
}
async function refreshStats() {
loading.value = true
try {
const stats = await agentApi.getHierarchyStats()
applyHierarchyStats(stats)
2026-03-21 10:13:35 +08:00
connectionStatus.value = 'connected'
stopDemoCircuit()
2026-03-21 10:13:35 +08:00
} catch {
applyFlatStats()
2026-03-21 10:13:35 +08:00
connectionStatus.value = 'disconnected'
startDemoCircuit()
} finally {
loading.value = false
2026-03-21 10:13:35 +08:00
}
}
function runTransition(
el: Element,
keyframes: Keyframe[],
options: KeyframeAnimationOptions,
done?: () => void,
) {
const animation = (el as HTMLElement).animate(keyframes, { fill: 'forwards', ...options })
const finish = () => done?.()
animation.addEventListener('finish', finish, { once: true })
cleanupFns.push(() => animation.cancel())
return animation
}
function animateIn(el: Element, done: () => void) {
runTransition(
el,
[{ opacity: 0, transform: 'translateX(80px)' }, { opacity: 1, transform: 'translateX(0)' }],
{ duration: 350, easing: 'cubic-bezier(0.4, 0, 0.2, 1)' },
done,
)
}
function animateOut(el: Element, done: () => void) {
runTransition(
el,
[{ opacity: 1, transform: 'translateX(0)' }, { opacity: 0, transform: 'translateX(80px)' }],
{ 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
onMounted(async () => {
await refreshStats()
pollInterval = setInterval(refreshStats, 5000)
})
onUnmounted(() => {
if (pollInterval) clearInterval(pollInterval)
stopDemoCircuit()
cleanupFns.forEach((cleanup) => cleanup())
2026-03-21 10:13:35 +08:00
})
</script>
<style scoped>
.agent-view {
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
2026-03-21 10:13:35 +08:00
position: relative;
background:
radial-gradient(circle at top, rgba(0, 245, 212, 0.07), transparent 34%),
linear-gradient(180deg, #03050a 0%, #07101a 35%, #03050a 100%);
2026-03-21 10:13:35 +08:00
}
.bg-grid,
.bg-glow,
.bg-circuit,
.bg-particles {
2026-03-21 10:13:35 +08:00
position: absolute;
inset: 0;
pointer-events: none;
}
.bg-grid {
2026-03-21 10:13:35 +08:00
background-image:
linear-gradient(rgba(0, 245, 212, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 245, 212, 0.04) 1px, transparent 1px);
2026-03-21 10:13:35 +08:00
background-size: 40px 40px;
}
2026-03-21 10:13:35 +08:00
.bg-glow {
background:
radial-gradient(circle at 20% 20%, rgba(0, 245, 212, 0.08), transparent 26%),
radial-gradient(circle at 80% 14%, rgba(123, 44, 191, 0.08), transparent 22%),
radial-gradient(circle at 50% 70%, rgba(0, 245, 212, 0.05), transparent 30%);
2026-03-21 10:13:35 +08:00
}
.bg-circuit {
opacity: 0.5;
background-image:
linear-gradient(90deg, transparent 12%, rgba(0, 245, 212, 0.08) 12%, rgba(0, 245, 212, 0.08) 12.4%, transparent 12.4%),
linear-gradient(transparent 20%, rgba(0, 245, 212, 0.06) 20%, rgba(0, 245, 212, 0.06) 20.5%, transparent 20.5%),
radial-gradient(circle at 12% 20%, rgba(0, 245, 212, 0.25) 0 2px, transparent 3px),
radial-gradient(circle at 88% 36%, rgba(0, 245, 212, 0.22) 0 2px, transparent 3px),
radial-gradient(circle at 58% 68%, rgba(0, 245, 212, 0.18) 0 2px, transparent 3px);
2026-03-21 10:13:35 +08:00
}
2026-03-21 10:13:35 +08:00
.bg-particle {
position: absolute;
border-radius: 50%;
background: var(--accent-cyan);
box-shadow: 0 0 4px rgba(0,245,212,0.6), 0 0 8px rgba(0,245,212,0.2);
animation: star-twinkle var(--d, 4s) ease-in-out infinite var(--delay, 0s);
}
2026-03-21 10:13:35 +08:00
@keyframes star-twinkle {
0%, 100% { opacity: var(--o, 0.4); transform: scale(1); }
50% { opacity: calc(var(--o, 0.4) * 0.3); transform: scale(0.45); }
2026-03-21 10:13:35 +08:00
}
.view-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
border-bottom: 1px solid var(--border-dim);
position: sticky;
top: 0;
2026-03-21 10:13:35 +08:00
z-index: 10;
background: rgba(5,8,16,0.72);
2026-03-21 10:13:35 +08:00
backdrop-filter: blur(8px);
}
.header-title {
font-family: var(--font-display);
font-size: 13px;
letter-spacing: 0.2em;
color: var(--text-primary);
}
.title-bracket {
color: var(--accent-cyan);
opacity: 0.6;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
2026-03-21 10:13:35 +08:00
.btn-icon {
width: 32px;
height: 32px;
border-radius: 10px;
border: 1px solid var(--border-dim);
background: rgba(255,255,255,0.02);
color: var(--accent-cyan);
display: inline-flex;
align-items: center;
justify-content: center;
2026-03-21 10:13:35 +08:00
}
.btn-icon.spinning svg {
animation: spin 1s linear infinite;
2026-03-21 10:13:35 +08:00
}
@keyframes spin {
to { transform: rotate(360deg); }
}
2026-03-21 10:13:35 +08:00
.status-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--border-dim);
border-radius: 999px;
background: rgba(255,255,255,0.02);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 999px;
box-shadow: 0 0 8px currentColor;
}
.status-dot.connected {
background: var(--accent-green);
color: var(--accent-green);
}
.status-dot.disconnected {
background: var(--accent-amber);
color: var(--accent-amber);
}
.status-label {
font-size: 11px;
color: var(--text-secondary);
letter-spacing: 0.08em;
}
.command-stage {
position: relative;
z-index: 1;
padding: 18px 20px 22px;
}
.board-shell {
2026-03-21 10:13:35 +08:00
position: relative;
overflow: hidden;
padding: 18px 18px 20px;
border-radius: 24px;
border: 1px solid rgba(0, 245, 212, 0.12);
background:
linear-gradient(180deg, rgba(8, 14, 24, 0.95), rgba(3, 8, 16, 0.96)),
linear-gradient(135deg, rgba(0, 245, 212, 0.03), transparent 35%);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 24px 60px rgba(0,0,0,0.32), 0 0 24px rgba(0,245,212,0.05);
2026-03-21 10:13:35 +08:00
}
.board-shell::before,
.board-shell::after {
content: '';
2026-03-21 10:13:35 +08:00
position: absolute;
inset: 14px;
border: 1px solid rgba(0, 245, 212, 0.07);
border-radius: 22px;
2026-03-21 10:13:35 +08:00
pointer-events: none;
}
.board-shell::after {
inset: auto 28px 28px auto;
width: 88px;
height: 88px;
border-radius: 50%;
filter: blur(12px);
background: radial-gradient(circle, rgba(0, 245, 212, 0.16), transparent 70%);
border: none;
}
.board-trace {
2026-03-21 10:13:35 +08:00
position: absolute;
background: linear-gradient(90deg, transparent, rgba(0,245,212,0.12), transparent);
opacity: 0.6;
2026-03-21 10:13:35 +08:00
}
.trace-1 { top: 96px; left: 10%; width: 22%; height: 1px; }
.trace-2 { top: 240px; right: 8%; width: 18%; height: 1px; }
.trace-3 { bottom: 120px; left: 6%; width: 16%; height: 1px; }
.topology-stage {
2026-03-21 10:13:35 +08:00
display: flex;
flex-direction: column;
gap: 18px;
}
.stage-caption {
display: inline-flex;
align-items: baseline;
gap: 10px;
margin: 0 auto 12px;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(0,245,212,0.12);
background: linear-gradient(90deg, rgba(3,8,16,0.78), rgba(7,14,24,0.9));
box-shadow: 0 0 18px rgba(0,245,212,0.06);
}
.stage-caption-kicker {
font-size: 10px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent-cyan);
}
.stage-caption-title {
font-size: 12px;
letter-spacing: 0.12em;
color: var(--text-secondary);
}
.bus-caption {
margin-bottom: 18px;
}
.topology-core-lane {
display: block;
}
.master-chip-shell {
min-height: 0;
background:
linear-gradient(180deg, rgba(9,15,28,0.2), transparent 28%),
radial-gradient(circle at top center, rgba(0,245,212,0.08), transparent 40%);
}
.topology-master {
justify-self: center;
width: min(720px, 100%);
margin: 0 auto;
}
.topology-master::before,
.topology-master::after {
content: '';
position: absolute;
pointer-events: none;
}
.topology-master::before {
inset: -10px 6%;
border-radius: 32px;
border: 1px solid rgba(0,245,212,0.08);
opacity: 0.8;
}
.topology-master::after {
inset: auto 12% -14px 12%;
height: 24px;
border-radius: 999px;
background: radial-gradient(circle, rgba(0,245,212,0.24), transparent 72%);
filter: blur(10px);
}
.embedded-commander-skills {
2026-03-21 10:13:35 +08:00
position: relative;
margin-top: 8px;
padding: 12px;
border-radius: 16px;
border: 1px solid rgba(0,245,212,0.12);
background:
linear-gradient(180deg, rgba(8,14,24,0.68), rgba(4,8,16,0.88)),
repeating-linear-gradient(90deg, rgba(0,245,212,0.03) 0 1px, transparent 1px 18px);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03), 0 0 18px rgba(0,245,212,0.05);
2026-03-21 10:13:35 +08:00
}
.embedded-commander-skills::before {
content: '';
position: absolute;
left: 16px;
right: 16px;
top: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(0,245,212,0.55), transparent);
2026-03-21 10:13:35 +08:00
}
.embedded-skills-header {
margin-bottom: 10px;
}
.embedded-skills-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-top: 0;
}
.embedded-skill-chip {
min-height: 84px;
padding: 10px;
background: linear-gradient(180deg, rgba(10,16,28,0.92), rgba(6,10,18,0.98));
}
.embedded-skill-chip .skill-title {
font-size: 12px;
}
.embedded-skill-chip .skill-description {
font-size: 10px;
line-height: 1.45;
}
.topology-trunk {
position: relative;
width: 4px;
height: 70px;
margin: -4px auto 0;
border-radius: 999px;
background: linear-gradient(180deg, rgba(0,245,212,0.15), rgba(0,245,212,0.7), rgba(0,245,212,0.12));
box-shadow: 0 0 16px rgba(0,245,212,0.12);
}
.main-grid-shell {
position: relative;
padding: 20px 18px 10px;
border-radius: 24px;
border: 1px solid rgba(0,245,212,0.08);
background:
linear-gradient(180deg, rgba(4,9,18,0.84), rgba(3,7,14,0.95)),
radial-gradient(circle at top center, rgba(0,245,212,0.08), transparent 42%);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03), 0 20px 44px rgba(0,0,0,0.22);
}
.main-grid-shell::before,
.main-grid-shell::after {
content: '';
position: absolute;
pointer-events: none;
}
.main-grid-shell::before {
inset: 12px;
border-radius: 20px;
border: 1px solid rgba(0,245,212,0.06);
}
.main-grid-shell::after {
left: 24px;
right: 24px;
top: 62px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(0,245,212,0.3), transparent);
}
.topology-trunk::before,
.topology-trunk::after,
.topology-drop::before,
.topology-drop::after,
.topology-sub-link .sub-link-line::before,
.topology-sub-link .sub-link-line::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
}
.topology-trunk-joint {
position: absolute;
left: 50%;
bottom: -8px;
width: 14px;
height: 14px;
transform: translateX(-50%);
border-radius: 50%;
background: rgba(0,245,212,0.78);
box-shadow: 0 0 18px rgba(0,245,212,0.55), 0 0 34px rgba(0,245,212,0.22);
}
.topology-trunk.energized::before,
.topology-drop.energized::before,
.topology-sub-link.energized .sub-link-line::before {
border-radius: inherit;
background: linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.2) 20%, rgba(130,255,245,0.98) 42%, rgba(0,245,212,1) 50%, rgba(130,255,245,0.98) 58%, rgba(255,255,255,0.14) 78%, transparent 100%);
animation: current-vertical 1.15s linear infinite;
}
.topology-trunk.energized::after,
.topology-drop.energized::after,
.topology-sub-link.energized .sub-link-line::after {
border-radius: inherit;
background: linear-gradient(180deg, transparent 0%, rgba(0,245,212,0.08) 20%, rgba(0,245,212,0.5) 50%, rgba(0,245,212,0.08) 80%, transparent 100%);
filter: blur(6px);
animation: current-trail-vertical 1.15s linear infinite;
}
.topology-main-grid {
position: relative;
display: grid;
grid-template-columns: repeat(4, minmax(190px, 1fr));
gap: 14px;
align-items: start;
}
.topology-main-grid::after {
content: '';
position: absolute;
left: 8%;
right: 8%;
top: 10px;
height: 44px;
border: 1px solid rgba(0,245,212,0.06);
border-bottom: none;
border-radius: 20px 20px 0 0;
pointer-events: none;
}
.topology-main-grid::before {
content: '';
position: absolute;
left: 12.5%;
right: 12.5%;
top: 10px;
height: 3px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(0,245,212,0.18), rgba(0,245,212,0.68), rgba(0,245,212,0.18));
box-shadow: 0 0 18px rgba(0,245,212,0.12);
}
.topology-column {
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.topology-drop {
position: relative;
width: 3px;
height: 46px;
margin: 0 auto;
border-radius: 999px;
background: linear-gradient(180deg, rgba(0,245,212,0.6), rgba(0,245,212,0.08));
}
.topology-drop .link-joint {
position: absolute;
left: 50%;
top: -6px;
transform: translateX(-50%);
}
.topology-main-chip {
width: 100%;
}
.topology-link-label {
justify-content: center;
min-height: 18px;
}
.topology-link-label.energized .main-link-label {
color: var(--accent-cyan);
text-shadow: 0 0 10px rgba(0,245,212,0.35);
}
.topology-sub-cluster {
display: grid;
gap: 12px;
}
.topology-sub-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.topology-sub-link {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.topology-sub-link .link-joint {
width: 9px;
height: 9px;
}
.topology-sub-link .sub-link-line {
width: 3px;
min-width: 3px;
min-height: 42px;
height: 42px;
border-radius: 999px;
background: linear-gradient(180deg, rgba(0,245,212,0.6), rgba(0,245,212,0.08));
}
.topology-sub-link .sub-link-label {
text-align: center;
white-space: normal;
}
.topology-skills-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.chip-card {
position: relative;
border-radius: 20px;
border: 1px solid rgba(0, 245, 212, 0.16);
background:
linear-gradient(180deg, rgba(9, 14, 26, 0.95), rgba(5, 9, 16, 0.98)),
linear-gradient(135deg, rgba(255,255,255,0.02), transparent 55%);
overflow: hidden;
transition: transform var(--transition-mid), border-color var(--transition-mid), box-shadow var(--transition-mid);
}
.chip-card:hover {
transform: translateY(-2px);
border-color: rgba(0, 245, 212, 0.28);
box-shadow: 0 14px 34px rgba(0,0,0,0.24), 0 0 18px rgba(0,245,212,0.08);
}
.chip-card.selected,
.chip-card.energized {
border-color: rgba(0, 245, 212, 0.46);
box-shadow: 0 0 0 1px rgba(0,245,212,0.18) inset, 0 0 24px rgba(0,245,212,0.15);
}
.chip-card.energized {
box-shadow:
0 0 0 1px rgba(0,245,212,0.22) inset,
0 0 18px rgba(0,245,212,0.18),
0 0 42px rgba(0,245,212,0.16),
0 0 70px rgba(0,245,212,0.07);
}
.chip-card.disabled {
opacity: 0.48;
filter: saturate(0.5);
}
.chip-shell {
position: relative;
min-height: 212px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.chip-shell.compact {
min-height: 168px;
}
.topology-main-chip .chip-shell.compact {
min-height: 182px;
padding-top: 20px;
}
.chip-pin {
position: absolute;
opacity: 0.45;
}
.pin-top,
.pin-bottom {
left: 24px;
right: 24px;
height: 8px;
background: repeating-linear-gradient(90deg, rgba(0,245,212,0.2) 0 10px, transparent 10px 18px);
}
.pin-top { top: 0; }
.pin-bottom { bottom: 0; }
.pin-left,
.pin-right {
top: 24px;
bottom: 24px;
width: 8px;
background: repeating-linear-gradient(180deg, rgba(0,245,212,0.2) 0 10px, transparent 10px 18px);
}
.pin-left { left: 0; }
.pin-right { right: 0; }
.chip-overlay {
position: absolute;
inset: 12px;
border-radius: 18px;
border: 1px solid rgba(0, 245, 212, 0.08);
background:
linear-gradient(135deg, rgba(255,255,255,0.04), transparent 34%),
linear-gradient(180deg, transparent, rgba(0,245,212,0.04));
pointer-events: none;
}
.chip-overlay::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(120deg, transparent 10%, rgba(0,245,212,0.15) 45%, transparent 70%);
transform: translateX(-120%);
animation: chip-scan 6s linear infinite;
}
@keyframes chip-scan {
to { transform: translateX(120%); }
}
@keyframes node-flicker {
0%, 100% {
transform: scale(1);
opacity: 0.92;
}
50% {
transform: scale(1.22);
opacity: 1;
}
}
.chip-status-strip,
.chip-title-row,
.chip-footer,
.skills-header,
.skill-chip-top,
.sub-node-topline,
.sub-footer,
.drawer-header,
.toggle-row,
.drawer-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.chip-state-dot,
.sub-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
}
.chip-state-dot.active,
.sub-node.active .sub-status-dot { color: var(--accent-green); }
.chip-state-dot.idle,
.sub-node.idle .sub-status-dot { color: var(--accent-cyan); }
.chip-state-dot.disabled,
.sub-node.disabled .sub-status-dot { color: var(--text-dim); }
.chip-state-label,
.skills-eyebrow,
.skill-label,
.sub-name,
.drawer-title {
font-family: var(--font-display);
letter-spacing: 0.16em;
}
.chip-state-label,
.skills-eyebrow,
.skill-label {
font-size: 11px;
color: var(--accent-cyan);
}
.chip-code,
.runtime-tag,
.skills-badge,
.skill-state,
.main-link-label,
.sub-link-label,
.sub-badge,
.node-task-tag,
.node-idle,
.toggle-count {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.chip-code {
color: var(--text-dim);
}
.chip-name,
.skills-title,
.skill-title {
color: var(--text-primary);
font-weight: 700;
}
.chip-name {
font-size: 22px;
letter-spacing: 0.08em;
}
.skills-title {
font-size: 16px;
}
.chip-role,
.skill-state,
.sub-role {
color: var(--accent-purple);
}
.chip-desc,
.skill-description,
.sub-desc {
color: var(--text-secondary);
line-height: 1.65;
}
.runtime-tag,
.skills-badge,
.skill-state,
.sub-badge,
.node-task-tag,
.node-idle,
.toggle-count {
padding: 4px 8px;
border-radius: 999px;
}
.runtime-tag,
.skills-badge,
.node-task-tag {
background: rgba(0,245,212,0.1);
color: var(--accent-cyan);
}
.node-idle {
background: rgba(255,255,255,0.04);
color: var(--text-dim);
}
.node-stat {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(255,255,255,0.04);
color: var(--text-secondary);
font-size: 11px;
}
.stat-label { color: var(--text-dim); }
.stat-val { color: var(--accent-cyan); }
.route-telemetry {
position: relative;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid rgba(0,245,212,0.14);
background:
linear-gradient(180deg, rgba(7,12,22,0.92), rgba(4,8,16,0.98)),
radial-gradient(circle at left center, rgba(0,245,212,0.08), transparent 45%);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03), 0 0 18px rgba(0,245,212,0.06);
}
.route-telemetry::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(90deg, transparent 0%, rgba(0,245,212,0.08) 30%, transparent 70%);
transform: translateX(-100%);
animation: telemetry-scan 4s linear infinite;
pointer-events: none;
}
.route-telemetry-header,
.route-telemetry-path {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.route-telemetry-header {
justify-content: space-between;
margin-bottom: 8px;
}
.route-telemetry-label,
.route-telemetry-state {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.route-telemetry-label {
color: var(--text-dim);
}
.route-telemetry-state {
padding: 4px 8px;
border-radius: 999px;
color: var(--accent-cyan);
background: rgba(0,245,212,0.1);
}
.route-segment {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid rgba(0,245,212,0.12);
background: rgba(255,255,255,0.03);
color: var(--text-primary);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.route-segment:not(:last-child)::after {
content: '→';
position: relative;
right: -4px;
color: rgba(0,245,212,0.72);
}
@keyframes telemetry-scan {
to { transform: translateX(100%); }
}
.commander-skills {
padding: 18px;
border-radius: 24px;
border: 1px solid rgba(0, 245, 212, 0.12);
background: linear-gradient(180deg, rgba(8, 14, 24, 0.9), rgba(4, 8, 16, 0.96));
}
.skills-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 16px;
}
.skill-chip {
position: relative;
min-height: 118px;
border-radius: 18px;
border: 1px solid rgba(0,245,212,0.1);
background: linear-gradient(180deg, rgba(9,16,28,0.9), rgba(6,10,18,0.96));
padding: 14px;
overflow: hidden;
}
.skill-chip::before,
.main-link-line::before,
.sub-link-line::before,
.bus-spine::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
}
.skill-chip::before {
background: linear-gradient(120deg, transparent 0%, rgba(0,245,212,0.08) 45%, transparent 65%);
transform: translateX(-140%);
}
.skill-chip.active {
border-color: rgba(0,245,212,0.4);
box-shadow: 0 0 22px rgba(0,245,212,0.12), inset 0 0 0 1px rgba(0,245,212,0.12);
}
.skill-chip.active::before {
animation: skill-sweep 2.8s linear infinite;
}
@keyframes skill-sweep {
to { transform: translateX(140%); }
}
.link-joint {
width: 8px;
height: 8px;
min-width: 8px;
border-radius: 50%;
background: rgba(0,245,212,0.65);
box-shadow: 0 0 10px rgba(0,245,212,0.35);
}
.pcb-link.energized .link-joint {
background: #9ffef4;
box-shadow: 0 0 12px rgba(159,254,244,0.9), 0 0 24px rgba(0,245,212,0.55), 0 0 40px rgba(0,245,212,0.28);
animation: node-flicker 0.9s ease-in-out infinite;
}
.main-link-line,
.sub-link-line {
position: relative;
flex: 1;
overflow: hidden;
}
.main-link-line {
height: 3px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(0,245,212,0.6), rgba(0,245,212,0.08));
}
.sub-link-line {
width: 3px;
min-width: 3px;
min-height: 34px;
background: linear-gradient(180deg, rgba(0,245,212,0.6), rgba(0,245,212,0.08));
}
.main-link-label,
.sub-link-label {
color: var(--text-dim);
white-space: nowrap;
}
.agent-chip .chip-name {
font-size: 22px;
}
.compact-footer {
margin-top: auto;
align-items: flex-start;
flex-wrap: wrap;
}
.cluster-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
border: 1px solid rgba(0,245,212,0.12);
border-radius: 12px;
padding: 10px 12px;
background: rgba(5,10,18,0.72);
color: var(--text-secondary);
font-size: 11px;
letter-spacing: 0.08em;
}
.toggle-count {
background: rgba(0,245,212,0.08);
color: var(--accent-cyan);
}
.sub-cluster {
display: grid;
gap: 12px;
}
.sub-node {
display: flex;
gap: 10px;
align-items: stretch;
}
.sub-node-card {
flex: 1;
border-radius: 16px;
border: 1px solid rgba(0,245,212,0.1);
background: rgba(10,14,24,0.9);
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-node-card.energized,
.sub-node.active .sub-node-card {
border-color: rgba(0,245,212,0.3);
box-shadow: 0 0 18px rgba(0,245,212,0.1);
}
.sub-node.disabled .sub-node-card {
opacity: 0.5;
}
.sub-name { color: var(--accent-cyan); font-size: 11px; }
.sub-badge { background: rgba(123, 44, 191, 0.14); color: var(--accent-purple); }
.sub-role { font-size: 12px; font-weight: 600; }
.sub-footer { font-size: 11px; color: var(--text-dim); margin-top: auto; justify-content: flex-start; }
.cluster-fade-enter-active,
.cluster-fade-leave-active {
transition: opacity 0.22s ease, transform 0.22s ease;
}
.cluster-fade-enter-from,
.cluster-fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
.config-drawer {
position: fixed;
top: 0;
right: 0;
width: min(420px, 100%);
height: 100%;
background: linear-gradient(180deg, rgba(9, 13, 23, 0.98), rgba(5, 8, 16, 0.98));
border-left: 1px solid rgba(0,245,212,0.14);
z-index: 30;
display: flex;
flex-direction: column;
}
.drawer-header {
padding: 18px 18px 14px;
border-bottom: 1px solid var(--border-dim);
}
.drawer-title {
font-size: 12px;
color: var(--accent-cyan);
}
.btn-close {
width: 32px;
height: 32px;
border-radius: 10px;
border: 1px solid var(--border-dim);
background: transparent;
color: var(--text-secondary);
display: inline-flex;
align-items: center;
justify-content: center;
}
.drawer-body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
flex: 1;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.flex-1 {
flex: 1;
}
.form-label {
font-size: 11px;
letter-spacing: 0.12em;
color: var(--text-dim);
}
.form-input,
.form-textarea {
width: 100%;
border-radius: 12px;
border: 1px solid var(--border-dim);
background: rgba(255,255,255,0.03);
color: var(--text-primary);
padding: 10px 12px;
}
.code-textarea {
font-family: var(--font-mono);
}
.toggle-label {
font-size: 11px;
color: var(--text-secondary);
}
.toggle-label.dim {
opacity: 0.45;
}
.toggle-btn {
width: 50px;
height: 28px;
border-radius: 999px;
border: 1px solid var(--border-dim);
background: rgba(255,255,255,0.04);
position: relative;
}
.toggle-btn.active {
background: rgba(0,245,212,0.12);
border-color: rgba(0,245,212,0.2);
}
.toggle-knob {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
border-radius: 999px;
background: var(--text-primary);
transition: transform 0.2s ease;
}
.toggle-btn.active .toggle-knob {
transform: translateX(22px);
background: var(--accent-cyan);
}
.btn-secondary,
.btn-primary {
border-radius: 12px;
padding: 10px 14px;
border: 1px solid var(--border-dim);
}
.btn-secondary {
background: rgba(255,255,255,0.03);
color: var(--text-secondary);
}
.btn-primary {
background: rgba(0,245,212,0.12);
border-color: rgba(0,245,212,0.24);
color: var(--accent-cyan);
}
.btn-loader {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 999px;
border: 2px solid currentColor;
border-right-color: transparent;
animation: spin 0.9s linear infinite;
margin-right: 6px;
}
.drawer-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
z-index: 20;
}
@media (max-width: 1400px) {
.command-stage { padding: 20px 18px 28px; }
.topology-main-grid {
grid-template-columns: repeat(2, minmax(240px, 1fr));
}
}
@media (max-width: 1100px) {
.embedded-skills-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 860px) {
.view-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.header-actions {
width: 100%;
justify-content: space-between;
}
.command-stage {
padding: 16px 14px 26px;
}
.board-shell {
padding: 16px;
}
.topology-main-grid,
.embedded-skills-grid {
grid-template-columns: 1fr;
}
.topology-main-grid::before {
left: 50%;
right: auto;
top: 0;
width: 3px;
height: calc(100% - 12px);
transform: translateX(-50%);
background: linear-gradient(180deg, rgba(0,245,212,0.18), rgba(0,245,212,0.68), rgba(0,245,212,0.18));
}
}
@media (prefers-reduced-motion: reduce) {
.bg-particle,
.chip-overlay::after,
.skill-chip.active::before,
.bus-spine.energized::before,
.main-link.energized .main-link-line::before,
.sub-link.energized .sub-link-line::before {
animation: none !important;
}
}
2026-03-21 10:13:35 +08:00
</style>