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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* Chat Store – V2 Dashboard
|
||||||
|
*
|
||||||
|
* Fetches chat messages from /api/dashboard/chat/messages and
|
||||||
|
* sends new messages via /api/dashboard/chat/send.
|
||||||
|
*
|
||||||
|
* Auto-refresh: every 10 seconds (incoming Iris messages).
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { apiFetch } from '../services/api'
|
||||||
|
import type { ChatMessage } from '../components/dashboard/v2/types'
|
||||||
|
|
||||||
|
/* ── API Response Shapes ──────────────────────────── */
|
||||||
|
|
||||||
|
interface MessageEntry {
|
||||||
|
role: string
|
||||||
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatResponse {
|
||||||
|
ok: boolean
|
||||||
|
reply: string | null
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChatStore = defineStore('chat', {
|
||||||
|
state: () => ({
|
||||||
|
messages: [] as ChatMessage[],
|
||||||
|
isThinking: false,
|
||||||
|
error: null as string | null,
|
||||||
|
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||||
|
/** Tracks last process timestamp to avoid duplicates */
|
||||||
|
lastProcessedTs: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
messageList: (state) => state.messages,
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/* ── API: Fetch history ─────────────────────── */
|
||||||
|
async fetchHistory() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/chat/messages?limit=50')
|
||||||
|
if (!res.ok) return
|
||||||
|
const data: MessageEntry[] = await res.json()
|
||||||
|
|
||||||
|
// Merge new messages (avoid duplicates)
|
||||||
|
let mostRecentTs = this.lastProcessedTs
|
||||||
|
for (const msg of data) {
|
||||||
|
const msgTs = new Date(msg.timestamp).getTime()
|
||||||
|
if (msgTs <= this.lastProcessedTs) continue
|
||||||
|
if (msgTs > mostRecentTs) mostRecentTs = msgTs
|
||||||
|
|
||||||
|
const sender = msg.role === 'assistant' ? 'iris' as const : 'user' as const
|
||||||
|
const tsFormatted = new Date(msg.timestamp).toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Avoid appending duplicates already present
|
||||||
|
const exists = this.messages.some(
|
||||||
|
m => m.sender === sender && m.text === msg.content && m.ts === tsFormatted
|
||||||
|
)
|
||||||
|
if (exists) continue
|
||||||
|
|
||||||
|
this.messages.push({
|
||||||
|
sender,
|
||||||
|
text: msg.content,
|
||||||
|
ts: tsFormatted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostRecentTs > this.lastProcessedTs) {
|
||||||
|
this.lastProcessedTs = mostRecentTs
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[ChatStore] fetchHistory failed', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── API: Send message ──────────────────────── */
|
||||||
|
async sendMessage(text: string) {
|
||||||
|
if (!text.trim()) return
|
||||||
|
|
||||||
|
const tsFormatted = new Date().toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Optimistic add: user message
|
||||||
|
this.messages.push({
|
||||||
|
sender: 'user',
|
||||||
|
text: text.trim(),
|
||||||
|
ts: tsFormatted,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isThinking = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/chat/send', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ message: text.trim() }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
const data: ChatResponse = await res.json()
|
||||||
|
|
||||||
|
if (data.ok && data.reply) {
|
||||||
|
this.messages.push({
|
||||||
|
sender: 'iris',
|
||||||
|
text: data.reply,
|
||||||
|
ts: new Date().toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} else if (data.error) {
|
||||||
|
this.messages.push({
|
||||||
|
sender: 'iris',
|
||||||
|
text: `⚠️ ${data.error}`,
|
||||||
|
ts: new Date().toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[ChatStore] sendMessage failed', err)
|
||||||
|
this.messages.push({
|
||||||
|
sender: 'iris',
|
||||||
|
text: '⚠️ Connection error. Please try again.',
|
||||||
|
ts: new Date().toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.isThinking = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Polling ─────────────────────────────────── */
|
||||||
|
startPolling() {
|
||||||
|
if (this.refreshInterval) return
|
||||||
|
this.fetchHistory()
|
||||||
|
this.refreshInterval = setInterval(() => {
|
||||||
|
this.fetchHistory()
|
||||||
|
}, 10000) // 10s for chat (more responsive)
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval)
|
||||||
|
this.refreshInterval = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* Task Store – V2 Dashboard
|
||||||
|
*
|
||||||
|
* Fetches tasks from /api/dashboard/tasks and maps them into
|
||||||
|
* TaskItem[] format for the TaskStrip component.
|
||||||
|
*
|
||||||
|
* Auto-refresh: every 30 seconds.
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { apiFetch } from '../services/api'
|
||||||
|
import type { TaskItem } from '../components/dashboard/v2/types'
|
||||||
|
|
||||||
|
/* ── API Response Shapes ──────────────────────────── */
|
||||||
|
|
||||||
|
interface DashboardTaskDto {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
detail: string | null
|
||||||
|
source: string
|
||||||
|
state: string
|
||||||
|
priority: string
|
||||||
|
assignedTo: string | null
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── State Mapping ────────────────────────────────── */
|
||||||
|
|
||||||
|
function mapPriority(priority: string): TaskItem['priority'] {
|
||||||
|
const p = priority.toLowerCase()
|
||||||
|
if (p === 'high' || p === 'critical' || p === 'urgent') return 'high'
|
||||||
|
if (p === 'low' || p === 'minor') return 'low'
|
||||||
|
return 'medium'
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapState(state: string): TaskItem['status'] {
|
||||||
|
const s = state.toLowerCase()
|
||||||
|
if (s === 'in progress' || s === 'active' || s === 'working') return 'active'
|
||||||
|
if (s === 'blocked' || s === 'block') return 'blocked'
|
||||||
|
return 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapProgress(state: string): number {
|
||||||
|
const s = state.toLowerCase()
|
||||||
|
if (s === 'in progress' || s === 'active' || s === 'working') return 50
|
||||||
|
if (s === 'done') return 100
|
||||||
|
if (s === 'blocked') return 30
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTask(t: DashboardTaskDto): TaskItem {
|
||||||
|
return {
|
||||||
|
id: t.id,
|
||||||
|
title: t.title,
|
||||||
|
agent: t.assignedTo ?? '—',
|
||||||
|
priority: mapPriority(t.priority),
|
||||||
|
status: mapState(t.state),
|
||||||
|
progress: mapProgress(t.state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTaskStore = defineStore('tasks', {
|
||||||
|
state: () => ({
|
||||||
|
tasks: [] as TaskItem[],
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
taskList: (state) => state.tasks,
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/* ── API: Fetch tasks ───────────────────────── */
|
||||||
|
async fetchTasks() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/tasks')
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
const data: DashboardTaskDto[] = await res.json()
|
||||||
|
this.tasks = data.map(mapTask)
|
||||||
|
this.error = null
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[TaskStore] fetchTasks failed', err)
|
||||||
|
this.error = 'Tasks could not be loaded'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── API: Add task ──────────────────────────── */
|
||||||
|
async addTask(title: string, detail?: string, priority?: string, assignedTo?: string) {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
detail: detail ?? null,
|
||||||
|
priority: priority ?? null,
|
||||||
|
assignedTo: assignedTo ?? null,
|
||||||
|
source: 'bao',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
// Refresh task list
|
||||||
|
await this.fetchTasks()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[TaskStore] addTask failed', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── API: Update task ───────────────────────── */
|
||||||
|
async updateTask(id: string, updates: { title?: string; detail?: string; priority?: string; assignedTo?: string }) {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`/api/dashboard/tasks/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
await this.fetchTasks()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[TaskStore] updateTask failed', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ── Polling ─────────────────────────────────── */
|
||||||
|
startPolling() {
|
||||||
|
if (this.refreshInterval) return
|
||||||
|
this.fetchTasks()
|
||||||
|
this.refreshInterval = setInterval(() => {
|
||||||
|
this.fetchTasks()
|
||||||
|
}, 30000)
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval)
|
||||||
|
this.refreshInterval = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -5,112 +5,62 @@
|
|||||||
* Layout:
|
* Layout:
|
||||||
* Stage (AlertBar + FlowCanvas) + Rail (IrisChat) + TaskStrip (unten)
|
* Stage (AlertBar + FlowCanvas) + Rail (IrisChat) + TaskStrip (unten)
|
||||||
*
|
*
|
||||||
* Integriert V2-Komponenten: AlertBar, FlowCanvas, IrisChat, TaskStrip
|
* Datenquellen:
|
||||||
* mit Mock-Daten aus useFlowLayout / agents.js.
|
* - 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 AlertBar from '../../components/dashboard/v2/AlertBar.vue'
|
||||||
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
|
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
|
||||||
import IrisChat from '../../components/dashboard/v2/IrisChat.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 TaskStrip from '../../components/dashboard/v2/TaskStrip.vue'
|
||||||
import type { TaskItem } from '../../components/dashboard/v2/types'
|
|
||||||
import AgentDetailModal from '../../components/dashboard/v2/AgentDetailModal.vue'
|
import AgentDetailModal from '../../components/dashboard/v2/AgentDetailModal.vue'
|
||||||
import type { AgentDetail, ThinkingItem } from '../../components/dashboard/v2/types'
|
import { buildAgentDetail } from '../../stores/agents'
|
||||||
import { mockAgents, extraAgentPool } from '../../composables/useFlowLayout'
|
|
||||||
import type { AgentNodeData } from '../../composables/useFlowLayout'
|
import type { AgentNodeData } from '../../composables/useFlowLayout'
|
||||||
|
import { extraAgentPool } from '../../composables/useFlowLayout'
|
||||||
|
|
||||||
/* ── Agent State ───────────────────────────────────── */
|
/* ── Stores ──────────────────────────────────────── */
|
||||||
const agents = ref<AgentNodeData[]>(mockAgents)
|
const agentStore = useAgentStore()
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
const taskStore = useTaskStore()
|
||||||
|
|
||||||
|
/* ── Agent Layout State ───────────────────────────── */
|
||||||
const agentPositions = ref<Record<string, { x: number; y: number }>>({})
|
const agentPositions = ref<Record<string, { x: number; y: number }>>({})
|
||||||
const enteringIds = ref<string[]>([])
|
const enteringIds = ref<string[]>([])
|
||||||
|
const localAgentPool = ref<AgentNodeData[]>([...extraAgentPool])
|
||||||
|
|
||||||
const agentPool = ref<AgentNodeData[]>(extraAgentPool)
|
/* ── Modal State ──────────────────────────────────── */
|
||||||
|
|
||||||
/* ── Agent Detail Modal State ──────────────────────── */
|
|
||||||
const selectedAgentId = ref<string | null>(null)
|
const selectedAgentId = ref<string | null>(null)
|
||||||
const modalOpen = computed(() => selectedAgentId.value !== null)
|
const modalOpen = computed(() => selectedAgentId.value !== null)
|
||||||
|
const agentOrder = computed(() => {
|
||||||
const agentOrder = computed(() => agents.value.map(a => a.id))
|
const ids = agentStore.agentList.map(a => a.id)
|
||||||
|
// Iris first
|
||||||
const availableModels = [
|
const irisIdx = ids.indexOf('iris')
|
||||||
{ id: 'deepseek-v4-flash', alias: 'Deepseek V4 Flash' },
|
if (irisIdx > 0) {
|
||||||
{ id: 'deepseek-v4-pro', alias: 'Deepseek V4 Pro' },
|
ids.splice(irisIdx, 1)
|
||||||
{ id: 'gpt-4o', alias: 'GPT-4o' },
|
ids.unshift('iris')
|
||||||
{ 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' })
|
|
||||||
}
|
}
|
||||||
|
return ids
|
||||||
// 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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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) {
|
function handleSelect(id: string) {
|
||||||
selectedAgentId.value = id
|
selectedAgentId.value = id
|
||||||
}
|
}
|
||||||
@@ -120,25 +70,23 @@ function handleCloseModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleAgentSelect(id: string) {
|
function handleAgentSelect(id: string) {
|
||||||
// Zum nächsten/vorherigen Agenten springen (Pfeiltasten)
|
|
||||||
selectedAgentId.value = id
|
selectedAgentId.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeModel(agentId: string, modelId: string) {
|
function handleChangeModel(agentId: string, modelAlias: string) {
|
||||||
const agent = agents.value.find(a => a.id === agentId)
|
// Modal emits the alias (display name); resolve to model ID for the API
|
||||||
if (agent) {
|
const model = agentStore.models.find(m => m.alias === modelAlias)
|
||||||
agent.model = modelId
|
const modelId = model?.id ?? modelAlias
|
||||||
}
|
agentStore.changeModel(agentId, modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
const pool = agentPool.value
|
const pool = localAgentPool.value
|
||||||
if (pool.length === 0) return
|
if (pool.length === 0) return
|
||||||
const next = pool.shift()!
|
const next = pool.shift()!
|
||||||
enteringIds.value.push(next.id)
|
enteringIds.value.push(next.id)
|
||||||
agents.value.push(next)
|
agentStore.agents.push(next)
|
||||||
|
|
||||||
// Remove "entering" after animation settles
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const idx = enteringIds.value.indexOf(next.id)
|
const idx = enteringIds.value.indexOf(next.id)
|
||||||
if (idx !== -1) enteringIds.value.splice(idx, 1)
|
if (idx !== -1) enteringIds.value.splice(idx, 1)
|
||||||
@@ -153,131 +101,26 @@ function handleUpdatePositions(pos: Record<string, { x: number; y: number }>) {
|
|||||||
agentPositions.value = { ...pos }
|
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() {
|
function handleBlockerClick() {
|
||||||
console.log('[FlowBoard] blocker clicked')
|
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) {
|
function handleChatSend(text: string) {
|
||||||
// Add user message
|
chatStore.sendMessage(text)
|
||||||
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 ──────────────────────────── */
|
/* ── Lifecycle ────────────────────────────────────── */
|
||||||
const tasks = ref<TaskItem[]>([
|
onMounted(() => {
|
||||||
{
|
agentStore.startPolling()
|
||||||
id: 't1',
|
chatStore.startPolling()
|
||||||
title: 'Healthcheck schlägt auf staging fehl',
|
taskStore.startPolling()
|
||||||
agent: 'Executor',
|
})
|
||||||
priority: 'high',
|
|
||||||
status: 'active',
|
onUnmounted(() => {
|
||||||
progress: 88,
|
agentStore.stopPolling()
|
||||||
},
|
chatStore.stopPolling()
|
||||||
{
|
taskStore.stopPolling()
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -287,17 +130,17 @@ const tasks = ref<TaskItem[]>([
|
|||||||
<!-- Stage: AlertBar + FlowCanvas + TaskStrip -->
|
<!-- Stage: AlertBar + FlowCanvas + TaskStrip -->
|
||||||
<div class="stage">
|
<div class="stage">
|
||||||
<AlertBar
|
<AlertBar
|
||||||
:active-count="activeCount"
|
:active-count="agentStore.activeCount"
|
||||||
:think-count="thinkCount"
|
:think-count="agentStore.thinkCount"
|
||||||
:idle-count="idleCount"
|
:idle-count="agentStore.idleCount"
|
||||||
:blocker-count="blockerCount"
|
:blocker-count="agentStore.blockerCount"
|
||||||
:today-cost="todayCost"
|
:today-cost="agentStore.todayCost"
|
||||||
:today-tokens="todayTokens"
|
:today-tokens="agentStore.todayTokens"
|
||||||
@blocker-click="handleBlockerClick"
|
@blocker-click="handleBlockerClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FlowCanvas
|
<FlowCanvas
|
||||||
:agents="agents"
|
:agents="agentStore.agentList"
|
||||||
:positions="agentPositions"
|
:positions="agentPositions"
|
||||||
:entering-ids="enteringIds"
|
:entering-ids="enteringIds"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
@@ -306,13 +149,13 @@ const tasks = ref<TaskItem[]>([
|
|||||||
@update-positions="handleUpdatePositions"
|
@update-positions="handleUpdatePositions"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TaskStrip :tasks="tasks" />
|
<TaskStrip :tasks="taskStore.taskList" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rail: IrisChat -->
|
<!-- Rail: IrisChat -->
|
||||||
<IrisChat
|
<IrisChat
|
||||||
:messages="chatMessages"
|
:messages="chatStore.messageList"
|
||||||
:is-thinking="isThinking"
|
:is-thinking="chatStore.isThinking"
|
||||||
@send="handleChatSend"
|
@send="handleChatSend"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user