feat(v2): Pinia stores (agents/tasks/chat) + live backend integration, remove mock data
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user