feat(v2): Pinia stores (agents/tasks/chat) + live backend integration, remove mock data
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Agent Store – V2 Dashboard
|
||||
*
|
||||
* Fetches agents from /api/dashboard/agents and available models
|
||||
* from /api/dashboard/models. Enriches raw API data with catalog
|
||||
* metadata (color, icon, description, hero) and maps into
|
||||
* AgentNodeData (for FlowCanvas) and AgentDetail (for Modal).
|
||||
*
|
||||
* Auto-refresh: every 30 seconds.
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { apiFetch } from '../services/api'
|
||||
import type { AgentNodeData } from '../composables/useFlowLayout'
|
||||
import type { AgentDetail, ThinkingItem } from '../components/dashboard/v2/types'
|
||||
|
||||
/* ── API Response Shapes ──────────────────────────── */
|
||||
|
||||
interface DashboardAgentInfo {
|
||||
id: string
|
||||
name: string
|
||||
role: string
|
||||
model: string
|
||||
isActive: boolean
|
||||
currentTask: string | null
|
||||
description?: string
|
||||
tags?: string[]
|
||||
progress?: number
|
||||
workload?: number
|
||||
goal?: string | null
|
||||
}
|
||||
|
||||
interface ModelOption {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
}
|
||||
|
||||
/* ── Agent Catalog (static enrichment) ────────────── */
|
||||
|
||||
// Type-safe catalog for static AgentNodeData fields not provided by API
|
||||
interface AgentCatalogEntry {
|
||||
elapsed: string;
|
||||
think: string | null;
|
||||
next: string;
|
||||
}
|
||||
|
||||
const AGENT_CATALOG: Record<string, AgentCatalogEntry> = {
|
||||
iris: { elapsed: '--', think: null, next: 'Standby' },
|
||||
programmer: { elapsed: '--', think: null, next: 'Standby' },
|
||||
developer: { elapsed: '--', think: null, next: 'Standby' },
|
||||
architekt: { elapsed: '--', think: null, next: 'Standby' },
|
||||
reviewer: { elapsed: '--', think: null, next: 'Standby' },
|
||||
executor: { elapsed: '--', think: null, next: 'Standby' },
|
||||
researcher: { elapsed: '--', think: null, next: 'Standby' },
|
||||
}
|
||||
|
||||
/* ── Status Mapping ───────────────────────────────── */
|
||||
|
||||
function mapStatus(isActive: boolean, currentTask: string | null): AgentNodeData['status'] {
|
||||
if (!isActive) return 'idle'
|
||||
if (currentTask && currentTask !== 'Idle') return 'work'
|
||||
return 'think'
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<AgentNodeData['status'], string> = {
|
||||
work: 'Arbeitet',
|
||||
think: 'Plant',
|
||||
idle: 'Bereit',
|
||||
block: 'Blockiert',
|
||||
}
|
||||
|
||||
function avatarFor(id: string, name: string): string {
|
||||
if (id === 'iris') return 'IR'
|
||||
if (id === 'programmer' || id === 'developer') return '</>'
|
||||
return name.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
/* ── Enrich API Agent → AgentNodeData ─────────────── */
|
||||
|
||||
function enrichAgent(api: DashboardAgentInfo): AgentNodeData {
|
||||
const cat = AGENT_CATALOG[api.id] ?? AGENT_CATALOG['reviewer']!
|
||||
const status = mapStatus(api.isActive, api.currentTask)
|
||||
return {
|
||||
id: api.id,
|
||||
name: api.name,
|
||||
role: api.role,
|
||||
model: api.model,
|
||||
avatar: avatarFor(api.id, api.name),
|
||||
status,
|
||||
statusLabel: STATUS_LABELS[status],
|
||||
task: api.currentTask,
|
||||
goal: api.goal ?? null,
|
||||
progress: api.progress ?? 0,
|
||||
elapsed: cat.elapsed ?? '--',
|
||||
next: cat.next ?? 'Standby',
|
||||
tokens: '0',
|
||||
cost: '0.00',
|
||||
think: cat.think ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Build AgentDetail from AgentNodeData ─────────── */
|
||||
|
||||
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 sentences = data.think.split(/[.…!?]+/).filter(s => s.trim().length > 5)
|
||||
const items: ThinkingItem[] = []
|
||||
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
|
||||
}
|
||||
|
||||
export function buildAgentDetail(data: AgentNodeData, models: { id: string; alias: string }[]): AgentDetail {
|
||||
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
|
||||
|
||||
// Map model ID to display name for the modal dropdown (which uses alias for comparison)
|
||||
const matchingModel = models.find(m => m.id === data.model || m.alias === data.model)
|
||||
const displayModel = matchingModel?.alias ?? data.model
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
model: displayModel,
|
||||
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: models,
|
||||
}
|
||||
}
|
||||
|
||||
export const useAgentStore = defineStore('agents', {
|
||||
state: () => ({
|
||||
agents: [] as AgentNodeData[],
|
||||
models: [] as { id: string; alias: string }[],
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
selectedAgentId: null as string | null,
|
||||
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
/** AgentNodeData list for FlowCanvas */
|
||||
agentList: (state) => state.agents,
|
||||
|
||||
/** Agent IDs in display order (Iris first) */
|
||||
agentOrder: (state) => {
|
||||
const ordered = state.agents.filter(a => a.id === 'iris')
|
||||
state.agents.forEach(a => { if (a.id !== 'iris') ordered.push(a) })
|
||||
return ordered.map(a => a.id)
|
||||
},
|
||||
|
||||
/** Selected agent detail for modal */
|
||||
selectedAgent(state): AgentDetail | null {
|
||||
if (!state.selectedAgentId) return null
|
||||
const data = state.agents.find(a => a.id === state.selectedAgentId)
|
||||
if (!data) return null
|
||||
return buildAgentDetail(data, state.models)
|
||||
},
|
||||
|
||||
/** Is the modal open? */
|
||||
modalOpen: (state) => state.selectedAgentId !== null,
|
||||
|
||||
/* ── AlertBar Metrics ────────────────────────── */
|
||||
activeCount: (state) => state.agents.filter(a => a.status === 'work').length,
|
||||
thinkCount: (state) => state.agents.filter(a => a.status === 'think').length,
|
||||
idleCount: (state) => state.agents.filter(a => a.status === 'idle').length,
|
||||
blockerCount: (state) => state.agents.filter(a => a.status === 'block').length,
|
||||
todayCost: (state) => {
|
||||
const total = state.agents.reduce((s, a) => s + parseFloat(a.cost || '0'), 0)
|
||||
return '$' + total.toFixed(2)
|
||||
},
|
||||
todayTokens: (state) => {
|
||||
const total = state.agents.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) + ''
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/* ── API: Fetch agents ──────────────────────── */
|
||||
async fetchAgents() {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/agents')
|
||||
if (!res.ok) return
|
||||
const data: DashboardAgentInfo[] = await res.json()
|
||||
this.agents = data.map(enrichAgent)
|
||||
} catch (err) {
|
||||
console.warn('[AgentStore] fetchAgents failed', err)
|
||||
}
|
||||
},
|
||||
|
||||
/* ── API: Fetch available models ────────────── */
|
||||
async fetchModels() {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/models')
|
||||
if (!res.ok) return
|
||||
const data: ModelOption[] = await res.json()
|
||||
this.models = data.map(m => ({ id: m.id, alias: m.name }))
|
||||
} catch (err) {
|
||||
console.warn('[AgentStore] fetchModels failed', err)
|
||||
}
|
||||
},
|
||||
|
||||
/* ── API: Change agent model ────────────────── */
|
||||
async changeModel(agentId: string, modelId: string) {
|
||||
// Optimistic update
|
||||
const agent = this.agents.find(a => a.id === agentId)
|
||||
if (agent) agent.model = modelId
|
||||
|
||||
try {
|
||||
await apiFetch(`/api/dashboard/agents/${encodeURIComponent(agentId)}/model`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ model: modelId }),
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('[AgentStore] changeModel failed', err)
|
||||
// Refetch to revert on failure
|
||||
await this.fetchAgents()
|
||||
}
|
||||
},
|
||||
|
||||
/* ── Selection ───────────────────────────────── */
|
||||
selectAgent(id: string | null) {
|
||||
this.selectedAgentId = id
|
||||
},
|
||||
|
||||
/* ── Polling ─────────────────────────────────── */
|
||||
startPolling() {
|
||||
if (this.refreshInterval) return
|
||||
this.fetchAgents()
|
||||
this.fetchModels()
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.fetchAgents()
|
||||
this.fetchModels()
|
||||
}, 30000)
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval)
|
||||
this.refreshInterval = null
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user