Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da9c256b43 | |||
| 1012d2c217 |
@@ -1,29 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
|
||||||
import { Bot, Sparkles } from '@lucide/vue'
|
import { Bot, Code2, Server, Shield, Search, Terminal } from '@lucide/vue'
|
||||||
import AgentNode from './AgentNode.vue'
|
|
||||||
import AgentModal from './AgentModal.vue'
|
interface AgentData {
|
||||||
import type { AgentNodeData } from '../../composables/useDashboardData'
|
id: string
|
||||||
|
name: string
|
||||||
|
role: string
|
||||||
|
description: string
|
||||||
|
tags: string[]
|
||||||
|
color: string
|
||||||
|
icon: string
|
||||||
|
hero?: boolean
|
||||||
|
task?: string
|
||||||
|
runtime?: string
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
agents: AgentNodeData[]
|
agents: AgentData[]
|
||||||
irisRuntime: string
|
heroId?: string
|
||||||
getAgentRuntime: (id: string) => string
|
activeAgents?: string[]
|
||||||
irisFocus: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const selectedAgent = ref<AgentNodeData | null>(null)
|
const emit = defineEmits<{
|
||||||
|
select: [id: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
function onAgentSelect(agentId: string): void {
|
// ── Layout refs ──
|
||||||
const agent = props.agents.find(a => a.id === agentId)
|
|
||||||
if (agent) selectedAgent.value = agent
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal(): void {
|
|
||||||
selectedAgent.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Layout measurement ──
|
|
||||||
const networkRef = ref<HTMLDivElement | null>(null)
|
const networkRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
interface CardBox {
|
interface CardBox {
|
||||||
@@ -40,33 +42,39 @@ const cardPositions = ref<Record<string, CardBox>>({})
|
|||||||
const svgWidth = ref(0)
|
const svgWidth = ref(0)
|
||||||
const svgHeight = ref(0)
|
const svgHeight = ref(0)
|
||||||
|
|
||||||
|
// ── Computed data ──
|
||||||
|
const hero = computed(() => props.agents.find(a => a.id === props.heroId) ?? props.agents[0])
|
||||||
|
const childAgents = computed(() => props.agents.filter(a => a.id !== props.heroId))
|
||||||
|
|
||||||
|
function isActive(id: string): boolean {
|
||||||
|
return props.activeAgents?.includes(id) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Icon resolver ──
|
||||||
|
function resolveIcon(iconName: string) {
|
||||||
|
switch (iconName) {
|
||||||
|
case 'bot': return Bot
|
||||||
|
case 'code': return Code2
|
||||||
|
case 'server': return Server
|
||||||
|
case 'shield': return Shield
|
||||||
|
case 'search': return Search
|
||||||
|
case 'terminal': return Terminal
|
||||||
|
default: return Bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Position measurement ──
|
||||||
function updatePositions() {
|
function updatePositions() {
|
||||||
if (!networkRef.value) return
|
if (!networkRef.value) return
|
||||||
const rect = networkRef.value.getBoundingClientRect()
|
const rect = networkRef.value.getBoundingClientRect()
|
||||||
svgWidth.value = rect.width
|
svgWidth.value = rect.width
|
||||||
svgHeight.value = rect.height
|
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]')
|
const cards = networkRef.value.querySelectorAll('[data-agent-id]')
|
||||||
|
const positions: Record<string, CardBox> = {}
|
||||||
cards.forEach(el => {
|
cards.forEach(el => {
|
||||||
const id = el.getAttribute('data-agent-id')
|
const id = el.getAttribute('data-agent-id')
|
||||||
if (!id || id === 'iris') return
|
if (!id) return
|
||||||
const r = el.getBoundingClientRect()
|
const r = el.getBoundingClientRect()
|
||||||
positions[id] = {
|
positions[id] = {
|
||||||
left: r.left - rect.left,
|
left: r.left - rect.left,
|
||||||
@@ -79,11 +87,10 @@ function updatePositions() {
|
|||||||
height: r.height,
|
height: r.height,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
cardPositions.value = positions
|
cardPositions.value = positions
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SVG connection paths ──
|
// ── SVG path computation ──
|
||||||
interface ConnectionPath {
|
interface ConnectionPath {
|
||||||
d: string
|
d: string
|
||||||
length: number
|
length: number
|
||||||
@@ -92,27 +99,42 @@ interface ConnectionPath {
|
|||||||
const connectionPaths = computed<Record<string, ConnectionPath | null>>(() => {
|
const connectionPaths = computed<Record<string, ConnectionPath | null>>(() => {
|
||||||
const result: Record<string, ConnectionPath | null> = {}
|
const result: Record<string, ConnectionPath | null> = {}
|
||||||
const pos = cardPositions.value
|
const pos = cardPositions.value
|
||||||
const iris = pos['iris']
|
const heroEntry = props.agents.find(a => a.id === props.heroId)
|
||||||
|
const heroId = heroEntry?.id ?? ''
|
||||||
|
const iris = heroId ? pos[heroId] : undefined
|
||||||
if (!iris) return result
|
if (!iris) return result
|
||||||
|
|
||||||
for (const agent of props.agents) {
|
const children = childAgents.value
|
||||||
|
const total = children.length
|
||||||
|
if (total === 0) return result
|
||||||
|
|
||||||
|
for (let idx = 0; idx < total; idx++) {
|
||||||
|
const agent = children[idx]
|
||||||
const agentPos = pos[agent.id]
|
const agentPos = pos[agent.id]
|
||||||
if (!agentPos) {
|
if (!agentPos) {
|
||||||
result[agent.id] = null
|
result[agent.id] = null
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const startX = iris.cx
|
// Spread start points across Iris bottom edge (30%-70% range)
|
||||||
|
const t = total > 1 ? idx / (total - 1) : 0.5
|
||||||
|
const startX = iris.left + iris.width * (0.30 + t * 0.40)
|
||||||
const startY = iris.bottom - 1
|
const startY = iris.bottom - 1
|
||||||
const endX = agentPos.cx
|
|
||||||
const endY = agentPos.top + 4
|
// Determine column: left or right of Iris center
|
||||||
|
const isLeftColumn = agentPos.cx < iris.cx
|
||||||
|
|
||||||
|
// End point: approach from side, 8px before card edge
|
||||||
|
const endX = isLeftColumn ? agentPos.right - 8 : agentPos.left + 8
|
||||||
|
const endY = agentPos.cy
|
||||||
|
|
||||||
// Bézier control points
|
// Bézier control points
|
||||||
const dy = endY - startY
|
const cp1x = startX
|
||||||
const cy1 = startY + dy * 0.4
|
const cp1y = startY + 40
|
||||||
const cy2 = startY + dy * 0.7
|
const cp2x = endX + (isLeftColumn ? 50 : -50)
|
||||||
|
const cp2y = endY - 10
|
||||||
|
|
||||||
const d = `M ${startX} ${startY} C ${startX} ${cy1}, ${endX} ${cy2}, ${endX} ${endY}`
|
const d = `M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`
|
||||||
result[agent.id] = { d, length: 0 }
|
result[agent.id] = { d, length: 0 }
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -139,33 +161,34 @@ function storePulseRef(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshPathLengths() {
|
function refreshPathLengths() {
|
||||||
for (const agent of props.agents) {
|
for (const id of childAgents.value.map(a => a.id)) {
|
||||||
const pathEl = pathElements.value[agent.id]
|
const pathEl = pathElements.value[id]
|
||||||
const pulseEl = pulseElements.value[agent.id]
|
const pulseEl = pulseElements.value[id]
|
||||||
const p = connectionPaths.value[agent.id]
|
const p = connectionPaths.value[id]
|
||||||
if (pathEl && p) {
|
if (pathEl && p) {
|
||||||
p.length = pathEl.getTotalLength()
|
p.length = pathEl.getTotalLength()
|
||||||
}
|
}
|
||||||
if (pulseEl && p && p.length > 0) {
|
if (pulseEl && p && p.length > 0) {
|
||||||
if (pulseOffsets.value[agent.id] === undefined) {
|
if (pulseOffsets.value[id] === undefined) {
|
||||||
pulseOffsets.value[agent.id] = 0
|
pulseOffsets.value[id] = 0
|
||||||
}
|
}
|
||||||
pulseEl.setAttribute('stroke-dasharray', `12 ${p.length}`)
|
pulseEl.setAttribute('stroke-dasharray', `10 ${p.length}`)
|
||||||
pulseEl.setAttribute('stroke-dashoffset', String(-pulseOffsets.value[agent.id]))
|
pulseEl.setAttribute('stroke-dashoffset', String(-pulseOffsets.value[id]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPulseAnimation() {
|
function startPulseAnimation() {
|
||||||
|
const speeds: Record<string, number> = {}
|
||||||
|
|
||||||
refreshPathLengths()
|
refreshPathLengths()
|
||||||
|
|
||||||
const speeds: Record<string, number> = {}
|
for (const id of childAgents.value.map(a => a.id)) {
|
||||||
for (const agent of props.agents) {
|
const p = connectionPaths.value[id]
|
||||||
const p = connectionPaths.value[agent.id]
|
|
||||||
if (p && p.length > 0) {
|
if (p && p.length > 0) {
|
||||||
speeds[agent.id] = p.length / 3000 // full traversal in ~3s
|
speeds[id] = p.length / 3000
|
||||||
if (pulseOffsets.value[agent.id] === undefined) {
|
if (pulseOffsets.value[id] === undefined) {
|
||||||
pulseOffsets.value[agent.id] = 0
|
pulseOffsets.value[id] = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,17 +199,23 @@ function startPulseAnimation() {
|
|||||||
const dt = now - lastAnimTime
|
const dt = now - lastAnimTime
|
||||||
lastAnimTime = now
|
lastAnimTime = now
|
||||||
|
|
||||||
for (const agent of props.agents) {
|
const children = childAgents.value
|
||||||
const pulseEl = pulseElements.value[agent.id]
|
for (let i = 0; i < children.length; i++) {
|
||||||
const p = connectionPaths.value[agent.id]
|
const id = children[i].id
|
||||||
if (!pulseEl || !p || p.length <= 0) continue
|
const pathEl = pathElements.value[id]
|
||||||
|
const pulseEl = pulseElements.value[id]
|
||||||
|
const p = connectionPaths.value[id]
|
||||||
|
if (!pathEl || !pulseEl || !p) continue
|
||||||
|
|
||||||
const currentOffset = pulseOffsets.value[agent.id] ?? 0
|
const len = p.length
|
||||||
const speed = speeds[agent.id] ?? p.length / 3000
|
if (len <= 0) continue
|
||||||
const newOffset = currentOffset + speed * dt
|
|
||||||
const cycleLen = p.length + 12
|
const currentOffset = pulseOffsets.value[id] ?? 0
|
||||||
pulseOffsets.value[agent.id] = newOffset > cycleLen ? newOffset % cycleLen : newOffset
|
const newOffset = currentOffset + (speeds[id] ?? len / 3000) * dt
|
||||||
pulseEl.setAttribute('stroke-dashoffset', String(-pulseOffsets.value[agent.id]))
|
const cycleLen = len + 10
|
||||||
|
pulseOffsets.value[id] = newOffset > cycleLen ? newOffset % cycleLen : newOffset
|
||||||
|
|
||||||
|
pulseEl.setAttribute('stroke-dashoffset', String(-pulseOffsets.value[id]))
|
||||||
}
|
}
|
||||||
|
|
||||||
animFrameId = requestAnimationFrame(tick)
|
animFrameId = requestAnimationFrame(tick)
|
||||||
@@ -208,13 +237,17 @@ let resizeObserver: ResizeObserver | null = null
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePositions()
|
updatePositions()
|
||||||
|
|
||||||
|
// Wait for SVG to render so path refs are populated
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePositions()
|
updatePositions()
|
||||||
refreshPathLengths()
|
refreshPathLengths()
|
||||||
|
|
||||||
startPulseAnimation()
|
startPulseAnimation()
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
updatePositions()
|
updatePositions()
|
||||||
|
// Paths changed — recalculate lengths and dasharrays
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
refreshPathLengths()
|
refreshPathLengths()
|
||||||
})
|
})
|
||||||
@@ -231,39 +264,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="networkRef" class="team-network">
|
<div ref="networkRef" class="ai-team-network">
|
||||||
<!-- 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" />
|
|
||||||
<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 Connection Layer -->
|
<!-- SVG Connection Layer -->
|
||||||
<svg
|
<svg
|
||||||
v-if="svgWidth > 0 && svgHeight > 0"
|
v-if="svgWidth > 0 && svgHeight > 0"
|
||||||
@@ -275,7 +276,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<filter
|
<filter
|
||||||
v-for="agent in agents"
|
v-for="agent in childAgents"
|
||||||
:key="`glow-${agent.id}`"
|
:key="`glow-${agent.id}`"
|
||||||
:id="`glow-${agent.id}`"
|
:id="`glow-${agent.id}`"
|
||||||
x="-30%" y="-30%" width="160%" height="160%"
|
x="-30%" y="-30%" width="160%" height="160%"
|
||||||
@@ -289,22 +290,23 @@ onUnmounted(() => {
|
|||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<template v-for="agent in agents" :key="agent.id">
|
<!-- Connection lines for each agent -->
|
||||||
<!-- Base connection line -->
|
<template v-for="agent in childAgents" :key="agent.id">
|
||||||
|
<!-- Base line -->
|
||||||
<path
|
<path
|
||||||
v-if="connectionPaths[agent.id]"
|
v-if="connectionPaths[agent.id]"
|
||||||
:ref="storePathRef(agent.id)"
|
:ref="storePathRef(agent.id)"
|
||||||
:d="connectionPaths[agent.id]!.d"
|
:d="connectionPaths[agent.id]!.d"
|
||||||
:stroke="agent.color"
|
:stroke="agent.color"
|
||||||
:stroke-width="agent.active ? 2.5 : 1.5"
|
:stroke-width="isActive(agent.id) ? 2.5 : 1.5"
|
||||||
fill="none"
|
fill="none"
|
||||||
:opacity="agent.active ? 0.7 : 0.25"
|
:opacity="isActive(agent.id) ? 0.7 : 0.25"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Glow for active agents -->
|
<!-- Glow line for active agent -->
|
||||||
<path
|
<path
|
||||||
v-if="agent.active && connectionPaths[agent.id]"
|
v-if="isActive(agent.id) && connectionPaths[agent.id]"
|
||||||
:d="connectionPaths[agent.id]!.d"
|
:d="connectionPaths[agent.id]!.d"
|
||||||
:stroke="agent.color"
|
:stroke="agent.color"
|
||||||
stroke-width="4"
|
stroke-width="4"
|
||||||
@@ -314,7 +316,7 @@ onUnmounted(() => {
|
|||||||
opacity="0.5"
|
opacity="0.5"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Moving pulse dot -->
|
<!-- Pulse line (white dashed segment moving along) -->
|
||||||
<path
|
<path
|
||||||
v-if="connectionPaths[agent.id]"
|
v-if="connectionPaths[agent.id]"
|
||||||
:ref="storePulseRef(agent.id)"
|
:ref="storePulseRef(agent.id)"
|
||||||
@@ -324,203 +326,279 @@ onUnmounted(() => {
|
|||||||
fill="none"
|
fill="none"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
:opacity="agent.active ? 1 : 0.4"
|
:opacity="isActive(agent.id) ? 1 : 0.4"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Agent Grid (dynamic: 2 columns, extends downward) -->
|
<!-- Cards Layer (above SVG) -->
|
||||||
<div class="agents-grid">
|
<div class="cards-layer">
|
||||||
<div
|
<!-- Hero: Iris centered top -->
|
||||||
v-for="agent in agents"
|
<div class="hero-slot" data-agent-id="iris">
|
||||||
:key="agent.id"
|
<article
|
||||||
:data-agent-id="agent.id"
|
class="agent-card hero-card"
|
||||||
class="agent-slot"
|
:style="{
|
||||||
>
|
'--card-color': hero.color,
|
||||||
<AgentNode
|
...(isActive(hero.id) ? {
|
||||||
:agent="agent"
|
boxShadow: `0 0 20px ${hero.color}44`,
|
||||||
:runtime="getAgentRuntime(agent.id)"
|
borderColor: hero.color
|
||||||
@select="onAgentSelect"
|
} : {})
|
||||||
/>
|
}"
|
||||||
|
@click="emit('select', hero.id)"
|
||||||
|
>
|
||||||
|
<div class="card-main">
|
||||||
|
<div class="card-icon-wrap" :style="{ background: `${hero.color}18`, color: hero.color }">
|
||||||
|
<component :is="resolveIcon(hero.icon)" :size="20" />
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-name-row">
|
||||||
|
<h3 class="card-name">{{ hero.name }}</h3>
|
||||||
|
<span class="card-role-tag" :style="{ background: `${hero.color}18`, color: hero.color, borderColor: `${hero.color}30` }">{{ hero.role }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">{{ hero.description }}</p>
|
||||||
|
<span v-if="hero.task" class="node-task">
|
||||||
|
<span class="node-task-dot">●</span>
|
||||||
|
{{ hero.task }}
|
||||||
|
</span>
|
||||||
|
<div class="card-tags">
|
||||||
|
<span v-for="tag in hero.tags" :key="tag" class="card-tag" :style="{ background: `${hero.color}18`, color: hero.color }">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-action">
|
||||||
|
<span>ROLE CARD</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span v-if="hero.runtime" class="node-runtime">{{ hero.runtime }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Agent Grid: 2 columns x 2 rows -->
|
||||||
|
<div class="agent-grid">
|
||||||
|
<div
|
||||||
|
v-for="agent in childAgents"
|
||||||
|
:key="agent.id"
|
||||||
|
:data-agent-id="agent.id"
|
||||||
|
class="agent-slot"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
class="agent-card"
|
||||||
|
:style="{
|
||||||
|
'--card-color': agent.color,
|
||||||
|
...(isActive(agent.id) ? {
|
||||||
|
boxShadow: `0 0 14px ${agent.color}55, 0 0 30px ${agent.color}22`,
|
||||||
|
borderColor: agent.color
|
||||||
|
} : {})
|
||||||
|
}"
|
||||||
|
@click="emit('select', agent.id)"
|
||||||
|
>
|
||||||
|
<div class="card-main">
|
||||||
|
<div class="card-icon-wrap" :style="{ background: `${agent.color}18`, color: agent.color }">
|
||||||
|
<component :is="resolveIcon(agent.icon)" :size="18" />
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-name-row">
|
||||||
|
<h3 class="card-name">{{ agent.name }}</h3>
|
||||||
|
<span class="card-role-tag" :style="{ background: `${agent.color}18`, color: agent.color, borderColor: `${agent.color}30` }">{{ agent.role }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="card-desc">{{ agent.description }}</p>
|
||||||
|
<span v-if="agent.task" class="node-task">
|
||||||
|
<span class="node-task-dot">●</span>
|
||||||
|
{{ agent.task }}
|
||||||
|
</span>
|
||||||
|
<div class="card-tags">
|
||||||
|
<span v-for="tag in agent.tags" :key="tag" class="card-tag" :style="{ background: `${agent.color}18`, color: agent.color }">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer-action">
|
||||||
|
<span>ROLE CARD</span>
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<span v-if="agent.runtime" class="node-runtime">{{ agent.runtime }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.team-network {
|
.ai-team-network {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 24px 20px 20px;
|
background: transparent;
|
||||||
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;
|
|
||||||
}
|
|
||||||
.team-network:hover {
|
|
||||||
border-color: rgba(139, 124, 246, 0.14);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Iris Node */
|
|
||||||
.iris-node {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
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;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 320px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
.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 Connection Lines */
|
|
||||||
.network-svg {
|
.network-svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Agent Grid — dynamic, 2 columns, extends downward as agents grow */
|
.cards-layer {
|
||||||
.agents-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
margin-top: 8px;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
.agent-slot {
|
align-items: center;
|
||||||
width: 100%;
|
gap: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus Banner */
|
.hero-slot {
|
||||||
.focus-banner {
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 820px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-slot {
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Agent Card (inlined from old AgentCard.vue) ── */
|
||||||
|
.agent-card {
|
||||||
|
background: var(--panel, #11141b);
|
||||||
|
border: 1px solid var(--line, #1f2330);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.agent-card:hover {
|
||||||
|
border-color: var(--card-color, #8b7cf6);
|
||||||
|
box-shadow: 0 0 16px color-mix(in srgb, var(--card-color, #8b7cf6) 10%, transparent);
|
||||||
|
}
|
||||||
|
.hero-card {
|
||||||
|
border-color: rgba(139, 124, 246, 0.2);
|
||||||
|
box-shadow: 0 0 20px rgba(139, 124, 246, 0.06);
|
||||||
|
}
|
||||||
|
.hero-card:hover {
|
||||||
|
border-color: #8b7cf6;
|
||||||
|
box-shadow: 0 0 24px rgba(139, 124, 246, 0.12);
|
||||||
|
}
|
||||||
|
.card-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.card-icon-wrap {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.card-name-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
margin-bottom: 5px;
|
||||||
padding: 10px 16px;
|
flex-wrap: wrap;
|
||||||
margin-top: 12px;
|
|
||||||
background: rgba(234, 179, 8, 0.05);
|
|
||||||
border: 1px solid rgba(234, 179, 8, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
.focus-icon {
|
.card-name {
|
||||||
color: #eab308;
|
margin: 0;
|
||||||
flex-shrink: 0;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e8eaf0;
|
||||||
}
|
}
|
||||||
.focus-banner span {
|
.card-role-tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 8.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.card-desc {
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
color: #eab308;
|
color: #7e8799;
|
||||||
line-height: 1.35;
|
line-height: 1.5;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.card-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.card-tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.card-footer-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--line, #1f2330);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7385;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
.card-footer-action .arrow {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.agent-card:hover .card-footer-action {
|
||||||
|
color: var(--card-color, #8b7cf6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
/* ── Node Task ── */
|
||||||
.agents-grid {
|
.node-task {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ea5b3;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.node-task-dot {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Node Runtime ── */
|
||||||
|
.node-runtime {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #6b7385;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.agent-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
.cards-layer {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user