Files
nexus/frontend/src/composables/useFlowLayout.ts
T
reviewer 6cedd8410f
CI - Build & Test / Backend (.NET) (push) Failing after 23s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
refactor(frontend): deduplicate CSS keyframes, unify types, extract format utils, add UI states, trim mock data
- Remove duplicate @keyframes pulse-* from 3 component files (already in nexus-tokens.css)
- Rename AgentDetail → AgentDetailData in dashboard types to avoid collision with types/agent.ts
- Extract shared formatNumber/initials/formatTime to utils/format.ts
- Simplify FlowBoard: use agentStore modal/selection getters instead of duplicating local state
- Add error banner + empty state to IrisChat; add loading skeleton + error/empty states to TaskStrip
- Remove 105-line unused mockAgents array from useFlowLayout
- Reduce operations store fallbacks from hardcoded preview data to minimal safe defaults
- Update operations store tests to match lean fallback structure
- Net: -73 lines, cleaner imports, fewer magic strings
2026-06-12 17:02:50 +02:00

197 lines
5.3 KiB
TypeScript

/**
* useFlowLayout — Auto-Layout und Edge-Formeln für das V2 FlowCanvas
*
* Portiert von agents.js (design_handoff_nexus_v2).
* Enthält alle Positionslogik, Edge-Erzeugung und den Typ AgentNodeData.
*/
export interface Point {
x: number
y: number
}
export interface AgentNodeData {
id: string
name: string
role: string
roleBadge?: string
model: string
avatar: string
status: 'work' | 'think' | 'idle' | 'block'
statusLabel: string
task: string | null
goal: string | null
progress: number
elapsed: string
next: string
tokens: string
cost: string
think: string | null
handoff?: string
from?: string
links?: string[]
md?: string
}
export interface EdgeData {
a: string
b: string
kind: 'orch' | 'flow'
}
/**
* Auto-Layout Algorithmus
* Iris immer top-center (x:50%, y:14%).
* Andere Agenten in Reihen (maxPerRow variiert nach Gesamtzahl).
*/
export function autoLayout(agents: AgentNodeData[]): Record<string, Point> {
const positions: Record<string, Point> = { iris: { x: 50, y: 14 } }
const others = agents.filter(a => a.id !== 'iris')
const n = others.length
if (n === 0) return positions
const maxPerRow = n <= 2 ? 2 : n <= 6 ? 3 : n <= 9 ? 3 : 4
const numRows = Math.ceil(n / maxPerRow)
const yStart = n <= 3 ? 58 : 30
const yEnd = 86
const yVals = numRows === 1
? [yStart]
: Array.from({ length: numRows }, (_, i) => yStart + (yEnd - yStart) * i / (numRows - 1))
let idx = 0
yVals.forEach(y => {
const rowN = Math.min(maxPerRow, n - idx)
const xSpan = Math.min(72, 24 * (rowN - 1) + 18)
const xOff = 50 - xSpan / 2
const xStep = rowN > 1 ? xSpan / (rowN - 1) : 0
for (let ci = 0; ci < rowN; ci++, idx++) {
positions[others[idx].id] = { x: xOff + ci * xStep, y }
}
})
return positions
}
/**
* Erzeugt die Kantenliste basierend auf Agenten-Daten
* (gleiche Logik wie buildEdges in agents.js).
*/
export function buildEdges(agents: AgentNodeData[]): EdgeData[] {
const edges: EdgeData[] = []
// Orchestrierung: Iris → jeder Agent
agents.filter(a => a.id !== 'iris').forEach(a => {
edges.push({ a: 'iris', b: a.id, kind: 'orch' })
})
// Spezifische Flows (wenn Agent existiert)
const hasId = (id: string) => agents.some(a => a.id === id)
if (hasId('dev') && hasId('rev')) edges.push({ a: 'dev', b: 'rev', kind: 'flow' })
if (hasId('arch') && hasId('exec')) edges.push({ a: 'arch', b: 'exec', kind: 'flow' })
if (hasId('res')) edges.push({ a: 'res', b: 'iris', kind: 'flow' })
if (hasId('qa') && hasId('rev')) edges.push({ a: 'qa', b: 'rev', kind: 'flow' })
if (hasId('security')) edges.push({ a: 'security', b: 'iris', kind: 'flow' })
if (hasId('pm')) edges.push({ a: 'pm', b: 'iris', kind: 'flow' })
if (hasId('devops') && hasId('exec')) edges.push({ a: 'exec', b: 'devops', kind: 'flow' })
return edges
}
/**
* Bézier-Kurve zwischen zwei Punkten
* Verwendet die README.p1 und p2)) Formel:
* M p1 Q Kontrollpunkt p2
* mx,my = Mittelpunkt; off = min(50, hypot*0.14)
* len = hypot(-dy, dx) (Normale)
* cp = (mx + (-dy/len)*off, my + (dx/len)*off)
*/
export function curve(p1: Point, p2: Point): string {
const mx = (p1.x + p2.x) / 2
const my = (p1.y + p2.y) / 2
const dx = p2.x - p1.x
const dy = p2.y - p1.y
const off = Math.min(50, Math.hypot(dx, dy) * 0.14)
const len = Math.hypot(-dy, dx) || 1
const cx = mx + (-dy / len) * off
const cy = my + (dx / len) * off
return `M${p1.x},${p1.y} Q${cx},${cy} ${p2.x},${p2.y}`
}
/**
* Extra agents that can be added to the FlowBoard dynamically
*/
export const extraAgentPool: AgentNodeData[] = [
{
id: 'qa',
name: 'QA Automator',
role: 'Test Automation',
roleBadge: 'badge-cyan',
avatar: 'QA',
status: 'idle',
statusLabel: 'Bereit',
task: 'End-to-End Tests schreiben',
goal: '100% Coverage für auth/',
progress: 0,
elapsed: '—',
next: 'Testplan erstellen',
model: 'Deepseek V4 Flash',
tokens: '0',
cost: '0.00',
think: null,
},
{
id: 'devops',
name: 'DevOps',
role: 'CI/CD Pipeline',
roleBadge: 'badge-amber',
avatar: 'DO',
status: 'idle',
statusLabel: 'Bereit',
task: 'GitHub Actions Workflow',
goal: 'Automatisches Deploy auf merge',
progress: 0,
elapsed: '—',
next: 'Pipeline konfigurieren',
model: 'Deepseek V4 Pro',
tokens: '0',
cost: '0.00',
think: null,
},
{
id: 'security',
name: 'Security Scanner',
role: 'Security Analysis',
roleBadge: 'badge-rose',
avatar: 'SC',
status: 'think',
statusLabel: 'Scannt',
task: 'Dependency-Audit durchführen',
goal: 'CVEs in api/ aufdecken',
progress: 18,
elapsed: '00:01:44',
next: 'Report an Iris',
model: 'Deepseek V4 Pro',
tokens: '9k',
cost: '0.18',
think: 'Analysiere package-lock.json auf bekannte Vulnerabilities…',
},
{
id: 'pm',
name: 'Project Manager',
role: 'Coordination',
roleBadge: 'badge-purple',
avatar: 'PM',
status: 'think',
statusLabel: 'Plant',
task: 'Sprint-Retrospektive vorbereiten',
goal: 'Blockers identifizieren',
progress: 35,
elapsed: '00:05:10',
next: 'Meeting-Summary an Team',
model: 'Deepseek V4 Flash',
tokens: '14k',
cost: '0.24',
think: 'Analysiere Velocity-Daten der letzten 3 Sprints…',
},
]