Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 611c343e0c | |||
| 2857c27b7c | |||
| 745e202e21 | |||
| 5244e9fd3d |
@@ -20,7 +20,7 @@ const activeView = computed(() => {
|
||||
})
|
||||
|
||||
const routePaths: Record<string, string> = {
|
||||
Dashboard: '/dashboard', Memory: '/memory', Docs: '/docs', Team: '/team', Security: '/security',
|
||||
Dashboard: '/dashboard', Memory: '/memory', Docs: '/docs', Security: '/security',
|
||||
Projects: '/projects', 'Task Board': '/tasks', Incidents: '/incidents', Calendar: '/calendar',
|
||||
Agents: '/agents', Models: '/models', Activity: '/activity', 'Mobile Chat': '/chat', Settings: '/settings',
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const navigate = (label: string) => {
|
||||
}
|
||||
const mobileNavOpen = ref(false)
|
||||
|
||||
const standaloneViews = computed(() => ['Dashboard', 'Settings', 'ProjectDetail', 'Memory', 'Docs', 'Team', 'Security', 'Incidents', 'Calendar', 'AgentDetail', 'Agents'].includes(activeView.value))
|
||||
const standaloneViews = computed(() => ['Dashboard', 'Settings', 'ProjectDetail', 'Memory', 'Docs', 'Security', 'Incidents', 'Calendar', 'AgentDetail', 'Agents'].includes(activeView.value))
|
||||
|
||||
onMounted(() => {
|
||||
if (auth.isAuthenticated) store.refresh()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { Bot, Sparkles } from '@lucide/vue'
|
||||
import AgentNode from './AgentNode.vue'
|
||||
import AgentModal from './AgentModal.vue'
|
||||
@@ -23,41 +23,217 @@ function closeModal(): void {
|
||||
selectedAgent.value = null
|
||||
}
|
||||
|
||||
const agentColorMap: Record<string, string> = {
|
||||
developer: '#3b82f6',
|
||||
devops: '#eab308',
|
||||
researcher: '#22c55e',
|
||||
reviewer: '#a855f7',
|
||||
// ── Layout measurement ──
|
||||
const networkRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
interface CardBox {
|
||||
left: number
|
||||
right: number
|
||||
top: number
|
||||
bottom: number
|
||||
cx: number
|
||||
cy: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
const cardPositions = ref<Record<string, CardBox>>({})
|
||||
const svgWidth = ref(0)
|
||||
const svgHeight = ref(0)
|
||||
|
||||
function updatePositions() {
|
||||
if (!networkRef.value) return
|
||||
const rect = networkRef.value.getBoundingClientRect()
|
||||
svgWidth.value = rect.width
|
||||
svgHeight.value = rect.height
|
||||
|
||||
const positions: Record<string, CardBox> = {}
|
||||
|
||||
const irisEl = networkRef.value.querySelector('[data-agent-id="iris"]')
|
||||
if (irisEl) {
|
||||
const r = irisEl.getBoundingClientRect()
|
||||
positions['iris'] = {
|
||||
left: r.left - rect.left,
|
||||
right: r.left + r.width - rect.left,
|
||||
top: r.top - rect.top,
|
||||
bottom: r.top + r.height - rect.top,
|
||||
cx: r.left + r.width / 2 - rect.left,
|
||||
cy: r.top + r.height / 2 - rect.top,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
}
|
||||
}
|
||||
|
||||
const cards = networkRef.value.querySelectorAll('[data-agent-id]')
|
||||
cards.forEach(el => {
|
||||
const id = el.getAttribute('data-agent-id')
|
||||
if (!id || id === 'iris') return
|
||||
const r = el.getBoundingClientRect()
|
||||
positions[id] = {
|
||||
left: r.left - rect.left,
|
||||
right: r.left + r.width - rect.left,
|
||||
top: r.top - rect.top,
|
||||
bottom: r.top + r.height - rect.top,
|
||||
cx: r.left + r.width / 2 - rect.left,
|
||||
cy: r.top + r.height / 2 - rect.top,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
}
|
||||
})
|
||||
|
||||
cardPositions.value = positions
|
||||
}
|
||||
|
||||
const agentLineActive: Record<string, boolean> = {
|
||||
developer: true,
|
||||
devops: false,
|
||||
researcher: true,
|
||||
reviewer: false,
|
||||
// ── SVG connection paths ──
|
||||
interface ConnectionPath {
|
||||
d: string
|
||||
length: number
|
||||
}
|
||||
|
||||
const NETWORK_W = 440
|
||||
const IRIS_CX = NETWORK_W / 2
|
||||
const IRIS_CY = 80
|
||||
const AGENT_START_Y = 170
|
||||
const connectionPaths = computed<Record<string, ConnectionPath | null>>(() => {
|
||||
const result: Record<string, ConnectionPath | null> = {}
|
||||
const pos = cardPositions.value
|
||||
const iris = pos['iris']
|
||||
if (!iris) return result
|
||||
|
||||
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 },
|
||||
])
|
||||
for (const agent of props.agents) {
|
||||
const agentPos = pos[agent.id]
|
||||
if (!agentPos) {
|
||||
result[agent.id] = null
|
||||
continue
|
||||
}
|
||||
|
||||
const activeLines = computed(() =>
|
||||
agentPositions.value.filter(p => agentLineActive[p.id])
|
||||
)
|
||||
const startX = iris.cx
|
||||
const startY = iris.bottom - 1
|
||||
const endX = agentPos.cx
|
||||
const endY = agentPos.top + 4
|
||||
|
||||
// Bézier control points
|
||||
const dy = endY - startY
|
||||
const cy1 = startY + dy * 0.4
|
||||
const cy2 = startY + dy * 0.7
|
||||
|
||||
const d = `M ${startX} ${startY} C ${startX} ${cy1}, ${endX} ${cy2}, ${endX} ${endY}`
|
||||
result[agent.id] = { d, length: 0 }
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
// ── Pulse animation (JS-driven via requestAnimationFrame) ──
|
||||
let animFrameId: number | null = null
|
||||
let lastAnimTime = 0
|
||||
|
||||
const pathElements = ref<Record<string, SVGPathElement | null>>({})
|
||||
const pulseElements = ref<Record<string, SVGPathElement | null>>({})
|
||||
const pulseOffsets = ref<Record<string, number>>({})
|
||||
|
||||
function storePathRef(id: string) {
|
||||
return (el: SVGPathElement | null) => {
|
||||
pathElements.value[id] = el
|
||||
}
|
||||
}
|
||||
|
||||
function storePulseRef(id: string) {
|
||||
return (el: SVGPathElement | null) => {
|
||||
pulseElements.value[id] = el
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPathLengths() {
|
||||
for (const agent of props.agents) {
|
||||
const pathEl = pathElements.value[agent.id]
|
||||
const pulseEl = pulseElements.value[agent.id]
|
||||
const p = connectionPaths.value[agent.id]
|
||||
if (pathEl && p) {
|
||||
p.length = pathEl.getTotalLength()
|
||||
}
|
||||
if (pulseEl && p && p.length > 0) {
|
||||
if (pulseOffsets.value[agent.id] === undefined) {
|
||||
pulseOffsets.value[agent.id] = 0
|
||||
}
|
||||
pulseEl.setAttribute('stroke-dasharray', `12 ${p.length}`)
|
||||
pulseEl.setAttribute('stroke-dashoffset', String(-pulseOffsets.value[agent.id]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startPulseAnimation() {
|
||||
refreshPathLengths()
|
||||
|
||||
const speeds: Record<string, number> = {}
|
||||
for (const agent of props.agents) {
|
||||
const p = connectionPaths.value[agent.id]
|
||||
if (p && p.length > 0) {
|
||||
speeds[agent.id] = p.length / 3000 // full traversal in ~3s
|
||||
if (pulseOffsets.value[agent.id] === undefined) {
|
||||
pulseOffsets.value[agent.id] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastAnimTime = performance.now()
|
||||
|
||||
function tick(now: number) {
|
||||
const dt = now - lastAnimTime
|
||||
lastAnimTime = now
|
||||
|
||||
for (const agent of props.agents) {
|
||||
const pulseEl = pulseElements.value[agent.id]
|
||||
const p = connectionPaths.value[agent.id]
|
||||
if (!pulseEl || !p || p.length <= 0) continue
|
||||
|
||||
const currentOffset = pulseOffsets.value[agent.id] ?? 0
|
||||
const speed = speeds[agent.id] ?? p.length / 3000
|
||||
const newOffset = currentOffset + speed * dt
|
||||
const cycleLen = p.length + 12
|
||||
pulseOffsets.value[agent.id] = newOffset > cycleLen ? newOffset % cycleLen : newOffset
|
||||
pulseEl.setAttribute('stroke-dashoffset', String(-pulseOffsets.value[agent.id]))
|
||||
}
|
||||
|
||||
animFrameId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
animFrameId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
function stopPulseAnimation() {
|
||||
if (animFrameId !== null) {
|
||||
cancelAnimationFrame(animFrameId)
|
||||
animFrameId = null
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lifecycle ──
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
updatePositions()
|
||||
await nextTick()
|
||||
updatePositions()
|
||||
refreshPathLengths()
|
||||
startPulseAnimation()
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
updatePositions()
|
||||
requestAnimationFrame(() => {
|
||||
refreshPathLengths()
|
||||
})
|
||||
})
|
||||
if (networkRef.value) {
|
||||
resizeObserver.observe(networkRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPulseAnimation()
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="team-network">
|
||||
<div ref="networkRef" class="team-network">
|
||||
<!-- Iris Node -->
|
||||
<div class="iris-node">
|
||||
<div class="iris-node" data-agent-id="iris">
|
||||
<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" />
|
||||
@@ -88,59 +264,86 @@ const activeLines = computed(() =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SVG Connections -->
|
||||
<!-- SVG Connection Layer -->
|
||||
<svg
|
||||
v-if="svgWidth > 0 && svgHeight > 0"
|
||||
class="network-svg"
|
||||
:viewBox="`0 0 ${NETWORK_W} 400`"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
:width="svgWidth"
|
||||
:height="svgHeight"
|
||||
:viewBox="`0 0 ${svgWidth} ${svgHeight}`"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<filter id="lineglow">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<filter
|
||||
v-for="agent in agents"
|
||||
:key="`glow-${agent.id}`"
|
||||
:id="`glow-${agent.id}`"
|
||||
x="-30%" y="-30%" width="160%" height="160%"
|
||||
>
|
||||
<feGaussianBlur stdDeviation="4" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<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)"
|
||||
<template v-for="agent in agents" :key="agent.id">
|
||||
<!-- Base connection line -->
|
||||
<path
|
||||
v-if="connectionPaths[agent.id]"
|
||||
:ref="storePathRef(agent.id)"
|
||||
:d="connectionPaths[agent.id]!.d"
|
||||
:stroke="agent.color"
|
||||
:stroke-width="agent.active ? 2.5 : 1.5"
|
||||
fill="none"
|
||||
:opacity="agent.active ? 0.7 : 0.25"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
|
||||
<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] }"
|
||||
<!-- Glow for active agents -->
|
||||
<path
|
||||
v-if="agent.active && connectionPaths[agent.id]"
|
||||
:d="connectionPaths[agent.id]!.d"
|
||||
:stroke="agent.color"
|
||||
stroke-width="4"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
:filter="`url(#glow-${agent.id})`"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<circle
|
||||
:cx="IRIS_CX" :cy="IRIS_CY + 32"
|
||||
r="2.5" fill="#ffffff"
|
||||
class="pulse-origin"
|
||||
|
||||
<!-- Moving pulse dot -->
|
||||
<path
|
||||
v-if="connectionPaths[agent.id]"
|
||||
:ref="storePulseRef(agent.id)"
|
||||
:d="connectionPaths[agent.id]!.d"
|
||||
stroke="white"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
:opacity="agent.active ? 1 : 0.4"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
</svg>
|
||||
|
||||
<!-- Agent Cards -->
|
||||
<!-- Agent Grid (dynamic: 2 columns, extends downward) -->
|
||||
<div class="agents-grid">
|
||||
<AgentNode
|
||||
<div
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
:data-agent-id="agent.id"
|
||||
class="agent-slot"
|
||||
>
|
||||
<AgentNode
|
||||
:agent="agent"
|
||||
:runtime="getAgentRuntime(agent.id)"
|
||||
@select="onAgentSelect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Focus Banner -->
|
||||
<div v-if="irisFocus" class="focus-banner">
|
||||
@@ -160,24 +363,21 @@ const activeLines = computed(() =>
|
||||
|
||||
<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;
|
||||
width: 100%;
|
||||
padding: 24px 20px 20px;
|
||||
background: rgba(22, 27, 34, 0.5);
|
||||
border: 1px solid rgba(139, 124, 246, 0.08);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
||||
min-height: 480px;
|
||||
transition: border-color 0.2s ease;
|
||||
overflow: hidden;
|
||||
min-height: 520px;
|
||||
}
|
||||
.team-network:hover {
|
||||
border-color: rgba(139, 124, 246, 0.18);
|
||||
border-color: rgba(139, 124, 246, 0.14);
|
||||
}
|
||||
|
||||
/* Iris Node */
|
||||
@@ -187,6 +387,7 @@ const activeLines = computed(() =>
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 20px 28px;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(167, 139, 250, 0.06);
|
||||
border: 1px solid rgba(167, 139, 250, 0.15);
|
||||
border-radius: 14px;
|
||||
@@ -194,6 +395,8 @@ const activeLines = computed(() =>
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.iris-avatar-ring {
|
||||
position: relative;
|
||||
@@ -267,50 +470,28 @@ const activeLines = computed(() =>
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
/* SVG Lines */
|
||||
/* SVG Connection 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; }
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Agent Cards */
|
||||
/* Agent Grid — dynamic, 2 columns, extends downward as agents grow */
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.agent-slot {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Focus Banner */
|
||||
@@ -320,10 +501,10 @@ const activeLines = computed(() =>
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
margin-top: 12px;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed } from 'vue'
|
||||
import {
|
||||
Activity, Bot, Boxes, Command, FileText,
|
||||
LayoutDashboard, ListTodo, LogOut, MessageSquareText, Settings,
|
||||
Shield, SlidersHorizontal, Sparkles, Users, BookOpen,
|
||||
Shield, SlidersHorizontal, Sparkles, BookOpen,
|
||||
AlertTriangle, Calendar,
|
||||
} from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -29,7 +29,6 @@ const navigation = [
|
||||
{ label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ label: 'Memory', icon: FileText },
|
||||
{ label: 'Docs', icon: BookOpen },
|
||||
{ label: 'Team', icon: Users },
|
||||
{ label: 'Security', icon: Shield },
|
||||
{ label: 'Projects', icon: Boxes },
|
||||
{ label: 'Task Board', icon: ListTodo },
|
||||
|
||||
@@ -4,7 +4,6 @@ import ProjectDetailView from './views/ProjectDetailView.vue'
|
||||
import SettingsView from './views/SettingsView.vue'
|
||||
import MemoryView from './views/MemoryView.vue'
|
||||
import DocsView from './views/DocsView.vue'
|
||||
import TeamView from './views/TeamView.vue'
|
||||
import AgentDetailView from './views/AgentDetailView.vue'
|
||||
import AgentsIndexView from './views/AgentsIndexView.vue'
|
||||
import SecurityView from './views/SecurityView.vue'
|
||||
@@ -18,7 +17,6 @@ const routes = [
|
||||
{ path: '/dashboard', name: 'Dashboard', component: DashboardView },
|
||||
{ path: '/memory', name: 'Memory', component: MemoryView },
|
||||
{ path: '/docs', name: 'Docs', component: DocsView },
|
||||
{ path: '/team', name: 'Team', component: TeamView },
|
||||
{ path: '/agents/:id', name: 'AgentDetail', component: AgentDetailView },
|
||||
{ path: '/security', name: 'Security', component: SecurityView },
|
||||
{ path: '/incidents', name: 'Incidents', component: IncidentsView },
|
||||
|
||||
@@ -45,12 +45,40 @@ function onQueueExecuteNow(id: string): void {
|
||||
<OperationsFeed :entries="feedEntries" />
|
||||
</div>
|
||||
<div class="col-center">
|
||||
<!-- Quote Pill -->
|
||||
<div class="quote-pill">
|
||||
<span class="quote-text">"An autonomous organization of AI agents that does work for me and produces value 24/7"</span>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="team-header">
|
||||
<h1 class="team-title">AI Team Network</h1>
|
||||
<p class="team-subtitle">{{ agents.length }} AI agents, connected in real-time.</p>
|
||||
<p class="team-description">Mission Control orchestriert ein Team spezialisierter Agenten — jeder mit eigener Identität, eigenem Workspace und klaren Verantwortlichkeiten. Die Pulse zeigen aktive Kommunikationsflüsse.</p>
|
||||
</div>
|
||||
|
||||
<TeamNetwork
|
||||
:agents="agents"
|
||||
:iris-runtime="irisRuntime"
|
||||
:get-agent-runtime="getAgentRuntime"
|
||||
:iris-focus="irisFocus"
|
||||
/>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="legend-row">
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot active-pulse"></span>
|
||||
<span>Aktive Verbindung</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot idle-pulse"></span>
|
||||
<span>Idle</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot pulse-dot"></span>
|
||||
<span>Datenfluss (Pulse)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-right">
|
||||
<ChatPanel :messages="chatMessages" :iris-busy="irisBusy" :iris-focus="irisFocus" @send="onChatSend" />
|
||||
@@ -74,7 +102,89 @@ function onQueueExecuteNow(id: string): void {
|
||||
.dashboard ::-webkit-scrollbar-thumb { background: rgba(139,124,246,0.2); border-radius: 3px; }
|
||||
.dashboard ::-webkit-scrollbar-thumb:hover { background: rgba(139,124,246,0.35); }
|
||||
.col-left { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; padding-right: 4px; }
|
||||
.col-center { overflow-y: auto; padding: 0 4px; min-height: 0; }
|
||||
.col-center { overflow-y: auto; padding: 0 4px; min-height: 0; display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
/* Quote Pill */
|
||||
.quote-pill {
|
||||
background: var(--panel);
|
||||
border: 1px solid rgba(139, 124, 246, 0.25);
|
||||
border-radius: 14px;
|
||||
padding: 14px 22px;
|
||||
box-shadow: 0 0 18px rgba(139, 124, 246, 0.06), inset 0 0 18px rgba(139, 124, 246, 0.03);
|
||||
text-align: center;
|
||||
}
|
||||
.quote-text {
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
color: #9ea5b3;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Team Header */
|
||||
.team-header {
|
||||
text-align: center;
|
||||
}
|
||||
.team-title {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #e8eaf0;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
.team-subtitle {
|
||||
font-size: 12px;
|
||||
color: #7e8799;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.team-description {
|
||||
font-size: 10.5px;
|
||||
color: #6b7385;
|
||||
margin: 0;
|
||||
max-width: 560px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Legend */
|
||||
.legend-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 12px 20px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 10px;
|
||||
color: #7e8799;
|
||||
}
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.active-pulse {
|
||||
background: #51d49a;
|
||||
box-shadow: 0 0 6px rgba(81, 212, 154, 0.6);
|
||||
}
|
||||
.idle-pulse {
|
||||
background: #3a3f4b;
|
||||
}
|
||||
.pulse-dot {
|
||||
background: white;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
animation: legend-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes legend-pulse {
|
||||
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1.2); }
|
||||
}
|
||||
.col-right { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; padding-left: 4px; }
|
||||
.missions-section { display: flex; flex-direction: column; gap: 8px; }
|
||||
.column-title { margin: 0; font-size: 13px; font-weight: 600; color: #e8eaf0; letter-spacing: 0.01em; }
|
||||
|
||||
Reference in New Issue
Block a user