kyd-shared-badge 0.3.4 → 0.3.6
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
package/src/chat/ChatWidget.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { FiMessageSquare, FiX, FiChevronRight } from 'react-icons/fi';
|
|
5
5
|
import { useChatStreaming } from './useChatStreaming';
|
|
6
6
|
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
|
|
@@ -47,11 +47,44 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
47
47
|
const [headerTop, setHeaderTop] = useState(0);
|
|
48
48
|
const [showHint, setShowHint] = useState(false);
|
|
49
49
|
const { messages, input, setInput, sending, sendMessage, cancel } = useChatStreaming({ api, badgeId });
|
|
50
|
-
const
|
|
50
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
52
|
+
const prevScrollHeightRef = useRef<number>(0);
|
|
51
53
|
|
|
54
|
+
// Detect user scroll position to toggle auto-scroll
|
|
52
55
|
useEffect(() => {
|
|
53
|
-
if (
|
|
54
|
-
|
|
56
|
+
if (!open) return;
|
|
57
|
+
const root = containerRef.current;
|
|
58
|
+
if (!root) return;
|
|
59
|
+
const scrollEl = root.querySelector('.cs-message-list') as HTMLElement | null;
|
|
60
|
+
if (!scrollEl) return;
|
|
61
|
+
const onScroll = () => {
|
|
62
|
+
const nearBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 8;
|
|
63
|
+
setAutoScroll(nearBottom);
|
|
64
|
+
};
|
|
65
|
+
scrollEl.addEventListener('scroll', onScroll, { passive: true } as AddEventListenerOptions);
|
|
66
|
+
return () => scrollEl.removeEventListener('scroll', onScroll);
|
|
67
|
+
}, [open]);
|
|
68
|
+
|
|
69
|
+
// Preserve viewport when new content streams. If autoScroll is off, compensate for height growth.
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
if (!open) return;
|
|
72
|
+
const root = containerRef.current;
|
|
73
|
+
if (!root) return;
|
|
74
|
+
const scrollEl = root.querySelector('.cs-message-list') as HTMLElement | null;
|
|
75
|
+
if (!scrollEl) return;
|
|
76
|
+
const newHeight = scrollEl.scrollHeight;
|
|
77
|
+
const prevHeight = prevScrollHeightRef.current || newHeight;
|
|
78
|
+
if (autoScroll) {
|
|
79
|
+
scrollEl.scrollTop = newHeight;
|
|
80
|
+
} else {
|
|
81
|
+
const delta = newHeight - prevHeight;
|
|
82
|
+
if (delta !== 0) {
|
|
83
|
+
scrollEl.scrollTop = scrollEl.scrollTop + delta;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
prevScrollHeightRef.current = newHeight;
|
|
87
|
+
}, [messages, open, autoScroll]);
|
|
55
88
|
|
|
56
89
|
// Optional hint (only shows if user hasn't dismissed before and when collapsed)
|
|
57
90
|
useEffect(() => {
|
|
@@ -208,6 +241,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
208
241
|
aria-label={'Chat sidebar'}
|
|
209
242
|
style={{ position: 'fixed', top: headerTop, right: 0, bottom: 0, width, maxWidth: '92vw' }}
|
|
210
243
|
className={'shadow-xl border flex flex-col overflow-hidden'}
|
|
244
|
+
ref={containerRef}
|
|
211
245
|
>
|
|
212
246
|
{/* Left-edge resizer */}
|
|
213
247
|
<div
|
|
@@ -222,7 +256,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
222
256
|
<div className={'flex-1'} style={{ minHeight: 0 }}>
|
|
223
257
|
<MainContainer style={{ height: '100%', position: 'relative', background: 'var(--content-card-background)', border: 'none'}}>
|
|
224
258
|
<ChatContainer style={{ height: '100%' }}>
|
|
225
|
-
<MessageList typingIndicator={undefined} autoScrollToBottom>
|
|
259
|
+
<MessageList typingIndicator={undefined} autoScrollToBottom={autoScroll}>
|
|
226
260
|
{messages.length === 0 && (
|
|
227
261
|
<Message model={{ message: 'Start a conversation. I can answer questions about this developer’s report.', sender: 'KYD', direction: 'incoming', position: 'single' }} />
|
|
228
262
|
)}
|
|
@@ -272,7 +306,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
272
306
|
</ChatContainer>
|
|
273
307
|
</MainContainer>
|
|
274
308
|
</div>
|
|
275
|
-
<form onSubmit={(e) => { e.preventDefault(); if (!sending && input.trim()) sendMessage(); }}>
|
|
309
|
+
<form onSubmit={(e) => { e.preventDefault(); if (!sending && input.trim()) { setAutoScroll(true); sendMessage(); } }}>
|
|
276
310
|
<div className={'flex items-end gap-2 p-2 border-t'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
|
|
277
311
|
<input
|
|
278
312
|
aria-label={'Type your message'}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState, useLayoutEffect } from 'react';
|
|
4
4
|
import './chat-overrides.css';
|
|
5
5
|
import { FiSend } from 'react-icons/fi';
|
|
6
6
|
import { useChatStreaming } from './useChatStreaming';
|
|
@@ -8,14 +8,33 @@ import { useChatStreaming } from './useChatStreaming';
|
|
|
8
8
|
export default function ChatWindowStreaming({ api = '/api/chat', badgeId }: { api?: string, badgeId: string }) {
|
|
9
9
|
const { messages, input, setInput, sending, sendMessage } = useChatStreaming({ api, badgeId });
|
|
10
10
|
const listRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
const autoScrollRef = useRef<boolean>(true);
|
|
12
|
+
const prevScrollHeightRef = useRef<number>(0);
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
const handleScroll = () => {
|
|
15
|
+
const el = listRef.current;
|
|
16
|
+
if (!el) return;
|
|
17
|
+
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
|
|
18
|
+
autoScrollRef.current = nearBottom;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
const el = listRef.current;
|
|
23
|
+
if (!el) return;
|
|
24
|
+
const newHeight = el.scrollHeight;
|
|
25
|
+
const prevHeight = prevScrollHeightRef.current || newHeight;
|
|
26
|
+
if (autoScrollRef.current) {
|
|
27
|
+
el.scrollTop = newHeight;
|
|
28
|
+
} else {
|
|
29
|
+
const delta = newHeight - prevHeight;
|
|
30
|
+
if (delta !== 0) el.scrollTop = el.scrollTop + delta;
|
|
31
|
+
}
|
|
32
|
+
prevScrollHeightRef.current = newHeight;
|
|
14
33
|
}, [messages]);
|
|
15
34
|
|
|
16
35
|
return (
|
|
17
36
|
<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">
|
|
37
|
+
<div ref={listRef} className="p-4 space-y-3 h-72 overflow-auto" onScroll={handleScroll}>
|
|
19
38
|
{messages.map(m => (
|
|
20
39
|
<div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}>
|
|
21
40
|
<div
|
|
@@ -35,13 +54,13 @@ export default function ChatWindowStreaming({ api = '/api/chat', badgeId }: { ap
|
|
|
35
54
|
<input
|
|
36
55
|
value={input}
|
|
37
56
|
onChange={e=>setInput(e.target.value)}
|
|
38
|
-
onKeyDown={e=>{ if (e.key==='Enter') sendMessage(); }}
|
|
57
|
+
onKeyDown={e=>{ if (e.key==='Enter') { autoScrollRef.current = true; sendMessage(); } }}
|
|
39
58
|
className="flex-1 px-3 py-2 rounded border kyd-chat-input"
|
|
40
59
|
style={{ background: 'var(--input-background)', color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)' }}
|
|
41
60
|
placeholder="Ask KYD…"
|
|
42
61
|
/>
|
|
43
62
|
<button
|
|
44
|
-
onClick={()=>sendMessage()}
|
|
63
|
+
onClick={()=>{ autoScrollRef.current = true; sendMessage(); }}
|
|
45
64
|
disabled={sending}
|
|
46
65
|
className="px-3 py-2 rounded disabled:opacity-60 kyd-chat-button"
|
|
47
66
|
style={{ background: 'var(--gradient-start)', color: '#fff' }}
|
|
@@ -58,8 +58,8 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
58
58
|
<div className="flex flex-col md:flex-row items-center md:items-stretch gap-6">
|
|
59
59
|
{/* Left Half: Badge Image with robust centered overlay */}
|
|
60
60
|
<div className="w-full md:w-1/3 flex items-center justify-center self-stretch">
|
|
61
|
-
<div className="relative w-full max-w-xs select-none"
|
|
62
|
-
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={
|
|
61
|
+
<div className="relative w-full max-w-xs select-none">
|
|
62
|
+
<Image src={finalBadgeImageUrl} alt="KYD Badge" width={400} height={400} unoptimized className='w-full h-auto pointer-events-none p-10'/>
|
|
63
63
|
{/* Centered overlay slightly lower on Y axis, responsive and readable */}
|
|
64
64
|
<div className="pointer-events-none absolute left-1/2 top-[66%] -translate-x-1/2 -translate-y-1/2">
|
|
65
65
|
<div className="font-extrabold text-black text-3xl " >
|
package/src/lib/auth-verify.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// import { NextRequest } from 'next/server';
|
|
2
|
-
// import { getServerSession } from 'next-auth';
|
|
3
|
-
// import { authOptions } from '@/lib/auth';
|
|
4
|
-
// import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
5
|
-
|
|
6
|
-
// export type VerifiedUser = {
|
|
7
|
-
// userId: string;
|
|
8
|
-
// companyId?: string | null;
|
|
9
|
-
// idToken: string;
|
|
10
|
-
// };
|
|
11
|
-
|
|
12
|
-
// // Verifies Cognito ID Token from the server session or Authorization header.
|
|
13
|
-
// // Returns userId (sub) and optional companyId claim if present.
|
|
14
|
-
// export async function verifyCognito(req: NextRequest): Promise<VerifiedUser> {
|
|
15
|
-
// // Try to get id_token from NextAuth session first
|
|
16
|
-
// const session = await getServerSession(authOptions as any);
|
|
17
|
-
// let idToken: string | undefined;
|
|
18
|
-
// if (session.id_token) {
|
|
19
|
-
// idToken = session.id_token as string;
|
|
20
|
-
// }
|
|
21
|
-
// if (!idToken) {
|
|
22
|
-
// const authz = req.headers.get('authorization') || req.headers.get('Authorization');
|
|
23
|
-
// if (authz && authz.toLowerCase().startsWith('bearer ')) {
|
|
24
|
-
// idToken = authz.slice(7).trim();
|
|
25
|
-
// }
|
|
26
|
-
// }
|
|
27
|
-
|
|
28
|
-
// if (!idToken) {
|
|
29
|
-
// throw new Response(JSON.stringify({ error: 'Not authenticated' }), { status: 401 }) as unknown as Error;
|
|
30
|
-
// }
|
|
31
|
-
|
|
32
|
-
// const region = process.env.AWS_REGION!;
|
|
33
|
-
// const userPoolId = process.env.COGNITO_USER_POOL_ID!;
|
|
34
|
-
// const issuer = `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`;
|
|
35
|
-
// const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
|
|
36
|
-
|
|
37
|
-
// try {
|
|
38
|
-
// const { payload } = await jwtVerify(idToken, jwks, { issuer });
|
|
39
|
-
// const sub = String(payload.sub || '');
|
|
40
|
-
// if (!sub) throw new Error('Invalid token');
|
|
41
|
-
// const companyId = (payload as any)['custom:companyId'] || (payload as any).companyId || null;
|
|
42
|
-
// return { userId: sub, companyId, idToken };
|
|
43
|
-
// } catch (e) {
|
|
44
|
-
// throw new Response(JSON.stringify({ error: 'Invalid token' }), { status: 401 }) as unknown as Error;
|
|
45
|
-
// }
|
|
46
|
-
// }
|
|
47
|
-
|
|
48
|
-
|