Repository navigation
feat(web-dashboard): ChatKit UI Integration - #8
Conversation
- Add ChatKitWidget component using @openai/chatkit-react library - Load ChatKit CDN script via Next.js Script with beforeInteractive strategy - Implement floating chat button with text selection "Ask" feature - Add page/project context awareness for chat interactions - Include auth integration requiring login for chat functionality - Add specs, plan, and tasks for ChatKit UI feature 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR integrates ChatKit UI for natural language task management in the TaskFlow dashboard, implementing a floating chat widget with authentication, context awareness, and text selection features.
Key Changes:
- ChatKit React component integration with OpenAI's
@openai/chatkit-reactlibrary - Custom authentication flow using httpOnly cookies and context injection
- Floating chat widget with text selection "Ask" functionality
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
web-dashboard/src/components/chat/ChatKitWidget.tsx |
Main chat widget component with authentication, context extraction, and ChatKit integration |
web-dashboard/src/components/chat/styles.module.css |
Comprehensive styling for chat widget, buttons, menus, and overlays with responsive design |
web-dashboard/src/components/chat/index.tsx |
Export barrel for chat components |
web-dashboard/src/app/layout.tsx |
Adds ChatKit CDN script loading with beforeInteractive strategy |
web-dashboard/src/app/projects/layout.tsx |
Integrates ChatKitWidget into projects layout |
web-dashboard/src/app/dashboard/layout.tsx |
Integrates ChatKitWidget into dashboard layout |
web-dashboard/package.json |
Adds @openai/chatkit-react dependency |
web-dashboard/pnpm-lock.yaml |
Lock file updates for new dependencies |
specs/005-chatkit-ui/* |
Comprehensive feature specification, planning documents, and task breakdown |
packages/api/AUTH_INTEGRATION.md |
Deleted authentication integration documentation file |
Files not reviewed (1)
- web-dashboard/pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {selectedText && selectionPosition && isAuthenticated && ( | ||
| <div | ||
| ref={selectionRef} | ||
| className={styles.askButton} | ||
| style={{ | ||
| left: `${selectionPosition.x}px`, | ||
| top: `${selectionPosition.y}px`, | ||
| }} | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| e.preventDefault(); | ||
| handleAskSelectedText(); | ||
| }} | ||
| onMouseDown={(e) => { | ||
| e.stopPropagation(); | ||
| e.preventDefault(); | ||
| }} | ||
| > | ||
| <svg | ||
| width="14" | ||
| height="14" | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| > | ||
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /> | ||
| </svg> | ||
| <span>Ask</span> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The "Ask" button that appears on text selection lacks proper keyboard accessibility. Users who navigate with keyboard cannot trigger this functionality since it only appears on mouse selection. Consider:
- Adding keyboard shortcut support (e.g., Ctrl+K or Cmd+K with selected text)
- Making the button focusable when it appears
- Announcing the button's appearance to screen readers using ARIA live regions
Example:
<div
ref={selectionRef}
className={styles.askButton}
role="button"
tabIndex={0}
aria-label="Ask about selected text"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAskSelectedText();
}
}}
// ... rest of props
>| useEffect(() => { | ||
| const handleSelection = () => { | ||
| const selection = window.getSelection(); | ||
| if (!selection || selection.rangeCount === 0) { | ||
| setSelectedText(""); | ||
| setSelectionPosition(null); | ||
| return; | ||
| } | ||
|
|
||
| const text = selection.toString().trim(); | ||
| if (text.length > 0) { | ||
| setSelectedText(text); | ||
|
|
||
| const range = selection.getRangeAt(0); | ||
| const rect = range.getBoundingClientRect(); | ||
| setSelectionPosition({ | ||
| x: rect.left + rect.width / 2, | ||
| y: rect.top - 10, | ||
| }); | ||
| } else { | ||
| setSelectedText(""); | ||
| setSelectionPosition(null); | ||
| } | ||
| }; | ||
|
|
||
| document.addEventListener("selectionchange", handleSelection); | ||
| document.addEventListener("mouseup", handleSelection); | ||
|
|
||
| return () => { | ||
| document.removeEventListener("selectionchange", handleSelection); | ||
| document.removeEventListener("mouseup", handleSelection); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
Memory leak: The selection event listeners are added unconditionally but never cleaned up when the component unmounts. While the cleanup function exists in the useEffect, if the component unmounts between adding listeners and the cleanup running, the listeners will persist. Consider adding a mounted ref check:
useEffect(() => {
if (!isBrowser) return;
const handleSelection = () => {
if (!isMountedRef.current) return;
// ... rest of handler
};
document.addEventListener("selectionchange", handleSelection);
document.addEventListener("mouseup", handleSelection);
return () => {
document.removeEventListener("selectionchange", handleSelection);
document.removeEventListener("mouseup", handleSelection);
};
}, []);| const getPageContext = useCallback(() => { | ||
| if (typeof window === "undefined") return null; | ||
|
|
||
| const metaDescription = | ||
| document | ||
| .querySelector('meta[name="description"]') | ||
| ?.getAttribute("content") || ""; | ||
|
|
||
| const mainContent = | ||
| document.querySelector("article") || | ||
| document.querySelector("main") || | ||
| document.querySelector('[role="main"]') || | ||
| document.body; | ||
|
|
||
| const headings = Array.from(mainContent.querySelectorAll("h1, h2, h3")) | ||
| .slice(0, 5) | ||
| .map((h) => h.textContent?.trim()) | ||
| .filter(Boolean) | ||
| .join(", "); | ||
|
|
||
| return { | ||
| url: window.location.href, | ||
| title: document.title, | ||
| path: window.location.pathname, | ||
| description: metaDescription, | ||
| headings: headings, | ||
| projectId: projectId, | ||
| timestamp: new Date().toISOString(), | ||
| }; | ||
| }, [projectId]); |
There was a problem hiding this comment.
The getPageContext callback is recreated on every render because it depends on projectId, but projectId changes when the URL changes. This causes the useChatKit configuration to be recreated frequently, potentially causing unnecessary re-renders or connection resets. Consider memoizing with useMemo or useCallback with proper dependencies.
| <head> | ||
| {/* ChatKit Web Component - must load before React components */} | ||
| <Script | ||
| src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js" | ||
| strategy="beforeInteractive" | ||
| /> | ||
| </head> |
There was a problem hiding this comment.
Using <head> tag directly in Next.js App Router is not recommended and may not work as expected. The <Script> component with strategy="beforeInteractive" should be placed directly in the layout without wrapping it in a <head> tag. Next.js automatically handles script placement. Remove the <head> wrapper:
<html lang="en" className="dark">
<Script
src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"
strategy="beforeInteractive"
/>
<body className="min-h-screen bg-background font-sans antialiased">
{children}
</body>
</html>| <head> | |
| {/* ChatKit Web Component - must load before React components */} | |
| <Script | |
| src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js" | |
| strategy="beforeInteractive" | |
| /> | |
| </head> | |
| {/* ChatKit Web Component - must load before React components */} | |
| <Script | |
| src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js" | |
| strategy="beforeInteractive" | |
| /> |
| /* Hide scroll indicators */ | ||
| .chatKitContainer :global([class*="scroll" i]), | ||
| .chatKitContainer :global([class*="Scroll"]), | ||
| .chatKitContainer :global([id*="scroll" i]), | ||
| .chatKitContainer :global([id*="Scroll"]), | ||
| .chatKitContainer :global([data-scroll]), | ||
| .chatKitContainer :global([aria-label*="scroll" i]), | ||
| .chatKitContainer :global([aria-label*="Scroll" i]) { | ||
| display: none !important; | ||
| visibility: hidden !important; | ||
| opacity: 0 !important; | ||
| } | ||
|
|
||
| /* Hide progress indicators */ | ||
| .chatKitContainer :global([class*="progress" i]), | ||
| .chatKitContainer :global([class*="Progress"]), | ||
| .chatKitContainer :global([role="progressbar"]), | ||
| .chatKitContainer :global([role="status"]) { |
There was a problem hiding this comment.
The overly aggressive hiding of scroll-related elements using wildcard selectors with !important may break ChatKit's internal functionality. Selectors like [class*="scroll" i] and [class*="progress" i] are too broad and could hide legitimate UI elements that contain these words in their class names (e.g., "description", "progressive", etc.). Consider more specific selectors targeting known ChatKit classes or using a less aggressive approach.
| /* Hide scroll indicators */ | |
| .chatKitContainer :global([class*="scroll" i]), | |
| .chatKitContainer :global([class*="Scroll"]), | |
| .chatKitContainer :global([id*="scroll" i]), | |
| .chatKitContainer :global([id*="Scroll"]), | |
| .chatKitContainer :global([data-scroll]), | |
| .chatKitContainer :global([aria-label*="scroll" i]), | |
| .chatKitContainer :global([aria-label*="Scroll" i]) { | |
| display: none !important; | |
| visibility: hidden !important; | |
| opacity: 0 !important; | |
| } | |
| /* Hide progress indicators */ | |
| .chatKitContainer :global([class*="progress" i]), | |
| .chatKitContainer :global([class*="Progress"]), | |
| .chatKitContainer :global([role="progressbar"]), | |
| .chatKitContainer :global([role="status"]) { | |
| /* Hide scroll indicators - more specific targeting */ | |
| /* Example: Only hide known ChatKit scrollbars or indicators */ | |
| .chatKitContainer :global(.ck-scrollbar), | |
| .chatKitContainer :global(.ck-scroll-indicator) { | |
| display: none !important; | |
| visibility: hidden !important; | |
| opacity: 0 !important; | |
| } | |
| /* Hide progress indicators - more specific targeting */ | |
| .chatKitContainer :global(.ck-progress-indicator), | |
| .chatKitContainer :global([role="progressbar"]) { |
| {/* ChatKit Web Component - must load before React components */} | ||
| <Script | ||
| src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js" | ||
| strategy="beforeInteractive" |
There was a problem hiding this comment.
Loading external scripts from CDN without Subresource Integrity (SRI) verification poses a security risk. If the CDN is compromised, malicious code could be injected. Consider either:
- Self-hosting the ChatKit script
- Adding
integrityandcrossOriginattributes if the CDN supports SRI - At minimum, documenting this security trade-off
Example with SRI (if available):
<Script
src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"
strategy="beforeInteractive"
integrity="sha384-..."
crossOrigin="anonymous"
/>| strategy="beforeInteractive" | |
| strategy="beforeInteractive" | |
| integrity="sha384-..." // TODO: Replace with actual SRI hash for the script version in use | |
| crossOrigin="anonymous" |
| z-index: 1000; | ||
| } | ||
|
|
||
| .chatButton { | ||
| width: 56px; | ||
| height: 56px; | ||
| border-radius: 50%; | ||
| background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--primary) / 0.8) 100%); | ||
| border: 2px solid hsl(var(--primary) / 0.3); | ||
| box-shadow: | ||
| 0 4px 12px hsl(var(--primary) / 0.3), | ||
| 0 0 20px hsl(var(--primary) / 0.2); | ||
| cursor: pointer; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: hsl(var(--primary-foreground)); | ||
| transition: all 0.3s ease; | ||
| } | ||
|
|
||
| .settingsButton { | ||
| width: 40px; | ||
| height: 40px; | ||
| border-radius: 50%; | ||
| background: hsl(var(--primary) / 0.1); | ||
| border: 1px solid hsl(var(--primary) / 0.3); | ||
| box-shadow: | ||
| 0 2px 8px hsl(var(--background) / 0.1), | ||
| 0 0 0 1px hsl(var(--primary) / 0.1); | ||
| cursor: pointer; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| color: hsl(var(--primary) / 0.9); | ||
| transition: all 0.2s ease; | ||
| backdrop-filter: blur(8px); | ||
| } | ||
|
|
||
| .settingsButton:hover { | ||
| background: hsl(var(--primary) / 0.2); | ||
| border-color: hsl(var(--primary) / 0.5); | ||
| transform: scale(1.1); | ||
| color: hsl(var(--primary)); | ||
| } | ||
|
|
||
| .settingsButton:active { | ||
| transform: scale(0.95); | ||
| } | ||
|
|
||
| .chatButton svg { | ||
| stroke: hsl(var(--primary-foreground)); | ||
| filter: drop-shadow(0 0 2px hsl(var(--primary-foreground) / 0.5)); | ||
| } | ||
|
|
||
| .chatButton:hover { | ||
| transform: scale(1.1); | ||
| box-shadow: 0 6px 16px hsl(var(--background) / 0.4); | ||
| } | ||
|
|
||
| .chatButton:active { | ||
| transform: scale(0.95); | ||
| } | ||
|
|
||
| /* ChatKit Container */ | ||
| .chatKitContainer { | ||
| position: fixed; | ||
| bottom: 90px; | ||
| right: 24px; | ||
| width: 400px; | ||
| height: 600px; | ||
| max-width: calc(100vw - 48px); | ||
| max-height: calc(100vh - 120px); | ||
| z-index: 2000; | ||
| box-shadow: 0 8px 32px hsl(var(--background) / 0.2); | ||
| border-radius: 16px; | ||
| overflow: hidden; | ||
| background: hsl(var(--background)); | ||
| border: 1px solid hsl(var(--border)); | ||
| animation: slideUp 0.3s ease-out; | ||
| scrollbar-width: none; | ||
| -ms-overflow-style: none; | ||
| } | ||
|
|
||
| .chatKitContainer::-webkit-scrollbar { | ||
| display: none; | ||
| } | ||
|
|
||
| @keyframes slideUp { | ||
| from { | ||
| opacity: 0; | ||
| transform: translateY(20px); | ||
| } | ||
| to { | ||
| opacity: 1; | ||
| transform: translateY(0); | ||
| } | ||
| } | ||
|
|
||
| @keyframes slideDown { | ||
| from { | ||
| opacity: 0; | ||
| transform: translateX(-50%) translateY(-10px); | ||
| } | ||
| to { | ||
| opacity: 1; | ||
| transform: translateX(-50%) translateY(0); | ||
| } | ||
| } | ||
|
|
||
| .chatKit { | ||
| width: 100%; | ||
| height: 100%; | ||
| border-radius: 16px; | ||
| scrollbar-width: none; | ||
| -ms-overflow-style: none; | ||
| } | ||
|
|
||
| .chatKit::-webkit-scrollbar { | ||
| display: none; | ||
| } | ||
|
|
||
| /* Hide scrollbars in ChatKit */ | ||
| .chatKitContainer :global(*) { | ||
| scrollbar-width: none; | ||
| -ms-overflow-style: none; | ||
| } | ||
|
|
||
| .chatKitContainer :global(*)::-webkit-scrollbar { | ||
| display: none; | ||
| } | ||
|
|
||
| /* Hide scroll indicators */ | ||
| .chatKitContainer :global([class*="scroll" i]), | ||
| .chatKitContainer :global([class*="Scroll"]), | ||
| .chatKitContainer :global([id*="scroll" i]), | ||
| .chatKitContainer :global([id*="Scroll"]), | ||
| .chatKitContainer :global([data-scroll]), | ||
| .chatKitContainer :global([aria-label*="scroll" i]), | ||
| .chatKitContainer :global([aria-label*="Scroll" i]) { | ||
| display: none !important; | ||
| visibility: hidden !important; | ||
| opacity: 0 !important; | ||
| } | ||
|
|
||
| /* Hide progress indicators */ | ||
| .chatKitContainer :global([class*="progress" i]), | ||
| .chatKitContainer :global([class*="Progress"]), | ||
| .chatKitContainer :global([role="progressbar"]), | ||
| .chatKitContainer :global([role="status"]) { | ||
| display: none !important; | ||
| visibility: hidden !important; | ||
| } | ||
|
|
||
| /* Ask Button for Selected Text */ | ||
| .askButton { | ||
| position: fixed; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 6px; | ||
| padding: 6px 12px; | ||
| background: hsl(var(--primary) / 0.95); | ||
| color: hsl(var(--primary-foreground)); | ||
| border: 1px solid hsl(var(--primary-foreground) / 0.2); | ||
| border-radius: 8px; | ||
| cursor: pointer; | ||
| font-size: 13px; | ||
| font-weight: 500; | ||
| box-shadow: | ||
| 0 2px 8px hsl(var(--background) / 0.15), | ||
| 0 0 0 1px hsl(var(--primary) / 0.1); | ||
| z-index: 1999; | ||
| transition: all 0.2s ease; | ||
| pointer-events: auto; | ||
| animation: slideDown 0.2s ease-out; | ||
| backdrop-filter: blur(8px); | ||
| transform: translateX(-50%); | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .askButton:hover { | ||
| transform: translateX(-50%) translateY(-2px); | ||
| box-shadow: | ||
| 0 4px 12px hsl(var(--primary) / 0.3), | ||
| 0 0 0 1px hsl(var(--primary) / 0.2); | ||
| background: hsl(var(--primary)); | ||
| } | ||
|
|
||
| .askButton:active { | ||
| transform: translateX(-50%) translateY(0); | ||
| } | ||
|
|
||
| .askButton svg { | ||
| flex-shrink: 0; | ||
| width: 14px; | ||
| height: 14px; | ||
| opacity: 0.9; | ||
| } | ||
|
|
||
| /* Login Prompt Overlay */ | ||
| .loginPrompt { | ||
| position: fixed; | ||
| bottom: 90px; | ||
| right: 24px; | ||
| width: 400px; | ||
| max-width: calc(100vw - 48px); | ||
| z-index: 2000; | ||
| background: hsl(var(--background)); | ||
| border: 2px solid hsl(var(--primary) / 0.3); | ||
| border-radius: 16px; | ||
| box-shadow: | ||
| 0 8px 32px hsl(var(--background) / 0.3), | ||
| 0 0 20px hsl(var(--primary) / 0.2); | ||
| animation: slideUp 0.3s ease-out; | ||
| } | ||
|
|
||
| .loginPromptContent { | ||
| padding: 2rem; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 1.5rem; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .loginPromptContent h3 { | ||
| margin: 0; | ||
| font-size: 1.5rem; | ||
| font-weight: 600; | ||
| color: hsl(var(--foreground)); | ||
| } | ||
|
|
||
| .loginPromptContent p { | ||
| margin: 0; | ||
| font-size: 1rem; | ||
| color: hsl(var(--muted-foreground)); | ||
| line-height: 1.6; | ||
| } | ||
|
|
||
| .loginButton { | ||
| padding: 0.75rem 1.5rem; | ||
| background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--primary) / 0.8) 100%); | ||
| color: hsl(var(--primary-foreground)); | ||
| border: none; | ||
| border-radius: 8px; | ||
| font-size: 1rem; | ||
| font-weight: 500; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| box-shadow: 0 4px 12px hsl(var(--primary) / 0.3); | ||
| } | ||
|
|
||
| .loginButton:hover { | ||
| transform: translateY(-2px); | ||
| box-shadow: 0 6px 16px hsl(var(--primary) / 0.4); | ||
| } | ||
|
|
||
| .loginButton:active { | ||
| transform: translateY(0); | ||
| } | ||
|
|
||
| .closeButton { | ||
| padding: 0.5rem 1rem; | ||
| background: transparent; | ||
| color: hsl(var(--muted-foreground)); | ||
| border: 1px solid hsl(var(--border)); | ||
| border-radius: 8px; | ||
| font-size: 0.875rem; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| } | ||
|
|
||
| .closeButton:hover { | ||
| background: hsl(var(--muted)); | ||
| color: hsl(var(--foreground)); | ||
| } | ||
|
|
||
| /* Personalize Menu */ | ||
| .personalizeMenu { | ||
| position: fixed; | ||
| bottom: 90px; | ||
| right: 24px; | ||
| width: 280px; | ||
| max-width: calc(100vw - 48px); | ||
| z-index: 2001; |
There was a problem hiding this comment.
[nitpick] The z-index values are hardcoded (1000, 1999, 2000, 2001) which may conflict with other elements in the application. Consider using CSS custom properties or a z-index scale defined in a central location to maintain consistent layering across the application:
.chatButtonContainer {
z-index: var(--z-floating-widget, 1000);
}
.askButton {
z-index: var(--z-overlay-below, 1999);
}
.chatKitContainer,
.loginPrompt {
z-index: var(--z-overlay, 2000);
}
.personalizeMenu {
z-index: var(--z-overlay-above, 2001);
}| const getPageContext = useCallback(() => { | ||
| if (typeof window === "undefined") return null; | ||
|
|
||
| const metaDescription = | ||
| document | ||
| .querySelector('meta[name="description"]') | ||
| ?.getAttribute("content") || ""; | ||
|
|
||
| const mainContent = | ||
| document.querySelector("article") || | ||
| document.querySelector("main") || | ||
| document.querySelector('[role="main"]') || | ||
| document.body; | ||
|
|
||
| const headings = Array.from(mainContent.querySelectorAll("h1, h2, h3")) | ||
| .slice(0, 5) | ||
| .map((h) => h.textContent?.trim()) | ||
| .filter(Boolean) | ||
| .join(", "); | ||
|
|
||
| return { | ||
| url: window.location.href, | ||
| title: document.title, | ||
| path: window.location.pathname, | ||
| description: metaDescription, | ||
| headings: headings, | ||
| projectId: projectId, | ||
| timestamp: new Date().toISOString(), | ||
| }; | ||
| }, [projectId]); |
There was a problem hiding this comment.
The component queries the DOM for page context (document.querySelector) inside getPageContext, which is called in the fetch interceptor. This could cause performance issues if called frequently. Consider caching the page context or only updating it when the pathname changes:
const [pageContext, setPageContext] = useState(null);
useEffect(() => {
setPageContext(getPageContext());
}, [pathname, projectId]);
// Then use cached pageContext in fetch| x: rect.left + rect.width / 2, | ||
| y: rect.top - 10, |
There was a problem hiding this comment.
The selection position is calculated using getBoundingClientRect() which returns coordinates relative to the viewport, not the document. When the page is scrolled, the "Ask" button will not appear in the correct position. Add scroll offset to fix this:
const rect = range.getBoundingClientRect();
setSelectionPosition({
x: rect.left + rect.width / 2 + window.scrollX,
y: rect.top - 10 + window.scrollY,
});And update the button style to use position: absolute instead of position: fixed or account for scroll in the positioning logic.
| x: rect.left + rect.width / 2, | |
| y: rect.top - 10, | |
| x: rect.left + rect.width / 2 + window.scrollX, | |
| y: rect.top - 10 + window.scrollY, |
| if (!isAuthenticated) { | ||
| throw new Error("User must be logged in to use chat"); | ||
| } | ||
|
|
||
| const userId = user!.sub; | ||
| const pageContext = getPageContext(); | ||
|
|
||
| const userInfo = { | ||
| id: user!.sub, | ||
| name: user!.name || user!.email || "User", | ||
| email: user!.email, | ||
| role: user!.role, |
There was a problem hiding this comment.
The component uses user!.sub, user!.name, user!.email, and user!.role with non-null assertions after checking isAuthenticated. However, there's no guarantee that the user object is populated when isAuthenticated is true, especially during hydration or race conditions. Add explicit null checks or use optional chaining:
if (!isAuthenticated || !user) {
throw new Error("User must be logged in to use chat");
}
const userId = user.sub;
const userInfo = {
id: user.sub,
name: user.name || user.email || "User",
email: user.email,
role: user.role,
};| if (!isAuthenticated) { | |
| throw new Error("User must be logged in to use chat"); | |
| } | |
| const userId = user!.sub; | |
| const pageContext = getPageContext(); | |
| const userInfo = { | |
| id: user!.sub, | |
| name: user!.name || user!.email || "User", | |
| email: user!.email, | |
| role: user!.role, | |
| if (!isAuthenticated || !user) { | |
| throw new Error("User must be logged in to use chat"); | |
| } | |
| const userId = user.sub; | |
| const pageContext = getPageContext(); | |
| const userInfo = { | |
| id: user.sub, | |
| name: user.name || user.email || "User", | |
| email: user.email, | |
| role: user.role, |
Summary
@openai/chatkit-reactlibrarybeforeInteractivestrategyChanges
Test plan
🤖 Generated with Claude Code