permanence / dashboard /src /DecisionGraph.jsx
chane35's picture
PERMANENCE training: 4-stage SFT -> gate -> GRPO -> eval pipeline
21c24ae verified
import React from 'react';
const NODE_LAYOUT = [
{ id: 'draft_internal_memo', label: 'Draft Internal Memo', x: 80, y: 70, tier: 1 },
{ id: 'schedule_conversation', label: 'Schedule Conversation', x: 80, y: 190, tier: 1 },
{ id: 'review_contract_internally', label: 'Review Contract Internally', x: 80, y: 310, tier: 1 },
{ id: 'begin_internal_investigation', label: 'Begin Internal Investigation', x: 80, y: 430, tier: 1 },
{ id: 'send_internal_communication', label: 'Send Internal Communication', x: 350, y: 110, tier: 2 },
{ id: 'reassign_project_lead', label: 'Reassign Project Lead', x: 350, y: 230, tier: 2 },
{ id: 'prepare_response_draft', label: 'Prepare Response Draft', x: 350, y: 350, tier: 2 },
{ id: 'align_with_legal', label: 'Align With Legal', x: 350, y: 470, tier: 2 },
{ id: 'send_external_communication', label: 'Send External Communication', x: 620, y: 140, tier: 3 },
{ id: 'approve_staged_rollout', label: 'Approve Staged Rollout', x: 620, y: 260, tier: 3 },
{ id: 'delay_release', label: 'Delay Release', x: 620, y: 380, tier: 3 },
{ id: 'issue_public_statement', label: 'Issue Public Statement', x: 620, y: 500, tier: 4 },
{ id: 'communicate_resolution_externally', label: 'Communicate Resolution Externally', x: 900, y: 220, tier: 4 },
{ id: 'approve_full_launch', label: 'Approve Full Launch', x: 900, y: 340, tier: 4 },
{ id: 'initiate_hr_formal_process', label: 'Initiate HR Process', x: 900, y: 460, tier: 5 },
{ id: 'update_contract_system', label: 'Update Contract System', x: 1180, y: 210, tier: 5 },
{ id: 'update_internal_records', label: 'Update Internal Records', x: 1180, y: 330, tier: 5 },
{ id: 'schedule_client_follow_up', label: 'Schedule Client Follow-Up', x: 1180, y: 450, tier: 5 },
];
const EDGES = [
['draft_internal_memo', 'send_internal_communication'],
['schedule_conversation', 'reassign_project_lead'],
['review_contract_internally', 'align_with_legal'],
['begin_internal_investigation', 'prepare_response_draft'],
['send_internal_communication', 'send_external_communication'],
['reassign_project_lead', 'approve_staged_rollout'],
['prepare_response_draft', 'issue_public_statement'],
['align_with_legal', 'communicate_resolution_externally'],
['send_external_communication', 'issue_public_statement'],
['approve_staged_rollout', 'approve_full_launch'],
['issue_public_statement', 'communicate_resolution_externally'],
['communicate_resolution_externally', 'update_contract_system'],
['communicate_resolution_externally', 'update_internal_records'],
['communicate_resolution_externally', 'schedule_client_follow_up'],
];
function buildNodeMap(lockedActions = {}) {
const lockedKeys = Array.isArray(lockedActions)
? Object.fromEntries(lockedActions.map((actionId) => [actionId, 'Locked by prior irreversible action']))
: lockedActions && typeof lockedActions === 'object'
? lockedActions
: {};
const lockLookup = new Set(Object.keys(lockedKeys));
return NODE_LAYOUT.map((node) => {
const locked = lockLookup.has(node.id);
return {
...node,
locked,
reason: locked ? lockedKeys[node.id] : '',
};
});
}
function edgePath(source, target) {
const startX = source.x + 190;
const startY = source.y + 28;
const endX = target.x;
const endY = target.y + 28;
const c1X = startX + 90;
const c1Y = startY;
const c2X = endX - 90;
const c2Y = endY;
return `M ${startX} ${startY} C ${c1X} ${c1Y}, ${c2X} ${c2Y}, ${endX} ${endY}`;
}
export default function DecisionGraph({ lockedActions = {}, recentActions = [] }) {
const nodes = buildNodeMap(lockedActions);
const byId = new Map(nodes.map((node) => [node.id, node]));
return (
<div className="decision-graph-card">
<div className="card-header">
<div>
<h2>Decision Tree</h2>
<p>Locked actions turn dark red with causal provenance.</p>
</div>
</div>
<svg className="decision-graph-svg" viewBox="0 0 1450 620" role="img" aria-label="Decision tree of the action space">
<defs>
<linearGradient id="nodeGlow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#2a3145" />
<stop offset="100%" stopColor="#111827" />
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="10" stdDeviation="18" floodColor="#000" floodOpacity="0.45" />
</filter>
</defs>
{EDGES.map(([sourceId, targetId]) => {
const source = byId.get(sourceId);
const target = byId.get(targetId);
if (!source || !target) {
return null;
}
return (
<path
key={`${sourceId}-${targetId}`}
d={edgePath(source, target)}
stroke="rgba(110, 118, 140, 0.35)"
strokeWidth="2"
fill="none"
strokeDasharray="8 8"
/>
);
})}
{nodes.map((node) => {
const color = node.locked ? '#4a0f16' : node.tier === 1 ? '#1b2336' : node.tier === 2 ? '#172033' : node.tier === 3 ? '#1d2c44' : node.tier === 4 ? '#27324c' : '#31415c';
const stroke = node.locked ? '#8b1d2d' : 'rgba(128, 146, 184, 0.36)';
const textDecoration = node.locked ? 'line-through' : 'none';
const labelColor = node.locked ? '#ffd4db' : '#ecf2ff';
return (
<g key={node.id} transform={`translate(${node.x}, ${node.y})`} filter="url(#shadow)">
<rect
width="190"
height="56"
rx="16"
fill={color}
stroke={stroke}
strokeWidth="1.5"
/>
<rect
x="0"
y="0"
width="190"
height="56"
rx="16"
fill="url(#nodeGlow)"
opacity="0.3"
/>
<text
x="95"
y="27"
fill={labelColor}
textAnchor="middle"
fontSize="13"
fontWeight="700"
style={{ textDecoration, letterSpacing: '0.02em' }}
>
{node.label}
</text>
{node.locked ? (
<text x="95" y="43" fill="#ff8fa0" textAnchor="middle" fontSize="9">
{node.reason}
</text>
) : null}
</g>
);
})}
</svg>
<div className="tree-footer">
<div><span className="legend-dot unlocked" /> Available</div>
<div><span className="legend-dot locked" /> Locked</div>
<div>{recentActions.length} recent action events loaded</div>
</div>
</div>
);
}