Compare commits

..

6 Commits

Author SHA1 Message Date
devops 1f6f5dd08c chore: bump version to v0.2.19 [skip ci] 2026-06-09 20:20:03 +00:00
developer 6e532f64f5 fix: AgentModal bei Klick auf Card verdrahtet
CI - Build & Test / Backend (.NET) (push) Successful in 26s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 17s
CI - Build & Test / Security Check (push) Successful in 3s
- @select Handler auf TeamNetwork
- selectedAgent State + onAgentSelect()
- AgentModal importiert und gerendert (v-if selectedAgent)
- Close via X oder Overlay
2026-06-09 22:19:12 +02:00
devops 7154c30b99 chore: bump version to v0.2.18 [skip ci] 2026-06-09 20:13:46 +00:00
developer ffe7baba78 fix: Vollbreite-Layout – max-width/margin entfernt, Content füllt Desktop-Breite
CI - Build & Test / Backend (.NET) (push) Successful in 24s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 2s
- .content: max-width:1320px + margin:auto entfernt
- padding reduziert: 36px 34px → 16px 16px
- Middle-Col (1fr) im Dashboard nutzt jetzt volle Restbreite
2026-06-09 22:12:55 +02:00
devops da9c256b43 chore: bump version to v0.2.17 [skip ci] 2026-06-09 20:10:25 +00:00
developer 1012d2c217 feat: altes AgentCard-Design in TeamNetwork + node-task / node-runtime
CI - Build & Test / Backend (.NET) (push) Successful in 26s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 17s
CI - Build & Test / Security Check (push) Successful in 3s
- Icon-Wrap + Name + Role-Tag + Description + Tags + Footer 'ROLE CARD →'
- node-task: aktuelle Aufgabe mit ●-Punkt (zwischen Description und Tags)
- node-runtime: Live-Runtime im Footer (tabular-nums)
- TeamNetwork background: transparent, Klasse → ai-team-network
- Modal-Klick erhalten
2026-06-09 22:09:02 +02:00
4 changed files with 375 additions and 280 deletions
+1 -1
View File
@@ -1 +1 @@
0.2.16 0.2.19
+355 -277
View File
@@ -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">&rarr;</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">&rarr;</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>
+1 -1
View File
@@ -41,7 +41,7 @@ main { min-width: 0; }
.connection.live { color: var(--green); } .connection.live { color: var(--green); }
.connection.preview { color: #e6b75d; } .connection.preview { color: #e6b75d; }
.ask, .refresh { display: flex; align-items: center; gap: 7px; padding: 8px 11px; border: 1px solid #37315e; border-radius: 7px; background: #18152a; color: #c4bbff; font-size: 10px; cursor: pointer; } .ask, .refresh { display: flex; align-items: center; gap: 7px; padding: 8px 11px; border: 1px solid #37315e; border-radius: 7px; background: #18152a; color: #c4bbff; font-size: 10px; cursor: pointer; }
.content { max-width: 1320px; margin: auto; padding: 36px 34px 60px; } .content { padding: 16px 16px 60px; }
.page-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 28px; } .page-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 28px; }
.eyebrow, .kicker { color: #7065c8; font-size: 9px; font-weight: 700; letter-spacing: .18em; } .eyebrow, .kicker { color: #7065c8; font-size: 9px; font-weight: 700; letter-spacing: .18em; }
h1 { margin: 7px 0 5px; font-size: 27px; letter-spacing: -.04em; } h1 { margin: 7px 0 5px; font-size: 27px; letter-spacing: -.04em; }
+18 -1
View File
@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import MissionCard from '../components/dashboard/MissionCard.vue' import MissionCard from '../components/dashboard/MissionCard.vue'
import OperationsFeed from '../components/dashboard/OperationsFeed.vue' import OperationsFeed from '../components/dashboard/OperationsFeed.vue'
import TeamNetwork from '../components/dashboard/TeamNetwork.vue' import TeamNetwork from '../components/dashboard/TeamNetwork.vue'
import ChatPanel from '../components/dashboard/ChatPanel.vue' import ChatPanel from '../components/dashboard/ChatPanel.vue'
import QueuePanel from '../components/dashboard/QueuePanel.vue' import QueuePanel from '../components/dashboard/QueuePanel.vue'
import AgentModal from '../components/dashboard/AgentModal.vue'
import { useDashboardData } from '../composables/useDashboardData' import { useDashboardData } from '../composables/useDashboardData'
import type { AgentNodeData } from '../../composables/useDashboardData'
const { const {
agents, missions, feedEntries, chatMessages, agents, missions, feedEntries, chatMessages,
@@ -14,6 +16,13 @@ const {
sendChat, removeQueueItem, moveQueueItem, changeQueuePriority, sendChat, removeQueueItem, moveQueueItem, changeQueuePriority,
} = useDashboardData() } = useDashboardData()
const selectedAgent = ref<AgentNodeData | null>(null)
function onAgentSelect(id: string) {
const agent = agents.value.find(a => a.id === id)
if (agent) selectedAgent.value = agent
}
onMounted(startRuntime) onMounted(startRuntime)
onUnmounted(stopRuntime) onUnmounted(stopRuntime)
@@ -62,6 +71,7 @@ function onQueueExecuteNow(id: string): void {
:iris-runtime="irisRuntime" :iris-runtime="irisRuntime"
:get-agent-runtime="getAgentRuntime" :get-agent-runtime="getAgentRuntime"
:iris-focus="irisFocus" :iris-focus="irisFocus"
@select="onAgentSelect"
/> />
<!-- Legend --> <!-- Legend -->
@@ -84,6 +94,13 @@ function onQueueExecuteNow(id: string): void {
<ChatPanel :messages="chatMessages" :iris-busy="irisBusy" :iris-focus="irisFocus" @send="onChatSend" /> <ChatPanel :messages="chatMessages" :iris-busy="irisBusy" :iris-focus="irisFocus" @send="onChatSend" />
<QueuePanel :items="queue" @remove="removeQueueItem" @move-up="onQueueMoveUp" @move-down="onQueueMoveDown" @change-priority="changeQueuePriority" @execute-now="onQueueExecuteNow" /> <QueuePanel :items="queue" @remove="removeQueueItem" @move-up="onQueueMoveUp" @move-down="onQueueMoveDown" @change-priority="changeQueuePriority" @execute-now="onQueueExecuteNow" />
</div> </div>
<AgentModal
v-if="selectedAgent"
:agent="selectedAgent"
:runtime="getAgentRuntime(selectedAgent.id)"
@close="selectedAgent = null"
/>
</div> </div>
</template> </template>