kyd-shared-badge 0.3.7 → 0.3.9
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
|
@@ -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,
|
|
@@ -534,7 +541,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
|
|
|
534
541
|
</footer>
|
|
535
542
|
</Reveal>
|
|
536
543
|
{/* Floating chat widget */}
|
|
537
|
-
<ChatWidget api={'/api/chat'} badgeId={badgeId} />
|
|
544
|
+
<ChatWidget api={chatProps?.api || '/api/chat'} badgeId={badgeId} title={chatProps?.title} hintText={chatProps?.hintText} loginPath={chatProps?.loginPath} headerOffset={chatProps?.headerOffset} />
|
|
538
545
|
</div>
|
|
539
546
|
</BusinessRulesProvider>
|
|
540
547
|
);
|
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,13 +49,26 @@ 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(() => {
|
|
53
56
|
if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
54
57
|
}, [messages, open]);
|
|
55
58
|
|
|
59
|
+
// Prefill input from URL chatPrompt and open the sidebar when present
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
try {
|
|
62
|
+
if (typeof window === 'undefined') return;
|
|
63
|
+
const url = new URL(window.location.href);
|
|
64
|
+
const prompt = url.searchParams.get('chatPrompt');
|
|
65
|
+
if (prompt && prompt.trim()) {
|
|
66
|
+
setInput(prompt);
|
|
67
|
+
setOpen(true);
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
}, [setInput]);
|
|
71
|
+
|
|
56
72
|
// Optional hint (only shows if user hasn't dismissed before and when collapsed)
|
|
57
73
|
useEffect(() => {
|
|
58
74
|
try {
|
|
@@ -104,13 +120,23 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
104
120
|
return () => window.removeEventListener('keydown', onKey);
|
|
105
121
|
}, [open]);
|
|
106
122
|
|
|
107
|
-
//
|
|
123
|
+
// Header offset handling to keep chat below any sticky header in host app
|
|
108
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
|
+
}
|
|
109
136
|
const measure = () => {
|
|
110
137
|
const el = document.querySelector('header');
|
|
111
138
|
const h = el ? Math.floor(el.getBoundingClientRect().height) : 0;
|
|
112
139
|
setHeaderTop(h);
|
|
113
|
-
// Keep collapsed tab within bounds beneath header
|
|
114
140
|
setTabTop(t => Math.max(h + 16, t));
|
|
115
141
|
};
|
|
116
142
|
measure();
|
|
@@ -120,7 +146,7 @@ export default function ChatWidget({ api = '/api/chat', title = 'KYD Bot', hintT
|
|
|
120
146
|
window.removeEventListener('resize', measure);
|
|
121
147
|
window.removeEventListener('scroll', measure);
|
|
122
148
|
};
|
|
123
|
-
}, []);
|
|
149
|
+
}, [headerOffset]);
|
|
124
150
|
|
|
125
151
|
// Drag to resize (left edge of sidebar)
|
|
126
152
|
useEffect(() => {
|
|
@@ -13,6 +13,18 @@ export default function ChatWindowStreaming({ api = '/api/chat', badgeId }: { ap
|
|
|
13
13
|
if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
14
14
|
}, [messages]);
|
|
15
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
|
+
|
|
16
28
|
return (
|
|
17
29
|
<div className="flex flex-col border rounded-lg overflow-hidden" style={{ background: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}>
|
|
18
30
|
<div ref={listRef} className="p-4 space-y-3 h-72 overflow-auto">
|
|
@@ -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);
|
|
@@ -28,6 +30,17 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
28
30
|
const assistantId = crypto.randomUUID();
|
|
29
31
|
setMessages(m => [...m, userMsg, { id: assistantId, role: 'assistant', content: '' }]);
|
|
30
32
|
|
|
33
|
+
const redirectToLogin = (promptText: string) => {
|
|
34
|
+
// Inform the user then redirect to login carrying callbackUrl and prompt
|
|
35
|
+
setMessages(m => m.map(msg => msg.id === assistantId ? { ...msg, content: 'You need to log in to use chat. Redirecting to login…' } : msg));
|
|
36
|
+
const currentPath = typeof window !== 'undefined' ? (window.location.pathname + window.location.search + window.location.hash) : '/';
|
|
37
|
+
const base = loginPath || '/auth/login';
|
|
38
|
+
const loginUrl = `${base}?callbackUrl=${encodeURIComponent(currentPath)}&chatPrompt=${encodeURIComponent(promptText)}`;
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
if (typeof window !== 'undefined') window.location.href = loginUrl;
|
|
41
|
+
}, 700);
|
|
42
|
+
};
|
|
43
|
+
|
|
31
44
|
try {
|
|
32
45
|
abortRef.current?.abort();
|
|
33
46
|
abortRef.current = new AbortController();
|
|
@@ -35,6 +48,7 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
35
48
|
let sid = sessionId;
|
|
36
49
|
if (!sid) {
|
|
37
50
|
const sres = await fetch(`${api}/session`, { method: 'POST' });
|
|
51
|
+
if (sres.status === 401) { redirectToLogin(content); return; }
|
|
38
52
|
if (!sres.ok) throw new Error('session');
|
|
39
53
|
const sj = await sres.json();
|
|
40
54
|
sid = sj.sessionId as string;
|
|
@@ -46,6 +60,7 @@ export function useChatStreaming(cfg: UseChatStreamingConfig) {
|
|
|
46
60
|
body: JSON.stringify({ content, sessionId: sid, badgeId }),
|
|
47
61
|
signal: abortRef.current.signal,
|
|
48
62
|
});
|
|
63
|
+
if (res.status === 401) { redirectToLogin(content); return; }
|
|
49
64
|
if (!res.ok || !res.body) throw new Error(`send: ${res.status}`);
|
|
50
65
|
|
|
51
66
|
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,
|