diff --git a/frontend/src/stores/agents.ts b/frontend/src/stores/agents.ts new file mode 100644 index 0000000..4c990e2 --- /dev/null +++ b/frontend/src/stores/agents.ts @@ -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 = { + 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 = { + 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 | 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 + } + }, + }, +}) diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts new file mode 100644 index 0000000..0ebb244 --- /dev/null +++ b/frontend/src/stores/chat.ts @@ -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 | 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 + } + }, + }, +}) diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts new file mode 100644 index 0000000..55d1eb0 --- /dev/null +++ b/frontend/src/stores/tasks.ts @@ -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 | 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 + } + }, + }, +}) diff --git a/frontend/src/views/Dashboard/FlowBoard.vue b/frontend/src/views/Dashboard/FlowBoard.vue index 8c51e2e..782dc1a 100644 --- a/frontend/src/views/Dashboard/FlowBoard.vue +++ b/frontend/src/views/Dashboard/FlowBoard.vue @@ -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(mockAgents) +/* ── Stores ──────────────────────────────────────── */ +const agentStore = useAgentStore() +const chatStore = useChatStore() +const taskStore = useTaskStore() + +/* ── Agent Layout State ───────────────────────────── */ const agentPositions = ref>({}) const enteringIds = ref([]) +const localAgentPool = ref([...extraAgentPool]) -const agentPool = ref(extraAgentPool) - -/* ── Agent Detail Modal State ──────────────────────── */ +/* ── Modal State ──────────────────────────────────── */ const selectedAgentId = ref(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(() => { - 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) { 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([ - { - 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([ - { - 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() +})