a5cbe98f25
- fetchChatMessages merged statt replace (verhindert Poll-wipe) - Chat/Send bereits korrekt via agentId=iris - Chat/Messages nutzt jetzt agent:iris:main als Session-Key - Cron-Job deaktiviert (verhinderte Selbst-Konversation)
488 lines
12 KiB
TypeScript
488 lines
12 KiB
TypeScript
import { ref, computed } from 'vue'
|
||
|
||
// ── Shared State (singleton: same state regardless of how many times useDashboardData() is called) ──
|
||
const sessionStart = Date.now()
|
||
|
||
// Intervals registry for cleanup
|
||
const intervals: ReturnType<typeof setInterval>[] = []
|
||
let cleanupRegistered = false
|
||
|
||
// ── Interfaces (exported for components) ──
|
||
|
||
export interface AgentNodeData {
|
||
id: string
|
||
name: string
|
||
role: string
|
||
description: string
|
||
tags: string[]
|
||
color: string
|
||
icon: string
|
||
model?: string
|
||
hero?: boolean
|
||
currentTask: string
|
||
goal: string
|
||
progress: number
|
||
workload: number // 0-100
|
||
active: boolean
|
||
runtimeSeconds: number
|
||
workingFeed: Array<{ time: string; text: string }>
|
||
thinkingStream?: Array<{ time: string; text: string }>
|
||
}
|
||
|
||
export interface OpenTask {
|
||
id: string
|
||
title: string
|
||
detail: string
|
||
source: 'bao' | 'iris'
|
||
createdAt: string
|
||
}
|
||
|
||
export interface FeedEntry {
|
||
time: string
|
||
agent: string
|
||
action: string
|
||
timestamp: string
|
||
}
|
||
|
||
export interface ChatMessage {
|
||
id: string
|
||
sender: 'user' | 'iris'
|
||
text: string
|
||
timestamp: number
|
||
}
|
||
|
||
export interface QueueItem {
|
||
id: string
|
||
text: string
|
||
priority: 'high' | 'medium' | 'low'
|
||
waitTime: string
|
||
}
|
||
|
||
// ── API Response Interfaces ──
|
||
|
||
interface DashboardStatusResponse {
|
||
gatewayOk: boolean
|
||
irisStatus: string
|
||
activeAgents: number
|
||
pendingTasks: number
|
||
}
|
||
|
||
interface DashboardAgentInfo {
|
||
id: string
|
||
name: string
|
||
role: string
|
||
model: string
|
||
isActive: boolean
|
||
currentTask: string
|
||
}
|
||
|
||
interface DashboardOperationEntry {
|
||
agent: string
|
||
action: string
|
||
timestamp: string
|
||
time: string
|
||
}
|
||
|
||
interface DashboardChatMessage {
|
||
role: 'user' | 'assistant'
|
||
content: string
|
||
timestamp: string
|
||
}
|
||
|
||
interface DashboardSendResponse {
|
||
ok: boolean
|
||
reply?: string
|
||
error?: string
|
||
}
|
||
|
||
interface DashboardQueueItem {
|
||
id: string
|
||
name: string
|
||
status: string
|
||
}
|
||
|
||
// ── Agent Catalog (static enrichment) ──
|
||
|
||
const AGENT_CATALOG: Record<string, Partial<AgentNodeData>> = {
|
||
iris: {
|
||
description: 'Koordiniert, delegiert, hält das Team tight. Die erste Anlaufstelle zwischen Boss und Maschine.',
|
||
tags: ['Orchestration', 'Delegation', 'Approval'],
|
||
color: '#8b7cf6',
|
||
icon: 'bot',
|
||
hero: true,
|
||
goal: 'Complete Mission Control v3',
|
||
progress: 85,
|
||
workload: 55,
|
||
workingFeed: [],
|
||
thinkingStream: [],
|
||
},
|
||
developer: {
|
||
description: 'Implements features across the stack with TypeScript, C#, and Vue.',
|
||
tags: ['Coding', 'Development', 'Builds'],
|
||
color: '#3b82f6',
|
||
icon: 'code',
|
||
goal: 'Complete Dungeon CRUD + room generation',
|
||
progress: 62,
|
||
workload: 65,
|
||
workingFeed: [],
|
||
thinkingStream: [],
|
||
},
|
||
devops: {
|
||
description: 'Manages Docker, deployment pipelines, and system reliability.',
|
||
tags: ['Deployment', 'Docker', 'CI/CD'],
|
||
color: '#eab308',
|
||
icon: 'server',
|
||
goal: 'Reduce build times by 40%',
|
||
progress: 45,
|
||
workload: 40,
|
||
workingFeed: [],
|
||
thinkingStream: [],
|
||
},
|
||
researcher: {
|
||
description: 'Researches APIs, patterns, and best practices. Maintains docs.',
|
||
tags: ['Research', 'Analysis', 'Docs'],
|
||
color: '#22c55e',
|
||
icon: 'search',
|
||
goal: 'Recommend real-time communication strategy',
|
||
progress: 30,
|
||
workload: 25,
|
||
workingFeed: [],
|
||
thinkingStream: [],
|
||
},
|
||
reviewer: {
|
||
description: 'Reviews pull requests, enforces standards, runs test suites.',
|
||
tags: ['Code Review', 'Testing', 'Quality'],
|
||
color: '#a855f7',
|
||
icon: 'shield',
|
||
goal: 'Zero critical findings before merge',
|
||
progress: 80,
|
||
workload: 50,
|
||
workingFeed: [],
|
||
thinkingStream: [],
|
||
},
|
||
}
|
||
|
||
function enrichAgent(api: DashboardAgentInfo): AgentNodeData {
|
||
const catalog = AGENT_CATALOG[api.id] ?? AGENT_CATALOG['developer']
|
||
return {
|
||
id: api.id,
|
||
name: api.name,
|
||
role: api.role,
|
||
model: api.model,
|
||
currentTask: api.currentTask ?? 'Idle',
|
||
active: api.isActive,
|
||
description: catalog.description ?? '',
|
||
tags: catalog.tags ?? [],
|
||
color: catalog.color ?? '#6b7385',
|
||
icon: catalog.icon ?? 'bot',
|
||
hero: catalog.hero ?? false,
|
||
goal: catalog.goal ?? 'No goal set',
|
||
progress: catalog.progress ?? 0,
|
||
workload: catalog.workload ?? 0,
|
||
runtimeSeconds: 0,
|
||
workingFeed: catalog.workingFeed ?? [],
|
||
thinkingStream: catalog.thinkingStream ?? [],
|
||
}
|
||
}
|
||
|
||
// ── Helper: API Fetch with auth ──
|
||
|
||
async function apiFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||
const base = '' // same-origin proxy
|
||
return fetch(`${base}${path}`, {
|
||
...init,
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(init.headers as Record<string, string> ?? {}),
|
||
},
|
||
})
|
||
}
|
||
|
||
// ── State ──
|
||
|
||
// Status
|
||
const gatewayOk = ref(true)
|
||
const irisStatus = ref('Active')
|
||
const activeAgents = ref(0)
|
||
const pendingTasks = ref(0)
|
||
|
||
// Agents
|
||
const agents = ref<AgentNodeData[]>([])
|
||
|
||
// Chat
|
||
const chatMessages = ref<ChatMessage[]>([])
|
||
const irisBusy = ref(false)
|
||
const irisFocus = ref('')
|
||
|
||
// Operations Feed
|
||
const feedEntries = ref<FeedEntry[]>([])
|
||
|
||
// Open Tasks (mock only – no API endpoint)
|
||
const openTasks = ref<OpenTask[]>([
|
||
{ id: 't1', title: 'Agent Thinking Panel visualisieren', detail: 'Live-Animation der Denkprozesse im AgentModal', source: 'iris', createdAt: '22:30' },
|
||
{ id: 't2', title: 'CI/CD Pipeline Monitoring Dashboard', detail: 'Echtzeit-Status der Gitea Actions im Dashboard', source: 'iris', createdAt: '21:15' },
|
||
{ id: 't3', title: 'Dungeon System Dokumentation', detail: 'API-Doku für Room-Generation-Endpunkte schreiben', source: 'bao', createdAt: '20:00' },
|
||
])
|
||
|
||
// Queue
|
||
const queue = ref<QueueItem[]>([])
|
||
|
||
// Runtime
|
||
const runtimeSeconds = ref(0)
|
||
let runtimeInterval: ReturnType<typeof setInterval> | null = null
|
||
|
||
// ── Fetch Functions ──
|
||
|
||
async function fetchStatus(): Promise<void> {
|
||
try {
|
||
const res = await apiFetch('/api/dashboard/status')
|
||
if (!res.ok) return
|
||
const data: DashboardStatusResponse = await res.json()
|
||
gatewayOk.value = data.gatewayOk
|
||
irisStatus.value = data.irisStatus
|
||
activeAgents.value = data.activeAgents
|
||
pendingTasks.value = data.pendingTasks
|
||
} catch {
|
||
// API unreachable – keep current values
|
||
}
|
||
}
|
||
|
||
async function fetchAgents(): Promise<void> {
|
||
try {
|
||
const res = await apiFetch('/api/dashboard/agents')
|
||
if (!res.ok) return
|
||
const data: DashboardAgentInfo[] = await res.json()
|
||
agents.value = data.map(enrichAgent)
|
||
} catch {
|
||
// API unreachable – keep current values
|
||
}
|
||
}
|
||
|
||
async function fetchOperations(): Promise<void> {
|
||
try {
|
||
const res = await apiFetch('/api/dashboard/operations?limit=20')
|
||
if (!res.ok) return
|
||
const data: DashboardOperationEntry[] = await res.json()
|
||
feedEntries.value = data.map((entry) => ({
|
||
time: entry.time,
|
||
agent: entry.agent,
|
||
action: entry.action,
|
||
timestamp: entry.timestamp,
|
||
}))
|
||
} catch {
|
||
// API unreachable – keep current values
|
||
}
|
||
}
|
||
|
||
async function fetchChatMessages(): Promise<void> {
|
||
try {
|
||
const res = await apiFetch('/api/dashboard/chat/messages?limit=50')
|
||
if (!res.ok) return
|
||
const data: DashboardChatMessage[] = await res.json()
|
||
// Merge instead of replace — only add messages not already present
|
||
const existingTexts = new Set(chatMessages.value.map(m => m.text))
|
||
const existingTimestamps = new Set(chatMessages.value.map(m => m.timestamp))
|
||
for (const msg of data) {
|
||
const msgTime = new Date(msg.timestamp).getTime()
|
||
if (existingTexts.has(msg.content) && existingTimestamps.has(msgTime)) continue
|
||
chatMessages.value.push({
|
||
id: `msg-${msgTime}-${msg.role}`,
|
||
sender: msg.role === 'assistant' ? 'iris' : 'user',
|
||
text: msg.content,
|
||
timestamp: msgTime,
|
||
})
|
||
}
|
||
} catch {
|
||
// API unreachable – keep current values
|
||
}
|
||
}
|
||
|
||
async function fetchQueue(): Promise<void> {
|
||
try {
|
||
const res = await apiFetch('/api/dashboard/queue')
|
||
if (!res.ok) return
|
||
const data: DashboardQueueItem[] = await res.json()
|
||
queue.value = data.map((item) => ({
|
||
id: item.id,
|
||
text: item.name,
|
||
priority: (item.status === 'high' || item.status === 'medium' || item.status === 'low')
|
||
? item.status as 'high' | 'medium' | 'low'
|
||
: 'medium',
|
||
waitTime: '--',
|
||
}))
|
||
} catch {
|
||
// API unreachable – keep current values
|
||
}
|
||
}
|
||
|
||
// ── Chat Send ──
|
||
|
||
async function sendChatMessage(text: string): Promise<void> {
|
||
if (!text.trim()) return
|
||
|
||
// Optimistic add
|
||
chatMessages.value.push({
|
||
id: `user-${Date.now()}`,
|
||
sender: 'user',
|
||
text: text.trim(),
|
||
timestamp: Date.now(),
|
||
})
|
||
|
||
irisBusy.value = true
|
||
|
||
try {
|
||
const res = await apiFetch('/api/dashboard/chat/send', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ message: text.trim() }),
|
||
})
|
||
const data: DashboardSendResponse = await res.json()
|
||
|
||
if (data.ok && data.reply) {
|
||
chatMessages.value.push({
|
||
id: `iris-${Date.now()}`,
|
||
sender: 'iris',
|
||
text: data.reply,
|
||
timestamp: Date.now(),
|
||
})
|
||
} else if (data.error) {
|
||
chatMessages.value.push({
|
||
id: `error-${Date.now()}`,
|
||
sender: 'iris',
|
||
text: `⚠️ ${data.error}`,
|
||
timestamp: Date.now(),
|
||
})
|
||
}
|
||
} catch {
|
||
chatMessages.value.push({
|
||
id: `error-${Date.now()}`,
|
||
sender: 'iris',
|
||
text: '⚠️ Connection error. Please try again.',
|
||
timestamp: Date.now(),
|
||
})
|
||
} finally {
|
||
irisBusy.value = false
|
||
irisFocus.value = text.trim()
|
||
}
|
||
}
|
||
|
||
// ── Queue Operations ──
|
||
|
||
function removeQueueItem(id: string): void {
|
||
const idx = queue.value.findIndex(q => q.id === id)
|
||
if (idx !== -1) queue.value.splice(idx, 1)
|
||
}
|
||
|
||
function moveQueueItem(fromIdx: number, toIdx: number): void {
|
||
if (toIdx < 0 || toIdx >= queue.value.length) return
|
||
const [item] = queue.value.splice(fromIdx, 1)
|
||
queue.value.splice(toIdx, 0, item)
|
||
}
|
||
|
||
function changeQueuePriority(id: string, priority: QueueItem['priority']): void {
|
||
const item = queue.value.find(q => q.id === id)
|
||
if (item) item.priority = priority
|
||
}
|
||
|
||
// ── Runtime ──
|
||
|
||
function startRuntime(): void {
|
||
const startTs = sessionStart
|
||
runtimeSeconds.value = Math.floor((Date.now() - startTs) / 1000)
|
||
runtimeInterval = setInterval(() => {
|
||
runtimeSeconds.value = Math.floor((Date.now() - startTs) / 1000)
|
||
}, 1000)
|
||
}
|
||
|
||
function stopRuntime(): void {
|
||
if (runtimeInterval) {
|
||
clearInterval(runtimeInterval)
|
||
runtimeInterval = null
|
||
}
|
||
}
|
||
|
||
const formatRuntime = (seconds: number): string => {
|
||
const m = Math.floor(seconds / 60)
|
||
const s = seconds % 60
|
||
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||
}
|
||
|
||
const irisRuntime = computed(() => formatRuntime(runtimeSeconds.value))
|
||
|
||
const getAgentRuntime = (_id: string): string => {
|
||
// Could be extended to track per-agent runtimes from API
|
||
return formatRuntime(runtimeSeconds.value)
|
||
}
|
||
|
||
// ── Polling starten (nur einmal) ──
|
||
|
||
function startPolling(): void {
|
||
if (cleanupRegistered) return
|
||
cleanupRegistered = true
|
||
|
||
// Initial fetches
|
||
fetchStatus()
|
||
fetchAgents()
|
||
fetchOperations()
|
||
fetchChatMessages()
|
||
fetchQueue()
|
||
|
||
// Polling intervals
|
||
intervals.push(setInterval(fetchStatus, 5000))
|
||
intervals.push(setInterval(fetchAgents, 10000))
|
||
intervals.push(setInterval(fetchOperations, 10000))
|
||
intervals.push(setInterval(fetchChatMessages, 3000))
|
||
intervals.push(setInterval(fetchQueue, 10000))
|
||
}
|
||
|
||
function stopPolling(): void {
|
||
for (const interval of intervals) {
|
||
clearInterval(interval)
|
||
}
|
||
intervals.length = 0
|
||
cleanupRegistered = false
|
||
}
|
||
|
||
// ── Composable Export ──
|
||
|
||
export function useDashboardData() {
|
||
// Start polling on first call
|
||
startPolling()
|
||
|
||
return {
|
||
// State
|
||
agents,
|
||
openTasks,
|
||
feedEntries,
|
||
chatMessages,
|
||
irisBusy,
|
||
irisFocus,
|
||
irisRuntime,
|
||
queue,
|
||
gatewayOk,
|
||
irisStatus,
|
||
pendingTasks,
|
||
activeAgents,
|
||
|
||
// Runtime
|
||
runtimeSeconds,
|
||
getAgentRuntime,
|
||
startRuntime,
|
||
stopRuntime,
|
||
formatRuntime,
|
||
|
||
// Actions
|
||
sendChatMessage,
|
||
removeQueueItem,
|
||
moveQueueItem,
|
||
changeQueuePriority,
|
||
|
||
// Fetch (for manual refresh)
|
||
fetchStatus,
|
||
fetchAgents,
|
||
fetchOperations,
|
||
fetchChatMessages,
|
||
fetchQueue,
|
||
}
|
||
}
|