kyd-shared-badge 0.3.100 → 0.3.102
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyd-shared-badge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.102",
|
|
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.7",
|
|
25
25
|
"@knowyourdeveloper/react-gauge-component": "^1.1.30",
|
|
26
26
|
"@radix-ui/react-slot": "^1.2.3",
|
|
27
27
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
@@ -224,6 +224,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
224
224
|
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
225
225
|
accountAuthenticity={assessmentResult?.account_authenticity}
|
|
226
226
|
companyName={badgeData.companyName}
|
|
227
|
+
sourcesProviders={(badgeData?.connectedAccounts || []).map(a => (a?.name || '').toLowerCase())}
|
|
227
228
|
/>
|
|
228
229
|
</div>
|
|
229
230
|
{/* Top-right: Role match section */}
|
|
@@ -97,8 +97,11 @@ export default function GaugeCard({
|
|
|
97
97
|
labels={{
|
|
98
98
|
valueLabel: {
|
|
99
99
|
// Hide center text; show tier labels around the arc instead
|
|
100
|
-
formatTextValue: () =>
|
|
100
|
+
formatTextValue: () => displayLabel,
|
|
101
101
|
matchColorWithArc: true,
|
|
102
|
+
style: {
|
|
103
|
+
textShadow: 'none'
|
|
104
|
+
}
|
|
102
105
|
},
|
|
103
106
|
tickLabels: tickLabels,
|
|
104
107
|
}}
|
|
@@ -5,6 +5,7 @@ import { formatLocalDate } from '../utils/date';
|
|
|
5
5
|
import countriesLib from 'i18n-iso-countries';
|
|
6
6
|
import enLocale from 'i18n-iso-countries/langs/en.json';
|
|
7
7
|
import { FiInfo, FiAlertTriangle } from 'react-icons/fi';
|
|
8
|
+
import { ProviderIcon } from '../utils/provider';
|
|
8
9
|
|
|
9
10
|
// Register English locale once at module import time
|
|
10
11
|
countriesLib.registerLocale(enLocale);
|
|
@@ -41,13 +42,28 @@ interface ReportHeaderProps {
|
|
|
41
42
|
countries?: string[];
|
|
42
43
|
accountAuthenticity?: { label?: string; description?: string };
|
|
43
44
|
companyName?: string;
|
|
45
|
+
sourcesProviders?: string[];
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImageUrl, summary, enterpriseMatch, countries = [], accountAuthenticity, companyName }: ReportHeaderProps) => {
|
|
48
|
+
const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImageUrl, summary, enterpriseMatch, countries = [], accountAuthenticity, companyName, sourcesProviders = [] }: ReportHeaderProps) => {
|
|
47
49
|
// Use the dynamic image if available, otherwise fall back to the score-based one.
|
|
48
50
|
const finalBadgeImageUrl = badgeImageUrl || getBadgeImageUrl(score || 0);
|
|
49
51
|
const tint = hexToRgba(pickTint(score || 0), 0.06);
|
|
50
52
|
const matchLabel = enterpriseMatch?.label;
|
|
53
|
+
const sources = (() => {
|
|
54
|
+
const primary = Array.isArray(sourcesProviders) ? sourcesProviders : [];
|
|
55
|
+
// Fallback to authenticity oauth list if no explicit sources provided
|
|
56
|
+
const fallback = (accountAuthenticity as any)?.oauth_connected as string[] | undefined;
|
|
57
|
+
const combined = (primary && primary.length ? primary : (fallback || [])).filter(Boolean);
|
|
58
|
+
// Deduplicate while preserving order
|
|
59
|
+
const seen: Record<string, boolean> = {};
|
|
60
|
+
return combined.filter((n) => {
|
|
61
|
+
const key = (n || '').toLowerCase();
|
|
62
|
+
if (seen[key]) return false;
|
|
63
|
+
seen[key] = true;
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
})();
|
|
51
67
|
|
|
52
68
|
return (
|
|
53
69
|
<div
|
|
@@ -111,6 +127,20 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
111
127
|
);
|
|
112
128
|
})()
|
|
113
129
|
)}
|
|
130
|
+
{sources.length > 0 && (
|
|
131
|
+
<p className={'flex items-center gap-2 mt-2'}>
|
|
132
|
+
<a href="#appendix-connected" className={'inline-flex items-center gap-2 group'} style={{ color: 'var(--text-secondary)' }}>
|
|
133
|
+
<span className={'font-semibold'}>Sources:</span>
|
|
134
|
+
<span className={'flex items-center gap-2'}>
|
|
135
|
+
{sources.map((provider, idx) => (
|
|
136
|
+
<span key={`${provider}-${idx}`} className={'text-sm'} style={{ color: 'var(--text-main)' }}>
|
|
137
|
+
<ProviderIcon name={provider} className={'inline-block align-middle'} />
|
|
138
|
+
</span>
|
|
139
|
+
))}
|
|
140
|
+
</span>
|
|
141
|
+
</a>
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
114
144
|
</div>
|
|
115
145
|
{(enterpriseMatch?.description || matchLabel) ? (
|
|
116
146
|
<div className={'hidden md:block text-sm space-y-2 pt-4'} style={{ borderTop: '1px solid var(--icon-button-secondary)' }}>
|
|
@@ -45,7 +45,7 @@ export default function RoleOverviewCard({
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
ticks: [
|
|
48
|
-
{ value: 0, valueConfig: { formatTextValue: () => '
|
|
48
|
+
{ value: 0, valueConfig: { formatTextValue: () => 'Very Weak' } }, // Incompatible is too long, gets cut off
|
|
49
49
|
{ value: 25, valueConfig: { formatTextValue: () => 'Weak' } },
|
|
50
50
|
{ value: 50, valueConfig: { formatTextValue: () => 'Partial' } },
|
|
51
51
|
{ value: 75, valueConfig: { formatTextValue: () => 'Strong' } },
|
|
@@ -55,14 +55,14 @@ export default function RoleOverviewCard({
|
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<div
|
|
58
|
-
className={'rounded-md
|
|
58
|
+
className={'rounded-md border flex flex-col h-full'}
|
|
59
59
|
style={{
|
|
60
60
|
backgroundColor: 'var(--content-card-background)',
|
|
61
61
|
borderColor: 'var(--icon-button-secondary)',
|
|
62
62
|
backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
|
|
63
63
|
}}
|
|
64
64
|
>
|
|
65
|
-
<div className="mb-3 flex items-start justify-between gap-2">
|
|
65
|
+
<div className="mb-3 px-5 pt-5 flex items-start justify-between gap-2">
|
|
66
66
|
<div>
|
|
67
67
|
<div className={'font-semibold'} style={{ color: 'var(--text-main)' }}>{title}</div>
|
|
68
68
|
<div className={'text-xs mt-1'} style={{ color: 'var(--text-secondary)' }}>How well the candidate aligns with the target role based on KYD evidence.</div>
|
|
@@ -77,11 +77,12 @@ export default function RoleOverviewCard({
|
|
|
77
77
|
</div>
|
|
78
78
|
</span>
|
|
79
79
|
</div>
|
|
80
|
-
<div className="flex-grow flex flex-col items-center justify-center" style={{ minHeight: 200 }}>
|
|
81
|
-
<div className="relative" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth:
|
|
80
|
+
<div className="flex-grow flex flex-col items-center justify-center pb-5" style={{ minHeight: 200 }}>
|
|
81
|
+
<div className="relative group" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth: 390 }}>
|
|
82
82
|
<GaugeComponent
|
|
83
83
|
type="semicircle"
|
|
84
|
-
style={{ width: '100%', height: '100%' }}
|
|
84
|
+
style={{ width: 'calc(100% - 16px)', height: '100%', marginLeft: 8, marginRight: 8 }}
|
|
85
|
+
marginInPercent={{ top: 0.08, bottom: 0.0, left: 0.1, right: 0.1 }}
|
|
85
86
|
value={pct}
|
|
86
87
|
minValue={0}
|
|
87
88
|
maxValue={100}
|
|
@@ -89,6 +90,9 @@ export default function RoleOverviewCard({
|
|
|
89
90
|
valueLabel: {
|
|
90
91
|
formatTextValue: () => displayLabel,
|
|
91
92
|
matchColorWithArc: true,
|
|
93
|
+
style: {
|
|
94
|
+
textShadow: 'none'
|
|
95
|
+
}
|
|
92
96
|
},
|
|
93
97
|
tickLabels: tickLabels,
|
|
94
98
|
}}
|
|
@@ -13,6 +13,8 @@ type SkillsRadarPoint = {
|
|
|
13
13
|
self_reported?: number;
|
|
14
14
|
certified?: number;
|
|
15
15
|
experience?: number; // 0-100 saturation driver
|
|
16
|
+
// Total evidence count (backend computed) per category
|
|
17
|
+
evidence_count_total?: number;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
type HoverTooltipState = {
|
|
@@ -50,7 +52,7 @@ const TooltipBox = ({ state }: { state: HoverTooltipState }) => {
|
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[] }>; headless?: boolean }) {
|
|
55
|
+
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, skillsMeta, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; skillsMeta?: Record<string, { presence?: 'certified' | 'observed' | 'self-reported'; presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>; years?: number; sources?: string[]; evidenceCount?: number }>; headless?: boolean }) {
|
|
54
56
|
const hasRadar = !!(skillsCategoryRadar && skillsCategoryRadar.length > 0);
|
|
55
57
|
const skillsRadarLimited = (skillsCategoryRadar || []).slice(0, 24);
|
|
56
58
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -85,7 +87,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
85
87
|
};
|
|
86
88
|
const hideLegendTooltip = () => setLegendTooltip(null);
|
|
87
89
|
|
|
88
|
-
// ratio drives size
|
|
90
|
+
// ratio drives size by default; prefer backend evidence_count_total when present
|
|
89
91
|
const bubbles = useMemo(() => {
|
|
90
92
|
const seriesAvg = (d: SkillsRadarPoint): number => {
|
|
91
93
|
const vals = [Number(d.observed || 0), Number(d.self_reported || 0), Number(d.certified || 0)];
|
|
@@ -94,10 +96,19 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
94
96
|
return Math.max(0, Math.min(100, Math.round(base.reduce((a, b) => a + b, 0) / (base.length || 1))));
|
|
95
97
|
};
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
// Prefer evidence_count_total across categories if available; fallback to ratio
|
|
100
|
+
const evidenceTotals = (skillsCategoryRadar || []).map((d) => Number((d as any).evidence_count_total || 0));
|
|
101
|
+
const evidenceAvailable = evidenceTotals.some((v) => v > 0);
|
|
102
|
+
|
|
103
|
+
const maxValue = evidenceAvailable
|
|
104
|
+
? Math.max(1, ...evidenceTotals)
|
|
105
|
+
: Math.max(1, ...skillsRadarLimited.map(seriesAvg));
|
|
98
106
|
|
|
99
107
|
return skillsRadarLimited.map((d) => {
|
|
100
|
-
const
|
|
108
|
+
const ratio = seriesAvg(d);
|
|
109
|
+
const evidenceCountTotal = Number((d as any).evidence_count_total || 0);
|
|
110
|
+
const valueRaw = evidenceAvailable ? evidenceCountTotal : ratio;
|
|
111
|
+
const value = Math.max(0, Number(valueRaw));
|
|
101
112
|
const experience = Math.max(0, Math.min(100, Number(d.experience || 0)));
|
|
102
113
|
const size = Math.max(2, Math.round((value / maxValue) * 100)); // 2..100
|
|
103
114
|
const color = 'var(--content-card-background)';
|
|
@@ -105,11 +116,13 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
105
116
|
label: d.axis,
|
|
106
117
|
value: size,
|
|
107
118
|
color,
|
|
108
|
-
tooltip:
|
|
109
|
-
|
|
119
|
+
tooltip: evidenceAvailable
|
|
120
|
+
? `${d.axis}\nEvidence: ${value} sources\nExperience: ${experience}`
|
|
121
|
+
: `${d.axis}\nRatio: ${ratio}\nExperience: ${experience}`,
|
|
122
|
+
data: { ratio, experience, evidence: value }
|
|
110
123
|
};
|
|
111
124
|
});
|
|
112
|
-
}, [skillsRadarLimited]);
|
|
125
|
+
}, [skillsRadarLimited, skillsCategoryRadar]);
|
|
113
126
|
|
|
114
127
|
const bubbleData = useMemo(() => {
|
|
115
128
|
return bubbles.map((b) => ({
|
|
@@ -245,39 +258,70 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
245
258
|
<span className={'inline-block h-2 w-2 rounded-full'} style={{ backgroundColor: entry ? 'var(--icon-button-secondary)' : 'transparent', flexShrink: 0 }} />
|
|
246
259
|
<span className="shrink-0 opacity-70 ">{idx + (isLeft ? 1 : 6)}.</span>
|
|
247
260
|
{entry && typeof entry !== 'string' ? (
|
|
248
|
-
<span className="truncate" title={entry.label}>
|
|
261
|
+
<span className="truncate" title={entry.label}>
|
|
262
|
+
{entry.label}
|
|
263
|
+
{/* Evidence count bullet */}
|
|
264
|
+
{(() => {
|
|
265
|
+
const meta = skillsMeta?.[entry.label];
|
|
266
|
+
const count = Number((meta as any)?.evidenceCount || 0);
|
|
267
|
+
return count > 0 ? (
|
|
268
|
+
<>
|
|
269
|
+
{' '}<span className="opacity-60">•</span>{' '}
|
|
270
|
+
<span
|
|
271
|
+
className="underline decoration-dotted underline-offset-2 cursor-help opacity-80"
|
|
272
|
+
onMouseEnter={(e) =>
|
|
273
|
+
showLegendTooltipAt(
|
|
274
|
+
e.currentTarget,
|
|
275
|
+
'Evidence count',
|
|
276
|
+
'Total number of sources across observed, certified, and self-reported evidence for this skill.'
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
onMouseLeave={hideLegendTooltip}
|
|
280
|
+
>
|
|
281
|
+
{count}
|
|
282
|
+
</span>
|
|
283
|
+
</>
|
|
284
|
+
) : null;
|
|
285
|
+
})()}
|
|
286
|
+
</span>
|
|
249
287
|
) : (
|
|
250
288
|
<span className="truncate">{typeof entry === 'string' ? entry : '\u00A0'}</span>
|
|
251
289
|
)}
|
|
252
290
|
</div>
|
|
253
291
|
<span className="text-xs text-[var(--text-secondary)] flex flex-wrap items-center gap-1">
|
|
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
|
-
|
|
292
|
+
{entry && typeof entry !== 'string' ? (
|
|
293
|
+
<>
|
|
294
|
+
<span
|
|
295
|
+
className="underline decoration-dotted underline-offset-2 cursor-help"
|
|
296
|
+
onMouseEnter={(e) => showLegendTooltipAt(e.currentTarget, 'Sources', 'The source where we observed this skill.')}
|
|
297
|
+
onMouseLeave={hideLegendTooltip}
|
|
298
|
+
>
|
|
299
|
+
Sources
|
|
300
|
+
</span>:
|
|
301
|
+
{Array.isArray((entry as any).sources) && (entry as any).sources.length > 0 ? (
|
|
302
|
+
(() => {
|
|
303
|
+
const sourceProviders: string[] = ((entry as any).sources as string[]).map((src: string) => {
|
|
304
|
+
const str = String(src);
|
|
305
|
+
let provider = str.split(':')[0] || '';
|
|
306
|
+
if (!provider || provider === str) {
|
|
307
|
+
// If split(':')[0] didn't find a delimiter or provider (i.e., no ':'), try split('.')
|
|
308
|
+
provider = str.split('.')[0] || '';
|
|
309
|
+
}
|
|
310
|
+
return provider.toLowerCase();
|
|
311
|
+
});
|
|
312
|
+
const uniqueProviders = Array.from(new Set<string>(sourceProviders));
|
|
313
|
+
const filteredProviders = uniqueProviders.filter((provider) =>
|
|
314
|
+
providers.includes(provider.toLowerCase())
|
|
315
|
+
);
|
|
316
|
+
return filteredProviders.map((provider) => (
|
|
317
|
+
<ProviderIcon key={provider} name={provider} />
|
|
318
|
+
));
|
|
319
|
+
})()
|
|
320
|
+
) : null}
|
|
321
|
+
</>
|
|
322
|
+
) : (
|
|
323
|
+
<span className="opacity-0 whitespace-nowrap">'\u00A0'</span>
|
|
324
|
+
)}
|
|
281
325
|
</span>
|
|
282
326
|
</div>
|
|
283
327
|
{entry && typeof entry !== 'string' ? (
|
|
@@ -348,7 +392,7 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, sk
|
|
|
348
392
|
>
|
|
349
393
|
<div className="flex items-center gap-2">
|
|
350
394
|
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green1 }} />
|
|
351
|
-
<span>Size =
|
|
395
|
+
<span>Size = evidence count per category</span>
|
|
352
396
|
</div>
|
|
353
397
|
<div className="flex items-center gap-2 mt-1">
|
|
354
398
|
<span className="inline-block h-3 w-3 rounded-full" style={{ background: green5 }} />
|
package/src/types.ts
CHANGED
|
@@ -397,11 +397,21 @@ export interface GraphInsightsPayload {
|
|
|
397
397
|
certified?: number; // 0-100
|
|
398
398
|
// New: experience metric (0-100) for color saturation
|
|
399
399
|
experience?: number;
|
|
400
|
+
// New: total evidence count across all skills in this category
|
|
401
|
+
evidence_count_total?: number;
|
|
400
402
|
}>;
|
|
401
403
|
// New: mapping of category -> list of skills contributing to that category
|
|
402
404
|
skillsByCategory?: Record<string, string[]>;
|
|
403
405
|
// New: per-skill metadata used by UI (e.g., presence label, experience years)
|
|
404
|
-
skillsMeta?: Record<string, {
|
|
406
|
+
skillsMeta?: Record<string, {
|
|
407
|
+
presence?: 'certified' | 'observed' | 'self-reported';
|
|
408
|
+
// New: list of presence types for multi-dot rendering
|
|
409
|
+
presenceTypes?: Array<'certified' | 'observed' | 'self-reported'>;
|
|
410
|
+
years?: number;
|
|
411
|
+
sources?: string[];
|
|
412
|
+
// New: total number of evidence sources across observed/self-reported/certified
|
|
413
|
+
evidenceCount?: number;
|
|
414
|
+
}>;
|
|
405
415
|
// New: Flattened list of business rule selections (for appendix)
|
|
406
416
|
business_rules_all?: Array<{
|
|
407
417
|
provider: string;
|