kyd-shared-badge 0.3.15 → 0.3.17
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 +6 -0
- package/src/components/ReportHeader.tsx +30 -3
- package/src/lib/context.ts +15 -10
- package/src/types.ts +10 -1
package/package.json
CHANGED
|
@@ -155,6 +155,12 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
155
155
|
isPublic={true}
|
|
156
156
|
badgeImageUrl={badgeData.badgeImageUrl || ''}
|
|
157
157
|
summary={report_summary}
|
|
158
|
+
enterpriseMatch={(() => {
|
|
159
|
+
const em = assessmentResult?.enterprise_match;
|
|
160
|
+
if (!em) return null;
|
|
161
|
+
const role = em.role || {};
|
|
162
|
+
return { score: typeof em.score === 'number' ? em.score : undefined, description: em.description, roleName: role.name };
|
|
163
|
+
})()}
|
|
158
164
|
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
159
165
|
/>
|
|
160
166
|
</Reveal>
|
|
@@ -4,6 +4,7 @@ import Image from 'next/image';
|
|
|
4
4
|
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
|
+
import { FiInfo } from 'react-icons/fi';
|
|
7
8
|
|
|
8
9
|
// Register English locale once at module import time
|
|
9
10
|
countriesLib.registerLocale(enLocale);
|
|
@@ -36,10 +37,11 @@ interface ReportHeaderProps {
|
|
|
36
37
|
isPublic: boolean;
|
|
37
38
|
badgeImageUrl: string;
|
|
38
39
|
summary?: string;
|
|
40
|
+
enterpriseMatch?: { score?: number; description?: string; roleName?: string } | null;
|
|
39
41
|
countries?: string[];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImageUrl, summary, countries = [] }: ReportHeaderProps) => {
|
|
44
|
+
const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImageUrl, summary, enterpriseMatch, countries = [] }: ReportHeaderProps) => {
|
|
43
45
|
// Use the dynamic image if available, otherwise fall back to the score-based one.
|
|
44
46
|
const finalBadgeImageUrl = badgeImageUrl || getBadgeImageUrl(score || 0);
|
|
45
47
|
const tint = hexToRgba(pickTint(score || 0), 0.06);
|
|
@@ -59,7 +61,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
59
61
|
{/* Left Half: Badge Image with robust centered overlay */}
|
|
60
62
|
<div className="w-full md:w-1/3 flex items-center justify-center self-stretch">
|
|
61
63
|
<div className="relative w-full max-w-xs select-none">
|
|
62
|
-
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400}
|
|
64
|
+
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} priority className='w-full h-auto pointer-events-none p-10'/>
|
|
63
65
|
{/* Centered overlay slightly lower on Y axis, responsive and readable */}
|
|
64
66
|
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
65
67
|
<div className="font-extrabold text-black text-3xl " >
|
|
@@ -96,7 +98,32 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
96
98
|
})()
|
|
97
99
|
)}
|
|
98
100
|
</div>
|
|
99
|
-
{
|
|
101
|
+
{(enterpriseMatch?.description || enterpriseMatch?.score !== undefined) ? (
|
|
102
|
+
<div className={'hidden md:block text-sm space-y-2 pt-4'} style={{ borderTop: '1px solid var(--icon-button-secondary)' }}>
|
|
103
|
+
<div className="flex items-center justify-between">
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<p className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Role Match{enterpriseMatch?.roleName ? ` — ${enterpriseMatch.roleName}` : ''}</p>
|
|
106
|
+
<span className={'relative inline-flex items-center group'} style={{ color: 'var(--text-secondary)' }}>
|
|
107
|
+
<FiInfo size={16} />
|
|
108
|
+
<div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
|
|
109
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
110
|
+
<div style={{ fontWeight: 600 }}>AI-generated</div>
|
|
111
|
+
<div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>Role match score and description are AI-generated.</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
{typeof enterpriseMatch?.score === 'number' && (
|
|
117
|
+
<span className="px-2 py-1 rounded text-xs font-semibold" style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>
|
|
118
|
+
{Math.round(enterpriseMatch.score)}%
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
{enterpriseMatch?.description && (
|
|
123
|
+
<p className={'text-sm'} style={{ color: 'var(--text-main)' }}>{enterpriseMatch.description}</p>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
) : summary && (
|
|
100
127
|
<div className={'hidden md:block text-sm space-y-2 pt-4'} style={{ borderTop: '1px solid var(--icon-button-secondary)' }}>
|
|
101
128
|
<div>
|
|
102
129
|
<p className={'font-semibold'} style={{ color: 'var(--text-secondary)' }}>Summary:</p>
|
package/src/lib/context.ts
CHANGED
|
@@ -8,46 +8,51 @@ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
|
8
8
|
import { GraphInsightsPayload } from '../types';
|
|
9
9
|
|
|
10
10
|
const region = process.env.AWS_REGION!;
|
|
11
|
-
const usersTableName = process.env.USER_TABLE_NAME
|
|
11
|
+
const usersTableName = process.env.USER_TABLE_NAME
|
|
12
12
|
const doc = DynamoDBDocumentClient.from(new DynamoDBClient({ region }));
|
|
13
13
|
const s3 = new S3Client({ region });
|
|
14
14
|
const providerDataBucket = process.env.PROVIDER_DATA_BUCKET_NAME!;
|
|
15
15
|
const badgeTableName = process.env.BADGE_TABLE_NAME!;
|
|
16
16
|
|
|
17
17
|
export async function aggregateUserData(userId: string, enterpriseMode?: boolean, companyId?: string | null): Promise<any> {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let userResp;
|
|
19
|
+
if (enterpriseMode) {
|
|
20
|
+
const linkTableName = process.env.USER_COMPANY_LINK_TABLE_NAME
|
|
21
|
+
userResp = await doc.send(new GetCommand({ TableName: linkTableName, Key: { userId, companyId } }));
|
|
22
|
+
} else {
|
|
23
|
+
userResp = await doc.send(new GetCommand({ TableName: usersTableName, Key: { userId } }));
|
|
24
|
+
}
|
|
20
25
|
const user = userResp.Item || {};
|
|
21
|
-
|
|
26
|
+
|
|
22
27
|
if (enterpriseMode && companyId) {
|
|
23
28
|
// Optionally merge enterprise overlay later; keeping simple for MVP.
|
|
24
|
-
|
|
29
|
+
user.companyId = companyId;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
// For each provider_* entry, fetch filtered_data_key JSON from S3 and attach under <provider>_analysis
|
|
28
|
-
const providerKeys = Object.keys(
|
|
33
|
+
const providerKeys = Object.keys(user).filter(k => k.startsWith('provider_'));
|
|
29
34
|
const fetches = providerKeys.map(async (key) => {
|
|
30
35
|
try {
|
|
31
|
-
const providerData =
|
|
36
|
+
const providerData = user[key];
|
|
32
37
|
if (!providerData || typeof providerData !== 'object') return;
|
|
33
38
|
const filteredKey = providerData['filtered_data_key'];
|
|
34
39
|
if (!filteredKey || typeof filteredKey !== 'string') return;
|
|
35
40
|
const obj = await getJsonFromS3(providerDataBucket, filteredKey);
|
|
36
41
|
const providerName = key.replace(/^provider_/, '');
|
|
37
42
|
const analysisKey = `${providerName}_analysis`;
|
|
38
|
-
|
|
43
|
+
user[analysisKey] = obj;
|
|
39
44
|
} catch (e) {
|
|
40
45
|
console.error('Failed to load provider filtered data', e);
|
|
41
46
|
// Non-fatal; annotate error
|
|
42
47
|
try {
|
|
43
48
|
const providerName = key.replace(/^provider_/, '');
|
|
44
|
-
|
|
49
|
+
user[`${providerName}_analysis`] = { error: 'Failed to load provider filtered data' };
|
|
45
50
|
} catch {}
|
|
46
51
|
}
|
|
47
52
|
});
|
|
48
53
|
await Promise.all(fetches);
|
|
49
54
|
|
|
50
|
-
return
|
|
55
|
+
return user;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
package/src/types.ts
CHANGED
|
@@ -154,6 +154,15 @@ export interface FBIWantedMatch {
|
|
|
154
154
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
155
|
raw_result: any;
|
|
156
156
|
}
|
|
157
|
+
export interface EnterpriseMatch {
|
|
158
|
+
role: {
|
|
159
|
+
name: string;
|
|
160
|
+
description: string;
|
|
161
|
+
};
|
|
162
|
+
score: number;
|
|
163
|
+
description: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
|
|
158
167
|
export interface AssessmentResult {
|
|
159
168
|
final_percent: number;
|
|
@@ -225,8 +234,8 @@ export interface AssessmentResult {
|
|
|
225
234
|
skills_matrix?: SkillsMatrix;
|
|
226
235
|
skills_all?: SkillsAll;
|
|
227
236
|
graph_insights?: GraphInsightsPayload;
|
|
237
|
+
enterprise_match?: EnterpriseMatch;
|
|
228
238
|
}
|
|
229
|
-
|
|
230
239
|
export interface ClientMetadata {
|
|
231
240
|
ipAddress: string;
|
|
232
241
|
userAgent: string;
|