kyd-shared-badge 0.3.6 → 0.3.8
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/chat/ChatWidget.tsx +17 -38
- package/src/chat/ChatWindowStreaming.tsx +18 -25
- package/src/chat/useChatStreaming.ts +12 -0
- package/src/lib/routes.ts +0 -3
package/package.json
CHANGED
package/src/chat/ChatWidget.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect,
|
|
3
|
+
import { useEffect, 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,44 +47,24 @@ 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
|
|
51
|
-
const [autoScroll, setAutoScroll] = useState(true);
|
|
52
|
-
const prevScrollHeightRef = useRef<number>(0);
|
|
50
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
53
51
|
|
|
54
|
-
// Detect user scroll position to toggle auto-scroll
|
|
55
52
|
useEffect(() => {
|
|
56
|
-
if (
|
|
57
|
-
|
|
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]);
|
|
53
|
+
if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
54
|
+
}, [messages, open]);
|
|
68
55
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (autoScroll) {
|
|
79
|
-
scrollEl.scrollTop = newHeight;
|
|
80
|
-
} else {
|
|
81
|
-
const delta = newHeight - prevHeight;
|
|
82
|
-
if (delta !== 0) {
|
|
83
|
-
scrollEl.scrollTop = scrollEl.scrollTop + delta;
|
|
56
|
+
// Prefill input from URL chatPrompt and open the sidebar when present
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof window === 'undefined') return;
|
|
60
|
+
const url = new URL(window.location.href);
|
|
61
|
+
const prompt = url.searchParams.get('chatPrompt');
|
|
62
|
+
if (prompt && prompt.trim()) {
|
|
63
|
+
setInput(prompt);
|
|
64
|
+
setOpen(true);
|
|
84
65
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
}, [messages, open, autoScroll]);
|
|
66
|
+
} catch {}
|
|
67
|
+
}, [setInput]);
|
|
88
68
|
|
|
89
69
|
// Optional hint (only shows if user hasn't dismissed before and when collapsed)
|
|
90
70
|
useEffect(() => {
|
|
@@ -241,7 +221,6 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
241
221
|
aria-label={'Chat sidebar'}
|
|
242
222
|
style={{ position: 'fixed', top: headerTop, right: 0, bottom: 0, width, maxWidth: '92vw' }}
|
|
243
223
|
className={'shadow-xl border flex flex-col overflow-hidden'}
|
|
244
|
-
ref={containerRef}
|
|
245
224
|
>
|
|
246
225
|
{/* Left-edge resizer */}
|
|
247
226
|
<div
|
|
@@ -256,7 +235,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
256
235
|
<div className={'flex-1'} style={{ minHeight: 0 }}>
|
|
257
236
|
<MainContainer style={{ height: '100%', position: 'relative', background: 'var(--content-card-background)', border: 'none'}}>
|
|
258
237
|
<ChatContainer style={{ height: '100%' }}>
|
|
259
|
-
<MessageList typingIndicator={undefined} autoScrollToBottom
|
|
238
|
+
<MessageList typingIndicator={undefined} autoScrollToBottom>
|
|
260
239
|
{messages.length === 0 && (
|
|
261
240
|
<Message model={{ message: 'Start a conversation. I can answer questions about this developer’s report.', sender: 'KYD', direction: 'incoming', position: 'single' }} />
|
|
262
241
|
)}
|
|
@@ -306,7 +285,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
306
285
|
</ChatContainer>
|
|
307
286
|
</MainContainer>
|
|
308
287
|
</div>
|
|
309
|
-
<form onSubmit={(e) => { e.preventDefault(); if (!sending && input.trim())
|
|
288
|
+
<form onSubmit={(e) => { e.preventDefault(); if (!sending && input.trim()) sendMessage(); }}>
|
|
310
289
|
<div className={'flex items-end gap-2 p-2 border-t'} style={{ borderColor: 'var(--icon-button-secondary)', background: 'var(--content-card-background)' }}>
|
|
311
290
|
<input
|
|
312
291
|
aria-label={'Type your message'}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
4
|
import './chat-overrides.css';
|
|
5
5
|
import { FiSend } from 'react-icons/fi';
|
|
6
6
|
import { useChatStreaming } from './useChatStreaming';
|
|
@@ -8,33 +8,26 @@ 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);
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
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;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
33
14
|
}, [messages]);
|
|
34
15
|
|
|
16
|
+
// Prefill input from URL chatPrompt if present
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
try {
|
|
19
|
+
if (typeof window === 'undefined') return;
|
|
20
|
+
const url = new URL(window.location.href);
|
|
21
|
+
const prompt = url.searchParams.get('chatPrompt');
|
|
22
|
+
if (prompt && prompt.trim()) {
|
|
23
|
+
setInput(prompt);
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
}, [setInput]);
|
|
27
|
+
|
|
35
28
|
return (
|
|
36
29
|
<div className="flex flex-col border rounded-lg overflow-hidden" style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}>
|
|
37
|
-
<div ref={listRef} className="p-4 space-y-3 h-72 overflow-auto"
|
|
30
|
+
<div ref={listRef} className="p-4 space-y-3 h-72 overflow-auto">
|
|
38
31
|
{messages.map(m => (
|
|
39
32
|
<div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}>
|
|
40
33
|
<div
|
|
@@ -54,13 +47,13 @@ export default function ChatWindowStreaming({ api = '/api/chat', badgeId }: { ap
|
|
|
54
47
|
<input
|
|
55
48
|
value={input}
|
|
56
49
|
onChange={e=>setInput(e.target.value)}
|
|
57
|
-
onKeyDown={e=>{ if (e.key==='Enter')
|
|
50
|
+
onKeyDown={e=>{ if (e.key==='Enter') sendMessage(); }}
|
|
58
51
|
className="flex-1 px-3 py-2 rounded border kyd-chat-input"
|
|
59
52
|
style={{ background: 'var(--input-background)', color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)' }}
|
|
60
53
|
placeholder="Ask KYD…"
|
|
61
54
|
/>
|
|
62
55
|
<button
|
|
63
|
-
onClick={()=>
|
|
56
|
+
onClick={()=>sendMessage()}
|
|
64
57
|
disabled={sending}
|
|
65
58
|
className="px-3 py-2 rounded disabled:opacity-60 kyd-chat-button"
|
|
66
59
|
style={{ background: 'var(--gradient-start)', color: '#fff' }}
|
|
@@ -28,6 +28,16 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
28
28
|
const assistantId = crypto.randomUUID();
|
|
29
29
|
setMessages(m => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
|
30
30
|
|
|
31
|
+
const redirectToLogin = (promptText: string) => {
|
|
32
|
+
// Inform the user then redirect to login carrying callbackUrl and prompt
|
|
33
|
+
setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, content: 'You need to log in to use chat. Redirecting to login…' } : msg));
|
|
34
|
+
const currentPath = typeof window !== 'undefined' ? (window.location.pathname + window.location.search + window.location.hash) : '/';
|
|
35
|
+
const loginUrl = `/auth/login?callbackUrl=${encodeURIComponent(currentPath)}&chatPrompt=${encodeURIComponent(promptText)}`;
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
if (typeof window !== 'undefined') window.location.href = loginUrl;
|
|
38
|
+
}, 700);
|
|
39
|
+
};
|
|
40
|
+
|
|
31
41
|
try {
|
|
32
42
|
abortRef.current?.abort();
|
|
33
43
|
abortRef.current = new AbortController();
|
|
@@ -35,6 +45,7 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
35
45
|
let sid = sessionId;
|
|
36
46
|
if (!sid) {
|
|
37
47
|
const sres = await fetch(`${api}/session`, { method: 'POST' });
|
|
48
|
+
if (sres.status === 401) { redirectToLogin(content); return; }
|
|
38
49
|
if (!sres.ok) throw new Error('session');
|
|
39
50
|
const sj = await sres.json();
|
|
40
51
|
sid = sj.sessionId as string;
|
|
@@ -46,6 +57,7 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
46
57
|
body: JSON.stringify({ content, sessionId: sid, badgeId }),
|
|
47
58
|
signal: abortRef.current.signal,
|
|
48
59
|
});
|
|
60
|
+
if (res.status === 401) { redirectToLogin(content); return; }
|
|
49
61
|
if (!res.ok || !res.body) throw new Error(`send: ${res.status}`);
|
|
50
62
|
|
|
51
63
|
const reader = res.body.getReader();
|
package/src/lib/routes.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { NextRequest } from 'next/server';
|
|
|
5
5
|
import { streamText } from 'ai';
|
|
6
6
|
import { bedrock } from '@ai-sdk/amazon-bedrock';
|
|
7
7
|
|
|
8
|
-
// import { verifyCognito } from './auth-verify';
|
|
9
8
|
import { getHistory, putMessage } from './chat-store';
|
|
10
9
|
import { aggregateUserData, cleanDeveloperProfile, buildAllContextPrompt, getReportGraphData } from './context';
|
|
11
10
|
import { checkAndConsumeToken } from './rate-limit';
|
|
@@ -17,7 +16,6 @@ export const runtime = 'nodejs';
|
|
|
17
16
|
|
|
18
17
|
export async function chatStreamRoute(req: NextRequest, userId: string, companyId?: string) {
|
|
19
18
|
try {
|
|
20
|
-
// const user = await verifyCognito(req);
|
|
21
19
|
|
|
22
20
|
const { sessionId, content, badgeId } = await req.json();
|
|
23
21
|
if (!content || !sessionId) {
|
|
@@ -70,7 +68,6 @@ export async function chatStreamRoute(req: NextRequest, userId: string, companyI
|
|
|
70
68
|
|
|
71
69
|
export async function createSessionRoute(req: NextRequest, userId: string, companyId?: string) {
|
|
72
70
|
try {
|
|
73
|
-
// const user = await verifyCognito(req);
|
|
74
71
|
const body = await req.json().catch(() => ({}));
|
|
75
72
|
const sessionId = await createSession({
|
|
76
73
|
userId: userId,
|