kyd-shared-badge 0.3.92 → 0.3.94
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.
- package/package.json +2 -2
- package/src/SharedBadgeDisplay.tsx +70 -72
- package/src/components/SkillsBubble.tsx +128 -70
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyd-shared-badge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.94",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@aws-sdk/lib-dynamodb": "^3.893.0",
|
|
22
22
|
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
|
23
23
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
|
24
|
-
"@knowyourdeveloper/react-bubble-chart": "^1.0.
|
|
24
|
+
"@knowyourdeveloper/react-bubble-chart": "^1.0.5",
|
|
25
25
|
"@radix-ui/react-slot": "^1.2.3",
|
|
26
26
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
27
27
|
"ai": "5.0.47",
|
|
@@ -207,80 +207,78 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
207
207
|
const OverviewSection = () => (
|
|
208
208
|
<div className={`${wrapperMaxWidth} mx-auto mt-6`}>
|
|
209
209
|
<Reveal headless={isHeadless} offsetY={8} durationMs={500}>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<div className={'
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<div className={'
|
|
238
|
-
<div className={'
|
|
239
|
-
|
|
240
|
-
<span className={'px-2 py-0.5 rounded text-xs font-semibold'} style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>{em.label}</span>
|
|
241
|
-
</div>
|
|
242
|
-
{em.description ? (
|
|
243
|
-
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{em.description}</div>
|
|
244
|
-
) : null}
|
|
210
|
+
{/* Quadrant layout */}
|
|
211
|
+
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
|
212
|
+
{/* Top-left: Name, countries, badge image handled by ReportHeader */}
|
|
213
|
+
<div>
|
|
214
|
+
<ReportHeader
|
|
215
|
+
badgeId={badgeId}
|
|
216
|
+
developerName={badgeData.developerName}
|
|
217
|
+
updatedAt={updatedAt}
|
|
218
|
+
score={overallFinalPercent || 0}
|
|
219
|
+
isPublic={true}
|
|
220
|
+
badgeImageUrl={badgeData.badgeImageUrl || ''}
|
|
221
|
+
summary={undefined}
|
|
222
|
+
enterpriseMatch={null}
|
|
223
|
+
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
224
|
+
accountAuthenticity={assessmentResult?.account_authenticity}
|
|
225
|
+
companyName={badgeData.companyName}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
{/* Top-right: Role match section */}
|
|
229
|
+
<div className={'rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
230
|
+
<div className={'text-lg font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>Role Alignment</div>
|
|
231
|
+
{(() => {
|
|
232
|
+
const em = assessmentResult?.enterprise_match;
|
|
233
|
+
if (!em) return <div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>No role match available.</div>;
|
|
234
|
+
const role = em.role || {};
|
|
235
|
+
return (
|
|
236
|
+
<div className={'space-y-2'}>
|
|
237
|
+
<div className={'flex items-center gap-2'}>
|
|
238
|
+
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{role?.name || 'Role'}</div>
|
|
239
|
+
<span className={'px-2 py-0.5 rounded text-xs font-semibold'} style={{ backgroundColor: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>{em.label}</span>
|
|
245
240
|
</div>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
{em.description ? (
|
|
242
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>{em.description}</div>
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
})()}
|
|
247
|
+
</div>
|
|
249
248
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
</div>
|
|
249
|
+
{/* Bottom-left: Technical score */}
|
|
250
|
+
<div>
|
|
251
|
+
{(() => {
|
|
252
|
+
const uiTech = graphInsights?.uiSummary?.technical || {};
|
|
253
|
+
const techPct = Math.round(Number(uiTech?.percent ?? 0));
|
|
254
|
+
const techLabel = uiTech?.label || 'EVIDENCE';
|
|
255
|
+
return (
|
|
256
|
+
<GaugeCard
|
|
257
|
+
title={'KYD Technical'}
|
|
258
|
+
description={'Composite of technical evidence; more right indicates stronger capability'}
|
|
259
|
+
percent={techPct}
|
|
260
|
+
label={techLabel}
|
|
261
|
+
topMovers={[]}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
})()}
|
|
265
|
+
</div>
|
|
266
|
+
{/* Bottom-right: Risk score */}
|
|
267
|
+
<div>
|
|
268
|
+
{(() => {
|
|
269
|
+
const uiRisk = graphInsights?.uiSummary?.risk || {};
|
|
270
|
+
const riskPctGood = Math.round(Number(uiRisk?.percent_good ?? 0));
|
|
271
|
+
const riskLabel = uiRisk?.label || 'RISK';
|
|
272
|
+
return (
|
|
273
|
+
<RiskCard
|
|
274
|
+
title={'KYD Risk'}
|
|
275
|
+
description={'Lower bar height indicates lower risk exposure'}
|
|
276
|
+
percentGood={riskPctGood}
|
|
277
|
+
label={riskLabel}
|
|
278
|
+
topMovers={[]}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
})()}
|
|
284
282
|
</div>
|
|
285
283
|
</div>
|
|
286
284
|
</Reveal>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
|
4
4
|
import { BubbleChart } from '@knowyourdeveloper/react-bubble-chart';
|
|
5
5
|
import '@knowyourdeveloper/react-bubble-chart/style.css';
|
|
6
|
+
import { green1, green2, green3, green4, green5 } from '../colors';
|
|
6
7
|
|
|
7
8
|
type SkillsRadarPoint = {
|
|
8
9
|
axis: string;
|
|
@@ -24,35 +25,44 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
|
24
25
|
if (!state || !state.visible) return null;
|
|
25
26
|
return (
|
|
26
27
|
<div
|
|
28
|
+
className="rounded-md border shadow-sm px-3 py-2 text-xs"
|
|
27
29
|
style={{
|
|
28
30
|
position: 'absolute',
|
|
29
31
|
left: state.x,
|
|
30
32
|
top: state.y,
|
|
31
33
|
pointerEvents: 'none',
|
|
32
34
|
background: 'var(--content-card-background)',
|
|
33
|
-
|
|
35
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
34
36
|
color: 'var(--text-main)',
|
|
35
|
-
|
|
36
|
-
borderRadius: 6,
|
|
37
|
+
zIndex: 10,
|
|
37
38
|
minWidth: 250,
|
|
38
39
|
maxWidth: 320,
|
|
39
|
-
zIndex: 10,
|
|
40
40
|
}}
|
|
41
41
|
>
|
|
42
|
-
<div style={{
|
|
42
|
+
<div className="font-medium" style={{ color: 'var(--text-main)' }}>{state.title}</div>
|
|
43
43
|
{state.body ? (
|
|
44
|
-
<div style={{
|
|
44
|
+
<div style={{ color: 'var(--text-secondary)' }}>{state.body}</div>
|
|
45
45
|
) : null}
|
|
46
46
|
</div>
|
|
47
47
|
);
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
const pickGreenByExperience = (experience: number): string => {
|
|
51
|
+
const exp = Math.max(0, Math.min(100, Number(experience || 0)));
|
|
52
|
+
if (exp >= 80) return green1;
|
|
53
|
+
if (exp >= 60) return green2;
|
|
54
|
+
if (exp >= 40) return green3;
|
|
55
|
+
if (exp >= 20) return green4;
|
|
56
|
+
return green5;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; headless?: boolean }) {
|
|
51
60
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
52
61
|
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
53
62
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
54
63
|
const legendRef = useRef<HTMLDivElement>(null);
|
|
55
64
|
const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
|
|
65
|
+
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
|
56
66
|
|
|
57
67
|
useEffect(() => {
|
|
58
68
|
if (typeof window !== 'undefined') {
|
|
@@ -78,9 +88,7 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
78
88
|
const value = seriesAvg(d);
|
|
79
89
|
const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
|
|
80
90
|
const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
|
|
81
|
-
|
|
82
|
-
const saturation = Math.max(20, Math.min(100, experience));
|
|
83
|
-
const color = `hsl(210 ${saturation}% 45%)`;
|
|
91
|
+
const color = 'var(--content-card-background)';
|
|
84
92
|
return {
|
|
85
93
|
label: d.axis,
|
|
86
94
|
value: size,
|
|
@@ -101,22 +109,69 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
101
109
|
}, [bubbles]);
|
|
102
110
|
|
|
103
111
|
const colorLegend = useMemo(() => {
|
|
104
|
-
|
|
105
|
-
return steps.map((s) => `hsl(210 ${s}% 45%)`);
|
|
112
|
+
return [green5, green4, green3, green2, green1];
|
|
106
113
|
}, []);
|
|
107
114
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
const defaultActiveCategory = useMemo(() => {
|
|
116
|
+
let best: { label: string; ratio: number } | null = null;
|
|
117
|
+
for (const b of bubbles) {
|
|
118
|
+
const ratio = Number(b.data?.ratio || 0);
|
|
119
|
+
if (!best || ratio > best.ratio) best = { label: b.label, ratio };
|
|
120
|
+
}
|
|
121
|
+
return best?.label || (skillsRadarLimited[0]?.axis || null);
|
|
122
|
+
}, [bubbles, skillsRadarLimited]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!activeCategory && defaultActiveCategory) setActiveCategory(defaultActiveCategory);
|
|
126
|
+
}, [activeCategory, defaultActiveCategory]);
|
|
127
|
+
|
|
128
|
+
const skillsGrid = useMemo(() => {
|
|
129
|
+
const cat = activeCategory || '';
|
|
130
|
+
const list = (skillsByCategory && cat in (skillsByCategory || {})) ? (skillsByCategory?.[cat] || []) : [];
|
|
131
|
+
const items = list.slice(0, 10);
|
|
132
|
+
const overflow = list.length - items.length;
|
|
133
|
+
const display: string[] = [];
|
|
134
|
+
for (let i = 0; i < Math.min(9, items.length); i++) display.push(items[i]);
|
|
135
|
+
if (list.length > 10) {
|
|
136
|
+
display.push(`Others (${overflow})`);
|
|
137
|
+
} else if (items.length >= 10) {
|
|
138
|
+
display.push(items[9]);
|
|
139
|
+
} else {
|
|
140
|
+
if (items.length > 9) display.push(items[9]);
|
|
141
|
+
}
|
|
142
|
+
while (display.length < 10) display.push('');
|
|
143
|
+
return display;
|
|
144
|
+
}, [activeCategory, skillsByCategory]);
|
|
114
145
|
|
|
115
146
|
if (!hasRadar) return null;
|
|
116
147
|
|
|
117
148
|
return (
|
|
118
149
|
<div className={'kyd-avoid-break'}>
|
|
119
|
-
<div ref={containerRef} style={{ width: '100%', height: 340 }}>
|
|
150
|
+
<div ref={containerRef} style={{ width: '100%', height: 340, position: 'relative' }}>
|
|
151
|
+
{!headless && (
|
|
152
|
+
<div
|
|
153
|
+
className="text-xs rounded-md border shadow-sm px-2 py-1"
|
|
154
|
+
style={{
|
|
155
|
+
position: 'absolute',
|
|
156
|
+
right: 8,
|
|
157
|
+
top: 8,
|
|
158
|
+
zIndex: 5,
|
|
159
|
+
pointerEvents: 'none',
|
|
160
|
+
background: 'var(--content-card-background)',
|
|
161
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
162
|
+
color: 'var(--text-secondary)'
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
|
|
167
|
+
<span>Size = ratio of observed/self-reported/certified</span>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex items-center gap-2 mt-1">
|
|
170
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
|
|
171
|
+
<span>Color = experience (darker = more)</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
120
175
|
<BubbleChart
|
|
121
176
|
className='w-full h-full'
|
|
122
177
|
data={bubbleData}
|
|
@@ -136,71 +191,74 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
136
191
|
}}
|
|
137
192
|
tooltipFunc={(node, d) => {
|
|
138
193
|
try {
|
|
194
|
+
try { setActiveCategory(String(d.displayText || d._id || '')); } catch {}
|
|
139
195
|
node.innerHTML = '';
|
|
196
|
+
node.className = 'rounded-md border shadow-sm px-3 py-2 text-xs';
|
|
197
|
+
(node as HTMLElement).style.background = 'var(--content-card-background)';
|
|
198
|
+
(node as HTMLElement).style.borderColor = 'var(--icon-button-secondary)';
|
|
199
|
+
(node as HTMLElement).style.color = 'var(--text-main)';
|
|
200
|
+
(node as HTMLElement).style.pointerEvents = 'none';
|
|
201
|
+
|
|
202
|
+
const header = document.createElement('div');
|
|
203
|
+
header.className = 'mb-1';
|
|
140
204
|
const title = document.createElement('div');
|
|
141
|
-
title.
|
|
205
|
+
title.className = 'font-medium';
|
|
206
|
+
title.style.color = 'var(--text-main)';
|
|
142
207
|
title.textContent = String(d.displayText || d._id || '');
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
208
|
+
header.appendChild(title);
|
|
209
|
+
|
|
210
|
+
const content = document.createElement('div');
|
|
211
|
+
content.className = 'space-y-1';
|
|
146
212
|
const ratio = Number(d.value || 0);
|
|
147
213
|
const experience = Number(d.colorValue || 0);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
214
|
+
|
|
215
|
+
const row1 = document.createElement('div');
|
|
216
|
+
row1.className = 'flex items-center justify-between gap-3';
|
|
217
|
+
const row1Left = document.createElement('span');
|
|
218
|
+
row1Left.style.color = 'var(--text-secondary)';
|
|
219
|
+
row1Left.textContent = 'Ratio';
|
|
220
|
+
const row1Right = document.createElement('span');
|
|
221
|
+
row1Right.className = 'font-medium';
|
|
222
|
+
row1Right.style.color = 'var(--text-main)';
|
|
223
|
+
row1Right.textContent = `${ratio}%`;
|
|
224
|
+
row1.appendChild(row1Left);
|
|
225
|
+
row1.appendChild(row1Right);
|
|
226
|
+
|
|
227
|
+
const row2 = document.createElement('div');
|
|
228
|
+
row2.className = 'flex items-center justify-between gap-3';
|
|
229
|
+
const row2Left = document.createElement('span');
|
|
230
|
+
row2Left.style.color = 'var(--text-secondary)';
|
|
231
|
+
row2Left.textContent = 'Experience';
|
|
232
|
+
const row2Right = document.createElement('span');
|
|
233
|
+
row2Right.className = 'font-medium';
|
|
234
|
+
row2Right.style.color = 'var(--text-main)';
|
|
235
|
+
row2Right.textContent = String(experience);
|
|
236
|
+
row2.appendChild(row2Left);
|
|
237
|
+
row2.appendChild(row2Right);
|
|
238
|
+
|
|
239
|
+
content.appendChild(row1);
|
|
240
|
+
content.appendChild(row2);
|
|
241
|
+
|
|
242
|
+
node.appendChild(header);
|
|
243
|
+
node.appendChild(content);
|
|
151
244
|
} catch {}
|
|
152
245
|
}}
|
|
153
246
|
/>
|
|
154
247
|
</div>
|
|
155
248
|
<div className={'mt-3'}>
|
|
156
249
|
<div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
157
|
-
<div className="
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const anchor = `#appendix-skills-cat-${encodeURIComponent(item.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''))}`;
|
|
167
|
-
const url = `#appendix${anchor}`;
|
|
168
|
-
window.location.hash = url;
|
|
169
|
-
}
|
|
170
|
-
} catch {}
|
|
171
|
-
}}
|
|
172
|
-
onMouseEnter={(e) => {
|
|
173
|
-
const rect = legendRef.current?.getBoundingClientRect();
|
|
174
|
-
if (!rect) return;
|
|
175
|
-
const x = e.clientX - rect.left + 12;
|
|
176
|
-
const y = e.clientY - rect.top + 12;
|
|
177
|
-
setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} • ${item.percent}% of ratio • Experience ${item.experience}` });
|
|
178
|
-
}}
|
|
179
|
-
onMouseMove={(e) => {
|
|
180
|
-
if (!legendTooltip || !legendRef.current) return;
|
|
181
|
-
const rect = legendRef.current.getBoundingClientRect();
|
|
182
|
-
setLegendTooltip({ ...legendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
|
|
183
|
-
}}
|
|
184
|
-
onMouseLeave={() => setLegendTooltip(null)}
|
|
185
|
-
>
|
|
186
|
-
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: `hsl(210 ${Math.max(20, Math.min(100, item.experience))}% 45%)`, flexShrink: 0 }} />
|
|
187
|
-
<span className="truncate">{item.label}</span>
|
|
188
|
-
<span className="ml-auto opacity-80">{item.percent}%</span>
|
|
189
|
-
</button>
|
|
250
|
+
<div className="mb-2 text-xs font-medium" style={{ color: 'var(--text-main)' }}>
|
|
251
|
+
{activeCategory ? `Category: ${activeCategory}` : 'Category'}
|
|
252
|
+
</div>
|
|
253
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs" style={{ color: 'var(--text-secondary)', height: 160 }}>
|
|
254
|
+
{skillsGrid.map((label, idx) => (
|
|
255
|
+
<div key={idx} className="flex items-center gap-2 min-w-0">
|
|
256
|
+
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: label ? 'var(--icon-button-secondary)' : 'transparent', flexShrink: 0 }} />
|
|
257
|
+
<span className="truncate" title={label}>{label || '\u00A0'}</span>
|
|
258
|
+
</div>
|
|
190
259
|
))}
|
|
191
260
|
</div>
|
|
192
261
|
{!headless && <TooltipBox state={legendTooltip} />}
|
|
193
|
-
{/* Legends */}
|
|
194
|
-
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
195
|
-
<div className="flex items-center gap-2">
|
|
196
|
-
<span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 80% 45%)' }} />
|
|
197
|
-
<span>Bubble size: relative ratio of observed/self-reported/certified</span>
|
|
198
|
-
</div>
|
|
199
|
-
<div className="flex items-center gap-2">
|
|
200
|
-
<span className="inline-block h-3 w-3 rounded-full" style={{ background: 'hsl(210 20% 45%)' }} />
|
|
201
|
-
<span>Color saturation: experience (darker = more experienced)</span>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
262
|
</div>
|
|
205
263
|
</div>
|
|
206
264
|
</div>
|
package/src/types.ts
CHANGED
|
@@ -381,6 +381,8 @@ export interface GraphInsightsPayload {
|
|
|
381
381
|
// New: experience metric (0-100) for color saturation
|
|
382
382
|
experience?: number;
|
|
383
383
|
}>;
|
|
384
|
+
// New: mapping of category -> list of skills contributing to that category
|
|
385
|
+
skillsByCategory?: Record<string, string[]>;
|
|
384
386
|
// New: Flattened list of business rule selections (for appendix)
|
|
385
387
|
business_rules_all?: Array<{
|
|
386
388
|
provider: string;
|