kyd-shared-badge 0.3.93 → 0.3.95
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 +1 -1
- package/src/SharedBadgeDisplay.tsx +71 -73
- package/src/components/SkillsBubble.tsx +102 -89
- package/src/types.ts +4 -0
package/package.json
CHANGED
|
@@ -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>
|
|
@@ -290,7 +288,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
290
288
|
<Reveal headless={isHeadless}>
|
|
291
289
|
<div className={'kyd-avoid-break'}>
|
|
292
290
|
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
|
|
293
|
-
<SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
291
|
+
<SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} skillsByCategory={assessmentResult?.graph_insights?.skillsByCategory} />
|
|
294
292
|
</div>
|
|
295
293
|
</Reveal>
|
|
296
294
|
<div className={'pt-6 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
@@ -56,13 +56,15 @@ const pickGreenByExperience = (experience: number): string => {
|
|
|
56
56
|
return green5;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export default function SkillsBubble({ skillsCategoryRadar, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; headless?: boolean }) {
|
|
59
|
+
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; years?: number }>; headless?: boolean }) {
|
|
60
60
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
61
61
|
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
62
62
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
63
63
|
const legendRef = useRef<HTMLDivElement>(null);
|
|
64
64
|
const [legendTooltip, setLegendTooltip] = useState<HoverTooltipState>(null);
|
|
65
|
-
|
|
65
|
+
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
|
66
|
+
console.log('skillsRadarLimited', skillsRadarLimited);
|
|
67
|
+
console.log('skillsByCategory', skillsByCategory);
|
|
66
68
|
useEffect(() => {
|
|
67
69
|
if (typeof window !== 'undefined') {
|
|
68
70
|
const id = window.setTimeout(() => {
|
|
@@ -111,18 +113,90 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
111
113
|
return [green5, green4, green3, green2, green1];
|
|
112
114
|
}, []);
|
|
113
115
|
|
|
114
|
-
const
|
|
116
|
+
const defaultActiveCategory = useMemo(() => {
|
|
117
|
+
let best: { label: string; ratio: number } | null = null;
|
|
118
|
+
for (const b of bubbles) {
|
|
119
|
+
const ratio = Number(b.data?.ratio || 0);
|
|
120
|
+
if (!best || ratio > best.ratio) best = { label: b.label, ratio };
|
|
121
|
+
}
|
|
122
|
+
return best?.label || (skillsRadarLimited[0]?.axis || null);
|
|
123
|
+
}, [bubbles, skillsRadarLimited]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!activeCategory && defaultActiveCategory) setActiveCategory(defaultActiveCategory);
|
|
127
|
+
}, [activeCategory, defaultActiveCategory]);
|
|
128
|
+
|
|
129
|
+
const skillsGrid = useMemo(() => {
|
|
130
|
+
const cat = activeCategory || '';
|
|
131
|
+
const list = (skillsByCategory && cat in (skillsByCategory || {})) ? (skillsByCategory?.[cat] || []) : [];
|
|
132
|
+
// Enrich with meta and sort by years desc
|
|
133
|
+
const enriched = list.map((name) => {
|
|
134
|
+
const meta = skillsMeta?.[name] || {};
|
|
135
|
+
return { name, years: Number(meta.years || 0), presence: (meta.presence as string) || '' };
|
|
136
|
+
}).sort((a, b) => b.years - a.years || a.name.localeCompare(b.name));
|
|
137
|
+
const items = enriched.slice(0, 10);
|
|
138
|
+
const overflow = list.length - items.length;
|
|
139
|
+
const display: Array<{ label: string; years?: number; presence?: string } | ''> = [];
|
|
140
|
+
for (let i = 0; i < Math.min(9, items.length); i++) {
|
|
141
|
+
const it = items[i];
|
|
142
|
+
const label = it.name ? `${it.name}` : '';
|
|
143
|
+
display.push({ label, years: it.years, presence: it.presence });
|
|
144
|
+
}
|
|
145
|
+
if (list.length > 10) {
|
|
146
|
+
display.push({ label: `Others (${overflow})` });
|
|
147
|
+
} else if (items.length >= 10) {
|
|
148
|
+
const it = items[9];
|
|
149
|
+
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence });
|
|
150
|
+
} else {
|
|
151
|
+
if (items.length > 9) {
|
|
152
|
+
const it = items[9];
|
|
153
|
+
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
while (display.length < 10) display.push('');
|
|
157
|
+
return display;
|
|
158
|
+
}, [activeCategory, skillsByCategory, skillsMeta]);
|
|
159
|
+
|
|
160
|
+
// Compute category percent share (by ratio) for header display
|
|
161
|
+
const categoryPercentMap = useMemo(() => {
|
|
115
162
|
const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
163
|
+
const map: Record<string, number> = {};
|
|
164
|
+
for (const b of bubbles) {
|
|
165
|
+
const p = total > 0 ? Math.round(((b.data?.ratio || 0) / total) * 100) : 0;
|
|
166
|
+
map[b.label] = p;
|
|
167
|
+
}
|
|
168
|
+
return map;
|
|
119
169
|
}, [bubbles]);
|
|
120
170
|
|
|
121
171
|
if (!hasRadar) return null;
|
|
122
172
|
|
|
123
173
|
return (
|
|
124
174
|
<div className={'kyd-avoid-break'}>
|
|
125
|
-
<div ref={containerRef} style={{ width: '100%', height: 340 }}>
|
|
175
|
+
<div ref={containerRef} style={{ width: '100%', height: 340, position: 'relative' }}>
|
|
176
|
+
{!headless && (
|
|
177
|
+
<div
|
|
178
|
+
className="text-xs rounded-md border shadow-sm px-2 py-1"
|
|
179
|
+
style={{
|
|
180
|
+
position: 'absolute',
|
|
181
|
+
right: 8,
|
|
182
|
+
top: 8,
|
|
183
|
+
zIndex: 5,
|
|
184
|
+
pointerEvents: 'none',
|
|
185
|
+
background: 'var(--content-card-background)',
|
|
186
|
+
borderColor: 'var(--icon-button-secondary)',
|
|
187
|
+
color: 'var(--text-secondary)'
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<div className="flex items-center gap-2">
|
|
191
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
|
|
192
|
+
<span>Size = ratio of observed/self-reported/certified</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="flex items-center gap-2 mt-1">
|
|
195
|
+
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
|
|
196
|
+
<span>Color = experience (darker = more)</span>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
126
200
|
<BubbleChart
|
|
127
201
|
className='w-full h-full'
|
|
128
202
|
data={bubbleData}
|
|
@@ -142,6 +216,7 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
142
216
|
}}
|
|
143
217
|
tooltipFunc={(node, d) => {
|
|
144
218
|
try {
|
|
219
|
+
try { setActiveCategory(String(d.displayText || d._id || '')); } catch {}
|
|
145
220
|
node.innerHTML = '';
|
|
146
221
|
node.className = 'rounded-md border shadow-sm px-3 py-2 text-xs';
|
|
147
222
|
(node as HTMLElement).style.background = 'var(--content-card-background)';
|
|
@@ -149,101 +224,39 @@ export default function SkillsBubble({ skillsCategoryRadar, headless }: { skills
|
|
|
149
224
|
(node as HTMLElement).style.color = 'var(--text-main)';
|
|
150
225
|
(node as HTMLElement).style.pointerEvents = 'none';
|
|
151
226
|
|
|
152
|
-
const header = document.createElement('div');
|
|
153
|
-
header.className = 'mb-1';
|
|
154
227
|
const title = document.createElement('div');
|
|
155
228
|
title.className = 'font-medium';
|
|
156
229
|
title.style.color = 'var(--text-main)';
|
|
157
230
|
title.textContent = String(d.displayText || d._id || '');
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const content = document.createElement('div');
|
|
161
|
-
content.className = 'space-y-1';
|
|
162
|
-
const ratio = Number(d.value || 0);
|
|
163
|
-
const experience = Number(d.colorValue || 0);
|
|
164
|
-
|
|
165
|
-
const row1 = document.createElement('div');
|
|
166
|
-
row1.className = 'flex items-center justify-between gap-3';
|
|
167
|
-
const row1Left = document.createElement('span');
|
|
168
|
-
row1Left.style.color = 'var(--text-secondary)';
|
|
169
|
-
row1Left.textContent = 'Ratio';
|
|
170
|
-
const row1Right = document.createElement('span');
|
|
171
|
-
row1Right.className = 'font-medium';
|
|
172
|
-
row1Right.style.color = 'var(--text-main)';
|
|
173
|
-
row1Right.textContent = `${ratio}%`;
|
|
174
|
-
row1.appendChild(row1Left);
|
|
175
|
-
row1.appendChild(row1Right);
|
|
176
|
-
|
|
177
|
-
const row2 = document.createElement('div');
|
|
178
|
-
row2.className = 'flex items-center justify-between gap-3';
|
|
179
|
-
const row2Left = document.createElement('span');
|
|
180
|
-
row2Left.style.color = 'var(--text-secondary)';
|
|
181
|
-
row2Left.textContent = 'Experience';
|
|
182
|
-
const row2Right = document.createElement('span');
|
|
183
|
-
row2Right.className = 'font-medium';
|
|
184
|
-
row2Right.style.color = 'var(--text-main)';
|
|
185
|
-
row2Right.textContent = String(experience);
|
|
186
|
-
row2.appendChild(row2Left);
|
|
187
|
-
row2.appendChild(row2Right);
|
|
188
|
-
|
|
189
|
-
content.appendChild(row1);
|
|
190
|
-
content.appendChild(row2);
|
|
191
|
-
|
|
192
|
-
node.appendChild(header);
|
|
193
|
-
node.appendChild(content);
|
|
231
|
+
node.appendChild(title);
|
|
194
232
|
} catch {}
|
|
195
233
|
}}
|
|
196
234
|
/>
|
|
197
235
|
</div>
|
|
198
236
|
<div className={'mt-3'}>
|
|
199
237
|
<div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
200
|
-
<div className="
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const y = e.clientY - rect.top + 12;
|
|
220
|
-
setLegendTooltip({ visible: true, x, y, title: item.label, body: `${item.label} • ${item.percent}% of ratio • Experience ${item.experience}` });
|
|
221
|
-
}}
|
|
222
|
-
onMouseMove={(e) => {
|
|
223
|
-
if (!legendTooltip || !legendRef.current) return;
|
|
224
|
-
const rect = legendRef.current.getBoundingClientRect();
|
|
225
|
-
setLegendTooltip({ ...legendTooltip, x: e.clientX - rect.left + 12, y: e.clientY - rect.top + 12 });
|
|
226
|
-
}}
|
|
227
|
-
onMouseLeave={() => setLegendTooltip(null)}
|
|
228
|
-
>
|
|
229
|
-
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: pickGreenByExperience(item.experience), flexShrink: 0 }} />
|
|
230
|
-
<span className="truncate">{item.label}</span>
|
|
231
|
-
<span className="ml-auto opacity-80">{item.percent}%</span>
|
|
232
|
-
</button>
|
|
238
|
+
<div className="mb-2 text-xs font-medium" style={{ color: 'var(--text-main)' }}>
|
|
239
|
+
{activeCategory ? `Category: ${activeCategory}` : 'Category'}
|
|
240
|
+
{activeCategory ? ` • ${categoryPercentMap[activeCategory] ?? 0}%` : ''}
|
|
241
|
+
</div>
|
|
242
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs" style={{ color: 'var(--text-secondary)', height: 160 }}>
|
|
243
|
+
{skillsGrid.map((entry, idx) => (
|
|
244
|
+
<div key={idx} className="flex items-center gap-2 min-w-0">
|
|
245
|
+
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: entry ? 'var(--icon-button-secondary)' : 'transparent', flexShrink: 0 }} />
|
|
246
|
+
<span className="shrink-0 opacity-70">{idx + 1}.</span>
|
|
247
|
+
{entry && typeof entry !== 'string' ? (
|
|
248
|
+
<span className="truncate" title={entry.label}>
|
|
249
|
+
{entry.label}
|
|
250
|
+
{entry.years ? ` • ${entry.years}y` : ''}
|
|
251
|
+
{entry.presence ? ` • ${entry.presence}` : ''}
|
|
252
|
+
</span>
|
|
253
|
+
) : (
|
|
254
|
+
<span className="truncate">{typeof entry === 'string' ? entry : '\u00A0'}</span>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
233
257
|
))}
|
|
234
258
|
</div>
|
|
235
259
|
{!headless && <TooltipBox state={legendTooltip} />}
|
|
236
|
-
{/* Legends */}
|
|
237
|
-
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
238
|
-
<div className="flex items-center gap-2">
|
|
239
|
-
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
|
|
240
|
-
<span>Bubble size: relative ratio of observed/self-reported/certified</span>
|
|
241
|
-
</div>
|
|
242
|
-
<div className="flex items-center gap-2">
|
|
243
|
-
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
|
|
244
|
-
<span>Color shade: experience (darker = more experienced)</span>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
260
|
</div>
|
|
248
261
|
</div>
|
|
249
262
|
</div>
|
package/src/types.ts
CHANGED
|
@@ -381,6 +381,10 @@ 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[]>;
|
|
386
|
+
// New: per-skill metadata used by UI (e.g., presence label, experience years)
|
|
387
|
+
skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; years?: number }>;
|
|
384
388
|
// New: Flattened list of business rule selections (for appendix)
|
|
385
389
|
business_rules_all?: Array<{
|
|
386
390
|
provider: string;
|