kyd-shared-badge 0.2.34 → 0.3.0

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.2.34",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -16,6 +16,12 @@
16
16
  "react-dom": "^19.0.0"
17
17
  },
18
18
  "dependencies": {
19
+ "@ai-sdk/amazon-bedrock": "^3.0.22",
20
+ "@aws-sdk/client-s3": "^3.893.0",
21
+ "@aws-sdk/lib-dynamodb": "^3.893.0",
22
+ "@chatscope/chat-ui-kit-react": "^2.1.1",
23
+ "@chatscope/chat-ui-kit-styles": "^1.4.0",
24
+ "ai": "5.0.47",
19
25
  "i18n-iso-countries": "^7.14.0",
20
26
  "react-icons": "^5.5.0",
21
27
  "recharts": "^2.15.4"
@@ -24,6 +30,7 @@
24
30
  "access": "public"
25
31
  },
26
32
  "devDependencies": {
33
+ "@types/node": "^24.5.2",
27
34
  "@types/react": "^19.1.10",
28
35
  "@types/react-dom": "^19.1.7",
29
36
  "rollup": "^4.46.2"
@@ -21,6 +21,8 @@ import CategoryBars from './components/CategoryBars';
21
21
  import SkillsAppendixTable from './components/SkillsAppendixTable';
22
22
  import { BusinessRulesProvider } from './components/BusinessRulesContext';
23
23
  import Reveal from './components/Reveal';
24
+ import { formatLocalDateTime } from './utils/date';
25
+ import ChatWidget from './chat/ChatWidget';
24
26
 
25
27
  // const hexToRgba = (hex: string, alpha: number) => {
26
28
  // const clean = hex.replace('#', '');
@@ -519,14 +521,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
519
521
 
520
522
  <Reveal>
521
523
  <div className={'pt-8 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
522
- Report Completed: {new Date(updatedAt).toLocaleString(undefined, {
523
- year: 'numeric',
524
- month: 'long',
525
- day: 'numeric',
526
- hour: 'numeric',
527
- minute: '2-digit',
528
- timeZoneName: 'short',
529
- })}
524
+ Report Completed: {formatLocalDateTime(updatedAt)}
530
525
  </div>
531
526
  </Reveal>
532
527
  </div>
@@ -538,6 +533,8 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
538
533
  </p>
539
534
  </footer>
540
535
  </Reveal>
536
+ {/* Floating chat widget */}
537
+ <ChatWidget api={'/api/chat'} badgeId={badgeId} />
541
538
  </div>
542
539
  </BusinessRulesProvider>
543
540
  );
@@ -0,0 +1,9 @@
1
+ declare module 'ai' {
2
+ export function streamText(args: any): any;
3
+ }
4
+
5
+ declare module '@ai-sdk/amazon-bedrock' {
6
+ export function bedrock(modelId: string, opts?: any): any;
7
+ }
8
+
9
+
@@ -0,0 +1,317 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { FiMessageSquare, FiX, FiChevronRight } from 'react-icons/fi';
5
+ import { useChatStreaming } from './useChatStreaming';
6
+ import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
7
+ import './chat-overrides.css';
8
+ import {
9
+ MainContainer,
10
+ ChatContainer,
11
+ MessageList,
12
+ Message,
13
+ TypingIndicator
14
+ } from '@chatscope/chat-ui-kit-react';
15
+ import EvidenceBlock from './EvidenceBlock';
16
+ import { tryParseEvidence } from './parseEvidence';
17
+ import Image from 'next/image';
18
+ import { FiSend, FiSquare } from 'react-icons/fi';
19
+
20
+ type Props = {
21
+ api?: string;
22
+ title?: string;
23
+ hintText?: string;
24
+ badgeId: string;
25
+ };
26
+
27
+ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintText = 'Ask anything about the developer', badgeId }: Props) {
28
+ // Sidebar open state (default expanded)
29
+ const [open, setOpen] = useState<boolean>(() => {
30
+ try { const s = localStorage.getItem('kydChatSidebarOpen'); return s ? s === '1' : true; } catch { return true; }
31
+ });
32
+ // Sidebar width with bounds and persistence
33
+ const [width, setWidth] = useState<number>(() => {
34
+ 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; }
35
+ });
36
+ const minWidth = Math.min(320, Math.floor((typeof window !== 'undefined' ? window.innerWidth : 1200) * 0.92));
37
+ const maxWidth = Math.max(minWidth, Math.floor((typeof window !== 'undefined' ? window.innerWidth : 1200) * 0.9));
38
+ // Collapsed tab vertical position
39
+ const [tabTop, setTabTop] = useState<number>(() => {
40
+ try { const t = Number(localStorage.getItem('kydChatCollapsedTop')); return Number.isFinite(t) ? t : 120; } catch { return 120; }
41
+ });
42
+ const [draggingResize, setDraggingResize] = useState(false);
43
+ const [draggingTab, setDraggingTab] = useState(false);
44
+ const dragStartXRef = useRef(0);
45
+ const startWidthRef = useRef(0);
46
+ const dragStartYRef = useRef(0);
47
+ const startTopRef = useRef(0);
48
+ const tabDraggedRef = useRef(false);
49
+ const [headerTop, setHeaderTop] = useState(0);
50
+ const [showHint, setShowHint] = useState(false);
51
+ const { messages, input, setInput, sending, sendMessage, cancel } = useChatStreaming({ api, badgeId });
52
+ const listRef = useRef<HTMLDivElement>(null);
53
+
54
+ useEffect(() => {
55
+ if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
56
+ }, [messages, open]);
57
+
58
+ // Optional hint (only shows if user hasn't dismissed before and when collapsed)
59
+ useEffect(() => {
60
+ try {
61
+ const dismissed = localStorage.getItem('kydChatHintDismissed');
62
+ if (!dismissed) {
63
+ const shouldShow = !open; // show only if collapsed on first load
64
+ if (shouldShow) {
65
+ setShowHint(true);
66
+ const t = setTimeout(() => setShowHint(false), 6000);
67
+ return () => clearTimeout(t);
68
+ }
69
+ }
70
+ } catch {}
71
+ }, [open]);
72
+
73
+ const onDismissHint = () => {
74
+ setShowHint(false);
75
+ try { localStorage.setItem('kydChatHintDismissed', '1'); } catch {}
76
+ };
77
+
78
+ const header = useMemo(() => (
79
+ <div className={'flex items-center justify-between px-3 py-3 border-b'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
80
+ <div>
81
+ <div className={'text-md font-semibold'} style={{ color: 'var(--text-main)' }}>{title}</div>
82
+ </div>
83
+ <button aria-label={'Collapse chat sidebar'} onClick={() => setOpen(false)} className={'p-1 rounded hover:opacity-80'}>
84
+ <FiX size={16} style={{ color: 'var(--text-secondary)' }} />
85
+ </button>
86
+ </div>
87
+ ), [title]);
88
+
89
+ // Persist sidebar UI state
90
+ useEffect(() => {
91
+ try { localStorage.setItem('kydChatSidebarOpen', open ? '1' : '0'); } catch {}
92
+ }, [open]);
93
+ useEffect(() => {
94
+ try { localStorage.setItem('kydChatSidebarWidth', String(width)); } catch {}
95
+ }, [width]);
96
+ useEffect(() => {
97
+ try { localStorage.setItem('kydChatCollapsedTop', String(tabTop)); } catch {}
98
+ }, [tabTop]);
99
+
100
+ // ESC collapses when open
101
+ useEffect(() => {
102
+ const onKey = (e: KeyboardEvent) => {
103
+ if (e.key === 'Escape' && open) setOpen(false);
104
+ };
105
+ window.addEventListener('keydown', onKey);
106
+ return () => window.removeEventListener('keydown', onKey);
107
+ }, [open]);
108
+
109
+ // Measure sticky header height so the sidebar does not overlap it
110
+ useEffect(() => {
111
+ const measure = () => {
112
+ const el = document.querySelector('header');
113
+ const h = el ? Math.floor(el.getBoundingClientRect().height) : 0;
114
+ setHeaderTop(h);
115
+ // Keep collapsed tab within bounds beneath header
116
+ setTabTop(t => Math.max(h + 16, t));
117
+ };
118
+ measure();
119
+ window.addEventListener('resize', measure);
120
+ window.addEventListener('scroll', measure, { passive: true });
121
+ return () => {
122
+ window.removeEventListener('resize', measure);
123
+ window.removeEventListener('scroll', measure);
124
+ };
125
+ }, []);
126
+
127
+ // Drag to resize (left edge of sidebar)
128
+ useEffect(() => {
129
+ const onMove = (e: MouseEvent) => {
130
+ if (!draggingResize) return;
131
+ const clientX = e.clientX;
132
+ const vw = window.innerWidth;
133
+ const newWidth = Math.min(maxWidth, Math.max(minWidth, vw - clientX));
134
+ setWidth(newWidth);
135
+ e.preventDefault();
136
+ };
137
+ const onUp = () => {
138
+ if (draggingResize) setDraggingResize(false);
139
+ document.body.style.cursor = '';
140
+ document.body.style.userSelect = '';
141
+ };
142
+ if (draggingResize) {
143
+ window.addEventListener('mousemove', onMove);
144
+ window.addEventListener('mouseup', onUp);
145
+ document.body.style.cursor = 'col-resize';
146
+ document.body.style.userSelect = 'none';
147
+ }
148
+ return () => {
149
+ window.removeEventListener('mousemove', onMove);
150
+ window.removeEventListener('mouseup', onUp);
151
+ };
152
+ }, [draggingResize, maxWidth]);
153
+
154
+ // Drag collapsed tab vertically
155
+ useEffect(() => {
156
+ const onMove = (e: MouseEvent) => {
157
+ if (!draggingTab) return;
158
+ const delta = e.clientY - dragStartYRef.current;
159
+ if (Math.abs(delta) > 3) tabDraggedRef.current = true;
160
+ const minTop = headerTop + 16;
161
+ const next = Math.max(minTop, Math.min(window.innerHeight - 160 - 16, startTopRef.current + delta));
162
+ setTabTop(next);
163
+ e.preventDefault();
164
+ };
165
+ const onUp = () => setDraggingTab(false);
166
+ if (draggingTab) {
167
+ window.addEventListener('mousemove', onMove);
168
+ window.addEventListener('mouseup', onUp);
169
+ document.body.style.userSelect = 'none';
170
+ }
171
+ return () => {
172
+ window.removeEventListener('mousemove', onMove);
173
+ window.removeEventListener('mouseup', onUp);
174
+ if (!draggingTab) document.body.style.userSelect = '';
175
+ };
176
+ }, [draggingTab]);
177
+
178
+ return (
179
+ <div className={'fixed z-50'} aria-live={'polite'}>
180
+ {/* Collapsed vertical tab (draggable) */}
181
+ {!open && (
182
+ <div style={{ position: 'fixed', right: 0, top: Math.max(headerTop + 16, tabTop), height: 160, width: 44 }}>
183
+ <button
184
+ aria-label={'Open chat sidebar'}
185
+ aria-expanded={open}
186
+ onMouseDown={(e) => { setDraggingTab(true); dragStartYRef.current = e.clientY; startTopRef.current = tabTop; tabDraggedRef.current = false; }}
187
+ onMouseUp={(e) => { e.preventDefault(); const dragged = tabDraggedRef.current; tabDraggedRef.current = false; setDraggingTab(false); if (!dragged) setOpen(true); }}
188
+ className={'h-full w-full shadow-lg border rounded-l-lg flex flex-col items-center justify-between transition hover:opacity-90'}
189
+ style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)', cursor: draggingTab ? 'grabbing' : 'grab' }}
190
+ >
191
+ <FiMessageSquare size={18} style={{ color: 'var(--text-main)', marginTop: 8 }} />
192
+ <span style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', color: 'var(--text-secondary)', fontSize: 12, letterSpacing: 1 }}>Chat</span>
193
+ <FiChevronRight size={16} style={{ color: 'var(--text-secondary)', marginBottom: 8 }} />
194
+ </button>
195
+ {showHint && (
196
+ <button onClick={() => { setOpen(true); onDismissHint(); }} className={'absolute -left-2 -top-8 group max-w-xs text-left'}>
197
+ <div className={'px-2 py-1 rounded shadow border'}
198
+ style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}>
199
+ <span className={'text-[10px]'} style={{ color: 'var(--text-main)' }}>{hintText}</span>
200
+ </div>
201
+ </button>
202
+ )}
203
+ </div>
204
+ )}
205
+
206
+ {/* Expanded right sidebar */}
207
+ {open && (
208
+ <aside
209
+ role={'complementary'}
210
+ aria-label={'Chat sidebar'}
211
+ style={{ position: 'fixed', top: headerTop, right: 0, bottom: 0, width, maxWidth: '92vw' }}
212
+ className={'shadow-xl border flex flex-col overflow-hidden'}
213
+ >
214
+ {/* Left-edge resizer */}
215
+ <div
216
+ onMouseDown={(e) => { setDraggingResize(true); dragStartXRef.current = e.clientX; startWidthRef.current = width; }}
217
+ aria-hidden={'true'}
218
+ style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 6, cursor: 'col-resize', zIndex: 1 }}
219
+ />
220
+ {/* Panel chrome */}
221
+ <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'}>
222
+ {header}
223
+ <div className={'flex-1 flex flex-col'} style={{ minHeight: 0 }}>
224
+ <div className={'flex-1'} style={{ minHeight: 0 }}>
225
+ <MainContainer style={{ height: '100%', position: 'relative', background: 'var(--content-card-background)', border: 'none'}}>
226
+ <ChatContainer style={{ height: '100%' }}>
227
+ <MessageList typingIndicator={undefined} autoScrollToBottom>
228
+ {messages.length === 0 && (
229
+ <Message model={{ message: 'Start a conversation. I can answer questions about this developer’s report.', sender: 'KYD', direction: 'incoming', position: 'single' }} />
230
+ )}
231
+ {messages
232
+ .filter(m => !(m.role === 'assistant' && (m.content || '').trim().length === 0))
233
+ .map(m => (
234
+ <Message key={m.id} model={{
235
+ message: m.content,
236
+ sender: m.role === 'user' ? 'You' : 'KYD',
237
+ direction: m.role === 'user' ? 'outgoing' : 'incoming',
238
+ position: 'single'
239
+ }} />
240
+ ))}
241
+ {(() => {
242
+ const last = messages[messages.length - 1];
243
+ const isAssistantPlaceholder = sending && last && last.role === 'assistant' && (last.content || '').trim().length === 0;
244
+ if (!isAssistantPlaceholder) return null;
245
+ return (
246
+ <div className={'mx-3 my-1'}>
247
+ <div style={{
248
+ background: 'var(--bubble-background)',
249
+ color: 'var(--bubble-foreground)',
250
+ border: '1px solid var(--icon-button-secondary)',
251
+ borderRadius: 12,
252
+ padding: '8px 12px',
253
+ display: 'inline-block'
254
+ }}>
255
+ <span className={'kyd-typing-dots'}>
256
+ <span />
257
+ <span />
258
+ <span />
259
+ </span>
260
+ </div>
261
+ </div>
262
+ );
263
+ })()}
264
+ {(() => {
265
+ const last = [...messages].reverse().find(mm => mm.role === 'assistant');
266
+ const evidence = last ? tryParseEvidence(last.content) : null;
267
+ return evidence ? (
268
+ <div className={'mx-2'}>
269
+ <EvidenceBlock data={evidence} />
270
+ </div>
271
+ ) : null;
272
+ })()}
273
+ </MessageList>
274
+ </ChatContainer>
275
+ </MainContainer>
276
+ </div>
277
+ <form onSubmit={(e) => { e.preventDefault(); if (!sending && input.trim()) sendMessage(); }}>
278
+ <div className={'flex items-end gap-2 p-2 border-t'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
279
+ <input
280
+ aria-label={'Type your message'}
281
+ className={'flex-1 px-3 py-2 rounded kyd-chat-input'}
282
+ placeholder={'Ask anything about the developer…'}
283
+ value={input}
284
+ onChange={e => setInput(e.target.value)}
285
+ />
286
+ {sending ? (
287
+ <button
288
+ type={'button'}
289
+ onClick={() => cancel()}
290
+ aria-label={'Stop generating'}
291
+ className={'rounded-full flex items-center justify-center transition-opacity'}
292
+ style={{ width: 40, height: 40, background: 'var(--text-main)' }}
293
+ >
294
+ <FiSquare size={18} style={{ color: 'var(--content-card-background)' }} />
295
+ </button>
296
+ ) : (
297
+ <button
298
+ type={'submit'}
299
+ aria-label={'Send message'}
300
+ disabled={!input.trim()}
301
+ className={'rounded-full flex items-center justify-center transition-opacity disabled:opacity-50'}
302
+ style={{ width: 40, height: 40, background: input.trim() ? 'var(--text-main)' : 'var(--icon-button-secondary)' }}
303
+ >
304
+ <FiSend size={18} style={{ color: input.trim() ? 'var(--content-card-background)' : 'var(--text-secondary)' }} />
305
+ </button>
306
+ )}
307
+ </div>
308
+ </form>
309
+ </div>
310
+ </div>
311
+ </aside>
312
+ )}
313
+ </div>
314
+ );
315
+ }
316
+
317
+
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import './chat-overrides.css';
5
+ import { FiSend } from 'react-icons/fi';
6
+ import { useChatStreaming } from './useChatStreaming';
7
+
8
+ export default function ChatWindowStreaming({ api = '/api/chat', badgeId }: { api?: string, badgeId: string }) {
9
+ const { messages, input, setInput, sending, sendMessage } = useChatStreaming({ api, badgeId });
10
+ const listRef = useRef<HTMLDivElement>(null);
11
+
12
+ useEffect(() => {
13
+ if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
14
+ }, [messages]);
15
+
16
+ return (
17
+ <div className="flex flex-col border rounded-lg overflow-hidden" style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}>
18
+ <div ref={listRef} className="p-4 space-y-3 h-72 overflow-auto">
19
+ {messages.map(m => (
20
+ <div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}>
21
+ <div
22
+ className={`inline-block px-3 py-2 rounded border`}
23
+ style={
24
+ m.role === 'user'
25
+ ? { background: 'linear-gradient(135deg, var(--gradient-start), var(--gradient-end))', color: '#fff', borderColor: 'transparent' }
26
+ : { background: 'var(--bubble-background)', color: 'var(--bubble-foreground)', borderColor: 'var(--icon-button-secondary)' }
27
+ }
28
+ >
29
+ {m.content}
30
+ </div>
31
+ </div>
32
+ ))}
33
+ </div>
34
+ <div className="p-3 border-t flex gap-2" style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)'}}>
35
+ <input
36
+ value={input}
37
+ onChange={e=>setInput(e.target.value)}
38
+ onKeyDown={e=>{ if (e.key==='Enter') sendMessage(); }}
39
+ className="flex-1 px-3 py-2 rounded border kyd-chat-input"
40
+ style={{ background: 'var(--input-background)', color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)' }}
41
+ placeholder="Ask KYD…"
42
+ />
43
+ <button
44
+ onClick={()=>sendMessage()}
45
+ disabled={sending}
46
+ className="px-3 py-2 rounded disabled:opacity-60 kyd-chat-button"
47
+ style={{ background: 'var(--gradient-start)', color: '#fff' }}
48
+ >
49
+ {sending ? 'Sending…' : <FiSend size={18} />}
50
+ </button>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { EvidencePayload } from './types';
4
+ import { FaGithub, FaGitlab, FaStackOverflow, FaLinkedin, FaGoogle, FaPython } from 'react-icons/fa';
5
+
6
+ function ProviderIcon({ provider }: { provider: string }) {
7
+ const n = (provider || '').toLowerCase();
8
+ if (n.includes('github')) return <FaGithub />;
9
+ if (n.includes('gitlab')) return <FaGitlab />;
10
+ if (n.includes('stack')) return <FaStackOverflow />;
11
+ if (n.includes('google')) return <FaGoogle />;
12
+ if (n.includes('linkedin')) return <FaLinkedin />;
13
+ if (n.includes('python')) return <FaPython />;
14
+ return <span className="inline-block w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--icon-button-secondary)' }} />;
15
+ }
16
+
17
+ export default function EvidenceBlock({ data }: { data: EvidencePayload }) {
18
+ if (!data?.claims || data.claims.length === 0) return null;
19
+ return (
20
+ <div className={'mt-2 p-2 rounded border text-sm'} style={{ borderColor: 'var(--icon-button-secondary)', color: 'var(--text-main)' }}>
21
+ {data.claims.map((c, idx) => (
22
+ <div key={idx} className={'mb-3 last:mb-0'}>
23
+ <div className={'font-semibold mb-1'}>{c.claim}</div>
24
+ <div className={'space-y-1'}>
25
+ {c.items.map((it, i) => (
26
+ <div key={i} className={'flex items-center gap-2'}>
27
+ <span className={'text-base'} style={{ color: 'var(--text-secondary)' }}><ProviderIcon provider={it.provider} /></span>
28
+ <a href={it.url} target={'_blank'} rel={'noopener noreferrer'} className={'underline'} style={{ color: 'var(--icon-accent)' }}>{it.title}</a>
29
+ {it.date ? <span className={'text-xs'} style={{ color: 'var(--text-secondary)' }}>{new Date(it.date).toLocaleDateString()}</span> : null}
30
+ </div>
31
+ ))}
32
+ </div>
33
+ </div>
34
+ ))}
35
+ </div>
36
+ );
37
+ }
38
+
39
+
@@ -0,0 +1,138 @@
1
+ /* Chat UI Kit theme overrides to align with globals.css variables */
2
+
3
+ /* Container backgrounds */
4
+ .cs-main-container, .cs-chat-container, .cs-message-list, .cs-typing-indicator {
5
+ background: var(--content-card-background) !important;
6
+ color: var(--text-main) !important;
7
+ }
8
+
9
+ /* Header area */
10
+ .cs-chat-header, .cs-message-list__typing-indicator-container {
11
+ background: var(--content-card-background) !important;
12
+ border-color: var(--icon-button-secondary) !important;
13
+ }
14
+
15
+ /* Message bubbles */
16
+ .cs-message__content {
17
+ background: var(--bubble-background) !important;
18
+ color: var(--bubble-foreground) !important;
19
+ border: 1px solid var(--icon-button-secondary) !important;
20
+ }
21
+
22
+ /* Incoming/outgoing differentiation */
23
+ .cs-message--incoming .cs-message__content {
24
+ background: var(--bubble-background) !important;
25
+ }
26
+ .cs-message--outgoing .cs-message__content {
27
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)) !important;
28
+ color: white !important;
29
+ border-color: transparent !important;
30
+ }
31
+
32
+ /* Input area */
33
+ .cs-message-input__content-editor, .cs-message-input__content-editor-wrapper {
34
+ background: var(--input-background) !important;
35
+ color: var(--text-main) !important;
36
+ }
37
+ .cs-message-input, .cs-message-input__tools, .cs-message-input__content-editor-wrapper {
38
+ border-color: var(--icon-button-secondary) !important;
39
+ }
40
+ .cs-message-input {
41
+ background: var(--content-card-background) !important;
42
+ }
43
+ .cs-message-input__send-button {
44
+ background: var(--gradient-start) !important;
45
+ color: #fff !important;
46
+ }
47
+ .cs-message-input__send-button:hover {
48
+ background: var(--icon-accent-hover) !important;
49
+ }
50
+
51
+ /* Placeholder color for chatscope contenteditable */
52
+ .cs-message-input__content-editor[data-placeholder]:empty::before {
53
+ color: var(--text-secondary) !important;
54
+ opacity: 1 !important;
55
+ }
56
+
57
+ /* Focus outlines/rings - remove white glow and use subtle border */
58
+ .cs-message-input__content-editor:focus, .cs-message-input__content-editor-wrapper:focus-within, .cs-message-input:focus-within {
59
+ outline: none !important;
60
+ box-shadow: none !important;
61
+ border-color: var(--icon-button-secondary) !important;
62
+ }
63
+ .cs-message-input__send-button:focus {
64
+ outline: none !important;
65
+ box-shadow: none !important;
66
+ }
67
+
68
+ /* Custom input in ChatWindowStreaming */
69
+ .kyd-chat-input {
70
+ background: var(--input-background);
71
+ color: var(--text-main);
72
+ border-color: var(--icon-button-secondary);
73
+ }
74
+ .kyd-chat-input::placeholder {
75
+ color: var(--text-secondary);
76
+ }
77
+ .kyd-chat-input:focus {
78
+ outline: none;
79
+ box-shadow: none;
80
+ border-color: var(--icon-button-secondary);
81
+ }
82
+ .kyd-chat-button {
83
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
84
+ color: #fff;
85
+ }
86
+ .kyd-chat-button:hover {
87
+ background: var(--icon-accent-hover);
88
+ }
89
+ .kyd-chat-button:focus {
90
+ outline: none;
91
+ box-shadow: none;
92
+ }
93
+
94
+ /* Scrollbar subtle styling */
95
+ .cs-message-list::-webkit-scrollbar {
96
+ width: 8px;
97
+ }
98
+ .cs-message-list::-webkit-scrollbar-thumb {
99
+ background: var(--icon-button-secondary);
100
+ border-radius: 8px;
101
+ }
102
+
103
+ /* Typing indicator */
104
+ .cs-typing-indicator__text {
105
+ color: var(--text-secondary) !important;
106
+ }
107
+
108
+ /* Pulsing three dots for assistant placeholder */
109
+ .kyd-typing-dots {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 4px;
113
+ }
114
+ .kyd-typing-dots span {
115
+ width: 6px;
116
+ height: 6px;
117
+ border-radius: 9999px;
118
+ background: var(--text-secondary);
119
+ display: inline-block;
120
+ animation: kyd-bounce 1.2s infinite ease-in-out;
121
+ }
122
+ .kyd-typing-dots span:nth-child(2) { animation-delay: 0.15s; }
123
+ .kyd-typing-dots span:nth-child(3) { animation-delay: 0.3s; }
124
+ @keyframes kyd-bounce {
125
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.6; }
126
+ 40% { transform: translateY(-3px); opacity: 1; }
127
+ }
128
+
129
+ /* Prevent scroll chaining to the page so background doesn't bounce */
130
+ .cs-main-container,
131
+ .cs-chat-container,
132
+ .cs-message-list {
133
+ overscroll-behavior: contain !important;
134
+ overscroll-behavior-y: contain !important;
135
+ -webkit-overflow-scrolling: touch;
136
+ }
137
+
138
+
@@ -0,0 +1,18 @@
1
+ import { EvidencePayload } from './types';
2
+
3
+ // Very forgiving parser: expects the assistant to include a JSON block delimited by ```json ... ```
4
+ // If no fenced block found, tries to JSON.parse the entire string.
5
+ export function tryParseEvidence(text: string): EvidencePayload | null {
6
+ try {
7
+ const fenceStart = text.indexOf('```json');
8
+ const fenceEnd = text.indexOf('```', fenceStart + 7);
9
+ const raw = fenceStart >= 0 && fenceEnd > fenceStart ? text.slice(fenceStart + 7, fenceEnd).trim() : text.trim();
10
+ const obj = JSON.parse(raw);
11
+ if (obj && obj.type === 'evidence' && Array.isArray(obj.claims)) {
12
+ return obj as EvidencePayload;
13
+ }
14
+ } catch {}
15
+ return null;
16
+ }
17
+
18
+
@@ -0,0 +1,18 @@
1
+ export type EvidenceItem = {
2
+ provider: string; // e.g., 'github'
3
+ title: string; // e.g., repo name
4
+ url: string; // link to evidence
5
+ date?: string; // ISO date
6
+ };
7
+
8
+ export type EvidenceClaim = {
9
+ claim: string; // e.g., 'This user has lots of python experience'
10
+ items: EvidenceItem[];
11
+ };
12
+
13
+ export type EvidencePayload = {
14
+ type: 'evidence';
15
+ claims: EvidenceClaim[];
16
+ };
17
+
18
+