feat(v2): Pinia stores (agents/tasks/chat) + live backend integration, remove mock data
CI - Build & Test / Backend (.NET) (push) Failing after 22s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 15s
CI - Build & Test / Security Check (push) Successful in 3s

This commit is contained in:
2026-06-12 00:57:28 +02:00
parent 9330de7af0
commit 676dbd7589
4 changed files with 648 additions and 227 deletions
+70 -227
View File
@@ -5,112 +5,62 @@
* Layout:
* Stage (AlertBar + FlowCanvas) + Rail (IrisChat) + TaskStrip (unten)
*
* Integriert V2-Komponenten: AlertBar, FlowCanvas, IrisChat, TaskStrip
* mit Mock-Daten aus useFlowLayout / agents.js.
* Datenquellen:
* - AgentStore: agents, models, AlertBar-Metriken, Modal-Status
* - ChatStore: messages, isThinking, sendMessage()
* - TaskStore: tasks
*
* Polling startet bei Mount, stoppt bei Unmount.
*/
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useAgentStore } from '../../stores/agents'
import { useChatStore } from '../../stores/chat'
import { useTaskStore } from '../../stores/tasks'
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 AgentDetailModal from '../../components/dashboard/v2/AgentDetailModal.vue'
import type { AgentDetail, ThinkingItem } from '../../components/dashboard/v2/types'
import { mockAgents, extraAgentPool } from '../../composables/useFlowLayout'
import { buildAgentDetail } from '../../stores/agents'
import type { AgentNodeData } from '../../composables/useFlowLayout'
import { extraAgentPool } from '../../composables/useFlowLayout'
/* ── Agent State ───────────────────────────────────── */
const agents = ref<AgentNodeData[]>(mockAgents)
/* ── Stores ──────────────────────────────────────── */
const agentStore = useAgentStore()
const chatStore = useChatStore()
const taskStore = useTaskStore()
/* ── Agent Layout State ───────────────────────────── */
const agentPositions = ref<Record<string, { x: number; y: number }>>({})
const enteringIds = ref<string[]>([])
const localAgentPool = ref<AgentNodeData[]>([...extraAgentPool])
const agentPool = ref<AgentNodeData[]>(extraAgentPool)
/* ── Agent Detail Modal State ──────────────────────── */
/* ── Modal State ──────────────────────────────────── */
const selectedAgentId = ref<string | null>(null)
const modalOpen = computed(() => selectedAgentId.value !== null)
const agentOrder = computed(() => agents.value.map(a => a.id))
const availableModels = [
{ id: 'deepseek-v4-flash', alias: 'Deepseek V4 Flash' },
{ id: 'deepseek-v4-pro', alias: 'Deepseek V4 Pro' },
{ id: 'gpt-4o', alias: 'GPT-4o' },
{ id: 'claude-35-sonnet', alias: 'Claude 3.5 Sonnet' },
]
/**
* Erzeugt simulierte Thinking-Items aus einem AgentNodeData.think-String.
*/
function buildThinkingItems(data: AgentNodeData): ThinkingItem[] {
if (!data.think) return []
const now = new Date()
const ts = (ago: number) => {
const d = new Date(now.getTime() - ago * 1000)
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
const agentOrder = computed(() => {
const ids = agentStore.agentList.map(a => a.id)
// Iris first
const irisIdx = ids.indexOf('iris')
if (irisIdx > 0) {
ids.splice(irisIdx, 1)
ids.unshift('iris')
}
// Aus dem think-String mehrere simulierte Items erzeugen
const items: ThinkingItem[] = []
const sentences = data.think.split(/[.…!?]+/).filter(s => s.trim().length > 5)
if (sentences.length >= 2) {
items.push({ type: 'thought', text: sentences[0].trim() + '.', ts: ts(30) })
items.push({ type: 'action', text: sentences[1].trim() + '…', ts: ts(18) })
if (sentences.length >= 3) {
items.push({ type: 'result', text: sentences[sentences.length - 1].trim() + '.', ts: ts(3) })
} else {
items.push({ type: 'result', text: 'Verarbeitung abgeschlossen.', ts: ts(3) })
}
} else if (sentences.length === 1) {
items.push({ type: 'thought', text: sentences[0].trim(), ts: ts(15) })
items.push({ type: 'action', text: 'Analysiere Daten und erstelle nächsten Schritt…', ts: ts(6) })
} else {
items.push({ type: 'thought', text: data.think, ts: ts(10) })
}
return items
}
/**
* Konvertiert AgentNodeData → AgentDetail für das Modal.
*/
function buildAgentDetail(data: AgentNodeData): AgentDetail {
// Aus tokens/cost die numerischen Werte extrahieren
const tokenNum = parseFloat(data.tokens?.replace(/[^0-9.]/g, '') || '0')
const tokenMultiplier = data.tokens?.includes('M') ? 1_000_000 : data.tokens?.includes('k') ? 1_000 : 1
const tokensToday = Math.round(tokenNum * tokenMultiplier)
const costNum = parseFloat(data.cost || '0')
const progress = data.progress || 0
return {
id: data.id,
name: data.name,
role: data.role,
model: data.model,
status: data.status === 'block' ? 'idle' : data.status,
tokensToday,
costToday: costNum,
workload: progress,
uptime: data.elapsed || '—',
lastActive: data.elapsed !== '—' ? 'Vor ' + data.elapsed : 'Nicht aktiv',
activeTaskCount: data.task ? 1 : 0,
thinking: buildThinkingItems(data),
availableModels,
}
}
const selectedAgent = computed<AgentDetail | null>(() => {
if (!selectedAgentId.value) return null
const data = agents.value.find(a => a.id === selectedAgentId.value)
if (!data) return null
return buildAgentDetail(data)
return ids
})
const selectedAgent = computed(() => {
if (!selectedAgentId.value) return null
const data = agentStore.agentList.find(a => a.id === selectedAgentId.value)
if (!data) return null
// Build AgentDetail using the store's available models
return agentStore.models.length > 0
? buildAgentDetail(data, agentStore.models)
: null
})
/* ── Event Handlers ───────────────────────────────── */
function handleSelect(id: string) {
selectedAgentId.value = id
}
@@ -120,25 +70,23 @@ function handleCloseModal() {
}
function handleAgentSelect(id: string) {
// Zum nächsten/vorherigen Agenten springen (Pfeiltasten)
selectedAgentId.value = id
}
function handleChangeModel(agentId: string, modelId: string) {
const agent = agents.value.find(a => a.id === agentId)
if (agent) {
agent.model = modelId
}
function handleChangeModel(agentId: string, modelAlias: string) {
// Modal emits the alias (display name); resolve to model ID for the API
const model = agentStore.models.find(m => m.alias === modelAlias)
const modelId = model?.id ?? modelAlias
agentStore.changeModel(agentId, modelId)
}
function handleAdd() {
const pool = agentPool.value
const pool = localAgentPool.value
if (pool.length === 0) return
const next = pool.shift()!
enteringIds.value.push(next.id)
agents.value.push(next)
agentStore.agents.push(next)
// Remove "entering" after animation settles
setTimeout(() => {
const idx = enteringIds.value.indexOf(next.id)
if (idx !== -1) enteringIds.value.splice(idx, 1)
@@ -153,131 +101,26 @@ 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 raw = a.tokens?.replace(/[^0-9.]/g, '') || '0'
const v = parseFloat(raw)
return Number.isFinite(v) ? s + 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)
chatStore.sendMessage(text)
}
/* ── 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,
},
])
/* ── Lifecycle ────────────────────────────────────── */
onMounted(() => {
agentStore.startPolling()
chatStore.startPolling()
taskStore.startPolling()
})
onUnmounted(() => {
agentStore.stopPolling()
chatStore.stopPolling()
taskStore.stopPolling()
})
</script>
<template>
@@ -287,17 +130,17 @@ const tasks = ref<TaskItem[]>([
<!-- 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"
:active-count="agentStore.activeCount"
:think-count="agentStore.thinkCount"
:idle-count="agentStore.idleCount"
:blocker-count="agentStore.blockerCount"
:today-cost="agentStore.todayCost"
:today-tokens="agentStore.todayTokens"
@blocker-click="handleBlockerClick"
/>
<FlowCanvas
:agents="agents"
:agents="agentStore.agentList"
:positions="agentPositions"
:entering-ids="enteringIds"
@select="handleSelect"
@@ -306,13 +149,13 @@ const tasks = ref<TaskItem[]>([
@update-positions="handleUpdatePositions"
/>
<TaskStrip :tasks="tasks" />
<TaskStrip :tasks="taskStore.taskList" />
</div>
<!-- Rail: IrisChat -->
<IrisChat
:messages="chatMessages"
:is-thinking="isThinking"
:messages="chatStore.messageList"
:is-thinking="chatStore.isThinking"
@send="handleChatSend"
/>
</div>