kyd-shared-badge 0.2.26 → 0.2.28
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 +7 -5
- package/src/SharedBadgeDisplay.tsx +464 -226
- package/src/components/AppendixTables.tsx +105 -11
- package/src/components/BusinessRuleLink.tsx +33 -0
- package/src/components/BusinessRulesContext.tsx +56 -0
- package/src/components/CategoryBars.tsx +100 -0
- package/src/components/ConnectedPlatforms.tsx +67 -0
- package/src/components/GaugeCard.tsx +99 -0
- package/src/components/GraphInsights.tsx +351 -0
- package/src/components/IpRiskAnalysisDisplay.tsx +4 -4
- package/src/components/ReportHeader.tsx +75 -42
- package/src/components/RiskCard.tsx +106 -0
- package/src/components/Skills.tsx +422 -0
- package/src/components/SkillsAppendixTable.tsx +83 -0
- package/src/types.ts +223 -11
|
@@ -1,65 +1,52 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
4
3
|
import { FiAlertTriangle } from 'react-icons/fi';
|
|
5
|
-
import { PublicBadgeData } from './types';
|
|
6
|
-
import ShareButton from './components/ShareButton';
|
|
4
|
+
import { PublicBadgeData, GraphInsightsPayload, ScoringSummary, BusinessRule, TopBusinessRule } from './types';
|
|
5
|
+
// import ShareButton from './components/ShareButton';
|
|
7
6
|
import ReportHeader from './components/ReportHeader';
|
|
8
7
|
import AppendixTables from './components/AppendixTables';
|
|
9
|
-
import
|
|
8
|
+
import BusinessRuleLink from './components/BusinessRuleLink';
|
|
10
9
|
import IpRiskAnalysisDisplay from './components/IpRiskAnalysisDisplay';
|
|
11
|
-
import Image from 'next/image';
|
|
12
|
-
import
|
|
10
|
+
// import Image from 'next/image';
|
|
11
|
+
import GraphInsights from './components/GraphInsights';
|
|
12
|
+
import ConnectedPlatforms from './components/ConnectedPlatforms';
|
|
13
|
+
import { FaGithub, FaGitlab, FaStackOverflow, FaLinkedin, FaGoogle, FaKaggle } from 'react-icons/fa';
|
|
14
|
+
import { SiCredly, SiFiverr } from 'react-icons/si';
|
|
15
|
+
import GaugeCard from './components/GaugeCard';
|
|
16
|
+
import RiskCard from './components/RiskCard';
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const hexToRgba = (hex: string, alpha: number) => {
|
|
21
|
-
const clean = hex.replace('#', '');
|
|
22
|
-
const r = parseInt(clean.substring(0, 2), 16);
|
|
23
|
-
const g = parseInt(clean.substring(2, 4), 16);
|
|
24
|
-
const b = parseInt(clean.substring(4, 6), 16);
|
|
25
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
26
|
-
};
|
|
18
|
+
import { yellow, green } from './colors';
|
|
19
|
+
import Skills from './components/Skills';
|
|
20
|
+
import CategoryBars from './components/CategoryBars';
|
|
21
|
+
import SkillsAppendixTable from './components/SkillsAppendixTable';
|
|
22
|
+
import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
27
23
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Helpers to map thresholds to icon images
|
|
36
|
-
const getTechnicalIconSrc = (score: number): string => {
|
|
37
|
-
if (score >= 80) return '/badge/codegreen.png';
|
|
38
|
-
if (score >= 65) return '/badge/codeyellow.png';
|
|
39
|
-
return '/badge/codered.png';
|
|
40
|
-
};
|
|
24
|
+
// const hexToRgba = (hex: string, alpha: number) => {
|
|
25
|
+
// const clean = hex.replace('#', '');
|
|
26
|
+
// const r = parseInt(clean.substring(0, 2), 16);
|
|
27
|
+
// const g = parseInt(clean.substring(2, 4), 16);
|
|
28
|
+
// const b = parseInt(clean.substring(4, 6), 16);
|
|
29
|
+
// return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
30
|
+
// };
|
|
41
31
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
33
|
+
const {
|
|
34
|
+
badgeId,
|
|
35
|
+
developerName,
|
|
36
|
+
assessmentResult,
|
|
37
|
+
updatedAt,
|
|
38
|
+
// connectedAccounts
|
|
39
|
+
} = badgeData;
|
|
47
40
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
if (d.indexOf('medium') !== -1 || d.indexOf('moderate') !== -1 || d.indexOf('balanced') !== -1) {
|
|
55
|
-
return '/badge/aiyellow.png';
|
|
56
|
-
}
|
|
57
|
-
return '/badge/aired.png';
|
|
58
|
-
};
|
|
41
|
+
const {
|
|
42
|
+
report_summary,
|
|
43
|
+
// developer_trust_explanation,
|
|
44
|
+
screening_sources,
|
|
45
|
+
// industry_considerations
|
|
46
|
+
} = assessmentResult;
|
|
59
47
|
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const { summary_scores, report_summary, developer_trust_explanation, key_skills, screening_sources, industry_considerations } = assessmentResult;
|
|
48
|
+
const graphInsights: GraphInsightsPayload = (assessmentResult)?.graph_insights || {} as GraphInsightsPayload;
|
|
49
|
+
const scoringSummary: ScoringSummary = (assessmentResult)?.scoring_summary || {} as ScoringSummary;
|
|
63
50
|
|
|
64
51
|
// const devTrustScore = summary_scores.developer_trust;
|
|
65
52
|
// const riskScore = summary_scores.risk_score;
|
|
@@ -67,219 +54,470 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
67
54
|
|
|
68
55
|
const wrapperMaxWidth = 'max-w-5xl';
|
|
69
56
|
|
|
57
|
+
// Overall and genre scores
|
|
58
|
+
const overallFinalPercent = Math.round(Number(scoringSummary?.overall?.combined?.percent_progress || 0));
|
|
59
|
+
|
|
60
|
+
// Build category display grouped by genres from scoring_summary.config.genre_mapping
|
|
61
|
+
const genreMapping = (scoringSummary?.config?.genre_mapping) || {};
|
|
62
|
+
const categoryScores = (scoringSummary?.category_scores) || {};
|
|
63
|
+
const categoryTopByGraph = (graphInsights?.categoryTopBusiness) || {};
|
|
64
|
+
|
|
65
|
+
const topBusinessForGenre = (genre: string) => {
|
|
66
|
+
const cats: string[] = (genreMapping)?.[genre] || [];
|
|
67
|
+
const items = [];
|
|
68
|
+
for (const c of cats) {
|
|
69
|
+
const arr = (categoryTopByGraph)?.[c] || [];
|
|
70
|
+
for (const it of arr) items.push(it);
|
|
71
|
+
}
|
|
72
|
+
items.sort((a, b) => Number(b?.weight || 0) - Number(a?.weight || 0));
|
|
73
|
+
return items.slice(0, 3) as TopBusinessRule[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const skillsMatrix = assessmentResult?.skills_matrix || { skills: [] };
|
|
77
|
+
const skillsAll = assessmentResult?.skills_all || { skills: [] };
|
|
78
|
+
const connected = badgeData?.connectedAccounts || [];
|
|
79
|
+
const ProviderIcon = ({ name }: { name?: string }) => {
|
|
80
|
+
const n = (name || '').toLowerCase();
|
|
81
|
+
if (n.includes('github')) return <FaGithub />;
|
|
82
|
+
if (n.includes('gitlab')) return <FaGitlab />;
|
|
83
|
+
if (n.includes('stack')) return <FaStackOverflow />;
|
|
84
|
+
if (n.includes('credly')) return <SiCredly />;
|
|
85
|
+
if (n.includes('fiverr')) return <SiFiverr />;
|
|
86
|
+
if (n.includes('kaggle')) return <FaKaggle />;
|
|
87
|
+
if (n.includes('google')) return <FaGoogle />;
|
|
88
|
+
if (n.includes('linkedin')) return <FaLinkedin />;
|
|
89
|
+
return <span className="inline-block w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--icon-button-secondary)' }} />;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const getProviderDisplayName = (name?: string): string => {
|
|
93
|
+
const n = (name || '').toLowerCase();
|
|
94
|
+
if (n.includes('github')) return 'GitHub';
|
|
95
|
+
if (n.includes('gitlab')) return 'GitLab';
|
|
96
|
+
if (n.includes('stack')) return 'Stack Overflow';
|
|
97
|
+
if (n.includes('credly')) return 'Credly';
|
|
98
|
+
if (n.includes('fiverr')) return 'Fiverr';
|
|
99
|
+
if (n.includes('kaggle')) return 'Kaggle';
|
|
100
|
+
if (n.includes('google')) return 'Google Scholar';
|
|
101
|
+
if (n.includes('linkedin')) return 'LinkedIn';
|
|
102
|
+
return name || 'Provider';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getProviderTooltipCopy = (provider?: string): string => {
|
|
106
|
+
const n = (provider || '').toLowerCase();
|
|
107
|
+
if (n.includes('github')) return 'Signals from open-source activity: commits, repos, stars, and collaboration patterns.';
|
|
108
|
+
if (n.includes('gitlab')) return 'Signals from GitLab projects and contributions that indicate delivery and collaboration.';
|
|
109
|
+
if (n.includes('stack')) return 'Signals from Q&A participation such as answers, reputation, and accepted solutions.';
|
|
110
|
+
if (n.includes('credly')) return 'Verified badges and certifications that validate specific skills or achievements.';
|
|
111
|
+
if (n.includes('fiverr')) return 'Client reviews, job history, and delivery consistency across freelance engagements.';
|
|
112
|
+
if (n.includes('kaggle')) return 'Competition results, notebooks, and dataset contributions that reflect analytical skill.';
|
|
113
|
+
if (n.includes('google')) return 'Publications, citations, and scholarly presence indicating research impact.';
|
|
114
|
+
if (n.includes('linkedin')) return 'Professional history, endorsements, and network signals indicating credibility.';
|
|
115
|
+
return 'Signals contributed from this provider relevant to capability and trust.';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getCategoryTooltipCopy = (category: string): string => {
|
|
119
|
+
const name = (category || '').toLowerCase();
|
|
120
|
+
if (/network|connection|collab|peer/.test(name)) return 'Signals from the developer’s professional connections, collaborations, and peer recognition.';
|
|
121
|
+
if (/project|repo|portfolio|work/.test(name)) return 'Signals from a developer’s visible projects, repositories, or published work that indicate breadth and quality of output.';
|
|
122
|
+
if (/skill|cert|assessment|endorse/.test(name)) return 'Signals tied to specific technical abilities, such as verified certifications, assessments, or endorsements.';
|
|
123
|
+
if (/experience|tenure|history/.test(name)) return 'Signals of tenure and diversity of professional or project involvement over time.';
|
|
124
|
+
if (/activity|recency|frequency|engage/.test(name)) return 'Signals of recency and frequency of developer engagement in professional or technical platforms.';
|
|
125
|
+
if (/sanction|legal|criminal|regulatory|ofac|fbi|watchlist/.test(name)) return 'Signals of legal, criminal, or regulatory red flags linked to an identity.';
|
|
126
|
+
if (/identity|authentic|consisten/.test(name)) return 'Signals that indicate whether a developer’s identity is genuine and consistent across platforms.';
|
|
127
|
+
if (/reputation|review|rating|feedback|perceive|peer/.test(name)) return 'Signals of how peers, clients, and communities perceive the developer.';
|
|
128
|
+
if (/geo|jurisdiction|country|region|ip|location/.test(name)) return 'Signals tied to a developer’s geographic or jurisdictional context.';
|
|
129
|
+
if (/security|cyber/.test(name)) return 'Signals of security posture and potential cyber-risk exposure.';
|
|
130
|
+
if (/risk/.test(name)) return 'KYD Risk surfaces signals of authenticity, reputation, and environmental telemetry that indicate potential risks in engaging with a developer.';
|
|
131
|
+
if (/tech|technical/.test(name)) return 'KYD Technical surfaces signals from a developer’s portfolio, skills, experience, activity, and network to indicate the likelihood of technical capability.';
|
|
132
|
+
return 'Share of overall contribution by category based on applied weights.';
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const barColor = (pct: number) => pct >= 60 ? green : pct >= 40 ? '#ffbb54' : '#EC6662';
|
|
136
|
+
|
|
70
137
|
return (
|
|
138
|
+
<BusinessRulesProvider items={graphInsights?.business_rules_all}>
|
|
71
139
|
<div className={`${wrapperMaxWidth} mx-auto`}>
|
|
72
140
|
{/* Share controls removed; app-level pages render their own actions */}
|
|
73
141
|
<ReportHeader
|
|
74
142
|
badgeId={badgeId}
|
|
75
143
|
developerName={badgeData.developerName}
|
|
76
144
|
updatedAt={updatedAt}
|
|
77
|
-
|
|
78
|
-
score={0}
|
|
145
|
+
score={overallFinalPercent || 0}
|
|
79
146
|
isPublic={true}
|
|
80
147
|
badgeImageUrl={badgeData.badgeImageUrl || ''}
|
|
148
|
+
summary={report_summary}
|
|
149
|
+
countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
|
|
81
150
|
/>
|
|
82
151
|
<div
|
|
83
152
|
className={'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
|
|
84
153
|
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
|
|
85
154
|
>
|
|
86
155
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
156
|
+
<div className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
157
|
+
<div className="pt-8 first:pt-0">
|
|
158
|
+
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</h4>
|
|
159
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
160
|
+
{/* Technical semicircle gauge (refactored) */}
|
|
161
|
+
{(() => {
|
|
162
|
+
const ui = graphInsights?.uiSummary?.technical || {};
|
|
163
|
+
const pct = Math.round(Number(ui?.percent ?? 0));
|
|
164
|
+
const label = ui?.label || 'EVIDENCE';
|
|
165
|
+
const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Technical');
|
|
166
|
+
return (
|
|
167
|
+
<GaugeCard
|
|
168
|
+
key={'technical-card'}
|
|
169
|
+
title={'KYD Technical'}
|
|
170
|
+
description={'The gauge visualization shows a weighted composite of technical evidence, with rightward movement indicating stronger indications of developer capability'}
|
|
171
|
+
percent={pct}
|
|
172
|
+
label={label}
|
|
173
|
+
topMovers={top?.map(t => ({ label: t?.label, uid: t?.uid }))}
|
|
174
|
+
topMoversTitle={'Top Score Movers'}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
})()}
|
|
106
178
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
179
|
+
{/* Risk descending bars card (abstracted) */}
|
|
180
|
+
{(() => {
|
|
181
|
+
const ui = graphInsights?.uiSummary?.risk || {};
|
|
182
|
+
const pctGood = Math.round(Number(ui?.percent_good ?? 0));
|
|
183
|
+
const label = ui?.label || 'RISK';
|
|
184
|
+
const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Risk');
|
|
185
|
+
const tooltip = 'Higher bar filled indicates lower overall risk; movement to the right reflects improved risk posture.';
|
|
186
|
+
return (
|
|
187
|
+
<RiskCard
|
|
188
|
+
title={'KYD Risk'}
|
|
189
|
+
description={'The bar chart visualizes relative risk levels, where shorter bars denote lower risk and taller bars indicate greater exposure.'}
|
|
190
|
+
percentGood={pctGood}
|
|
191
|
+
label={label}
|
|
192
|
+
topMovers={top?.map(t => ({ label: t?.label, uid: t?.uid }))}
|
|
193
|
+
topMoversTitle={'Top Score Movers'}
|
|
194
|
+
tooltipText={tooltip}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
})()}
|
|
198
|
+
|
|
199
|
+
{/* AI transparency semicircle gauge */}
|
|
200
|
+
{(() => {
|
|
201
|
+
const ai_usage_summary = assessmentResult?.ai_usage_summary;
|
|
202
|
+
const label = 'AI Transparency'// TODO: calculate label frontend
|
|
203
|
+
const topMovers = ai_usage_summary?.key_findings || []
|
|
204
|
+
return (
|
|
205
|
+
<GaugeCard
|
|
206
|
+
key={'ai-card'}
|
|
207
|
+
title={'KYD AI'}
|
|
208
|
+
description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
|
|
209
|
+
percent={ai_usage_summary?.transparency_score}
|
|
210
|
+
label={label}
|
|
211
|
+
// id non-functional
|
|
212
|
+
topMovers={topMovers.map(t => ({ label: t, uid: 'ai-usage' }))}
|
|
213
|
+
topMoversTitle={'Key Findings'}
|
|
115
214
|
/>
|
|
215
|
+
);
|
|
216
|
+
})()}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* Technical Scores */}
|
|
221
|
+
<div className="mt-8" >
|
|
222
|
+
<div key={'Technical'} className='pt-8 space-y-8' style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
223
|
+
<h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical</h4>
|
|
224
|
+
{/* technical graph insights */}
|
|
225
|
+
<div className="">
|
|
226
|
+
<GraphInsights
|
|
227
|
+
graphInsights={graphInsights}
|
|
228
|
+
categories={genreMapping?.['Technical'] as string[]}
|
|
229
|
+
genre={'Technical'}
|
|
230
|
+
scoringSummary={scoringSummary}
|
|
231
|
+
/>
|
|
116
232
|
</div>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
233
|
+
|
|
234
|
+
{/* category bars and contributing factors */}
|
|
235
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 w-full gap-8 items-stretch py-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
236
|
+
|
|
237
|
+
{/* Left: Bars */}
|
|
238
|
+
<div className="lg:col-span-8 h-full">
|
|
239
|
+
<CategoryBars
|
|
240
|
+
title={'Technical Category Contributions - Percentages'}
|
|
241
|
+
categories={genreMapping?.['Technical'] as string[]}
|
|
242
|
+
categoryScores={categoryScores}
|
|
243
|
+
barColor={barColor}
|
|
244
|
+
getCategoryTooltipCopy={getCategoryTooltipCopy}
|
|
245
|
+
barHeight={16}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Right: Contributing Factors */}
|
|
250
|
+
<div className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start">
|
|
251
|
+
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
252
|
+
<div className="space-y-4">
|
|
253
|
+
{(genreMapping?.['Technical'] || []).map((cat: string) => {
|
|
254
|
+
const topRules = (categoryTopByGraph?.[cat] || []).slice(0, 3) as BusinessRule[];
|
|
255
|
+
if (!topRules || topRules.length === 0) return null;
|
|
256
|
+
return (
|
|
257
|
+
<div key={cat} className="pt-3 first:pt-0">
|
|
258
|
+
<div className={'text-xs font-semibold mb-1'} style={{ color: 'var(--text-main)' }}>{cat}</div>
|
|
259
|
+
<div className="space-y-1">
|
|
260
|
+
{topRules.map((r, idx: number) => (
|
|
261
|
+
<div key={idx} className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
262
|
+
<span className={'relative inline-flex items-center group'} style={{ color: 'var(--text-secondary)' }}>
|
|
263
|
+
<ProviderIcon name={r.provider} />
|
|
264
|
+
<div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
|
|
265
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
266
|
+
<div style={{ fontWeight: 600 }}>{getProviderDisplayName(r.provider)}</div>
|
|
267
|
+
<div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>{getProviderTooltipCopy(r.provider)}</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
125
270
|
</span>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
</p>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
271
|
+
<BusinessRuleLink uid={r.uid} label={r.label} />
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
133
275
|
</div>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<span className={'font-medium'} style={{ color: 'var(--text-main)' }}>{f.title || 'Match'}</span>
|
|
196
|
-
{f.url ? <> — <a href={f.url} target='_blank' rel='noopener noreferrer' className={'underline'} style={{ color: 'var(--icon-accent)' }}>Details</a></> : null}
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
))}
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div className="pt-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
|
|
283
|
+
<h3 className={'text-xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
|
|
284
|
+
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
285
|
+
<Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} />
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
<div className="pt-8 space-y-8">
|
|
295
|
+
<h3 className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</h3>
|
|
296
|
+
|
|
297
|
+
{/* Risk Graph Insights and Category Bars */}
|
|
298
|
+
<div className="">
|
|
299
|
+
<GraphInsights
|
|
300
|
+
graphInsights={graphInsights}
|
|
301
|
+
categories={genreMapping?.['Risk'] as string[]}
|
|
302
|
+
genre={'Risk'}
|
|
303
|
+
scoringSummary={scoringSummary}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 w-full items-stretch py-8 border-y" style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
307
|
+
{/* Left: Bars */}
|
|
308
|
+
<div className="lg:col-span-8 h-full">
|
|
309
|
+
<CategoryBars
|
|
310
|
+
title={'KYD Risk - Category Insights'}
|
|
311
|
+
categories={genreMapping?.['Risk'] as string[]}
|
|
312
|
+
categoryScores={categoryScores}
|
|
313
|
+
barColor={barColor}
|
|
314
|
+
getCategoryTooltipCopy={getCategoryTooltipCopy}
|
|
315
|
+
barHeight={16}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
{/* Right: Contributing Factors */}
|
|
319
|
+
<div className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start">
|
|
320
|
+
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
321
|
+
<div className="space-y-4">
|
|
322
|
+
{genreMapping?.['Risk']?.map((cat: string) => {
|
|
323
|
+
const topRules = (categoryTopByGraph?.[cat] || []).slice(0, 3) as BusinessRule[];
|
|
324
|
+
if (!topRules || topRules.length === 0) return null;
|
|
325
|
+
return (
|
|
326
|
+
<div key={cat} className="pt-3 first:pt-0">
|
|
327
|
+
<div className={'text-xs font-semibold mb-1'} style={{ color: 'var(--text-main)' }}>{cat}</div>
|
|
328
|
+
<div className="space-y-1">
|
|
329
|
+
{topRules.map((r, idx: number) => (
|
|
330
|
+
<div key={idx} className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
|
331
|
+
<span className={'relative inline-flex items-center group'} style={{ color: 'var(--text-secondary)' }}>
|
|
332
|
+
<ProviderIcon name={r.provider} />
|
|
333
|
+
<div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
|
|
334
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6 }}>
|
|
335
|
+
<div style={{ fontWeight: 600 }}>{getProviderDisplayName(r.provider)}</div>
|
|
336
|
+
<div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>{getProviderTooltipCopy(r.provider)}</div>
|
|
200
337
|
</div>
|
|
201
338
|
</div>
|
|
202
|
-
|
|
339
|
+
</span>
|
|
340
|
+
<BusinessRuleLink uid={r.uid} label={r.label} />
|
|
203
341
|
</div>
|
|
204
|
-
);
|
|
205
|
-
})()}
|
|
206
|
-
<IpRiskAnalysisDisplay ipRiskAnalysis={screening_sources?.ip_risk_analysis} />
|
|
207
|
-
</>
|
|
208
|
-
)}
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
<div className="pt-8">
|
|
212
|
-
<h3 className={'text-xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>4. KYD AI™ Signals <span className="text-sm font-semibold bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 px-2 py-1 rounded-full align-middle">Beta</span></h3>
|
|
213
|
-
<div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
|
|
214
|
-
<p>{assessmentResult.ai_usage_summary?.explanation}</p>
|
|
215
|
-
{assessmentResult.ai_usage_summary?.key_findings && (
|
|
216
|
-
<ul className="list-disc list-inside">
|
|
217
|
-
{assessmentResult.ai_usage_summary.key_findings.map((finding, index) => (
|
|
218
|
-
<li key={index}>{finding}</li>
|
|
219
342
|
))}
|
|
220
|
-
</
|
|
221
|
-
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
})}
|
|
222
347
|
</div>
|
|
223
348
|
</div>
|
|
349
|
+
</div>
|
|
224
350
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
351
|
+
{/* cyber risk display */}
|
|
352
|
+
{badgeData.optOutScreening ? (
|
|
353
|
+
<div className={'p-4 rounded-lg border'} style={{ backgroundColor: 'var(--icon-button-secondary)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
354
|
+
<div className="flex items-start">
|
|
355
|
+
<span className="h-5 w-5 mr-3 mt-0.5 flex-shrink-0" style={{ color: yellow }}>
|
|
356
|
+
<FiAlertTriangle size={20} />
|
|
357
|
+
</span>
|
|
358
|
+
<div>
|
|
359
|
+
<h4 className={'font-bold'} style={{ color: 'var(--text-main)' }}>User Opted Out of Screening</h4>
|
|
360
|
+
<p className={'text-sm mt-1'} style={{ color: 'var(--text-secondary)' }}>
|
|
361
|
+
The user chose not to participate in the automated sanctions and risk screening process. The risk score reflects this decision and is for informational purposes only.
|
|
362
|
+
</p>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
) : (
|
|
367
|
+
<>
|
|
368
|
+
{(() => {
|
|
369
|
+
const ss = assessmentResult?.screening_sources;
|
|
370
|
+
const ofacMatches = ss?.ofac_screen?.matches && (ss.ofac_screen.matches.length > 0);
|
|
371
|
+
const cslDetails = ss?.csl_details && (Array.isArray(ss.csl_details) ? ss.csl_details.length > 0 : Object.keys(ss.csl_details).length > 0);
|
|
372
|
+
const fbiMatches = ss?.fbi_matches && (ss.fbi_matches.length > 0);
|
|
373
|
+
if (!(ofacMatches || cslDetails || fbiMatches)) return null;
|
|
374
|
+
return (
|
|
375
|
+
<div className={'mb-8 rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
|
|
376
|
+
<h4 className={'text-lg font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>3A. Sanctions Matches</h4>
|
|
377
|
+
{/* OFAC matches */}
|
|
378
|
+
{ofacMatches && (
|
|
379
|
+
<div className={'mb-4'}>
|
|
380
|
+
<h5 className={'font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>OFAC API Matches</h5>
|
|
229
381
|
<div>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
250
|
-
})()}
|
|
382
|
+
{ss!.ofac_screen!.matches!.map((m, i: number) => {
|
|
383
|
+
const s = m?.sanction;
|
|
384
|
+
const title = s?.name || 'Unknown';
|
|
385
|
+
const programs = (s?.programs && s?.programs.length) ? ` — Programs: ${s?.programs.join(', ')}` : '';
|
|
386
|
+
return (
|
|
387
|
+
<div key={i} style={{ display: 'grid', gridTemplateColumns: '12px 1fr', columnGap: 8, alignItems: 'start', marginBottom: 6 }}>
|
|
388
|
+
<div aria-hidden="true" style={{ width: 6, height: 6, borderRadius: '50%', marginTop: 6, backgroundColor: 'var(--text-secondary)' }} />
|
|
389
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
390
|
+
<span className={'font-medium'} style={{ color: 'var(--text-main)' }}>{title}</span>
|
|
391
|
+
<span>{programs}</span>
|
|
392
|
+
{s?.entityLink ? (
|
|
393
|
+
<>
|
|
394
|
+
{' '}— <a href={s.entityLink} target='_blank' rel='noopener noreferrer' className={'underline'} style={{ color: 'var(--icon-accent)' }}>Details</a>
|
|
395
|
+
</>
|
|
396
|
+
) : null}
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
})}
|
|
251
401
|
</div>
|
|
402
|
+
<details className={'mt-2'}>
|
|
403
|
+
<summary className={'cursor-pointer text-sm font-semibold'} style={{ color: 'var(--text-main)' }}>View OFAC Raw JSON</summary>
|
|
404
|
+
<pre className={'mt-2 overflow-auto text-xs p-3 rounded'} style={{ background: 'rgba(0,0,0,0.04)', color: 'var(--text-main)' }}>{JSON.stringify(ss!.ofac_screen!.raw, null, 2)}</pre>
|
|
405
|
+
</details>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
{/* CSL details */}
|
|
409
|
+
{cslDetails && (
|
|
410
|
+
<div className={'mb-4'}>
|
|
411
|
+
<h5 className={'font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>U.S. CSL Details</h5>
|
|
412
|
+
<details>
|
|
413
|
+
<summary className={'cursor-pointer text-sm font-semibold'} style={{ color: 'var(--text-main)' }}>View CSL Raw JSON</summary>
|
|
414
|
+
<pre className={'mt-2 overflow-auto text-xs p-3 rounded'} style={{ background: 'rgba(0,0,0,0.04)', color: 'var(--text-main)' }}>{JSON.stringify(ss!.csl_details, null, 2)}</pre>
|
|
415
|
+
</details>
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
{/* FBI matches */}
|
|
419
|
+
{fbiMatches && (
|
|
420
|
+
<div className={'mb-2'}>
|
|
421
|
+
<h5 className={'font-semibold mb-2'} style={{ color: 'var(--text-main)' }}>FBI Wanted List Matches</h5>
|
|
252
422
|
<div>
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
423
|
+
{ss!.fbi_matches!.map((f, i: number) => (
|
|
424
|
+
<div key={i} style={{ display: 'grid', gridTemplateColumns: '12px 1fr', columnGap: 8, alignItems: 'start', marginBottom: 6 }}>
|
|
425
|
+
<div aria-hidden="true" style={{ width: 6, height: 6, borderRadius: '50%', marginTop: 6, backgroundColor: 'var(--text-secondary)' }} />
|
|
426
|
+
<div className={'text-sm'} style={{ color: 'var(--text-secondary)' }}>
|
|
427
|
+
<span className={'font-medium'} style={{ color: 'var(--text-main)' }}>{f.title || 'Match'}</span>
|
|
428
|
+
{f.url ? <> — <a href={f.url} target='_blank' rel='noopener noreferrer' className={'underline'} style={{ color: 'var(--icon-accent)' }}>Details</a></> : null}
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
))}
|
|
260
432
|
</div>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
})()}
|
|
438
|
+
<IpRiskAnalysisDisplay ipRiskAnalysis={screening_sources?.ip_risk_analysis} />
|
|
439
|
+
</>
|
|
440
|
+
)}
|
|
264
441
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
{/* Connected Platforms */}
|
|
445
|
+
<ConnectedPlatforms accounts={connected} />
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
<div className="pt-8">
|
|
449
|
+
<h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix: Data Sources</h3>
|
|
450
|
+
<div className="space-y-8">
|
|
451
|
+
|
|
452
|
+
{/* Skills */}
|
|
453
|
+
<div>
|
|
454
|
+
<h4 id="appendix-skills" className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Skills</h4>
|
|
455
|
+
<SkillsAppendixTable skillsAll={skillsAll} />
|
|
274
456
|
</div>
|
|
457
|
+
|
|
458
|
+
{/* Observations */}
|
|
459
|
+
{Array.isArray(graphInsights?.business_rules_all) && graphInsights.business_rules_all.length > 0 && (
|
|
460
|
+
<div>
|
|
461
|
+
<h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Observations</h4>
|
|
462
|
+
<AppendixTables
|
|
463
|
+
type="business_rules"
|
|
464
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
465
|
+
sources={graphInsights.business_rules_all as any}
|
|
466
|
+
searchedAt={updatedAt}
|
|
467
|
+
developerName={developerName || 'this developer'}
|
|
468
|
+
/>
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{/* Sanctions & Watchlists */}
|
|
473
|
+
{!badgeData.optOutScreening && screening_sources && (
|
|
474
|
+
<div>
|
|
475
|
+
<h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Sanctions & Watchlists</h4>
|
|
476
|
+
{(() => {
|
|
477
|
+
const ofacProvided = screening_sources.ofac_screen?.sources || [];
|
|
478
|
+
const lists = [...(screening_sources.ofac_lists || []), ...ofacProvided];
|
|
479
|
+
const seen: { [k: string]: boolean } = {};
|
|
480
|
+
const dedup: string[] = [];
|
|
481
|
+
for (let i = 0; i < lists.length; i++) {
|
|
482
|
+
const val = lists[i];
|
|
483
|
+
if (!seen[val]) { seen[val] = true; dedup.push(val); }
|
|
484
|
+
}
|
|
485
|
+
const detailed = screening_sources.sanctions_sources_detailed || [];
|
|
486
|
+
const useDetailed = detailed && detailed.length > 0;
|
|
487
|
+
return (
|
|
488
|
+
<AppendixTables
|
|
489
|
+
type="sanctions"
|
|
490
|
+
sources={useDetailed ? detailed : dedup}
|
|
491
|
+
searchedAt={updatedAt}
|
|
492
|
+
developerName={developerName || 'this developer'}
|
|
493
|
+
/>
|
|
494
|
+
);
|
|
495
|
+
})()}
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<div className={'pt-8 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
|
|
503
|
+
Report Completed: {new Date(updatedAt).toLocaleString(undefined, {
|
|
504
|
+
year: 'numeric',
|
|
505
|
+
month: 'long',
|
|
506
|
+
day: 'numeric',
|
|
507
|
+
hour: 'numeric',
|
|
508
|
+
minute: '2-digit',
|
|
509
|
+
timeZoneName: 'short',
|
|
510
|
+
})}
|
|
275
511
|
</div>
|
|
512
|
+
</div>
|
|
276
513
|
</div>
|
|
277
514
|
<footer className={'mt-12 pt-6 border-t'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
515
|
+
<p className={'text-center text-xs max-w-4xl mx-auto'} style={{ color: 'var(--text-secondary)' }}>
|
|
516
|
+
© 2025 Know Your Developer, LLC. All rights reserved. KYD Self-Check™, and associated marks are trademarks of Know Your Developer, LLC. This document is confidential, proprietary, and intended solely for the individual or entity to whom it is addressed. Unauthorized use, disclosure, copying, or distribution of this document or any of its contents is strictly prohibited and may be unlawful. Know Your Developer, LLC assumes no responsibility or liability for any errors or omissions contained herein. Report validity subject to the terms and conditions stated on the official Know Your Developer website located at https://knowyourdeveloper.ai.
|
|
517
|
+
</p>
|
|
281
518
|
</footer>
|
|
282
519
|
</div>
|
|
520
|
+
</BusinessRulesProvider>
|
|
283
521
|
);
|
|
284
522
|
};
|
|
285
523
|
|