feat(v2): IrisChat + TaskStrip components, mock data integration
This commit is contained in:
@@ -1,46 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* FlowBoard – Das neue V2 Dashboard
|
||||
* Main content area rendered inside NexusLayout.
|
||||
*
|
||||
* Layout:
|
||||
* Stage (AlertBar + FlowCanvas) + Rail (IrisChat) + TaskStrip (unten)
|
||||
*
|
||||
* Integriert V2-Komponenten: AlertBar, FlowCanvas, IrisChat, TaskStrip
|
||||
* mit Mock-Daten aus useFlowLayout / agents.js.
|
||||
*/
|
||||
import { useOperationsStore } from '../../stores/operations'
|
||||
import { ref, computed } from 'vue'
|
||||
import AlertBar from '../../components/dashboard/v2/AlertBar.vue'
|
||||
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
|
||||
import IrisChat from '../../components/dashboard/v2/IrisChat.vue'
|
||||
import type { ChatMessage } from '../../components/dashboard/v2/types'
|
||||
import TaskStrip from '../../components/dashboard/v2/TaskStrip.vue'
|
||||
import type { TaskItem } from '../../components/dashboard/v2/types'
|
||||
import { mockAgents, extraAgentPool } from '../../composables/useFlowLayout'
|
||||
import type { AgentNodeData } from '../../composables/useFlowLayout'
|
||||
|
||||
const store = useOperationsStore()
|
||||
/* ── Agent State ───────────────────────────────────── */
|
||||
const agents = ref<AgentNodeData[]>(mockAgents)
|
||||
const agentPositions = ref<Record<string, { x: number; y: number }>>({})
|
||||
const enteringIds = ref<string[]>([])
|
||||
|
||||
const agentPool = ref<AgentNodeData[]>(extraAgentPool)
|
||||
|
||||
function handleSelect(id: string) {
|
||||
console.log('[FlowBoard] selected agent:', id)
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
const pool = agentPool.value
|
||||
if (pool.length === 0) return
|
||||
const next = pool.shift()!
|
||||
enteringIds.value.push(next.id)
|
||||
agents.value.push(next)
|
||||
|
||||
// Remove "entering" after animation settles
|
||||
setTimeout(() => {
|
||||
const idx = enteringIds.value.indexOf(next.id)
|
||||
if (idx !== -1) enteringIds.value.splice(idx, 1)
|
||||
}, 600)
|
||||
}
|
||||
|
||||
function handleResetLayout() {
|
||||
agentPositions.value = {}
|
||||
}
|
||||
|
||||
function handleUpdatePositions(pos: Record<string, { x: number; y: number }>) {
|
||||
agentPositions.value = { ...pos }
|
||||
}
|
||||
|
||||
/* ── AlertBar computed props ──────────────────────── */
|
||||
const activeCount = computed(() => agents.value.filter(a => a.status === 'work').length)
|
||||
const thinkCount = computed(() => agents.value.filter(a => a.status === 'think').length)
|
||||
const idleCount = computed(() => agents.value.filter(a => a.status === 'idle').length)
|
||||
const blockerCount = computed(() => agents.value.filter(a => a.status === 'block').length)
|
||||
const todayCost = computed(() => {
|
||||
const total = agents.value.reduce((s, a) => s + parseFloat(a.cost || '0'), 0)
|
||||
return '$' + total.toFixed(2)
|
||||
})
|
||||
const todayTokens = computed(() => {
|
||||
const total = agents.value.reduce((s, a) => {
|
||||
const v = a.tokens?.replace(/[^0-9.]/g, '')
|
||||
return v ? s + parseFloat(v) : s
|
||||
}, 0)
|
||||
return total >= 1000 ? Math.round(total / 1000) + 'k' : Math.round(total) + ''
|
||||
})
|
||||
|
||||
function handleBlockerClick() {
|
||||
console.log('[FlowBoard] blocker clicked')
|
||||
}
|
||||
|
||||
/* ── IrisChat Mock Data ────────────────────────────── */
|
||||
const chatMessages = ref<ChatMessage[]>([
|
||||
{
|
||||
sender: 'iris',
|
||||
text: 'Guten Morgen. Status: 4 Agents aktiv, 1 Blocker. Der Healthcheck auf staging schlägt seit dem letzten Deploy fehl — ich habe das dem Executor mit P0 zugewiesen.',
|
||||
ts: '12:48',
|
||||
},
|
||||
{
|
||||
sender: 'user',
|
||||
text: 'Was ist die Ursache?',
|
||||
ts: '12:49',
|
||||
},
|
||||
{
|
||||
sender: 'iris',
|
||||
text: 'Reviewer hat in PR #142 eine fehlende Rate-Limit-Prüfung gefunden, die den /health Endpoint blockiert. Developer arbeitet bereits am Fix (72%).',
|
||||
ts: '12:49',
|
||||
tool: 'gelesen: reviews/pr-142.md',
|
||||
},
|
||||
{
|
||||
sender: 'user',
|
||||
text: 'Gut. Halte mich beim Deploy auf dem Laufenden.',
|
||||
ts: '12:51',
|
||||
},
|
||||
{
|
||||
sender: 'iris',
|
||||
text: 'Verstanden. Ich melde mich, sobald der Executor den Healthcheck verifiziert hat. Geschätzte Fertigstellung: ~6 Min.',
|
||||
ts: '12:51',
|
||||
},
|
||||
])
|
||||
|
||||
const isThinking = ref(false)
|
||||
|
||||
function handleChatSend(text: string) {
|
||||
// Add user message
|
||||
chatMessages.value.push({
|
||||
sender: 'user',
|
||||
text,
|
||||
ts: new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }),
|
||||
})
|
||||
|
||||
// Simulate thinking + Iris response
|
||||
isThinking.value = true
|
||||
setTimeout(() => {
|
||||
isThinking.value = false
|
||||
chatMessages.value.push({
|
||||
sender: 'iris',
|
||||
text: `👍 Ich habe Deine Anfrage erhalten: "${text}". Ich arbeite daran und melde mich mit Ergebnissen.`,
|
||||
ts: new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }),
|
||||
})
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
/* ── TaskStrip Mock Data ──────────────────────────── */
|
||||
const tasks = ref<TaskItem[]>([
|
||||
{
|
||||
id: 't1',
|
||||
title: 'Healthcheck schlägt auf staging fehl',
|
||||
agent: 'Executor',
|
||||
priority: 'high',
|
||||
status: 'active',
|
||||
progress: 88,
|
||||
},
|
||||
{
|
||||
id: 't2',
|
||||
title: 'Review PR #142 — Auth-Refactor',
|
||||
agent: 'Reviewer',
|
||||
priority: 'high',
|
||||
status: 'active',
|
||||
progress: 35,
|
||||
},
|
||||
{
|
||||
id: 't3',
|
||||
title: 'JWT-Rotation: Unit-Tests',
|
||||
agent: 'Developer',
|
||||
priority: 'high',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: 't4',
|
||||
title: 'Terraform-Plan VPS-Skalierung',
|
||||
agent: 'Architekt',
|
||||
priority: 'medium',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: 't5',
|
||||
title: 'Standup-Report generieren',
|
||||
agent: 'Iris',
|
||||
priority: 'medium',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: 't6',
|
||||
title: 'LLM-Cost-Benchmark',
|
||||
agent: 'Researcher',
|
||||
priority: 'low',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flow-board">
|
||||
<!-- Placeholder: V2 Dashboard Content -->
|
||||
<div class="board-hero">
|
||||
<h2 class="board-title">Flow Board</h2>
|
||||
<p class="board-subtitle">Real-time Agent Operations Dashboard</p>
|
||||
</div>
|
||||
<!-- Stage + Rail row -->
|
||||
<div class="board-body">
|
||||
<!-- Stage: AlertBar + FlowCanvas + TaskStrip -->
|
||||
<div class="stage">
|
||||
<AlertBar
|
||||
:active-count="activeCount"
|
||||
:think-count="thinkCount"
|
||||
:idle-count="idleCount"
|
||||
:blocker-count="blockerCount"
|
||||
:today-cost="todayCost"
|
||||
:today-tokens="todayTokens"
|
||||
@blocker-click="handleBlockerClick"
|
||||
/>
|
||||
|
||||
<!-- Metrics Row -->
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Connected Agents</span>
|
||||
<strong class="metric-value">{{ store.snapshot.metrics.runningAgents ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Queued Tasks</span>
|
||||
<strong class="metric-value">{{ store.snapshot.metrics.queuedTasks ?? 0 }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Incidents</span>
|
||||
<strong class="metric-value">{{ store.snapshot.metrics.incidents ?? 0 }}</strong>
|
||||
<FlowCanvas
|
||||
:agents="agents"
|
||||
:positions="agentPositions"
|
||||
:entering-ids="enteringIds"
|
||||
@select="handleSelect"
|
||||
@add="handleAdd"
|
||||
@reset-layout="handleResetLayout"
|
||||
@update-positions="handleUpdatePositions"
|
||||
/>
|
||||
|
||||
<TaskStrip :tasks="tasks" />
|
||||
</div>
|
||||
|
||||
<!-- Rail: IrisChat -->
|
||||
<IrisChat
|
||||
:messages="chatMessages"
|
||||
:is-thinking="isThinking"
|
||||
@send="handleChatSend"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flow-board {
|
||||
animation: fade-in 0.35s ease-out;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
min-height: 0;
|
||||
animation: fade-in 0.35s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
@@ -48,53 +230,21 @@ const store = useOperationsStore()
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.board-hero {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.board-title {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
color: var(--tx);
|
||||
}
|
||||
|
||||
.board-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--tx-3);
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.metrics-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
.board-body {
|
||||
flex: 1;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--r);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .08em;
|
||||
color: var(--tx-3);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--tx);
|
||||
font-variant-numeric: tabular-nums;
|
||||
.stage {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 0 18px 0 0;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user