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

397 lines
16 KiB
Vue
Raw Normal View History

<template>
2026-03-21 10:13:35 +08:00
<div class="agent-view scanlines">
<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>
<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>
<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
<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="hud-panels">
<div class="hud-panel route-telemetry" data-testid="route-telemetry">
<div class="route-main">{{ activeMainRouteLabel }}</div>
<div class="route-child">{{ activeChildRouteLabel }}</div>
</div>
</div>
2026-03-25 11:27:16 +08:00
<div class="nodes-viewport" :style="viewportStyle">
<div class="nodes-stage" :style="stageStyle">
<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>
2026-03-25 11:27:16 +08:00
<path
v-for="agent in mainAgents"
:key="`bus-${agent.id}`"
:d="getBusLinePath(agent.id)"
2026-03-25 11:27:16 +08:00
class="conn-path"
:class="{ energized: activeMainId === agent.id }"
:data-testid="`bus-link-${agent.id}`"
/>
<path
v-for="agent in activeMainAgents"
:key="`bus-current-${agent.id}`"
:d="getBusLinePath(agent.id)"
class="conn-current"
:data-testid="`bus-current-${agent.id}`"
/>
<path
v-for="child in childAgents"
:key="`sub-${child.id}`"
:d="getSubLinePath(child.id)"
class="conn-path conn-path-sub"
:class="{ energized: activeChildId === child.id }"
:data-testid="`sub-link-${child.id}`"
/>
<path
v-for="child in activeChildAgents"
:key="`sub-current-${child.id}`"
:d="getSubLinePath(child.id)"
class="conn-current conn-current-sub"
:data-testid="`sub-current-${child.id}`"
2026-03-25 11:27:16 +08:00
/>
</svg>
<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>
2026-03-25 11:27:16 +08:00
</span>
<span v-if="agentData.master?.currentTask" class="node-task-tag">{{ agentData.master.currentTask }}</span>
</div>
</div>
2026-03-21 10:13:35 +08:00
</div>
2026-03-25 11:27:16 +08:00
<div
v-for="agent in mainAgents"
:key="agent.id"
:ref="el => setNodeRef(agent.id, el as HTMLElement)"
2026-03-25 11:27:16 +08:00
class="node-card node-sub"
:class="{ selected: selectedAgentId === agent.id, disabled: !localAgents[agent.id]?.enabled }"
:style="getMainNodeStyle(agent.id)"
:data-testid="`agent-chip-${agent.id}`"
@click="selectAgent(agent.id)"
2026-03-25 11:27:16 +08:00
>
<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(agent.id)">
2026-03-25 11:27:16 +08:00
<span class="status-ring"></span>
</div>
<div class="node-label">{{ getAgentName(agent.id) }}</div>
<div class="node-role">{{ getAgentRole(agent.id) }}</div>
<div class="node-desc">{{ getAgentDesc(agent.id) }}</div>
2026-03-25 11:27:16 +08:00
<div class="node-footer">
<span class="node-stat">
<span class="stat-label">璋冪敤</span>
<span class="stat-val">{{ agentData[agent.id]?.callCount || 0 }}</span>
2026-03-25 11:27:16 +08:00
</span>
<span v-if="agentData[agent.id]?.currentTask" class="node-task-tag">{{ agentData[agent.id]?.currentTask }}</span>
<span v-else class="node-idle">待命中</span>
</div>
<div class="rel-label">{{ relationLabels[`master-${agent.id}`] }}</div>
</div>
</div>
<div
v-for="child in childAgents"
:key="child.id"
:ref="el => setNodeRef(child.id, el as HTMLElement)"
class="node-card node-sub node-child"
:class="{ selected: selectedAgentId === child.id, disabled: !localAgents[child.id]?.enabled }"
:style="getChildNodeStyle(child.id)"
:data-testid="`agent-chip-${child.id}`"
@click="selectAgent(child.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(child.id)">
<span class="status-ring"></span>
</div>
<div class="node-label">{{ child.name }}</div>
<div class="node-role">{{ child.role }}</div>
<div class="node-desc">{{ child.description }}</div>
<div class="node-footer">
<span class="node-stat">
<span class="stat-label">璋冪敤</span>
<span class="stat-val">{{ agentData[child.id]?.callCount || 0 }}</span>
2026-03-25 11:27:16 +08:00
</span>
<span v-if="agentData[child.id]?.currentTask" class="node-task-tag">{{ agentData[child.id]?.currentTask }}</span>
<span v-else class="node-idle">待命中</span>
</div>
<div class="rel-label">{{ child.toolScopeLabel }}</div>
2026-03-25 11:27:16 +08:00
</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>
2026-03-25 11:27:16 +08:00
</button>
<button class="control-chip zoom-readout" @click="resetView" title="閲嶇疆瑙嗗浘">
2026-03-25 11:27:16 +08:00
<span class="chip-value">{{ zoomPercent }}</span>
</button>
<button class="control-chip zoom-chip" @click="zoomIn" title="鏀惧ぇ瑙嗗浘">
2026-03-25 11:27:16 +08:00
<span class="chip-symbol">+</span>
</button>
2026-03-21 10:13:35 +08:00
</div>
</div>
<Transition :css="false" @enter="animateIn" @leave="animateOut">
<div v-if="drawerOpen" class="config-drawer" data-testid="agent-config-drawer">
2026-03-21 10:13:35 +08:00
<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 = { ...editAgent, enabled: !editAgent.enabled }">
2026-03-21 10:13:35 +08:00
<span class="toggle-knob"></span>
</button>
<span class="toggle-label" :class="{ dim: !editAgent.enabled }">ENABLED</span>
</div>
</div>
<div class="form-group linked-skills-group" data-testid="linked-skills-section">
<label class="form-label">// LINKED SKILLS</label>
<div v-if="selectedNodePackages.length > 0" class="linked-skill-packages">
<span
v-for="pkg in selectedNodePackages"
:key="pkg.id"
class="linked-skill-package"
:data-testid="`linked-skills-package-${pkg.id}`"
>
<strong>{{ pkg.title }}</strong>
<span>{{ pkg.stateLabel }}</span>
</span>
</div>
<div v-if="skillsLoading" class="linked-skills-state" data-testid="linked-skills-loading">鍔犺浇鎶鑳戒腑...</div>
<div v-else-if="skillsError" class="linked-skills-state linked-skills-error" data-testid="linked-skills-error">{{ skillsError }}</div>
<div v-else-if="saveError" class="linked-skills-state linked-skills-error" data-testid="linked-skills-save-error">{{ saveError }}</div>
<div v-else-if="selectedNodeSkills.length === 0" class="linked-skills-state" data-testid="linked-skills-empty">
暂无可关联技能请先到左侧能力中心创建或维护相关 Skill
</div>
<div v-else class="linked-skill-list">
<label
v-for="skill in selectedNodeSkills"
:key="skill.id"
class="linked-skill-item"
:data-testid="`linked-skill-item-${skill.id}`"
>
<input
:checked="editAgent.selectedSkillIds.includes(skill.id)"
type="checkbox"
class="linked-skill-checkbox"
:data-testid="`linked-skill-checkbox-${skill.id}`"
@change="toggleSkillSelection(skill.id, ($event.target as HTMLInputElement).checked)"
/>
<div class="linked-skill-copy">
<div class="linked-skill-head">
<span class="linked-skill-name">{{ skill.name }}</span>
<span class="linked-skill-agent-type">{{ skill.agent_type }}</span>
</div>
<div class="linked-skill-desc">{{ skill.description || '鏆傛棤鎻忚堪' }}</div>
<div v-if="skill.tools.length > 0" class="linked-skill-tools">
<span v-for="tool in skill.tools" :key="tool" class="linked-skill-tool">{{ tool }}</span>
</div>
</div>
</label>
</div>
</div>
2026-03-21 10:13:35 +08:00
<div class="drawer-actions">
<button class="btn-secondary" data-testid="linked-skills-reset" @click="resetConfig">閲嶇疆</button>
<button class="btn-primary" data-testid="linked-skills-save" @click="saveConfig" :disabled="saving">
2026-03-21 10:13:35 +08:00
<span v-if="saving" class="btn-loader"></span>
{{ saving ? '淇濆瓨涓?..' : '淇濆瓨閰嶇疆' }}
2026-03-21 10:13:35 +08:00
</button>
</div>
</div>
</div>
</Transition>
2026-03-25 11:27:16 +08:00
<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" />
2026-03-25 11:27:16 +08:00
</div>
<div class="form-group">
<label class="form-label">// ROLE KEY (鑻辨枃鍞竴鏍囪瘑)</label>
<input v-model="newAgent.roleKey" type="text" class="form-input" placeholder="渚嬪: coder" />
2026-03-25 11:27:16 +08:00
</div>
<div class="form-group">
<label class="form-label">// ROLE</label>
<input v-model="newAgent.role" type="text" class="form-input" placeholder="例如: 中文角色名" />
2026-03-25 11:27:16 +08:00
</div>
<div class="form-group">
<label class="form-label">// DESCRIPTION</label>
<textarea v-model="newAgent.description" class="form-textarea" rows="2" placeholder="鎻忚堪姝?Agent 鐨勮亴璐?.."></textarea>
2026-03-25 11:27:16 +08:00
</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>
2026-03-25 11:27:16 +08:00
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="addModalOpen = false">鍙栨秷</button>
2026-03-25 11:27:16 +08:00
<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">
import { RefreshCw, X } from 'lucide-vue-next'
import { useAgentsPage } from './composables/useAgentsPage'
const {
bgParticles,
canvasRef,
svgRef,
masterCardRef,
isPanning,
connectionStatus,
connectionLabel,
loading,
drawerOpen,
addModalOpen,
editAgent,
newAgent,
skillsLoading,
skillsError,
saveError,
saving,
mainAgents,
childAgents,
relationLabels,
activeMainId,
activeChildId,
activeMainAgents,
activeChildAgents,
activeMainRouteLabel,
activeChildRouteLabel,
selectedAgentId,
selectedNodePackages,
selectedNodeSkills,
agentData,
localAgents,
viewportStyle,
stageStyle,
masterNodeStyle,
zoomPercent,
refreshStats,
startPan,
handleWheel,
getBusLinePath,
getSubLinePath,
selectAgent,
setNodeRef,
getStatusClass,
getAgentName,
getAgentRole,
getAgentDesc,
getMainNodeStyle,
getChildNodeStyle,
zoomOut,
zoomIn,
resetView,
toggleSkillSelection,
resetConfig,
saveConfig,
addAgent,
animateIn,
animateOut,
fadeIn,
fadeOut,
} = useAgentsPage()
2026-03-21 10:13:35 +08:00
</script>
<style scoped src="./agentsPage.css"></style>