@agenticmail/enterprise 0.5.214 → 0.5.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,336 +2,245 @@ import { h, useState, useEffect, useCallback, useRef, Fragment, useApp, engineCa
|
|
|
2
2
|
import { I } from '../components/icons.js';
|
|
3
3
|
import { HelpButton } from '../components/help-button.js';
|
|
4
4
|
|
|
5
|
+
// ─── Inject theme CSS once ───────────────────────────────
|
|
6
|
+
var _injected = false;
|
|
7
|
+
function injectCSS() {
|
|
8
|
+
if (_injected) return; _injected = true;
|
|
9
|
+
var s = document.createElement('style');
|
|
10
|
+
s.textContent = `
|
|
11
|
+
:root { --oc-bg: var(--bg-primary, #0a0c14); --oc-text: var(--text-primary, #fff); --oc-dim: var(--text-muted, rgba(255,255,255,0.45)); --oc-faint: rgba(255,255,255,0.12); --oc-card: rgba(255,255,255,0.02); --oc-card-h: rgba(255,255,255,0.06); --oc-toolbar: rgba(0,0,0,0.3); --oc-border: rgba(255,255,255,0.08); --oc-edge: rgba(255,255,255,0.25); --oc-edge-dim: rgba(255,255,255,0.06); --oc-tip-bg: rgba(15,17,23,0.95); --oc-btn-bg: rgba(255,255,255,0.08); --oc-btn-border: rgba(255,255,255,0.12); --oc-metrics: rgba(0,0,0,0.12); }
|
|
12
|
+
[data-theme="light"], :root:not(.dark) { --oc-bg: var(--bg-primary, #f8fafc); --oc-text: var(--text-primary, #1e293b); --oc-dim: var(--text-muted, #64748b); --oc-faint: var(--border, rgba(0,0,0,0.08)); --oc-card: rgba(0,0,0,0.02); --oc-card-h: rgba(0,0,0,0.05); --oc-toolbar: rgba(0,0,0,0.03); --oc-border: var(--border, rgba(0,0,0,0.08)); --oc-edge: rgba(0,0,0,0.2); --oc-edge-dim: rgba(0,0,0,0.05); --oc-tip-bg: var(--bg-primary, #fff); --oc-btn-bg: var(--bg-secondary, rgba(0,0,0,0.04)); --oc-btn-border: var(--border, rgba(0,0,0,0.1)); --oc-metrics: rgba(0,0,0,0.02); }
|
|
13
|
+
@media (prefers-color-scheme: light) { :root:not(.dark) { --oc-bg: var(--bg-primary, #f8fafc); --oc-text: var(--text-primary, #1e293b); --oc-dim: var(--text-muted, #64748b); --oc-faint: var(--border, rgba(0,0,0,0.08)); --oc-card: rgba(0,0,0,0.02); --oc-card-h: rgba(0,0,0,0.05); --oc-toolbar: rgba(0,0,0,0.03); --oc-border: var(--border, rgba(0,0,0,0.08)); --oc-edge: rgba(0,0,0,0.2); --oc-edge-dim: rgba(0,0,0,0.05); --oc-tip-bg: var(--bg-primary, #fff); --oc-btn-bg: var(--bg-secondary, rgba(0,0,0,0.04)); --oc-btn-border: var(--border, rgba(0,0,0,0.1)); --oc-metrics: rgba(0,0,0,0.02); } }
|
|
14
|
+
`;
|
|
15
|
+
document.head.appendChild(s);
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
// ─── Layout Constants ────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
var NODE_W = 220;
|
|
20
|
+
var NODE_H = 72;
|
|
21
|
+
var H_GAP = 40;
|
|
22
|
+
var V_GAP = 80;
|
|
23
|
+
var PAD = 60;
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
running: '#22c55e',
|
|
15
|
-
stopped: '#6b7394',
|
|
16
|
-
error: '#ef4444',
|
|
17
|
-
paused: '#f59e0b',
|
|
18
|
-
deploying: '#06b6d4',
|
|
19
|
-
};
|
|
20
|
-
const ACCENT = '#6366f1';
|
|
21
|
-
const EDGE_COLOR = 'rgba(255,255,255,0.25)';
|
|
22
|
-
const EDGE_HIGHLIGHT = 'rgba(99,102,241,0.7)';
|
|
23
|
-
const BG = '#0a0c14';
|
|
25
|
+
var STATE_COLORS = { running: '#22c55e', stopped: '#6b7394', error: '#ef4444', paused: '#f59e0b', deploying: '#06b6d4' };
|
|
26
|
+
var ACCENT = '#6366f1';
|
|
24
27
|
|
|
25
|
-
// ─── Tree Layout
|
|
28
|
+
// ─── Tree Layout ─────────────────────────────────────────
|
|
26
29
|
function layoutTree(nodes) {
|
|
27
30
|
if (!nodes || !nodes.length) return { positioned: [], width: 0, height: 0 };
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (const n of byId.values()) {
|
|
35
|
-
if (n.managerId && byId.has(n.managerId)) {
|
|
36
|
-
byId.get(n.managerId).children.push(n);
|
|
37
|
-
} else {
|
|
38
|
-
roots.push(n);
|
|
39
|
-
}
|
|
31
|
+
var byId = new Map();
|
|
32
|
+
nodes.forEach(function(n) { byId.set(n.agentId, Object.assign({}, n, { children: [], x: 0, y: 0, subtreeW: 0 })); });
|
|
33
|
+
var roots = [];
|
|
34
|
+
for (var n of byId.values()) {
|
|
35
|
+
if (n.managerId && byId.has(n.managerId)) byId.get(n.managerId).children.push(n);
|
|
36
|
+
else roots.push(n);
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
externalManagers.set(key, {
|
|
49
|
-
agentId: key,
|
|
50
|
-
name: n.managerName,
|
|
51
|
-
role: 'External Manager',
|
|
52
|
-
state: 'external',
|
|
53
|
-
managerType: 'none',
|
|
54
|
-
managerId: null,
|
|
55
|
-
subordinateIds: [],
|
|
56
|
-
subordinateCount: 0,
|
|
57
|
-
isManager: true,
|
|
58
|
-
level: -1,
|
|
59
|
-
clockedIn: true,
|
|
60
|
-
activeTasks: 0,
|
|
61
|
-
errorsToday: 0,
|
|
62
|
-
isExternal: true,
|
|
63
|
-
children: [],
|
|
64
|
-
x: 0, y: 0, subtreeW: 0,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
const extNode = externalManagers.get(key);
|
|
68
|
-
// Remove from roots, add as child of external
|
|
69
|
-
const idx = roots.indexOf(n);
|
|
38
|
+
var externalManagers = new Map();
|
|
39
|
+
for (var n2 of byId.values()) {
|
|
40
|
+
if (n2.managerType === 'external' && n2.managerName) {
|
|
41
|
+
var key = 'ext-' + (n2.managerEmail || n2.managerName);
|
|
42
|
+
if (!externalManagers.has(key)) externalManagers.set(key, { agentId: key, name: n2.managerName, role: 'External Manager', state: 'external', managerType: 'none', managerId: null, subordinateIds: [], subordinateCount: 0, isManager: true, level: -1, clockedIn: true, activeTasks: 0, errorsToday: 0, isExternal: true, children: [], x: 0, y: 0, subtreeW: 0 });
|
|
43
|
+
var ext = externalManagers.get(key);
|
|
44
|
+
var idx = roots.indexOf(n2);
|
|
70
45
|
if (idx >= 0) roots.splice(idx, 1);
|
|
71
|
-
|
|
46
|
+
ext.children.push(n2);
|
|
72
47
|
}
|
|
73
48
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (ext.children.length > 0) roots.push(ext);
|
|
77
|
-
}
|
|
49
|
+
for (var e of externalManagers.values()) { if (e.children.length > 0) roots.push(e); }
|
|
50
|
+
if (roots.length === 0 && byId.size > 0) roots.push.apply(roots, Array.from(byId.values()));
|
|
78
51
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Pass 1: compute subtree widths bottom-up
|
|
85
|
-
function computeWidth(node) {
|
|
86
|
-
if (node.children.length === 0) {
|
|
87
|
-
node.subtreeW = NODE_W;
|
|
88
|
-
return NODE_W;
|
|
89
|
-
}
|
|
90
|
-
let total = 0;
|
|
91
|
-
node.children.forEach(c => { total += computeWidth(c); });
|
|
92
|
-
total += (node.children.length - 1) * H_GAP;
|
|
93
|
-
node.subtreeW = Math.max(NODE_W, total);
|
|
94
|
-
return node.subtreeW;
|
|
52
|
+
function computeW(node) {
|
|
53
|
+
if (node.children.length === 0) { node.subtreeW = NODE_W; return NODE_W; }
|
|
54
|
+
var t = 0; node.children.forEach(function(c) { t += computeW(c); }); t += (node.children.length - 1) * H_GAP;
|
|
55
|
+
node.subtreeW = Math.max(NODE_W, t); return node.subtreeW;
|
|
95
56
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
function assignPositions(node, x, y) {
|
|
99
|
-
node.x = x + node.subtreeW / 2 - NODE_W / 2;
|
|
100
|
-
node.y = y;
|
|
57
|
+
function assignPos(node, x, y) {
|
|
58
|
+
node.x = x + node.subtreeW / 2 - NODE_W / 2; node.y = y;
|
|
101
59
|
if (node.children.length === 0) return;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
childX = node.x + NODE_W / 2 - childrenTotalW / 2;
|
|
106
|
-
node.children.forEach(c => {
|
|
107
|
-
assignPositions(c, childX, y + NODE_H + V_GAP);
|
|
108
|
-
childX += c.subtreeW + H_GAP;
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Layout all root trees side by side
|
|
113
|
-
let totalW = 0;
|
|
114
|
-
roots.forEach(r => { totalW += computeWidth(r); });
|
|
115
|
-
totalW += (roots.length - 1) * H_GAP * 2;
|
|
116
|
-
|
|
117
|
-
let cx = PAD;
|
|
118
|
-
roots.forEach(r => {
|
|
119
|
-
assignPositions(r, cx, PAD);
|
|
120
|
-
cx += r.subtreeW + H_GAP * 2;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Flatten
|
|
124
|
-
const positioned = [];
|
|
125
|
-
let maxX = 0, maxY = 0;
|
|
126
|
-
function flatten(node) {
|
|
127
|
-
positioned.push(node);
|
|
128
|
-
maxX = Math.max(maxX, node.x + NODE_W);
|
|
129
|
-
maxY = Math.max(maxY, node.y + NODE_H);
|
|
130
|
-
node.children.forEach(flatten);
|
|
60
|
+
var cw = node.children.reduce(function(s, c) { return s + c.subtreeW; }, 0) + (node.children.length - 1) * H_GAP;
|
|
61
|
+
var cx = node.x + NODE_W / 2 - cw / 2;
|
|
62
|
+
node.children.forEach(function(c) { assignPos(c, cx, y + NODE_H + V_GAP); cx += c.subtreeW + H_GAP; });
|
|
131
63
|
}
|
|
64
|
+
roots.forEach(function(r) { computeW(r); });
|
|
65
|
+
var cx = PAD; roots.forEach(function(r) { assignPos(r, cx, PAD); cx += r.subtreeW + H_GAP * 2; });
|
|
66
|
+
var positioned = [], maxX = 0, maxY = 0;
|
|
67
|
+
function flatten(node) { positioned.push(node); maxX = Math.max(maxX, node.x + NODE_W); maxY = Math.max(maxY, node.y + NODE_H); node.children.forEach(flatten); }
|
|
132
68
|
roots.forEach(flatten);
|
|
133
|
-
|
|
134
|
-
return { positioned, width: maxX + PAD, height: maxY + PAD + 40 };
|
|
69
|
+
return { positioned: positioned, width: maxX + PAD, height: maxY + PAD + 40 };
|
|
135
70
|
}
|
|
136
71
|
|
|
137
|
-
// ─── SVG Edge Path (curved, child→parent = reports to) ──
|
|
138
72
|
function edgePath(parent, child) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
73
|
+
var x1 = child.x + NODE_W / 2, y1 = child.y, x2 = parent.x + NODE_W / 2, y2 = parent.y + NODE_H;
|
|
74
|
+
var midY = y1 + (y2 - y1) * 0.5;
|
|
75
|
+
return 'M ' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + midY + ', ' + x2 + ' ' + midY + ', ' + x2 + ' ' + y2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
79
|
+
var toolbarBtnStyle = { background: 'var(--oc-btn-bg)', border: '1px solid var(--oc-btn-border)', borderRadius: 6, color: 'var(--oc-text)', fontSize: 14, fontWeight: 600, padding: '4px 8px', cursor: 'pointer', lineHeight: '1.2' };
|
|
80
|
+
function tagStyle(color) { return { fontSize: 9, fontWeight: 600, padding: '1px 5px', borderRadius: 4, background: color + '22', color: color, letterSpacing: '0.02em' }; }
|
|
81
|
+
function tooltipRow(label, value, color) {
|
|
82
|
+
return h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11 } },
|
|
83
|
+
h('span', { style: { color: 'var(--oc-dim)' } }, label),
|
|
84
|
+
h('span', { style: { fontWeight: 600, color: color || 'var(--oc-text)' } }, value));
|
|
85
|
+
}
|
|
86
|
+
function legendDot(color, label) {
|
|
87
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
|
|
88
|
+
h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: color } }),
|
|
89
|
+
h('span', { style: { color: 'var(--oc-dim)', fontSize: 12 } }, label));
|
|
90
|
+
}
|
|
91
|
+
function timeAgo(iso) {
|
|
92
|
+
var diff = Date.now() - new Date(iso).getTime();
|
|
93
|
+
var mins = Math.floor(diff / 60000);
|
|
94
|
+
if (mins < 1) return 'Just now'; if (mins < 60) return mins + 'm ago';
|
|
95
|
+
var hrs = Math.floor(mins / 60); if (hrs < 24) return hrs + 'h ago';
|
|
96
|
+
return Math.floor(hrs / 24) + 'd ago';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Summary Metrics ─────────────────────────────────────
|
|
100
|
+
function OrgSummary(props) {
|
|
101
|
+
var nodes = props.nodes;
|
|
102
|
+
var running = 0, stopped = 0, errored = 0, paused = 0, external = 0, managers = 0, totalTasks = 0, totalErrors = 0;
|
|
103
|
+
nodes.forEach(function(n) {
|
|
104
|
+
if (n.isExternal) { external++; return; }
|
|
105
|
+
if (n.state === 'running') running++;
|
|
106
|
+
else if (n.state === 'error') errored++;
|
|
107
|
+
else if (n.state === 'paused') paused++;
|
|
108
|
+
else stopped++;
|
|
109
|
+
if (n.isManager || (n.children && n.children.length > 0)) managers++;
|
|
110
|
+
totalTasks += n.activeTasks || 0;
|
|
111
|
+
totalErrors += n.errorsToday || 0;
|
|
112
|
+
});
|
|
113
|
+
function chip(label, value, color) {
|
|
114
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4, padding: '3px 8px', background: 'var(--oc-card)', borderRadius: 6 } },
|
|
115
|
+
h('span', { style: { fontSize: 10, color: 'var(--oc-dim)' } }, label),
|
|
116
|
+
h('span', { style: { fontSize: 11, fontWeight: 700, color: color } }, value));
|
|
117
|
+
}
|
|
118
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 16px', borderBottom: '1px solid var(--oc-border)', background: 'var(--oc-metrics)', flexShrink: 0, overflowX: 'auto', fontSize: 11 } },
|
|
119
|
+
chip('Agents', nodes.length - external, 'var(--oc-text)'),
|
|
120
|
+
chip('Running', running, '#22c55e'),
|
|
121
|
+
stopped > 0 && chip('Stopped', stopped, '#6b7394'),
|
|
122
|
+
errored > 0 && chip('Error', errored, '#ef4444'),
|
|
123
|
+
paused > 0 && chip('Paused', paused, '#f59e0b'),
|
|
124
|
+
external > 0 && chip('Human', external, '#8b5cf6'),
|
|
125
|
+
managers > 0 && chip('Managers', managers, ACCENT),
|
|
126
|
+
totalTasks > 0 && h(Fragment, null,
|
|
127
|
+
h('div', { style: { width: 1, height: 14, background: 'var(--oc-faint)' } }),
|
|
128
|
+
chip('Active Tasks', totalTasks, '#f59e0b')
|
|
129
|
+
),
|
|
130
|
+
totalErrors > 0 && chip('Errors Today', totalErrors, '#ef4444')
|
|
131
|
+
);
|
|
145
132
|
}
|
|
146
133
|
|
|
147
134
|
// ─── Main Component ─────────────────────────────────────
|
|
148
135
|
export function OrgChartPage() {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
]
|
|
170
|
-
|
|
171
|
-
(agentRes.agents || []).forEach(a
|
|
172
|
-
avatarMap[a.id] = a.config?.identity?.avatar || a.config?.avatar || a.config?.persona?.avatar || null;
|
|
173
|
-
});
|
|
136
|
+
injectCSS();
|
|
137
|
+
var app = useApp();
|
|
138
|
+
var toast = app.toast;
|
|
139
|
+
var _nodes = useState([]); var nodes = _nodes[0]; var setNodes = _nodes[1];
|
|
140
|
+
var _loading = useState(true); var loading = _loading[0]; var setLoading = _loading[1];
|
|
141
|
+
var _error = useState(null); var error = _error[0]; var setError = _error[1];
|
|
142
|
+
var _hoveredId = useState(null); var hoveredId = _hoveredId[0]; var setHoveredId = _hoveredId[1];
|
|
143
|
+
var _zoom = useState(1); var zoom = _zoom[0]; var setZoom = _zoom[1];
|
|
144
|
+
var _pan = useState({ x: 0, y: 0 }); var pan = _pan[0]; var setPan = _pan[1];
|
|
145
|
+
var _dragging = useState(false); var dragging = _dragging[0]; var setDragging = _dragging[1];
|
|
146
|
+
var _dragStart = useState({ x: 0, y: 0 }); var dragStart = _dragStart[0]; var setDragStart = _dragStart[1];
|
|
147
|
+
var _mousePos = useState({ x: 0, y: 0 }); var mousePos = _mousePos[0]; var setMousePos = _mousePos[1];
|
|
148
|
+
var containerRef = useRef(null);
|
|
149
|
+
|
|
150
|
+
var load = useCallback(function() {
|
|
151
|
+
setLoading(true); setError(null);
|
|
152
|
+
Promise.all([
|
|
153
|
+
engineCall('/hierarchy/org-chart').catch(function() { return null; }),
|
|
154
|
+
engineCall('/agents?orgId=' + getOrgId()).catch(function() { return { agents: [] }; }),
|
|
155
|
+
]).then(function(res) {
|
|
156
|
+
var hierRes = res[0]; var agentRes = res[1];
|
|
157
|
+
var avatarMap = {};
|
|
158
|
+
(agentRes.agents || []).forEach(function(a) { avatarMap[a.id] = a.config?.identity?.avatar || a.config?.avatar || a.config?.persona?.avatar || null; });
|
|
174
159
|
if (hierRes && hierRes.nodes) {
|
|
175
|
-
setNodes(hierRes.nodes.map(n
|
|
160
|
+
setNodes(hierRes.nodes.map(function(n) { return Object.assign({}, n, { avatar: avatarMap[n.agentId] || null }); }));
|
|
176
161
|
} else {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
setNodes(agents.map(a => ({
|
|
180
|
-
agentId: a.id,
|
|
181
|
-
name: a.config?.name || a.id,
|
|
182
|
-
role: a.config?.role || 'Agent',
|
|
183
|
-
state: a.state || 'stopped',
|
|
184
|
-
managerId: a.config?.managerId || null,
|
|
185
|
-
managerType: a.config?.externalManagerEmail ? 'external' : a.config?.managerId ? 'internal' : 'none',
|
|
186
|
-
managerName: a.config?.externalManagerName || null,
|
|
187
|
-
managerEmail: a.config?.externalManagerEmail || null,
|
|
188
|
-
subordinateIds: [],
|
|
189
|
-
subordinateCount: 0,
|
|
190
|
-
isManager: false,
|
|
191
|
-
level: 0,
|
|
192
|
-
clockedIn: a.state === 'running',
|
|
193
|
-
activeTasks: 0,
|
|
194
|
-
errorsToday: 0,
|
|
195
|
-
avatar: a.config?.identity?.avatar || a.config?.avatar || a.config?.persona?.avatar || null,
|
|
196
|
-
comm: a.config?.comm || {},
|
|
197
|
-
})));
|
|
162
|
+
var agents = agentRes.agents || [];
|
|
163
|
+
setNodes(agents.map(function(a) { return { agentId: a.id, name: a.config?.name || a.id, role: a.config?.role || 'Agent', state: a.state || 'stopped', managerId: a.config?.managerId || null, managerType: a.config?.externalManagerEmail ? 'external' : a.config?.managerId ? 'internal' : 'none', managerName: a.config?.externalManagerName || null, managerEmail: a.config?.externalManagerEmail || null, subordinateIds: [], subordinateCount: 0, isManager: false, level: 0, clockedIn: a.state === 'running', activeTasks: 0, errorsToday: 0, avatar: avatarMap[a.id] || null, comm: a.config?.comm || {} }; }));
|
|
198
164
|
}
|
|
199
|
-
}
|
|
200
|
-
setError(e.message || 'Failed to load hierarchy');
|
|
201
|
-
}
|
|
165
|
+
}).catch(function(e) { setError(e.message || 'Failed to load'); });
|
|
202
166
|
setLoading(false);
|
|
203
167
|
}, []);
|
|
204
168
|
|
|
205
|
-
useEffect(()
|
|
206
|
-
|
|
207
|
-
// Layout
|
|
208
|
-
const { positioned, width: treeW, height: treeH } = layoutTree(nodes);
|
|
209
|
-
|
|
210
|
-
// Zoom
|
|
211
|
-
const handleWheel = useCallback((e) => {
|
|
212
|
-
e.preventDefault();
|
|
213
|
-
const delta = e.deltaY > 0 ? -0.08 : 0.08;
|
|
214
|
-
setZoom(z => Math.min(3, Math.max(0.15, z + delta)));
|
|
215
|
-
}, []);
|
|
216
|
-
|
|
217
|
-
// Pan
|
|
218
|
-
const handleMouseDown = useCallback((e) => {
|
|
219
|
-
if (e.button !== 0) return;
|
|
220
|
-
// Only start drag on background (not on nodes)
|
|
221
|
-
if (e.target.closest('.org-node')) return;
|
|
222
|
-
setDragging(true);
|
|
223
|
-
setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y });
|
|
224
|
-
}, [pan]);
|
|
225
|
-
|
|
226
|
-
const handleMouseMove = useCallback((e) => {
|
|
227
|
-
if (!dragging) return;
|
|
228
|
-
setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
|
|
229
|
-
}, [dragging, dragStart]);
|
|
169
|
+
useEffect(function() { load(); }, []);
|
|
230
170
|
|
|
231
|
-
|
|
171
|
+
var layout = layoutTree(nodes);
|
|
172
|
+
var positioned = layout.positioned; var treeW = layout.width; var treeH = layout.height;
|
|
232
173
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
174
|
+
var handleWheel = useCallback(function(e) { e.preventDefault(); setZoom(function(z) { return Math.min(3, Math.max(0.15, z + (e.deltaY > 0 ? -0.08 : 0.08))); }); }, []);
|
|
175
|
+
var handleMouseDown = useCallback(function(e) { if (e.button !== 0 || e.target.closest('.org-node')) return; setDragging(true); setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); }, [pan]);
|
|
176
|
+
var handleMouseMove = useCallback(function(e) { if (!dragging) return; setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }, [dragging, dragStart]);
|
|
177
|
+
var handleMouseUp = useCallback(function() { setDragging(false); }, []);
|
|
178
|
+
useEffect(function() {
|
|
179
|
+
if (dragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return function() { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }
|
|
239
180
|
}, [dragging, handleMouseMove, handleMouseUp]);
|
|
240
181
|
|
|
241
|
-
|
|
242
|
-
const fitToView = useCallback(() => {
|
|
182
|
+
var fitToView = useCallback(function() {
|
|
243
183
|
if (!containerRef.current || !treeW || !treeH) return;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const scale = Math.min(scaleX, scaleY, 1.5);
|
|
248
|
-
setZoom(scale);
|
|
249
|
-
setPan({
|
|
250
|
-
x: (rect.width - treeW * scale) / 2,
|
|
251
|
-
y: (rect.height - treeH * scale) / 2,
|
|
252
|
-
});
|
|
184
|
+
var rect = containerRef.current.getBoundingClientRect();
|
|
185
|
+
var scale = Math.min((rect.width - 40) / treeW, (rect.height - 40) / treeH, 1.5);
|
|
186
|
+
setZoom(scale); setPan({ x: (rect.width - treeW * scale) / 2, y: (rect.height - treeH * scale) / 2 });
|
|
253
187
|
}, [treeW, treeH]);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
let cur = byId.get(id);
|
|
264
|
-
while (cur && cur.managerId && byId.has(cur.managerId)) {
|
|
265
|
-
connected.add(cur.managerId);
|
|
266
|
-
cur = byId.get(cur.managerId);
|
|
267
|
-
}
|
|
268
|
-
// Walk down
|
|
269
|
-
function addDesc(node) {
|
|
270
|
-
node.children.forEach(c => { connected.add(c.agentId); addDesc(c); });
|
|
271
|
-
}
|
|
272
|
-
const node = byId.get(id);
|
|
273
|
-
if (node) addDesc(node);
|
|
188
|
+
useEffect(function() { if (positioned.length > 0) fitToView(); }, [positioned.length]);
|
|
189
|
+
|
|
190
|
+
var getConnected = useCallback(function(id) {
|
|
191
|
+
var connected = new Set([id]);
|
|
192
|
+
var byId = new Map(); positioned.forEach(function(n) { byId.set(n.agentId, n); });
|
|
193
|
+
var cur = byId.get(id);
|
|
194
|
+
while (cur && cur.managerId && byId.has(cur.managerId)) { connected.add(cur.managerId); cur = byId.get(cur.managerId); }
|
|
195
|
+
function addDesc(node) { node.children.forEach(function(c) { connected.add(c.agentId); addDesc(c); }); }
|
|
196
|
+
var node = byId.get(id); if (node) addDesc(node);
|
|
274
197
|
return connected;
|
|
275
198
|
}, [positioned]);
|
|
276
199
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
positioned.forEach(node => {
|
|
282
|
-
node.children.forEach(child => {
|
|
283
|
-
edges.push({ parent: node, child });
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
// Hovered node for tooltip
|
|
288
|
-
const hoveredNode = hoveredId ? positioned.find(n => n.agentId === hoveredId) : null;
|
|
200
|
+
var connected = hoveredId ? getConnected(hoveredId) : null;
|
|
201
|
+
var edges = [];
|
|
202
|
+
positioned.forEach(function(node) { node.children.forEach(function(child) { edges.push({ parent: node, child: child }); }); });
|
|
203
|
+
var hoveredNode = hoveredId ? positioned.find(function(n) { return n.agentId === hoveredId; }) : null;
|
|
289
204
|
|
|
290
205
|
if (loading) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading organization chart...');
|
|
291
206
|
if (error) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--danger)' } }, 'Error: ' + error);
|
|
292
207
|
if (positioned.length === 0) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } },
|
|
293
|
-
h('div', { style: {
|
|
208
|
+
h('div', { style: { width: 48, height: 48, borderRadius: 12, background: 'rgba(99,102,241,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px', color: ACCENT } }, I.orgChart()),
|
|
294
209
|
h('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 8 } }, 'No Organization Hierarchy Yet'),
|
|
295
210
|
h('div', { style: { color: 'var(--text-secondary)' } }, 'Add agents and configure manager relationships to see the org chart.')
|
|
296
211
|
);
|
|
297
212
|
|
|
298
|
-
return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background:
|
|
213
|
+
return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--oc-bg)', borderRadius: 'var(--radius-lg)', overflow: 'hidden' } },
|
|
299
214
|
// Toolbar
|
|
300
|
-
h('div', { style: { display: 'flex', alignItems: 'center', gap:
|
|
301
|
-
h('div', { style: { fontWeight: 700, fontSize:
|
|
302
|
-
|
|
303
|
-
h(
|
|
304
|
-
|
|
305
|
-
h('
|
|
306
|
-
h('
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
h('
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 } }, h('strong', null, 'Tip: '), 'Purple nodes represent external (human) managers. Configure manager relationships in each agent\'s settings.')
|
|
317
|
-
)),
|
|
318
|
-
h('div', { style: { color: 'rgba(255,255,255,0.4)', fontSize: 13 } }, positioned.length + ' agents'),
|
|
215
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 16px', borderBottom: '1px solid var(--oc-border)', background: 'var(--oc-toolbar)', flexShrink: 0, flexWrap: 'wrap' } },
|
|
216
|
+
h('div', { style: { fontWeight: 700, fontSize: 14, color: 'var(--oc-text)', display: 'flex', alignItems: 'center', gap: 6 } },
|
|
217
|
+
I.orgChart(), 'Organization Chart',
|
|
218
|
+
h(HelpButton, { label: 'Organization Chart' },
|
|
219
|
+
h('p', null, 'Visual hierarchy of all agents in your organization. Shows reporting relationships, status, and activity at a glance.'),
|
|
220
|
+
h('h4', { style: { marginTop: 16, marginBottom: 8, fontSize: 14 } }, 'Interactions'),
|
|
221
|
+
h('ul', { style: { paddingLeft: 20, margin: '4px 0 8px' } },
|
|
222
|
+
h('li', null, h('strong', null, 'Hover'), ' \u2014 Highlights the agent\'s full chain and shows a detail tooltip.'),
|
|
223
|
+
h('li', null, h('strong', null, 'Scroll'), ' \u2014 Zoom in/out.'),
|
|
224
|
+
h('li', null, h('strong', null, 'Drag'), ' \u2014 Pan the canvas.'),
|
|
225
|
+
h('li', null, h('strong', null, 'Fit'), ' \u2014 Auto-zoom to fit all agents.')
|
|
226
|
+
),
|
|
227
|
+
h('div', { style: { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 } }, h('strong', null, 'Tip: '), 'Purple nodes represent external (human) managers.')
|
|
228
|
+
)
|
|
229
|
+
),
|
|
230
|
+
h('div', { style: { color: 'var(--oc-dim)', fontSize: 12 } }, positioned.length + ' agents'),
|
|
319
231
|
h('div', { style: { flex: 1 } }),
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
h('
|
|
327
|
-
// Zoom controls
|
|
328
|
-
h('button', { onClick: () => setZoom(z => Math.min(3, z + 0.2)), style: toolbarBtnStyle }, '+'),
|
|
329
|
-
h('div', { style: { color: 'rgba(255,255,255,0.5)', fontSize: 12, minWidth: 40, textAlign: 'center' } }, Math.round(zoom * 100) + '%'),
|
|
330
|
-
h('button', { onClick: () => setZoom(z => Math.max(0.15, z - 0.2)), style: toolbarBtnStyle }, '\u2212'),
|
|
331
|
-
h('button', { onClick: fitToView, style: { ...toolbarBtnStyle, fontSize: 11, padding: '4px 10px' } }, 'Fit'),
|
|
332
|
-
h('button', { onClick: load, style: { ...toolbarBtnStyle, fontSize: 11, padding: '4px 10px' } }, 'Refresh'),
|
|
232
|
+
legendDot('#22c55e', 'Running'), legendDot('#6b7394', 'Stopped'), legendDot('#ef4444', 'Error'), legendDot('#f59e0b', 'Paused'), legendDot('#8b5cf6', 'External'),
|
|
233
|
+
h('div', { style: { width: 1, height: 14, background: 'var(--oc-faint)', margin: '0 4px' } }),
|
|
234
|
+
h('button', { onClick: function() { setZoom(function(z) { return Math.min(3, z + 0.2); }); }, style: toolbarBtnStyle }, '+'),
|
|
235
|
+
h('div', { style: { color: 'var(--oc-dim)', fontSize: 11, minWidth: 36, textAlign: 'center' } }, Math.round(zoom * 100) + '%'),
|
|
236
|
+
h('button', { onClick: function() { setZoom(function(z) { return Math.max(0.15, z - 0.2); }); }, style: toolbarBtnStyle }, '\u2212'),
|
|
237
|
+
h('button', { onClick: fitToView, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Fit'),
|
|
238
|
+
h('button', { onClick: load, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Refresh'),
|
|
333
239
|
),
|
|
334
240
|
|
|
241
|
+
// Summary metrics
|
|
242
|
+
h(OrgSummary, { nodes: positioned }),
|
|
243
|
+
|
|
335
244
|
// Canvas
|
|
336
245
|
h('div', {
|
|
337
246
|
ref: containerRef,
|
|
@@ -339,129 +248,81 @@ export function OrgChartPage() {
|
|
|
339
248
|
onMouseDown: handleMouseDown,
|
|
340
249
|
onWheel: handleWheel,
|
|
341
250
|
},
|
|
342
|
-
h('div', { style: { transform:
|
|
251
|
+
h('div', { style: { transform: 'translate(' + pan.x + 'px, ' + pan.y + 'px) scale(' + zoom + ')', transformOrigin: '0 0', position: 'absolute', top: 0, left: 0 } },
|
|
343
252
|
// SVG edges
|
|
344
253
|
h('svg', { width: treeW, height: treeH, style: { position: 'absolute', top: 0, left: 0, pointerEvents: 'none' } },
|
|
345
254
|
h('defs', null,
|
|
346
255
|
h('marker', { id: 'arrowhead', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
|
|
347
|
-
h('polygon', { points: '0 0, 8 3, 0 6', fill:
|
|
256
|
+
h('polygon', { points: '0 0, 8 3, 0 6', fill: 'var(--oc-edge)' })
|
|
348
257
|
),
|
|
349
258
|
h('marker', { id: 'arrowhead-hl', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
|
|
350
|
-
h('polygon', { points: '0 0, 8 3, 0 6', fill:
|
|
351
|
-
)
|
|
259
|
+
h('polygon', { points: '0 0, 8 3, 0 6', fill: 'rgba(99,102,241,0.7)' })
|
|
260
|
+
)
|
|
352
261
|
),
|
|
353
|
-
edges.map((e, i)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return h('path', {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
stroke: isHl ? EDGE_HIGHLIGHT : dim ? 'rgba(255,255,255,0.06)' : EDGE_COLOR,
|
|
360
|
-
strokeWidth: isHl ? 2.5 : 1.5,
|
|
361
|
-
fill: 'none',
|
|
262
|
+
edges.map(function(e, i) {
|
|
263
|
+
var isHl = connected && connected.has(e.parent.agentId) && connected.has(e.child.agentId);
|
|
264
|
+
var dim = connected && !isHl;
|
|
265
|
+
return h('path', { key: i, d: edgePath(e.parent, e.child),
|
|
266
|
+
stroke: isHl ? 'rgba(99,102,241,0.7)' : dim ? 'var(--oc-edge-dim)' : 'var(--oc-edge)',
|
|
267
|
+
strokeWidth: isHl ? 2.5 : 1.5, fill: 'none',
|
|
362
268
|
markerEnd: isHl ? 'url(#arrowhead-hl)' : 'url(#arrowhead)',
|
|
363
|
-
style: { transition: 'stroke 0.2s,
|
|
364
|
-
});
|
|
269
|
+
style: { transition: 'stroke 0.2s, opacity 0.2s', opacity: dim ? 0.3 : 1 } });
|
|
365
270
|
})
|
|
366
271
|
),
|
|
367
272
|
// Nodes
|
|
368
|
-
positioned.map(node
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
273
|
+
positioned.map(function(node) {
|
|
274
|
+
var isHovered = hoveredId === node.agentId;
|
|
275
|
+
var dim = connected && !connected.has(node.agentId);
|
|
276
|
+
var stateColor = node.isExternal ? '#8b5cf6' : (STATE_COLORS[node.state] || '#6b7394');
|
|
372
277
|
return h('div', {
|
|
373
|
-
key: node.agentId,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
onMouseLeave: () => setHoveredId(null),
|
|
278
|
+
key: node.agentId, className: 'org-node',
|
|
279
|
+
onMouseEnter: function(ev) { setHoveredId(node.agentId); setMousePos({ x: ev.clientX, y: ev.clientY }); },
|
|
280
|
+
onMouseMove: function(ev) { if (isHovered) setMousePos({ x: ev.clientX, y: ev.clientY }); },
|
|
281
|
+
onMouseLeave: function() { setHoveredId(null); },
|
|
378
282
|
style: {
|
|
379
|
-
position: 'absolute',
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
border: `1.5px solid ${isHovered ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.12)'}`,
|
|
386
|
-
borderRadius: 12,
|
|
387
|
-
padding: '10px 14px',
|
|
388
|
-
cursor: 'pointer',
|
|
389
|
-
transition: 'all 0.2s',
|
|
390
|
-
opacity: dim ? 0.2 : 1,
|
|
391
|
-
backdropFilter: 'blur(8px)',
|
|
392
|
-
display: 'flex',
|
|
393
|
-
alignItems: 'center',
|
|
394
|
-
gap: 10,
|
|
395
|
-
userSelect: 'none',
|
|
283
|
+
position: 'absolute', left: node.x, top: node.y, width: NODE_W, height: NODE_H,
|
|
284
|
+
background: isHovered ? 'var(--oc-card-h)' : 'var(--oc-card)',
|
|
285
|
+
border: '1.5px solid ' + (isHovered ? stateColor + '66' : 'var(--oc-faint)'),
|
|
286
|
+
borderRadius: 12, padding: '10px 14px', cursor: 'pointer',
|
|
287
|
+
transition: 'all 0.2s', opacity: dim ? 0.2 : 1, backdropFilter: 'blur(8px)',
|
|
288
|
+
display: 'flex', alignItems: 'center', gap: 10, userSelect: 'none',
|
|
396
289
|
},
|
|
397
290
|
},
|
|
398
|
-
// Status dot + avatar
|
|
399
291
|
h('div', { style: { position: 'relative', flexShrink: 0 } },
|
|
400
292
|
node.avatar
|
|
401
|
-
? h('img', { src: node.avatar, style: {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
objectFit: 'cover',
|
|
405
|
-
}})
|
|
406
|
-
: h('div', { style: {
|
|
407
|
-
width: 36, height: 36, borderRadius: '50%',
|
|
408
|
-
background: node.isExternal ? 'linear-gradient(135deg, #7c3aed, #a78bfa)' : `linear-gradient(135deg, ${stateColor}33, ${stateColor}11)`,
|
|
409
|
-
border: `2px solid ${stateColor}`,
|
|
410
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
411
|
-
fontSize: 14, fontWeight: 700, color: node.isExternal ? '#fff' : stateColor,
|
|
412
|
-
}}, (node.name || '?').charAt(0).toUpperCase()),
|
|
413
|
-
// Online indicator
|
|
414
|
-
h('div', { style: {
|
|
415
|
-
position: 'absolute', bottom: -1, right: -1,
|
|
416
|
-
width: 10, height: 10, borderRadius: '50%',
|
|
417
|
-
background: stateColor,
|
|
418
|
-
border: '2px solid ' + BG,
|
|
419
|
-
}}),
|
|
293
|
+
? h('img', { src: node.avatar, style: { width: 36, height: 36, borderRadius: '50%', border: '2px solid ' + stateColor, objectFit: 'cover' } })
|
|
294
|
+
: h('div', { style: { width: 36, height: 36, borderRadius: '50%', background: node.isExternal ? 'linear-gradient(135deg, #7c3aed, #a78bfa)' : stateColor + '22', border: '2px solid ' + stateColor, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, fontWeight: 700, color: node.isExternal ? '#fff' : stateColor } }, (node.name || '?').charAt(0).toUpperCase()),
|
|
295
|
+
h('div', { style: { position: 'absolute', bottom: -1, right: -1, width: 10, height: 10, borderRadius: '50%', background: stateColor, border: '2px solid var(--oc-bg)' } })
|
|
420
296
|
),
|
|
421
|
-
// Text
|
|
422
297
|
h('div', { style: { overflow: 'hidden', flex: 1, minWidth: 0 } },
|
|
423
|
-
h('div', { style: { fontSize: 13, fontWeight: 600, color: '
|
|
424
|
-
h('div', { style: { fontSize: 11, color: '
|
|
425
|
-
node.role || (node.isExternal ? 'External Manager' : 'Agent')
|
|
426
|
-
),
|
|
427
|
-
// Tags row
|
|
298
|
+
h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--oc-text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, node.name || node.agentId),
|
|
299
|
+
h('div', { style: { fontSize: 11, color: 'var(--oc-dim)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 2 } }, node.role || (node.isExternal ? 'External Manager' : 'Agent')),
|
|
428
300
|
!node.isExternal && h('div', { style: { display: 'flex', gap: 4, marginTop: 4 } },
|
|
429
|
-
node.isManager && h('span', { style: tagStyle(
|
|
301
|
+
node.isManager && h('span', { style: tagStyle(ACCENT) }, 'MGR'),
|
|
430
302
|
node.activeTasks > 0 && h('span', { style: tagStyle('#f59e0b') }, node.activeTasks + ' tasks'),
|
|
431
|
-
node.errorsToday > 0 && h('span', { style: tagStyle('#ef4444') }, node.errorsToday + ' err')
|
|
432
|
-
)
|
|
433
|
-
)
|
|
303
|
+
node.errorsToday > 0 && h('span', { style: tagStyle('#ef4444') }, node.errorsToday + ' err')
|
|
304
|
+
)
|
|
305
|
+
)
|
|
434
306
|
);
|
|
435
|
-
})
|
|
436
|
-
)
|
|
307
|
+
})
|
|
308
|
+
)
|
|
437
309
|
),
|
|
438
310
|
|
|
439
311
|
// Hover tooltip
|
|
440
312
|
hoveredNode && h('div', { style: {
|
|
441
|
-
position: 'fixed',
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
backdropFilter: 'blur(12px)',
|
|
446
|
-
border: '1px solid rgba(255,255,255,0.15)',
|
|
447
|
-
borderRadius: 10,
|
|
448
|
-
padding: '12px 16px',
|
|
449
|
-
pointerEvents: 'none',
|
|
450
|
-
zIndex: 1000,
|
|
451
|
-
minWidth: 200,
|
|
452
|
-
maxWidth: 280,
|
|
313
|
+
position: 'fixed', left: mousePos.x + 16, top: mousePos.y - 10,
|
|
314
|
+
background: 'var(--oc-tip-bg)', backdropFilter: 'blur(12px)',
|
|
315
|
+
border: '1px solid var(--oc-faint)', borderRadius: 10,
|
|
316
|
+
padding: '12px 16px', pointerEvents: 'none', zIndex: 1000, minWidth: 200, maxWidth: 280,
|
|
453
317
|
}},
|
|
454
|
-
// Header with avatar
|
|
455
318
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 } },
|
|
456
319
|
hoveredNode.avatar
|
|
457
320
|
? h('img', { src: hoveredNode.avatar, style: { width: 32, height: 32, borderRadius: '50%', border: '2px solid ' + (STATE_COLORS[hoveredNode.state] || '#6b7394'), objectFit: 'cover' } })
|
|
458
|
-
: h('div', { style: { width: 32, height: 32, borderRadius: '50%', background: '
|
|
321
|
+
: h('div', { style: { width: 32, height: 32, borderRadius: '50%', background: 'var(--oc-card-h)', border: '2px solid ' + (STATE_COLORS[hoveredNode.state] || '#6b7394'), display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13, fontWeight: 700, color: 'var(--oc-text)' } }, (hoveredNode.name || '?').charAt(0).toUpperCase()),
|
|
459
322
|
h('div', null,
|
|
460
|
-
h('div', { style: { fontSize: 13, fontWeight: 600, color: '
|
|
461
|
-
h('div', { style: { fontSize: 11, color: '
|
|
462
|
-
),
|
|
323
|
+
h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--oc-text)' } }, hoveredNode.name),
|
|
324
|
+
h('div', { style: { fontSize: 11, color: 'var(--oc-dim)' } }, hoveredNode.role))
|
|
463
325
|
),
|
|
464
|
-
// Info rows
|
|
465
326
|
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
466
327
|
tooltipRow('State', hoveredNode.state || 'unknown', STATE_COLORS[hoveredNode.state]),
|
|
467
328
|
!hoveredNode.isExternal && tooltipRow('Clocked In', hoveredNode.clockedIn ? 'Yes' : 'No', hoveredNode.clockedIn ? '#22c55e' : '#6b7394'),
|
|
@@ -470,59 +331,8 @@ export function OrgChartPage() {
|
|
|
470
331
|
hoveredNode.subordinateCount > 0 && tooltipRow('Direct Reports', String(hoveredNode.subordinateCount), ACCENT),
|
|
471
332
|
hoveredNode.activeTasks > 0 && tooltipRow('Active Tasks', String(hoveredNode.activeTasks), '#f59e0b'),
|
|
472
333
|
hoveredNode.errorsToday > 0 && tooltipRow('Errors Today', String(hoveredNode.errorsToday), '#ef4444'),
|
|
473
|
-
hoveredNode.lastActivityAt && tooltipRow('Last Active', timeAgo(hoveredNode.lastActivityAt))
|
|
474
|
-
)
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
// (legend moved to toolbar)
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ─── Helpers ─────────────────────────────────────────────
|
|
482
|
-
const toolbarBtnStyle = {
|
|
483
|
-
background: 'rgba(255,255,255,0.08)',
|
|
484
|
-
border: '1px solid rgba(255,255,255,0.12)',
|
|
485
|
-
borderRadius: 6,
|
|
486
|
-
color: '#fff',
|
|
487
|
-
fontSize: 14,
|
|
488
|
-
fontWeight: 600,
|
|
489
|
-
padding: '4px 8px',
|
|
490
|
-
cursor: 'pointer',
|
|
491
|
-
lineHeight: '1.2',
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
function tagStyle(color) {
|
|
495
|
-
return {
|
|
496
|
-
fontSize: 9,
|
|
497
|
-
fontWeight: 600,
|
|
498
|
-
padding: '1px 5px',
|
|
499
|
-
borderRadius: 4,
|
|
500
|
-
background: color + '22',
|
|
501
|
-
color: color,
|
|
502
|
-
letterSpacing: '0.02em',
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function tooltipRow(label, value, color) {
|
|
507
|
-
return h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11 } },
|
|
508
|
-
h('span', { style: { color: 'rgba(255,255,255,0.4)' } }, label),
|
|
509
|
-
h('span', { style: { fontWeight: 600, color: color || '#fff' } }, value),
|
|
334
|
+
hoveredNode.lastActivityAt && tooltipRow('Last Active', timeAgo(hoveredNode.lastActivityAt))
|
|
335
|
+
)
|
|
336
|
+
)
|
|
510
337
|
);
|
|
511
338
|
}
|
|
512
|
-
|
|
513
|
-
function legendDot(color, label) {
|
|
514
|
-
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
|
|
515
|
-
h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: color } }),
|
|
516
|
-
h('span', { style: { color: 'rgba(255,255,255,0.5)' } }, label),
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function timeAgo(iso) {
|
|
521
|
-
const diff = Date.now() - new Date(iso).getTime();
|
|
522
|
-
const mins = Math.floor(diff / 60000);
|
|
523
|
-
if (mins < 1) return 'Just now';
|
|
524
|
-
if (mins < 60) return mins + 'm ago';
|
|
525
|
-
const hrs = Math.floor(mins / 60);
|
|
526
|
-
if (hrs < 24) return hrs + 'h ago';
|
|
527
|
-
return Math.floor(hrs / 24) + 'd ago';
|
|
528
|
-
}
|