kyd-shared-badge 0.3.8 → 0.3.10
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 +16 -6
- package/src/chat/ChatWidget.tsx +18 -5
- package/src/chat/useChatStreaming.ts +5 -2
- package/src/colors.ts +24 -0
- package/src/components/CategoryBars.tsx +22 -18
- package/src/components/GaugeCard.tsx +24 -1
- package/src/components/GraphInsights.tsx +63 -11
- package/src/components/RiskCard.tsx +18 -0
package/package.json
CHANGED
|
@@ -23,6 +23,13 @@ import { BusinessRulesProvider } from './components/BusinessRulesContext';
|
|
|
23
23
|
import Reveal from './components/Reveal';
|
|
24
24
|
import { formatLocalDateTime } from './utils/date';
|
|
25
25
|
import ChatWidget from './chat/ChatWidget';
|
|
26
|
+
type ChatWidgetProps = Partial<{
|
|
27
|
+
api: string;
|
|
28
|
+
title: string;
|
|
29
|
+
hintText: string;
|
|
30
|
+
loginPath: string;
|
|
31
|
+
headerOffset: 'auto' | 'none' | number;
|
|
32
|
+
}>;
|
|
26
33
|
|
|
27
34
|
// const hexToRgba = (hex: string, alpha: number) => {
|
|
28
35
|
// const clean = hex.replace('#', '');
|
|
@@ -32,7 +39,7 @@ import ChatWidget from './chat/ChatWidget';
|
|
|
32
39
|
// return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
33
40
|
// };
|
|
34
41
|
|
|
35
|
-
const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
42
|
+
const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeData, chatProps?: ChatWidgetProps }) => {
|
|
36
43
|
const {
|
|
37
44
|
badgeId,
|
|
38
45
|
developerName,
|
|
@@ -206,7 +213,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
206
213
|
return (
|
|
207
214
|
<GaugeCard
|
|
208
215
|
key={'ai-card'}
|
|
209
|
-
title={'KYD AI'}
|
|
216
|
+
title={'KYD AI (Beta)'}
|
|
210
217
|
description={'Indicates the degree to which AI-assisted code is explicitly disclosed across analyzed files.'}
|
|
211
218
|
percent={ai_usage_summary?.transparency_score}
|
|
212
219
|
label={label}
|
|
@@ -242,7 +249,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
242
249
|
{/* Left: Bars */}
|
|
243
250
|
<Reveal className="lg:col-span-8 h-full">
|
|
244
251
|
<CategoryBars
|
|
245
|
-
title={'Technical Category Contributions
|
|
252
|
+
title={'Technical Category Contributions'}
|
|
246
253
|
categories={genreMapping?.['Technical'] as string[]}
|
|
247
254
|
categoryScores={categoryScores}
|
|
248
255
|
barColor={barColor}
|
|
@@ -459,13 +466,16 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
459
466
|
|
|
460
467
|
|
|
461
468
|
<div className="pt-8">
|
|
462
|
-
<h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix
|
|
469
|
+
<h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix</h3>
|
|
463
470
|
<div className="space-y-8">
|
|
464
471
|
|
|
465
472
|
{/* Skills */}
|
|
466
473
|
<Reveal>
|
|
467
474
|
<div>
|
|
468
|
-
<h4 id="appendix-skills" className={'text-lg font-bold mb-
|
|
475
|
+
<h4 id="appendix-skills" className={'text-lg font-bold mb-1'} style={{ color: 'var(--text-main)' }}>Skills</h4>
|
|
476
|
+
<div className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
|
|
477
|
+
Skills are grouped by evidence: Observed when demonstrated in code, Self-reported when declared by the developer without independent verification, and Certified when confirmed through a credential. These categories distinguish between use, claim, and third-party validation.
|
|
478
|
+
</div>
|
|
469
479
|
<SkillsAppendixTable skillsAll={skillsAll} />
|
|
470
480
|
</div>
|
|
471
481
|
</Reveal>
|
|
@@ -534,7 +544,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
534
544
|
</footer>
|
|
535
545
|
</Reveal>
|
|
536
546
|
{/* Floating chat widget */}
|
|
537
|
-
<ChatWidget api={'/api/chat'} badgeId={badgeId} />
|
|
547
|
+
<ChatWidget api={chatProps?.api || '/api/chat'} badgeId={badgeId} title={chatProps?.title} hintText={chatProps?.hintText} loginPath={chatProps?.loginPath} headerOffset={chatProps?.headerOffset} />
|
|
538
548
|
</div>
|
|
539
549
|
</BusinessRulesProvider>
|
|
540
550
|
);
|
package/src/chat/ChatWidget.tsx
CHANGED
|
@@ -20,9 +20,12 @@ type Props = {
|
|
|
20
20
|
title?: string;
|
|
21
21
|
hintText?: string;
|
|
22
22
|
badgeId: string;
|
|
23
|
+
// Optional: customize login path and header offset behavior for different host apps
|
|
24
|
+
loginPath?: string; // e.g., '/login' in enterprise; defaults to '/auth/login'
|
|
25
|
+
headerOffset?: 'auto' | 'none' | number; // default 'auto' (measure <header>), use 'none' when no header
|
|
23
26
|
};
|
|
24
27
|
|
|
25
|
-
export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId }: Props) {
|
|
28
|
+
export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId, loginPath = '/auth/login', headerOffset = 'auto' }: Props) {
|
|
26
29
|
// Sidebar open state (default expanded)
|
|
27
30
|
const [open, setOpen] = useState<boolean>(() => {
|
|
28
31
|
try { const s = localStorage.getItem('kydChatSidebarOpen'); return s ? s === '1' : true; } catch { return true; }
|
|
@@ -46,7 +49,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
46
49
|
const tabDraggedRef = useRef(false);
|
|
47
50
|
const [headerTop, setHeaderTop] = useState(0);
|
|
48
51
|
const [showHint, setShowHint] = useState(false);
|
|
49
|
-
const { messages, input, setInput, sending, sendMessage, cancel } = useChatStreaming({ api, badgeId });
|
|
52
|
+
const { messages, input, setInput, sending, sendMessage, cancel } = useChatStreaming({ api, badgeId, loginPath });
|
|
50
53
|
const listRef = useRef<HTMLDivElement>(null);
|
|
51
54
|
|
|
52
55
|
useEffect(() => {
|
|
@@ -117,13 +120,23 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
117
120
|
return () => window.removeEventListener('keydown', onKey);
|
|
118
121
|
}, [open]);
|
|
119
122
|
|
|
120
|
-
//
|
|
123
|
+
// Header offset handling to keep chat below any sticky header in host app
|
|
121
124
|
useEffect(() => {
|
|
125
|
+
if (headerOffset === 'none') {
|
|
126
|
+
setHeaderTop(0);
|
|
127
|
+
setTabTop(t => Math.max(16, t));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (typeof headerOffset === 'number' && Number.isFinite(headerOffset)) {
|
|
131
|
+
const h = Math.max(0, Math.floor(headerOffset));
|
|
132
|
+
setHeaderTop(h);
|
|
133
|
+
setTabTop(t => Math.max(h + 16, t));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
122
136
|
const measure = () => {
|
|
123
137
|
const el = document.querySelector('header');
|
|
124
138
|
const h = el ? Math.floor(el.getBoundingClientRect().height) : 0;
|
|
125
139
|
setHeaderTop(h);
|
|
126
|
-
// Keep collapsed tab within bounds beneath header
|
|
127
140
|
setTabTop(t => Math.max(h + 16, t));
|
|
128
141
|
};
|
|
129
142
|
measure();
|
|
@@ -133,7 +146,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
133
146
|
window.removeEventListener('resize', measure);
|
|
134
147
|
window.removeEventListener('scroll', measure);
|
|
135
148
|
};
|
|
136
|
-
}, []);
|
|
149
|
+
}, [headerOffset]);
|
|
137
150
|
|
|
138
151
|
// Drag to resize (left edge of sidebar)
|
|
139
152
|
useEffect(() => {
|
|
@@ -8,10 +8,12 @@ export type ChatMessage = { id: string; role: Role; content: string };
|
|
|
8
8
|
export type UseChatStreamingConfig = {
|
|
9
9
|
api: string; // e.g. /api/chat
|
|
10
10
|
badgeId: string;
|
|
11
|
+
// Optional login path for redirect when 401 occurs. Defaults to '/auth/login' for portability across apps.
|
|
12
|
+
loginPath?: string;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
14
|
-
const { api, badgeId } = cfg;
|
|
16
|
+
const { api, badgeId, loginPath = '/auth/login' } = cfg;
|
|
15
17
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
16
18
|
const [input, setInput] = useState('');
|
|
17
19
|
const [sending, setSending] = useState(false);
|
|
@@ -32,7 +34,8 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
32
34
|
// Inform the user then redirect to login carrying callbackUrl and prompt
|
|
33
35
|
setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, content: 'You need to log in to use chat. Redirecting to login…' } : msg));
|
|
34
36
|
const currentPath = typeof window !== 'undefined' ? (window.location.pathname + window.location.search + window.location.hash) : '/';
|
|
35
|
-
const
|
|
37
|
+
const base = loginPath || '/auth/login';
|
|
38
|
+
const loginUrl = `${base}?callbackUrl=${encodeURIComponent(currentPath)}&chatPrompt=${encodeURIComponent(promptText)}`;
|
|
36
39
|
setTimeout(() => {
|
|
37
40
|
if (typeof window !== 'undefined') window.location.href = loginUrl;
|
|
38
41
|
}, 700);
|
package/src/colors.ts
CHANGED
|
@@ -41,3 +41,27 @@ export { red, yellow, green }
|
|
|
41
41
|
// #D5E8E2
|
|
42
42
|
|
|
43
43
|
// #EEF6F4
|
|
44
|
+
|
|
45
|
+
const green1 = '#02a389'
|
|
46
|
+
const green2 = '#7BB9AA'
|
|
47
|
+
const green3 = '#A9D1C6'
|
|
48
|
+
const green4 = '#D5E8E2'
|
|
49
|
+
const green5 = '#EEF6F4'
|
|
50
|
+
|
|
51
|
+
export { green1, green2, green3, green4, green5 }
|
|
52
|
+
|
|
53
|
+
const yellow1 = '#F4BE66'
|
|
54
|
+
const yellow2 = '#F7CF8F'
|
|
55
|
+
const yellow3 = '#F9DFB6'
|
|
56
|
+
const yellow4 = '#FDEFDB'
|
|
57
|
+
const yellow5 = '#FEF9F0'
|
|
58
|
+
|
|
59
|
+
export { yellow1, yellow2, yellow3, yellow4, yellow5 }
|
|
60
|
+
|
|
61
|
+
const red1 = '#EC6662'
|
|
62
|
+
const red2 = '#F1908C'
|
|
63
|
+
const red3 = '#F5B7B3'
|
|
64
|
+
const red4 = '#FADBDA'
|
|
65
|
+
const red5 = '#FDF0EF'
|
|
66
|
+
|
|
67
|
+
export { red1, red2, red3, red4, red5 }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import { green, red } from '../colors';
|
|
4
5
|
|
|
5
6
|
type CategoryBarsProps = {
|
|
6
7
|
title: string;
|
|
@@ -22,6 +23,9 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
|
|
|
22
23
|
return (
|
|
23
24
|
<div className="relative flex flex-col h-full">
|
|
24
25
|
<div className="font-semibold text-xl mb-2" style={{ color: 'var(--text-main)' }}>{title}</div>
|
|
26
|
+
<div className="text-sm mb-6" style={{ color: 'var(--text-secondary)' }}>
|
|
27
|
+
Each bar represents a category's net evidence: positive values extend right in green, negative values extend left, and bar length denotes contribution magnitude.
|
|
28
|
+
</div>
|
|
25
29
|
<div className="flex-1 flex flex-col justify-between relative">
|
|
26
30
|
<div
|
|
27
31
|
className="absolute top-0 bottom-0 w-px"
|
|
@@ -53,11 +57,11 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
|
|
|
53
57
|
const fillWidth = absPercent / 2; // half-bar represents 100%
|
|
54
58
|
const left = isNegative ? `calc(50% - ${fillWidth}%)` : '50%';
|
|
55
59
|
return (
|
|
56
|
-
<div key={category} className="first:pt-0">
|
|
60
|
+
<div key={category} className="first:pt-0 group relative">
|
|
57
61
|
<div className={'font-semibold mb-1'} style={{ color: 'var(--text-main)' }}>
|
|
58
62
|
{category}
|
|
59
63
|
</div>
|
|
60
|
-
<div className="relative
|
|
64
|
+
<div className="relative">
|
|
61
65
|
<div
|
|
62
66
|
className="w-full rounded-full overflow-hidden relative"
|
|
63
67
|
style={{
|
|
@@ -67,23 +71,23 @@ const CategoryBars: React.FC<CategoryBarsProps> = ({
|
|
|
67
71
|
}}
|
|
68
72
|
>
|
|
69
73
|
{/* signed fill originating from center */}
|
|
70
|
-
<div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ?
|
|
74
|
+
<div className="absolute top-0 h-full" style={{ left, width: `${fillWidth}%`, backgroundColor: isNegative ? `var(--status-negative, ${red})` : `var(--status-positive, ${green})` }} />
|
|
71
75
|
</div>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
</div>
|
|
77
|
+
<div className="hidden group-hover:block absolute z-30 left-1/2 -translate-x-1/2 top-full mt-2 w-80">
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
background: 'var(--content-card-background)',
|
|
81
|
+
border: '1px solid var(--icon-button-secondary)',
|
|
82
|
+
color: 'var(--text-main)',
|
|
83
|
+
padding: 10,
|
|
84
|
+
borderRadius: 6,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<div style={{ fontWeight: 600 }}>{category}</div>
|
|
88
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{label}</div>
|
|
89
|
+
<div style={{ marginTop: 6, fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
90
|
+
{getCategoryTooltipCopy(category)}
|
|
87
91
|
</div>
|
|
88
92
|
</div>
|
|
89
93
|
</div>
|
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import BusinessRuleLink from './BusinessRuleLink';
|
|
5
5
|
import { FiInfo } from 'react-icons/fi';
|
|
6
|
+
import { green, yellow, red } from '../colors';
|
|
6
7
|
|
|
7
8
|
type TopMover = { label?: string; uid?: string };
|
|
8
9
|
|
|
10
|
+
const hexToRgba = (hex: string, alpha: number) => {
|
|
11
|
+
const clean = hex.replace('#', '');
|
|
12
|
+
const r = parseInt(clean.substring(0, 2), 16);
|
|
13
|
+
const g = parseInt(clean.substring(2, 4), 16);
|
|
14
|
+
const b = parseInt(clean.substring(4, 6), 16);
|
|
15
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const pickTint = (score: number) => {
|
|
19
|
+
if (score >= 75) return green;
|
|
20
|
+
if (score >= 50) return yellow;
|
|
21
|
+
return red;
|
|
22
|
+
};
|
|
23
|
+
|
|
9
24
|
export default function GaugeCard({
|
|
10
25
|
title,
|
|
11
26
|
description,
|
|
@@ -31,6 +46,13 @@ export default function GaugeCard({
|
|
|
31
46
|
const circumference = Math.PI * radius;
|
|
32
47
|
const progress = pct / 100;
|
|
33
48
|
const dash = circumference * progress;
|
|
49
|
+
const progressColor =
|
|
50
|
+
pct <= 33
|
|
51
|
+
? `var(--status-negative, ${red})`
|
|
52
|
+
: pct <= 66
|
|
53
|
+
? `var(--status-neutral, ${yellow})`
|
|
54
|
+
: `var(--status-positive, ${green})`;
|
|
55
|
+
const headerTint = hexToRgba(pickTint(pct), 0.06);
|
|
34
56
|
|
|
35
57
|
return (
|
|
36
58
|
<div
|
|
@@ -38,6 +60,7 @@ export default function GaugeCard({
|
|
|
38
60
|
style={{
|
|
39
61
|
backgroundColor: 'var(--content-card-background)',
|
|
40
62
|
borderColor: 'var(--icon-button-secondary)',
|
|
63
|
+
backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
|
|
41
64
|
}}
|
|
42
65
|
>
|
|
43
66
|
<div className="mb-3 flex items-start justify-between gap-2">
|
|
@@ -64,7 +87,7 @@ export default function GaugeCard({
|
|
|
64
87
|
<div className="relative group" style={{ width: size, height: size / 2 }}>
|
|
65
88
|
<svg width={size} height={size / 2} viewBox={`0 0 ${size} ${size/2}`}>
|
|
66
89
|
<path d={`M ${strokeWidth/2} ${size/2} A ${radius} ${radius} 0 0 1 ${size-strokeWidth/2} ${size/2}`} stroke={'var(--icon-button-secondary)'} strokeWidth={strokeWidth} fill="none" strokeLinecap="round" />
|
|
67
|
-
<path d={`M ${strokeWidth/2} ${size/2} A ${radius} ${radius} 0 0 1 ${size-strokeWidth/2} ${size/2}`} stroke={
|
|
90
|
+
<path d={`M ${strokeWidth/2} ${size/2} A ${radius} ${radius} 0 0 1 ${size-strokeWidth/2} ${size/2}`} stroke={progressColor} strokeWidth={strokeWidth} fill="none" strokeLinecap="round" strokeDasharray={`${dash},${circumference}`} />
|
|
68
91
|
<line x1={size/2} y1={size/2} x2={size/2 + radius * Math.cos(Math.PI * progress - Math.PI)} y2={size/2 + radius * Math.sin(Math.PI * progress - Math.PI)} stroke={'var(--text-main)'} strokeWidth="2" />
|
|
69
92
|
</svg>
|
|
70
93
|
{(tooltipText || description) && (
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
// import { red, yellow, green } from '@/components/badge/colors';
|
|
5
5
|
import { GraphInsightsPayload, ScoringSummary } from '../types';
|
|
6
|
+
import { green1, green2, green3, green4, green5 } from '../colors';
|
|
6
7
|
import {
|
|
7
8
|
ResponsiveContainer,
|
|
8
9
|
Tooltip,
|
|
@@ -188,6 +189,46 @@ const GraphInsights = ({
|
|
|
188
189
|
|
|
189
190
|
if (!graphInsights) return null;
|
|
190
191
|
|
|
192
|
+
// Tooltip for axis labels (outside of Recharts' built-in hover zones)
|
|
193
|
+
const [labelHover, setLabelHover] = React.useState<
|
|
194
|
+
| {
|
|
195
|
+
x: number;
|
|
196
|
+
y: number;
|
|
197
|
+
name: string;
|
|
198
|
+
score: number;
|
|
199
|
+
avg?: number;
|
|
200
|
+
}
|
|
201
|
+
| null
|
|
202
|
+
>(null);
|
|
203
|
+
|
|
204
|
+
const radarLookup = React.useMemo(() => {
|
|
205
|
+
const map = new Map<string, { score: number; avg?: number }>();
|
|
206
|
+
for (const d of radarData) map.set(d.axis, { score: d.score, avg: d.avg });
|
|
207
|
+
return map;
|
|
208
|
+
}, [radarData]);
|
|
209
|
+
|
|
210
|
+
// Custom tick renderer to attach hover handlers to category names
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
const renderAngleTick = (props: any) => {
|
|
213
|
+
const { payload, x, y, textAnchor } = props;
|
|
214
|
+
const name: string = payload?.value ?? '';
|
|
215
|
+
const info = radarLookup.get(name) || { score: 0, avg: undefined };
|
|
216
|
+
return (
|
|
217
|
+
<text
|
|
218
|
+
x={x}
|
|
219
|
+
y={y}
|
|
220
|
+
textAnchor={textAnchor}
|
|
221
|
+
dominantBaseline="central"
|
|
222
|
+
style={{ fill: 'var(--text-secondary)', fontSize: 12, cursor: 'default' }}
|
|
223
|
+
onMouseEnter={() => setLabelHover({ x, y, name, score: info.score, avg: info.avg })}
|
|
224
|
+
onMouseMove={() => setLabelHover({ x, y, name, score: info.score, avg: info.avg })}
|
|
225
|
+
onMouseLeave={() => setLabelHover(null)}
|
|
226
|
+
>
|
|
227
|
+
{name}
|
|
228
|
+
</text>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
191
232
|
return (
|
|
192
233
|
<div className="grid grid-cols-1 gap-6">
|
|
193
234
|
{/* Spider Chart: Category Balance (genre-scoped) */}
|
|
@@ -197,7 +238,7 @@ const GraphInsights = ({
|
|
|
197
238
|
|
|
198
239
|
{/* Spider Chart: Category Scores */}
|
|
199
240
|
<div className="" style={{ width: '100%', height: 450 }}>
|
|
200
|
-
<div className="" style={{ width: '100%', height: 375 }}>
|
|
241
|
+
<div className="relative" style={{ width: '100%', height: 375 }}>
|
|
201
242
|
<div className="mb-2">
|
|
202
243
|
<div className={'font-medium'} style={{ color: 'var(--text-main)' }}>{genre ? `${genre} ` : ''} Category Contributions - Percentages</div>
|
|
203
244
|
<div className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>The spider diagram displays the KYD {genre} score across its sub-categories, with each point representing the strength of available evidence signals</div>
|
|
@@ -205,7 +246,7 @@ const GraphInsights = ({
|
|
|
205
246
|
<ResponsiveContainer>
|
|
206
247
|
<RadarChart data={radarData} cx="50%" cy="50%" outerRadius="70%">
|
|
207
248
|
<PolarGrid stroke="var(--icon-button-secondary)" />
|
|
208
|
-
<PolarAngleAxis dataKey="axis" tick={
|
|
249
|
+
<PolarAngleAxis dataKey="axis" tick={renderAngleTick} />
|
|
209
250
|
<PolarRadiusAxis angle={55} domain={[0, 100]} tick={{ fill: 'var(--text-secondary)' }} className="text-sm" />
|
|
210
251
|
{/* Primary area */}
|
|
211
252
|
<Radar name="Category Score" dataKey="score" stroke={'var(--text-main)'} fill={'var(--text-main)'} fillOpacity={0.22} />
|
|
@@ -214,6 +255,23 @@ const GraphInsights = ({
|
|
|
214
255
|
<Tooltip content={<RadarCategoryTooltip />} />
|
|
215
256
|
</RadarChart>
|
|
216
257
|
</ResponsiveContainer>
|
|
258
|
+
{labelHover && (
|
|
259
|
+
<div
|
|
260
|
+
className="pointer-events-none absolute z-30"
|
|
261
|
+
style={{
|
|
262
|
+
left: Math.max(0, labelHover.x - 150),
|
|
263
|
+
top: Math.max(0, labelHover.y + 8),
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<div style={{ background: 'var(--content-card-background)', border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)', padding: 10, borderRadius: 6, maxWidth: 320 }}>
|
|
267
|
+
<div style={{ fontWeight: 600 }}>{labelHover.name}</div>
|
|
268
|
+
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
|
269
|
+
Score: {Math.round(labelHover.score)}{labelHover.avg !== undefined ? ` • Avg: ${Math.round(labelHover.avg)}` : ''}
|
|
270
|
+
</div>
|
|
271
|
+
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text-secondary)' }}>{getCategoryTooltipCopy(labelHover.name)}</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
217
275
|
</div>
|
|
218
276
|
{/* Custom legend below the spider chart */}
|
|
219
277
|
<div className="mt-3 flex items-center justify-center gap-4" style={{ color: 'var(--text-secondary)', fontSize: 12 }}>
|
|
@@ -238,10 +296,7 @@ const GraphInsights = ({
|
|
|
238
296
|
<ResponsiveContainer>
|
|
239
297
|
<PieChart>
|
|
240
298
|
{(() => {
|
|
241
|
-
const
|
|
242
|
-
const palette = !isDarkMode
|
|
243
|
-
? ['#E0E0E0', '#BDBDBD', '#9E9E9E', '#757575', '#616161', '#424242', '#212121']
|
|
244
|
-
: ['#212121', '#424242', '#616161', '#757575', '#9E9E9E', '#BDBDBD', '#E0E0E0'];
|
|
299
|
+
const palette = [green1, green2, green3, green4, green5];
|
|
245
300
|
const total = categoryData.reduce((sum, d) => sum + (d.value || 0), 0);
|
|
246
301
|
const pieData = categoryData.map(d => ({
|
|
247
302
|
name: d.category,
|
|
@@ -249,7 +304,7 @@ const GraphInsights = ({
|
|
|
249
304
|
share: total > 0 ? (d.value / total) : 0,
|
|
250
305
|
}));
|
|
251
306
|
|
|
252
|
-
// Smaller
|
|
307
|
+
// Smaller labels
|
|
253
308
|
const renderLabel = (props: {x: number, y: number, textAnchor: string, percent: number, name: string, payload: {name: string}}) => {
|
|
254
309
|
const pct = Math.round(((props?.percent ?? 0) * 100));
|
|
255
310
|
if (pct < 4) return null;
|
|
@@ -290,10 +345,7 @@ const GraphInsights = ({
|
|
|
290
345
|
{/* Legend below the pie chart: includes 0% categories */}
|
|
291
346
|
<div className="mt-3">
|
|
292
347
|
{(() => {
|
|
293
|
-
const
|
|
294
|
-
const palette = !isDarkMode
|
|
295
|
-
? ['#E0E0E0', '#BDBDBD', '#9E9E9E', '#757575', '#616161', '#424242', '#212121']
|
|
296
|
-
: ['#212121', '#424242', '#616161', '#757575', '#9E9E9E', '#BDBDBD', '#E0E0E0'];
|
|
348
|
+
const palette = [green1, green2, green3, green4, green5];
|
|
297
349
|
const pieNames = categoryData.map((d) => d.category);
|
|
298
350
|
const getColor = (name: string) => {
|
|
299
351
|
const idx = pieNames.indexOf(name);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import BusinessRuleLink from './BusinessRuleLink';
|
|
5
5
|
import { FiInfo } from 'react-icons/fi';
|
|
6
|
+
import { green, yellow, red } from '../colors';
|
|
6
7
|
|
|
7
8
|
type TopMover = { label?: string; uid?: string };
|
|
8
9
|
|
|
@@ -26,6 +27,22 @@ export default function RiskCard({
|
|
|
26
27
|
const pctGood = Math.max(0, Math.min(100, Math.round(Number(percentGood ?? 0))));
|
|
27
28
|
const displayLabel = label || '';
|
|
28
29
|
|
|
30
|
+
const hexToRgba = (hex: string, alpha: number) => {
|
|
31
|
+
const clean = hex.replace('#', '');
|
|
32
|
+
const r = parseInt(clean.substring(0, 2), 16);
|
|
33
|
+
const g = parseInt(clean.substring(2, 4), 16);
|
|
34
|
+
const b = parseInt(clean.substring(4, 6), 16);
|
|
35
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const pickTint = (score: number) => {
|
|
39
|
+
if (score >= 75) return green;
|
|
40
|
+
if (score >= 50) return yellow;
|
|
41
|
+
return red;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const headerTint = hexToRgba(pickTint(pctGood), 0.06);
|
|
45
|
+
|
|
29
46
|
// bar heights ascending representation
|
|
30
47
|
const bars = [40, 60, 85, 110, 140];
|
|
31
48
|
let activeIndex = 0; // Default to the shortest bar (highest risk)
|
|
@@ -45,6 +62,7 @@ export default function RiskCard({
|
|
|
45
62
|
style={{
|
|
46
63
|
backgroundColor: 'var(--content-card-background)',
|
|
47
64
|
borderColor: 'var(--icon-button-secondary)',
|
|
65
|
+
backgroundImage: `linear-gradient(${headerTint}, ${headerTint})`,
|
|
48
66
|
}}
|
|
49
67
|
>
|
|
50
68
|
<div className="mb-3 flex items-start justify-between gap-2">
|