kyd-shared-badge 0.3.11 → 0.3.12
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
|
@@ -29,6 +29,7 @@ type ChatWidgetProps = Partial<{
|
|
|
29
29
|
hintText: string;
|
|
30
30
|
loginPath: string;
|
|
31
31
|
headerOffset: 'auto' | 'none' | number;
|
|
32
|
+
developerName: string;
|
|
32
33
|
}>;
|
|
33
34
|
|
|
34
35
|
// const hexToRgba = (hex: string, alpha: number) => {
|
|
@@ -258,8 +259,8 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
258
259
|
/>
|
|
259
260
|
</Reveal>
|
|
260
261
|
|
|
261
|
-
{/* Right: Contributing Factors */}
|
|
262
|
-
<Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
|
|
262
|
+
{/* Right: Contributing Factors (hidden on small screens) */}
|
|
263
|
+
<Reveal className="lg:col-span-4 w-full ml-0 lg:ml-20 hidden lg:flex flex-col items-start justify-start" delayMs={80}>
|
|
263
264
|
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
264
265
|
<div className="space-y-4">
|
|
265
266
|
{(genreMapping?.['Technical'] || []).map((cat: string) => {
|
|
@@ -331,8 +332,8 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
331
332
|
barHeight={16}
|
|
332
333
|
/>
|
|
333
334
|
</Reveal>
|
|
334
|
-
{/* Right: Contributing Factors */}
|
|
335
|
-
<Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
|
|
335
|
+
{/* Right: Contributing Factors (hidden on small screens) */}
|
|
336
|
+
<Reveal className="lg:col-span-4 w-full ml-0 lg:ml-20 hidden lg:flex flex-col items-start justify-start" delayMs={80}>
|
|
336
337
|
<div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
|
|
337
338
|
<div className="space-y-4">
|
|
338
339
|
{genreMapping?.['Risk']?.map((cat: string) => {
|
|
@@ -544,7 +545,7 @@ const SharedBadgeDisplay = ({ badgeData, chatProps }: { badgeData: PublicBadgeDa
|
|
|
544
545
|
</footer>
|
|
545
546
|
</Reveal>
|
|
546
547
|
{/* Floating chat widget */}
|
|
547
|
-
<ChatWidget api={chatProps?.api || '/api/chat'} badgeId={badgeId} title={chatProps?.title} hintText={chatProps?.hintText} loginPath={chatProps?.loginPath} headerOffset={chatProps?.headerOffset} />
|
|
548
|
+
<ChatWidget api={chatProps?.api || '/api/chat'} badgeId={badgeId} title={chatProps?.title} hintText={chatProps?.hintText} loginPath={chatProps?.loginPath} headerOffset={chatProps?.headerOffset} developerName={developerName} />
|
|
548
549
|
</div>
|
|
549
550
|
</BusinessRulesProvider>
|
|
550
551
|
);
|
package/src/chat/ChatWidget.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
4
5
|
import { FiMessageSquare, FiX, FiChevronRight } from 'react-icons/fi';
|
|
5
6
|
import { useChatStreaming } from './useChatStreaming';
|
|
6
7
|
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
|
|
@@ -23,13 +24,33 @@ type Props = {
|
|
|
23
24
|
// Optional: customize login path and header offset behavior for different host apps
|
|
24
25
|
loginPath?: string; // e.g., '/login' in enterprise; defaults to '/auth/login'
|
|
25
26
|
headerOffset?: 'auto' | 'none' | number; // default 'auto' (measure <header>), use 'none' when no header
|
|
27
|
+
developerName?: string; // used for starter questions
|
|
26
28
|
};
|
|
27
29
|
|
|
28
|
-
export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId, loginPath = '/auth/login', headerOffset = 'auto' }: Props) {
|
|
29
|
-
//
|
|
30
|
-
const [
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId, loginPath = '/auth/login', headerOffset = 'auto', developerName }: Props) {
|
|
31
|
+
// Responsive breakpoint detection (mobile if < 768px)
|
|
32
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const check = () => setIsMobile(typeof window !== 'undefined' && window.innerWidth < 768);
|
|
35
|
+
check();
|
|
36
|
+
window.addEventListener('resize', check);
|
|
37
|
+
return () => window.removeEventListener('resize', check);
|
|
38
|
+
}, []);
|
|
39
|
+
// Sidebar open state (default expanded on desktop, collapsed on mobile)
|
|
40
|
+
const [open, setOpen] = useState<boolean>(true);
|
|
41
|
+
// Avoid hydration mismatches by rendering only after mount
|
|
42
|
+
const [hasMounted, setHasMounted] = useState(false);
|
|
43
|
+
useEffect(() => { setHasMounted(true); }, []);
|
|
44
|
+
// On first mount, default collapsed on mobile (unless URL prefilled opens it)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!hasMounted) return;
|
|
47
|
+
try {
|
|
48
|
+
const url = typeof window !== 'undefined' ? new URL(window.location.href) : null;
|
|
49
|
+
const prompt = url ? url.searchParams.get('chatPrompt') : null;
|
|
50
|
+
if (!prompt && isMobile) setOpen(false);
|
|
51
|
+
} catch {}
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [hasMounted, isMobile]);
|
|
33
54
|
// Sidebar width with bounds and persistence
|
|
34
55
|
const [width, setWidth] = useState<number>(() => {
|
|
35
56
|
try { const w = Number(localStorage.getItem('kydChatSidebarWidth')); return Number.isFinite(w) && w >= 320 ? Math.min(w, Math.floor(window.innerWidth * 0.9)) : 420; } catch { return 420; }
|
|
@@ -100,6 +121,14 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
100
121
|
</div>
|
|
101
122
|
), [title]);
|
|
102
123
|
|
|
124
|
+
// Prevent background scroll on mobile when open fullscreen
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!isMobile) return;
|
|
127
|
+
const original = document.body.style.overflow;
|
|
128
|
+
document.body.style.overflow = open ? 'hidden' : original;
|
|
129
|
+
return () => { document.body.style.overflow = original; };
|
|
130
|
+
}, [isMobile, open]);
|
|
131
|
+
|
|
103
132
|
// Persist sidebar UI state
|
|
104
133
|
useEffect(() => {
|
|
105
134
|
try { localStorage.setItem('kydChatSidebarOpen', open ? '1' : '0'); } catch {}
|
|
@@ -199,58 +228,114 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
199
228
|
};
|
|
200
229
|
}, [draggingTab]);
|
|
201
230
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
231
|
+
const nameForCopy = (developerName && developerName.trim().length > 0) ? developerName : 'this developer';
|
|
232
|
+
const starterQuestions: string[] = [
|
|
233
|
+
`Tell me about ${nameForCopy}'s experience with Python.`,
|
|
234
|
+
`What are ${nameForCopy}'s strongest technical areas?`,
|
|
235
|
+
`Are there any risk or sanctions flags for ${nameForCopy}?`
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
const onStarterClick = (q: string) => {
|
|
239
|
+
if (!open) setOpen(true);
|
|
240
|
+
if (!sending) {
|
|
241
|
+
// Populate input for UI and send using explicit text to avoid async state issues
|
|
242
|
+
setInput(q);
|
|
243
|
+
sendMessage(q);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (!hasMounted) return null;
|
|
248
|
+
|
|
249
|
+
const ui = (
|
|
250
|
+
<div className={'fixed z-50'} aria-live={'polite'} style={{ zIndex: 9999 }}>
|
|
251
|
+
{/* Collapsed trigger */}
|
|
205
252
|
{!open && (
|
|
206
|
-
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<span style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', color: 'var(--text-secondary)', fontSize: 12, letterSpacing: 1 }}>Chat</span>
|
|
217
|
-
<FiChevronRight size={16} style={{ color: 'var(--text-secondary)', marginBottom: 8 }} />
|
|
218
|
-
</button>
|
|
219
|
-
{showHint && (
|
|
220
|
-
<button onClick={() => { setOpen(true); onDismissHint(); }} className={'absolute -left-2 -top-8 group max-w-xs text-left'}>
|
|
221
|
-
<div className={'px-2 py-1 rounded shadow border'}
|
|
222
|
-
style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
223
|
-
<span className={'text-[10px]'} style={{ color: 'var(--text-main)' }}>{hintText}</span>
|
|
224
|
-
</div>
|
|
253
|
+
isMobile ? (
|
|
254
|
+
<div style={{ position: 'fixed', right: 16, bottom: 16, zIndex: 9999 }}>
|
|
255
|
+
<button
|
|
256
|
+
aria-label={'Open chat'}
|
|
257
|
+
aria-expanded={open}
|
|
258
|
+
onClick={() => setOpen(true)}
|
|
259
|
+
className={'shadow-lg border rounded-full flex items-center justify-center transition hover:opacity-90'}
|
|
260
|
+
style={{ width: 56, height: 56, background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
|
|
261
|
+
>
|
|
262
|
+
<FiMessageSquare size={22} style={{ color: 'var(--text-main)' }} />
|
|
225
263
|
</button>
|
|
226
|
-
|
|
227
|
-
|
|
264
|
+
{showHint && (
|
|
265
|
+
<button onClick={() => { setOpen(true); onDismissHint(); }} className={'absolute right-full mr-2 bottom-1/2 translate-y-1/2'}>
|
|
266
|
+
<div className={'px-2 py-1 rounded shadow border'}
|
|
267
|
+
style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
268
|
+
<span className={'text-[10px]'} style={{ color: 'var(--text-main)' }}>{hintText}</span>
|
|
269
|
+
</div>
|
|
270
|
+
</button>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
) : (
|
|
274
|
+
<div style={{ position: 'fixed', right: 0, top: Math.max(headerTop + 16, tabTop), height: 160, width: 44 }}>
|
|
275
|
+
<button
|
|
276
|
+
aria-label={'Open chat sidebar'}
|
|
277
|
+
aria-expanded={open}
|
|
278
|
+
onMouseDown={(e) => { setDraggingTab(true); dragStartYRef.current = e.clientY; startTopRef.current = tabTop; tabDraggedRef.current = false; }}
|
|
279
|
+
onMouseUp={(e) => { e.preventDefault(); const dragged = tabDraggedRef.current; tabDraggedRef.current = false; setDraggingTab(false); if (!dragged) setOpen(true); }}
|
|
280
|
+
className={'h-full w-full shadow-lg border rounded-l-lg flex flex-col items-center justify-between transition hover:opacity-90'}
|
|
281
|
+
style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', cursor: draggingTab ? 'grabbing' : 'grab' }}
|
|
282
|
+
>
|
|
283
|
+
<FiMessageSquare size={18} style={{ color: 'var(--text-main)', marginTop: 8 }} />
|
|
284
|
+
<span style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', color: 'var(--text-secondary)', fontSize: 12, letterSpacing: 1 }}>Chat</span>
|
|
285
|
+
<FiChevronRight size={16} style={{ color: 'var(--text-secondary)', marginBottom: 8 }} />
|
|
286
|
+
</button>
|
|
287
|
+
{showHint && (
|
|
288
|
+
<button onClick={() => { setOpen(true); onDismissHint(); }} className={'absolute -left-2 -top-8 group max-w-xs text-left'}>
|
|
289
|
+
<div className={'px-2 py-1 rounded shadow border'}
|
|
290
|
+
style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
|
|
291
|
+
<span className={'text-[10px]'} style={{ color: 'var(--text-main)' }}>{hintText}</span>
|
|
292
|
+
</div>
|
|
293
|
+
</button>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
228
297
|
)}
|
|
229
298
|
|
|
230
|
-
{/* Expanded
|
|
299
|
+
{/* Expanded panel */}
|
|
231
300
|
{open && (
|
|
232
301
|
<aside
|
|
233
302
|
role={'complementary'}
|
|
234
303
|
aria-label={'Chat sidebar'}
|
|
235
|
-
style={{ position: 'fixed', top: headerTop, right: 0, bottom: 0, width, maxWidth: '92vw' }}
|
|
304
|
+
style={isMobile ? { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, width: '100vw', zIndex: 10000 } : { position: 'fixed', top: headerTop, right: 0, bottom: 0, width, maxWidth: '92vw', zIndex: 9999 }}
|
|
236
305
|
className={'shadow-xl border flex flex-col overflow-hidden'}
|
|
237
306
|
>
|
|
238
|
-
{/* Left-edge resizer */}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
307
|
+
{/* Left-edge resizer (desktop only) */}
|
|
308
|
+
{!isMobile && (
|
|
309
|
+
<div
|
|
310
|
+
onMouseDown={(e) => { setDraggingResize(true); dragStartXRef.current = e.clientX; startWidthRef.current = width; }}
|
|
311
|
+
aria-hidden={'true'}
|
|
312
|
+
style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 6, cursor: 'col-resize', zIndex: 1 }}
|
|
313
|
+
/>
|
|
314
|
+
)}
|
|
244
315
|
{/* Panel chrome */}
|
|
245
316
|
<div style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', height: '100%', display: 'flex', flexDirection: 'column' }} className={'bg-[var(--content-card-background)] border-l'}>
|
|
246
317
|
{header}
|
|
247
318
|
<div className={'flex-1 flex flex-col'} style={{ minHeight: 0 }}>
|
|
248
319
|
<div className={'flex-1'} style={{ minHeight: 0 }}>
|
|
249
320
|
<MainContainer style={{ height: '100%', position: 'relative', background: 'var(--content-card-background)', border: 'none'}}>
|
|
250
|
-
<ChatContainer style={{ height: '100%' }}>
|
|
321
|
+
<ChatContainer style={{ height: '100%', border: 'none'}}>
|
|
251
322
|
<MessageList typingIndicator={undefined} autoScrollToBottom>
|
|
252
323
|
{messages.length === 0 && (
|
|
253
|
-
<
|
|
324
|
+
<div className={'px-3 py-2'}>
|
|
325
|
+
<div className={'text-xs mb-2'} style={{ color: 'var(--text-secondary)' }}>Try asking:</div>
|
|
326
|
+
<div className={'flex flex-wrap gap-2'}>
|
|
327
|
+
{starterQuestions.map((q, i) => (
|
|
328
|
+
<button
|
|
329
|
+
key={i}
|
|
330
|
+
type={'button'}
|
|
331
|
+
onClick={() => onStarterClick(q)}
|
|
332
|
+
className={'px-2 py-2 text-start rounded-lg border text-xs hover:text-[var(--icon-accent)] transition bg-[var(--bubble-background)] text-[var(--bubble-foreground)] border-[var(--icon-button-secondary)]'}
|
|
333
|
+
>
|
|
334
|
+
{q}
|
|
335
|
+
</button>
|
|
336
|
+
))}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
254
339
|
)}
|
|
255
340
|
{messages
|
|
256
341
|
.filter(m => !(m.role === 'assistant' && (m.content || '').trim().length === 0))
|
|
@@ -299,7 +384,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
299
384
|
</MainContainer>
|
|
300
385
|
</div>
|
|
301
386
|
<form onSubmit={(e) => { e.preventDefault(); if (!sending && input.trim()) sendMessage(); }}>
|
|
302
|
-
<div className={'flex items-end gap-2 p-2 border-t'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
|
|
387
|
+
<div className={'flex items-end gap-2 p-2 border-t'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)', paddingBottom: isMobile ? 'max(8px, env(safe-area-inset-bottom))' : undefined }}>
|
|
303
388
|
<input
|
|
304
389
|
aria-label={'Type your message'}
|
|
305
390
|
className={'flex-1 px-3 py-2 rounded kyd-chat-input'}
|
|
@@ -336,6 +421,8 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
336
421
|
)}
|
|
337
422
|
</div>
|
|
338
423
|
);
|
|
424
|
+
|
|
425
|
+
return typeof document !== 'undefined' ? createPortal(ui, document.body) : ui;
|
|
339
426
|
}
|
|
340
427
|
|
|
341
428
|
|
|
@@ -40,6 +40,7 @@ export default function GaugeCard({
|
|
|
40
40
|
}) {
|
|
41
41
|
const pct = Math.max(0, Math.min(100, Math.round(Number(percent ?? 0))));
|
|
42
42
|
const displayLabel = label || '';
|
|
43
|
+
// Use a fixed internal coordinate system and scale the SVG to container for responsiveness
|
|
43
44
|
const size = 280;
|
|
44
45
|
const strokeWidth = 32;
|
|
45
46
|
const radius = (size - strokeWidth) / 2;
|
|
@@ -83,9 +84,9 @@ export default function GaugeCard({
|
|
|
83
84
|
</span>
|
|
84
85
|
)}
|
|
85
86
|
</div>
|
|
86
|
-
<div className="flex-grow flex flex-col items-center justify-center" style={{ minHeight:
|
|
87
|
-
<div className="relative group" style={{ width:
|
|
88
|
-
<svg width={
|
|
87
|
+
<div className="flex-grow flex flex-col items-center justify-center" style={{ minHeight: 200 }}>
|
|
88
|
+
<div className="relative group" style={{ width: '100%', aspectRatio: '2 / 1', maxWidth: 360 }}>
|
|
89
|
+
<svg width={'100%'} height={'100%'} viewBox={`0 0 ${size} ${size/2}`} preserveAspectRatio={'xMidYMid meet'}>
|
|
89
90
|
<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" />
|
|
90
91
|
<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}`} />
|
|
91
92
|
<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" />
|
|
@@ -151,9 +151,7 @@ const GraphInsights = ({
|
|
|
151
151
|
percentList = Object.entries(scoringSummary.category_scores).map(([cat, entry]) => ({
|
|
152
152
|
category: cat,
|
|
153
153
|
// Prefer business, else combined, else atomic percent_progress
|
|
154
|
-
percent: Number(
|
|
155
|
-
(entry?.business?.percent_progress ?? entry?.combined?.percent_progress ?? entry?.atomic?.percent_progress ?? 0)
|
|
156
|
-
),
|
|
154
|
+
percent: Number(entry?.business?.percent_progress),
|
|
157
155
|
}));
|
|
158
156
|
}
|
|
159
157
|
|
package/src/lib/context.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function buildAllContextPrompt(cleanedData: any, reportGraphData: GraphIn
|
|
|
93
93
|
'Prefer concise, direct answers.',
|
|
94
94
|
'Use ONLY the JSON provided to answer the user.',
|
|
95
95
|
'Never invent or fetch external data.',
|
|
96
|
-
'If data is missing, say so briefly.',
|
|
96
|
+
'If data is missing, say so briefly. Assume the question is about the report, and assume the user is asking about the report, do not assume the user is asking you to perform a task other than looking at the report information.',
|
|
97
97
|
'Return natural language for the main reply.',
|
|
98
98
|
'When referring to the input JSON, use the term "Report Information" or "Report"',
|
|
99
99
|
'You are speaking to a non-technical user, ensure you do not use variable-esk language like "based on linkedin_provider", instead use "based the LinkedIn profile"',
|