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

397 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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>
<span class="title-text">AGENT COMMAND CENTER</span>
<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="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>
<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>
<path
v-for="agent in mainAgents"
:key="`bus-${agent.id}`"
:d="getBusLinePath(agent.id)"
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}`"
/>
</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>
</span>
<span v-if="agentData.master?.currentTask" class="node-task-tag">{{ agentData.master.currentTask }}</span>
</div>
</div>
</div>
<div
v-for="agent in mainAgents"
:key="agent.id"
:ref="el => setNodeRef(agent.id, el as HTMLElement)"
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)"
>
<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)">
<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>
<div class="node-footer">
<span class="node-stat">
<span class="stat-label">璋冪敤</span>
<span class="stat-val">{{ agentData[agent.id]?.callCount || 0 }}</span>
</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>
</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>
</div>
</div>
</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>
</div>
</div>
<Transition :css="false" @enter="animateIn" @leave="animateOut">
<div v-if="drawerOpen" class="config-drawer" data-testid="agent-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 = { ...editAgent, enabled: !editAgent.enabled }">
<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>
<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">
<span v-if="saving" class="btn-loader"></span>
{{ saving ? '淇濆瓨涓?..' : '淇濆瓨閰嶇疆' }}
</button>
</div>
</div>
</div>
</Transition>
<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>
<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()
</script>
<style scoped src="./agentsPage.css"></style>