6cedd8410f
- 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
197 lines
5.3 KiB
TypeScript
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…',
|
|
},
|
|
]
|