Files
nexus/frontend/src/components/dashboard/TeamNetwork.vue
T
developer 3c95281119
CI - Build & Test / Backend (.NET) (push) Successful in 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
feat: Mission Control Dashboard – AI Team Network, Chat, Queue
- 3-Spalten-Layout: Missions/Feed | Team Network | Chat/Queue
- AI Team Network (Herzstück): Iris + 4 Agenten mit SVG-Linien
- AgentNode: Workload-Ring, Pulse-Animation, Farbcodierung
- AgentModal: Task, Goal, Progress, Working Feed
- MissionCard: Glass-Morphism, Progress-Bar, Status-Badges
- ChatPanel: Iris Chat mit Focus-Banner, Auto-Scroll
- QueuePanel: Drag&Drop, Prioritäten, Force-Execute
- Composables: useTimer, useDashboardData
2026-06-09 21:00:23 +02:00

346 lines
7.7 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue'
import { Bot, Sparkles } from '@lucide/vue'
import AgentNode from './AgentNode.vue'
import AgentModal from './AgentModal.vue'
import type { AgentNodeData } from '../../composables/useDashboardData'
const props = defineProps<{
agents: AgentNodeData[]
irisRuntime: string
getAgentRuntime: (id: string) => string
irisFocus: string
}>()
const selectedAgent = ref<AgentNodeData | null>(null)
function onAgentSelect(agentId: string): void {
const agent = props.agents.find(a => a.id === agentId)
if (agent) selectedAgent.value = agent
}
function closeModal(): void {
selectedAgent.value = null
}
const agentColorMap: Record<string, string> = {
developer: '#3b82f6',
devops: '#eab308',
researcher: '#22c55e',
reviewer: '#a855f7',
}
const agentLineActive: Record<string, boolean> = {
developer: true,
devops: false,
researcher: true,
reviewer: false,
}
const NETWORK_W = 440
const IRIS_CX = NETWORK_W / 2
const IRIS_CY = 80
const AGENT_START_Y = 170
const agentPositions = computed(() => [
{ id: 'developer', x: 60, y: AGENT_START_Y },
{ id: 'researcher', x: NETWORK_W - 60, y: AGENT_START_Y },
{ id: 'devops', x: 60, y: AGENT_START_Y + 110 },
{ id: 'reviewer', x: NETWORK_W - 60, y: AGENT_START_Y + 110 },
])
const activeLines = computed(() =>
agentPositions.value.filter(p => agentLineActive[p.id])
)
</script>
<template>
<div class="team-network">
<!-- Iris Node -->
<div class="iris-node">
<div class="iris-avatar-ring">
<svg class="ring-svg" viewBox="0 0 60 60" width="60" height="60">
<circle cx="30" cy="30" r="27" fill="none" stroke="rgba(167,139,250,0.12)" stroke-width="2" />
<circle
cx="30" cy="30" r="27"
fill="none" stroke="#a78bfa" stroke-width="2"
stroke-dasharray="169.6" stroke-dashoffset="42"
stroke-linecap="round"
transform="rotate(-90 30 30)"
class="ring-arc"
/>
</svg>
<div class="iris-avatar-inner">
<Bot :size="26" />
</div>
</div>
<div class="iris-name-block">
<h2>Iris</h2>
<span class="iris-role-label">Chief of Staff</span>
</div>
<p class="iris-tagline">Breaking down tasks and coordinating specialists</p>
<div class="iris-runtime">
<span class="rt-label">Session Runtime</span>
<span class="rt-value">{{ irisRuntime }}</span>
</div>
</div>
<!-- SVG Connections -->
<svg
class="network-svg"
:viewBox="`0 0 ${NETWORK_W} 400`"
preserveAspectRatio="xMidYMid meet"
>
<defs>
<filter id="lineglow">
<feGaussianBlur stdDeviation="2" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<line
v-for="pos in agentPositions"
:key="'conn-' + pos.id"
:x1="IRIS_CX" :y1="IRIS_CY + 32"
:x2="pos.x + 22" :y2="pos.y"
:stroke="agentColorMap[pos.id]"
:stroke-width="agentLineActive[pos.id] ? 1.5 : 1"
:opacity="agentLineActive[pos.id] ? 0.4 : 0.1"
class="conn-line"
:class="{ 'line-pulse': agentLineActive[pos.id] }"
filter="url(#lineglow)"
/>
<g v-for="pos in activeLines" :key="'fx-' + pos.id">
<circle
:cx="pos.x + 22" :cy="pos.y"
r="2.5" fill="#ffffff"
class="pulse-end" :style="{ '--pulse-color': agentColorMap[pos.id] }"
/>
<circle
:cx="IRIS_CX" :cy="IRIS_CY + 32"
r="2.5" fill="#ffffff"
class="pulse-origin"
/>
</g>
</svg>
<!-- Agent Cards -->
<div class="agents-grid">
<AgentNode
v-for="agent in agents"
:key="agent.id"
:agent="agent"
:runtime="getAgentRuntime(agent.id)"
@select="onAgentSelect"
/>
</div>
<!-- Focus Banner -->
<div v-if="irisFocus" class="focus-banner">
<Sparkles :size="12" class="focus-icon" />
<span>{{ irisFocus }}</span>
</div>
<!-- Agent Modal -->
<AgentModal
v-if="selectedAgent"
:agent="selectedAgent"
:runtime="getAgentRuntime(selectedAgent.id)"
@close="closeModal"
/>
</div>
</template>
<style scoped>
.team-network {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 24px 20px 20px;
background: rgba(22, 27, 34, 0.75);
border: 1px solid rgba(139, 124, 246, 0.12);
border-radius: 16px;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
transition: border-color 0.2s ease;
position: relative;
overflow: hidden;
min-height: 520px;
}
.team-network:hover {
border-color: rgba(139, 124, 246, 0.18);
}
/* Iris Node */
.iris-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 20px 28px;
background: rgba(167, 139, 250, 0.06);
border: 1px solid rgba(167, 139, 250, 0.15);
border-radius: 14px;
position: relative;
z-index: 2;
width: 100%;
max-width: 320px;
}
.iris-avatar-ring {
position: relative;
width: 60px;
height: 60px;
}
.ring-svg {
position: absolute;
inset: 0;
}
.ring-arc {
animation: iris-spin 3s linear infinite;
}
@keyframes iris-spin {
to { transform: rotate(270deg); }
}
.iris-avatar-inner {
width: 60px;
height: 60px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(167, 139, 250, 0.15);
color: #a78bfa;
}
.iris-name-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.iris-name-block h2 {
margin: 0;
font-size: 18px;
font-weight: 700;
color: #e8eaf0;
}
.iris-role-label {
font-size: 9px;
color: #a78bfa;
font-weight: 600;
letter-spacing: 0.05em;
}
.iris-tagline {
margin: 2px 0 0;
font-size: 10.5px;
color: #6b7385;
text-align: center;
line-height: 1.35;
}
.iris-runtime {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
padding: 5px 14px;
background: rgba(167, 139, 250, 0.08);
border: 1px solid rgba(167, 139, 250, 0.1);
border-radius: 20px;
}
.rt-label {
font-size: 8px;
color: #7e8799;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.rt-value {
font-size: 14px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: #a78bfa;
}
/* SVG Lines */
.network-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.conn-line {
transition: opacity 0.4s ease, stroke-width 0.4s ease;
}
.line-pulse {
animation: line-glow 2s ease-in-out infinite;
}
@keyframes line-glow {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.7; }
}
.pulse-origin {
animation: pulse-origin 2s ease-in-out infinite;
}
.pulse-end {
animation: pulse-end 2s ease-in-out infinite;
}
@keyframes pulse-origin {
0%, 100% { opacity: 0; r: 1.5; }
50% { opacity: 0.8; r: 3.5; }
}
@keyframes pulse-end {
0%, 100% { opacity: 0.2; r: 1.5; }
50% { opacity: 0.9; r: 3; }
}
/* Agent Cards */
.agents-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
width: 100%;
position: relative;
z-index: 2;
margin-top: 4px;
}
/* Focus Banner */
.focus-banner {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 10px 16px;
background: rgba(234, 179, 8, 0.05);
border: 1px solid rgba(234, 179, 8, 0.1);
border-radius: 10px;
margin-top: 4px;
position: relative;
z-index: 2;
}
.focus-icon {
color: #eab308;
flex-shrink: 0;
}
.focus-banner span {
font-size: 10.5px;
color: #eab308;
line-height: 1.35;
}
@media (max-width: 700px) {
.agents-grid {
grid-template-columns: 1fr;
}
}
</style>