Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3599513128 | |||
| 7dd8f53f2f | |||
| 90bb7251e3 | |||
| e57bef95e5 | |||
| 71b4465595 | |||
| 9b63e5368e | |||
| 8f265d00ba | |||
| 5a3a099b94 | |||
| 1f6f5dd08c | |||
| 6e532f64f5 | |||
| 7154c30b99 | |||
| ffe7baba78 | |||
| da9c256b43 | |||
| 1012d2c217 |
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { X } from '@lucide/vue'
|
import { X, ExternalLink } from '@lucide/vue'
|
||||||
import type { AgentNodeData } from '../../composables/useDashboardData'
|
import type { AgentNodeData } from '../../composables/useDashboardData'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -27,6 +27,9 @@ defineEmits<{
|
|||||||
<span class="modal-role">{{ agent.role }}</span>
|
<span class="modal-role">{{ agent.role }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a :href="`/agents/${agent.id}`" class="agent-link-btn" title="Open agent config">
|
||||||
|
<ExternalLink :size="14" />
|
||||||
|
</a>
|
||||||
<button class="modal-close-btn" @click="$emit('close')" aria-label="Close">
|
<button class="modal-close-btn" @click="$emit('close')" aria-label="Close">
|
||||||
<X :size="16" />
|
<X :size="16" />
|
||||||
</button>
|
</button>
|
||||||
@@ -71,6 +74,31 @@ defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Live Thinking -->
|
||||||
|
<section class="modal-section">
|
||||||
|
<h3 class="section-label">Live Thinking</h3>
|
||||||
|
<div class="thinking-panel">
|
||||||
|
<div class="thinking-stream" ref="thinkingStreamRef">
|
||||||
|
<div
|
||||||
|
v-for="(msg, idx) in agent.thinkingStream"
|
||||||
|
:key="idx"
|
||||||
|
class="thinking-entry"
|
||||||
|
:style="{ animationDelay: `${idx * 0.05}s` }"
|
||||||
|
>
|
||||||
|
<span class="entry-time">{{ msg.time }}</span>
|
||||||
|
<span class="entry-text">{{ msg.text }}</span>
|
||||||
|
<span class="thinking-dots" v-if="idx === agent.thinkingStream.length - 1">
|
||||||
|
<span class="thinking-dot blue"></span>
|
||||||
|
<span class="thinking-dot violet"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!agent.thinkingStream?.length" class="thinking-placeholder">
|
||||||
|
Waiting for thought stream...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Footer Stats -->
|
<!-- Footer Stats -->
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<span class="footer-badge">Runtime: {{ runtime }}</span>
|
<span class="footer-badge">Runtime: {{ runtime }}</span>
|
||||||
@@ -168,6 +196,25 @@ defineEmits<{
|
|||||||
color: #6b7385;
|
color: #6b7385;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.agent-link-btn {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7385;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.agent-link-btn:hover {
|
||||||
|
border-color: var(--agent-color);
|
||||||
|
color: var(--agent-color);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-close-btn {
|
.modal-close-btn {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -241,6 +288,77 @@ defineEmits<{
|
|||||||
transition: width 0.5s ease;
|
transition: width 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Live Thinking */
|
||||||
|
.thinking-panel {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(139, 124, 246, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
background: rgba(12, 16, 22, 0.6);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: panel-pulse 2.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes panel-pulse {
|
||||||
|
0%, 100% { border-color: rgba(139, 124, 246, 0.25); box-shadow: 0 0 12px rgba(139,124,246,0.08); }
|
||||||
|
50% { border-color: rgba(139, 124, 246, 0.5); box-shadow: 0 0 24px rgba(139,124,246,0.18), 0 0 40px rgba(139,124,246,0.06); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-dots {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
animation: spin-dots 2s linear infinite;
|
||||||
|
}
|
||||||
|
.thinking-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.thinking-dot.blue { background: #3b82f6; box-shadow: 0 0 8px #3b82f6; }
|
||||||
|
.thinking-dot.violet { background: #8b7cf6; box-shadow: 0 0 8px #8b7cf6; }
|
||||||
|
@keyframes spin-dots {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-stream {
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.thinking-entry {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: baseline;
|
||||||
|
animation: slide-in-right 0.3s ease-out both;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
@keyframes slide-in-right {
|
||||||
|
from { opacity: 0; transform: translateX(-16px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
.entry-time {
|
||||||
|
font-size: 8.5px;
|
||||||
|
color: #6b7385;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 42px;
|
||||||
|
}
|
||||||
|
.entry-text {
|
||||||
|
color: #9ea5b3;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.thinking-placeholder {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #4a5160;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* Working Feed */
|
/* Working Feed */
|
||||||
.work-feed {
|
.work-feed {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -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.38 + t * 0.24)
|
||||||
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 + 70
|
||||||
const cy2 = startY + dy * 0.7
|
const cp2x = endX + (isLeftColumn ? 35 : -35)
|
||||||
|
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">
|
||||||
|
<!-- Hero: Iris centered top -->
|
||||||
|
<div class="hero-slot" :data-agent-id="hero.id">
|
||||||
|
<article
|
||||||
|
class="agent-card hero-card"
|
||||||
|
:style="{
|
||||||
|
'--card-color': hero.color,
|
||||||
|
...(isActive(hero.id) ? {
|
||||||
|
boxShadow: `0 0 20px ${hero.color}44`,
|
||||||
|
borderColor: hero.color
|
||||||
|
} : {})
|
||||||
|
}"
|
||||||
|
@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
|
<div
|
||||||
v-for="agent in agents"
|
v-for="agent in childAgents"
|
||||||
:key="agent.id"
|
:key="agent.id"
|
||||||
:data-agent-id="agent.id"
|
:data-agent-id="agent.id"
|
||||||
class="agent-slot"
|
class="agent-slot"
|
||||||
>
|
>
|
||||||
<AgentNode
|
<article
|
||||||
:agent="agent"
|
class="agent-card"
|
||||||
:runtime="getAgentRuntime(agent.id)"
|
:style="{
|
||||||
@select="onAgentSelect"
|
'--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>
|
||||||
|
|
||||||
<!-- Focus Banner -->
|
|
||||||
<div v-if="irisFocus" class="focus-banner">
|
|
||||||
<Sparkles :size="12" class="focus-icon" />
|
|
||||||
<span>{{ irisFocus }}</span>
|
|
||||||
</div>
|
</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: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface AgentNodeData {
|
|||||||
active: boolean
|
active: boolean
|
||||||
runtimeSeconds: number
|
runtimeSeconds: number
|
||||||
workingFeed: string[]
|
workingFeed: string[]
|
||||||
|
thinkingStream?: Array<{ time: string; text: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MissionData {
|
export interface MissionData {
|
||||||
@@ -81,6 +82,7 @@ export function useDashboardData() {
|
|||||||
|
|
||||||
// Agent runtimes (simulated)
|
// Agent runtimes (simulated)
|
||||||
const agentStartTimes = reactive<Record<string, number>>({
|
const agentStartTimes = reactive<Record<string, number>>({
|
||||||
|
iris: now - 28800000,
|
||||||
developer: now - 3600000,
|
developer: now - 3600000,
|
||||||
devops: now - 1800000,
|
devops: now - 1800000,
|
||||||
researcher: now - 2700000,
|
researcher: now - 2700000,
|
||||||
@@ -98,6 +100,31 @@ export function useDashboardData() {
|
|||||||
|
|
||||||
// Agents
|
// Agents
|
||||||
const agents = ref<AgentNodeData[]>([
|
const agents = ref<AgentNodeData[]>([
|
||||||
|
{
|
||||||
|
id: 'iris',
|
||||||
|
name: 'Iris',
|
||||||
|
role: 'Chief of Staff',
|
||||||
|
description: 'Koordiniert, delegiert, hält das Team tight. Die erste Anlaufstelle zwischen Boss und Maschine.',
|
||||||
|
color: '#8b7cf6',
|
||||||
|
icon: 'bot',
|
||||||
|
currentTask: 'Orchestrating Nexus Dashboard redesign',
|
||||||
|
goal: 'Complete Mission Control v3',
|
||||||
|
progress: 85,
|
||||||
|
workload: 55,
|
||||||
|
active: true,
|
||||||
|
runtimeSeconds: 28800,
|
||||||
|
workingFeed: [
|
||||||
|
'Analyzed user feedback on Dashboard',
|
||||||
|
'Delegated card redesign to Developer',
|
||||||
|
'Verifying full-width layout deployment',
|
||||||
|
'Reviewing AgentModal integration',
|
||||||
|
],
|
||||||
|
thinkingStream: [
|
||||||
|
{ time: '22:24', text: 'Analysing constraint: full-width layout' },
|
||||||
|
{ time: '22:25', text: 'Removing max-width from global CSS' },
|
||||||
|
{ time: '22:26', text: 'Verifying Dashboard grid reflow' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'developer',
|
id: 'developer',
|
||||||
name: 'Developer',
|
name: 'Developer',
|
||||||
@@ -117,6 +144,11 @@ export function useDashboardData() {
|
|||||||
'Implementing room generation algorithm',
|
'Implementing room generation algorithm',
|
||||||
'Writing unit tests for RoomFactory',
|
'Writing unit tests for RoomFactory',
|
||||||
],
|
],
|
||||||
|
thinkingStream: [
|
||||||
|
{ time: '22:22', text: 'Parsing dungeon spec from Iris' },
|
||||||
|
{ time: '22:23', text: 'Designing RoomFactory interface' },
|
||||||
|
{ time: '22:24', text: 'Implementing corridor connection logic' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'devops',
|
id: 'devops',
|
||||||
@@ -137,6 +169,11 @@ export function useDashboardData() {
|
|||||||
'Added .dockerignore for node_modules',
|
'Added .dockerignore for node_modules',
|
||||||
'Testing incremental builds',
|
'Testing incremental builds',
|
||||||
],
|
],
|
||||||
|
thinkingStream: [
|
||||||
|
{ time: '22:20', text: 'Checking build cache hit rates' },
|
||||||
|
{ time: '22:21', text: 'Benchmarking multi-stage vs single-stage' },
|
||||||
|
{ time: '22:22', text: 'Calculating potential speedup from caching' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'researcher',
|
id: 'researcher',
|
||||||
@@ -156,6 +193,11 @@ export function useDashboardData() {
|
|||||||
'Documented SignalR limitations',
|
'Documented SignalR limitations',
|
||||||
'Prototyping WebSocket fallback',
|
'Prototyping WebSocket fallback',
|
||||||
],
|
],
|
||||||
|
thinkingStream: [
|
||||||
|
{ time: '22:18', text: 'Cross-referencing WebSocket latency benchmarks' },
|
||||||
|
{ time: '22:19', text: 'Checking SSE browser support matrix' },
|
||||||
|
{ time: '22:20', text: 'Drafting recommendation summary' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'reviewer',
|
id: 'reviewer',
|
||||||
@@ -176,6 +218,11 @@ export function useDashboardData() {
|
|||||||
'Approved RoomValidator',
|
'Approved RoomValidator',
|
||||||
'Running integration tests',
|
'Running integration tests',
|
||||||
],
|
],
|
||||||
|
thinkingStream: [
|
||||||
|
{ time: '22:15', text: 'Analyzing DungeonController PR diff' },
|
||||||
|
{ time: '22:16', text: 'Checking RoomValidator edge cases' },
|
||||||
|
{ time: '22:17', text: 'Verifying integration test coverage' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -58,10 +67,12 @@ function onQueueExecuteNow(id: string): void {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TeamNetwork
|
<TeamNetwork
|
||||||
|
hero-id="iris"
|
||||||
:agents="agents"
|
:agents="agents"
|
||||||
: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 +95,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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user