kyd-shared-badge 0.3.94 → 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 +1 -1
- package/src/components/SkillsBubble.tsx +48 -50
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -288,7 +288,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps, headless }: { badgeData: Pub
|
|
|
288
288
|
<Reveal headless={isHeadless}>
|
|
289
289
|
<div className={'kyd-avoid-break'}>
|
|
290
290
|
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Skills Footprint</h4>
|
|
291
|
-
<SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} />
|
|
291
|
+
<SkillsBubble skillsCategoryRadar={graphInsights?.skillsCategoryRadar} headless={isHeadless} skillsByCategory={assessmentResult?.graph_insights?.skillsByCategory} />
|
|
292
292
|
</div>
|
|
293
293
|
</Reveal>
|
|
294
294
|
<div className={'pt-6 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
@@ -56,14 +56,15 @@ const pickGreenByExperience = (experience: number): string => {
|
|
|
56
56
|
return green5;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, headless }: { skillsCategoryRadar?: SkillsRadarPoint[]; skillsByCategory?: Record<string, string[]>; 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
|
-
|
|
66
|
+
console.log('skillsRadarLimited', skillsRadarLimited);
|
|
67
|
+
console.log('skillsByCategory', skillsByCategory);
|
|
67
68
|
useEffect(() => {
|
|
68
69
|
if (typeof window !== 'undefined') {
|
|
69
70
|
const id = window.setTimeout(() => {
|
|
@@ -128,20 +129,44 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, he
|
|
|
128
129
|
const skillsGrid = useMemo(() => {
|
|
129
130
|
const cat = activeCategory || '';
|
|
130
131
|
const list = (skillsByCategory && cat in (skillsByCategory || {})) ? (skillsByCategory?.[cat] || []) : [];
|
|
131
|
-
|
|
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);
|
|
132
138
|
const overflow = list.length - items.length;
|
|
133
|
-
const display: string
|
|
134
|
-
for (let i = 0; i < Math.min(9, items.length); i++)
|
|
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
|
+
}
|
|
135
145
|
if (list.length > 10) {
|
|
136
|
-
display.push(`Others (${overflow})`);
|
|
146
|
+
display.push({ label: `Others (${overflow})` });
|
|
137
147
|
} else if (items.length >= 10) {
|
|
138
|
-
|
|
148
|
+
const it = items[9];
|
|
149
|
+
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence });
|
|
139
150
|
} else {
|
|
140
|
-
if (items.length > 9)
|
|
151
|
+
if (items.length > 9) {
|
|
152
|
+
const it = items[9];
|
|
153
|
+
display.push({ label: it?.name || '', years: it?.years, presence: it?.presence });
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
while (display.length < 10) display.push('');
|
|
143
157
|
return display;
|
|
144
|
-
}, [activeCategory, skillsByCategory]);
|
|
158
|
+
}, [activeCategory, skillsByCategory, skillsMeta]);
|
|
159
|
+
|
|
160
|
+
// Compute category percent share (by ratio) for header display
|
|
161
|
+
const categoryPercentMap = useMemo(() => {
|
|
162
|
+
const total = bubbles.reduce((sum, b) => sum + (b.data?.ratio || 0), 0);
|
|
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;
|
|
169
|
+
}, [bubbles]);
|
|
145
170
|
|
|
146
171
|
if (!hasRadar) return null;
|
|
147
172
|
|
|
@@ -199,48 +224,11 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, he
|
|
|
199
224
|
(node as HTMLElement).style.color = 'var(--text-main)';
|
|
200
225
|
(node as HTMLElement).style.pointerEvents = 'none';
|
|
201
226
|
|
|
202
|
-
const header = document.createElement('div');
|
|
203
|
-
header.className = 'mb-1';
|
|
204
227
|
const title = document.createElement('div');
|
|
205
228
|
title.className = 'font-medium';
|
|
206
229
|
title.style.color = 'var(--text-main)';
|
|
207
230
|
title.textContent = String(d.displayText || d._id || '');
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const content = document.createElement('div');
|
|
211
|
-
content.className = 'space-y-1';
|
|
212
|
-
const ratio = Number(d.value || 0);
|
|
213
|
-
const experience = Number(d.colorValue || 0);
|
|
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);
|
|
231
|
+
node.appendChild(title);
|
|
244
232
|
} catch {}
|
|
245
233
|
}}
|
|
246
234
|
/>
|
|
@@ -249,12 +237,22 @@ export default function SkillsBubble({ skillsCategoryRadar, skillsByCategory, he
|
|
|
249
237
|
<div ref={legendRef} className={'kyd-avoid-break'} style={{ position: 'relative', breakInside: 'avoid', pageBreakInside: 'avoid' as unknown as undefined }}>
|
|
250
238
|
<div className="mb-2 text-xs font-medium" style={{ color: 'var(--text-main)' }}>
|
|
251
239
|
{activeCategory ? `Category: ${activeCategory}` : 'Category'}
|
|
240
|
+
{activeCategory ? ` • ${categoryPercentMap[activeCategory] ?? 0}%` : ''}
|
|
252
241
|
</div>
|
|
253
242
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs" style={{ color: 'var(--text-secondary)', height: 160 }}>
|
|
254
|
-
{skillsGrid.map((
|
|
243
|
+
{skillsGrid.map((entry, idx) => (
|
|
255
244
|
<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:
|
|
257
|
-
<span className="
|
|
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
|
+
)}
|
|
258
256
|
</div>
|
|
259
257
|
))}
|
|
260
258
|
</div>
|
package/src/types.ts
CHANGED
|
@@ -383,6 +383,8 @@ export interface GraphInsightsPayload {
|
|
|
383
383
|
}>;
|
|
384
384
|
// New: mapping of category -> list of skills contributing to that category
|
|
385
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 }>;
|
|
386
388
|
// New: Flattened list of business rule selections (for appendix)
|
|
387
389
|
business_rules_all?: Array<{
|
|
388
390
|
provider: string;
|