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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -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
  );
@@ -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
- // Measure sticky header height so the sidebar does not overlap it
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,