Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a shared AI chat UI module, refactors ask-ai to use it, and integrates a new AIChatWidget into the stack companion sidebar; also adjusts a list button event handler to stop propagation. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant AIChatWidget
participant useChat
participant API as /api/latest/ai/query/stream
participant useWordStreaming
participant UI as MessageRenderer
User->>AIChatWidget: submit message
AIChatWidget->>useChat: sendMessage (includes system prompt, tools, auth)
useChat->>API: open streaming request
API-->>useChat: stream response chunks
useChat->>AIChatWidget: emit assistant message parts (including tool invocations)
AIChatWidget->>useWordStreaming: provide assistant content for progressive reveal
useWordStreaming-->>UI: incremental display updates (word-by-word)
UI->>AIChatWidget: render messages & tool cards, update scroll
AIChatWidget->>User: display streamed content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds an "Ask AI" panel to the Stack Companion sidebar by introducing a new Key changes:
Confidence Score: 5/5Safe to merge; all remaining findings are P2 style/cleanup suggestions with no correctness or data-integrity impact. The feature is well-structured (shared module, keyed remounting for conversation resets, proper scroll handling). The only open items are: an unused variable that produces a lint warning but has no runtime impact, a project-convention suggestion around runAsynchronouslyWithAlert, and a code-duplication note about the DefaultChatTransport config. None of these affect correctness or safety. apps/dashboard/src/components/commands/ask-ai.tsx (unused variable), apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx (duplicated transport config) Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant AIChatWidget
participant AIChatWidgetInner
participant useChat
participant Backend as /api/latest/ai/query/stream
User->>AIChatWidget: types question, hits Enter
AIChatWidget->>AIChatWidgetInner: setConversationStarted(true)
AIChatWidgetInner->>useChat: sendMessage({ text })
useChat->>Backend: POST (systemPrompt, tools, messages)
Backend-->>useChat: streaming response (tokens)
useChat-->>AIChatWidgetInner: messages[], status="streaming"
AIChatWidgetInner->>AIChatWidgetInner: useWordStreaming reveals words
AIChatWidgetInner-->>User: AssistantMessage rendered progressively
User->>AIChatWidgetInner: types follow-up, hits Enter
AIChatWidgetInner->>useChat: sendMessage({ text })
useChat->>Backend: POST (full conversation history)
Backend-->>useChat: streaming response
useChat-->>AIChatWidgetInner: updated messages[]
AIChatWidgetInner-->>User: new AssistantMessage rendered
User->>AIChatWidgetInner: clicks "New conversation"
AIChatWidgetInner->>AIChatWidget: onNewConversation()
AIChatWidget->>AIChatWidget: setConversationKey(prev+1), reset state
AIChatWidget->>AIChatWidgetInner: remount via key change
AIChatWidgetInner-->>User: idle input screen shown
|
| transport: new DefaultChatTransport({ | ||
| api: `${backendBaseUrl}/api/latest/ai/query/stream`, | ||
| headers: () => buildStackAuthHeaders(currentUser), | ||
| prepareSendMessagesRequest: async ({ messages: uiMessages, headers }) => { | ||
| const modelMessages = await convertToModelMessages(uiMessages); | ||
| return { | ||
| body: { | ||
| systemPrompt: "command-center-ask-ai", | ||
| tools: ["docs", "sql-query"], | ||
| quality: "smart", | ||
| speed: "slow", | ||
| projectId, | ||
| messages: modelMessages.map(m => ({ | ||
| role: m.role, | ||
| content: m.content, | ||
| })), | ||
| }, | ||
| headers, | ||
| }; | ||
| }, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Duplicated
DefaultChatTransport configuration
The entire useChat / DefaultChatTransport setup here (API URL, headers, prepareSendMessagesRequest body shape) is a verbatim copy of the same block in ask-ai.tsx (lines 58–79). Any future change to the system prompt key, tool list, or body schema must be applied in two places.
Consider extracting the transport factory into a shared utility in ai-chat-shared.tsx or a dedicated lib/ai-chat-transport.ts file, then importing it in both consumers.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/dashboard/src/components/stack-companion.tsx (1)
432-436: Consider updating the Playground page to showcase the new AI chat widget.The conditional layout change for
ask-ai(no padding, flex column, overflow hidden) makes sense for a chat interface that manages its own scrolling. As per coding guidelines, any design components added to the dashboard should have corresponding updates in the Playground page.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/stack-companion.tsx` around lines 432 - 436, Playground page is missing the updated layout and AIChatWidget rendering when activeItem === 'ask-ai'; update the Playground component to apply the same conditional className logic used here (using cn with "flex-1 overflow-x-hidden no-drag cursor-auto" and the branch that sets "overflow-hidden p-0 flex flex-col" for activeItem === 'ask-ai' vs "overflow-y-auto p-5") and render <AIChatWidget isActive={true} /> when activeItem === 'ask-ai'; ensure AIChatWidget is imported and that the Playground's activeItem state/value uses the same 'ask-ai' key so the widget receives the correct isActive prop and the container handles its scrolling.apps/dashboard/src/components/commands/ai-chat-shared.tsx (1)
407-411: Type cast throughunknownbypasses type safety.The
as unknown as ToolInvocationPartcast assumes the filtered parts match theToolInvocationPartstructure. While thestartsWith("tool-")check provides some validation, this doesn't guarantee the full structure. Consider using a type guard or runtime validation for safer typing.♻️ Suggested type guard approach
+function isToolInvocationPart(part: unknown): part is ToolInvocationPart { + if (typeof part !== 'object' || part === null) return false; + const p = part as Record<string, unknown>; + return ( + typeof p.type === 'string' && + p.type.startsWith('tool-') && + typeof p.toolCallId === 'string' && + typeof p.state === 'string' + ); +} + // Helper to extract tool invocations from UIMessage parts export function getToolInvocations(message: UIMessage): ToolInvocationPart[] { return message.parts - .filter((part) => part.type.startsWith("tool-")) - .map((part) => part as unknown as ToolInvocationPart); + .filter(isToolInvocationPart); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 407 - 411, The cast in getToolInvocations blindly forces parts to ToolInvocationPart via "as unknown as ToolInvocationPart"; replace that with a proper type guard or runtime validation (e.g., implement isToolInvocationPart(part: any): part is ToolInvocationPart that checks required fields like type, name, args/result shapes) and use it in the filter (message.parts.filter(isToolInvocationPart)). Ensure the guard references the ToolInvocationPart shape and return the typed array without double-casting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 407-411: The cast in getToolInvocations blindly forces parts to
ToolInvocationPart via "as unknown as ToolInvocationPart"; replace that with a
proper type guard or runtime validation (e.g., implement
isToolInvocationPart(part: any): part is ToolInvocationPart that checks required
fields like type, name, args/result shapes) and use it in the filter
(message.parts.filter(isToolInvocationPart)). Ensure the guard references the
ToolInvocationPart shape and return the typed array without double-casting.
In `@apps/dashboard/src/components/stack-companion.tsx`:
- Around line 432-436: Playground page is missing the updated layout and
AIChatWidget rendering when activeItem === 'ask-ai'; update the Playground
component to apply the same conditional className logic used here (using cn with
"flex-1 overflow-x-hidden no-drag cursor-auto" and the branch that sets
"overflow-hidden p-0 flex flex-col" for activeItem === 'ask-ai' vs
"overflow-y-auto p-5") and render <AIChatWidget isActive={true} /> when
activeItem === 'ask-ai'; ensure AIChatWidget is imported and that the
Playground's activeItem state/value uses the same 'ask-ai' key so the widget
receives the correct isActive prop and the container handles its scrolling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3917f0c5-7955-49ce-9a5a-a843b3e2cf2e
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
apps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/commands/ask-ai.tsxapps/dashboard/src/components/design-components/list.tsxapps/dashboard/src/components/stack-companion.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
There was a problem hiding this comment.
Pull request overview
Introduces an “Ask AI” experience inside the Stack Companion UI, sharing chat rendering logic between the command palette preview and the new companion widget.
Changes:
- Adds a new Stack Companion sidebar item and full-page chat widget for “Ask AI”.
- Extracts shared AI chat rendering utilities/components into
ai-chat-shared.tsxand wiresask-aito use them. - Updates list-row button behavior to prevent row click handlers from firing when interacting with action buttons.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Updates lockfile for dependency graph changes introduced by the PR. |
| apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx | New Stack Companion AI chat widget implementation (streaming, scrolling, input handling). |
| apps/dashboard/src/components/stack-companion.tsx | Adds “Ask AI” to the companion sidebar and renders the new widget when selected. |
| apps/dashboard/src/components/design-components/list.tsx | Stops click propagation from row action buttons to the row onClick handler. |
| apps/dashboard/src/components/commands/ask-ai.tsx | Refactors the command palette AI preview to use shared chat UI helpers. |
| apps/dashboard/src/components/commands/ai-chat-shared.tsx | New shared components/utilities for AI chat rendering (markdown, tool invocations, streaming). |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const pathname = usePathname(); | ||
| const projectId = pathname.startsWith("/projects/") ? pathname.split("/")[2] : undefined; | ||
|
|
||
| const backendBaseUrl = getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set"); |
There was a problem hiding this comment.
The fallback error message is misleading: throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set") is reached when both NEXT_PUBLIC_BROWSER_STACK_API_URL and NEXT_PUBLIC_STACK_API_URL are missing. Update the message to mention both variables (or include the actual resolved env var names) so configuration issues are easier to diagnose.
| const backendBaseUrl = getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_BROWSER_STACK_API_URL is not set"); | |
| const backendBaseUrl = getPublicEnvVar("NEXT_PUBLIC_BROWSER_STACK_API_URL") ?? getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("Neither NEXT_PUBLIC_BROWSER_STACK_API_URL nor NEXT_PUBLIC_STACK_API_URL is set"); |
| const aiLoading = status === "submitted" || status === "streaming"; | ||
|
|
||
| // Word streaming for the last assistant message | ||
| const lastAssistantMessage = messages.findLast((m: UIMessage) => m.role === "assistant"); |
There was a problem hiding this comment.
Array.prototype.findLast isn't polyfilled by the app's polyfills.tsx, and the dashboard TS target is es2015, so this can throw at runtime in older browsers. Consider replacing with a backwards-compatible loop (iterate from the end) or another supported approach instead of findLast.
| const lastAssistantMessage = messages.findLast((m: UIMessage) => m.role === "assistant"); | |
| const lastAssistantMessage = (() => { | |
| for (let i = messages.length - 1; i >= 0; i--) { | |
| const m = messages[i] as UIMessage; | |
| if (m.role === "assistant") { | |
| return m; | |
| } | |
| } | |
| return undefined; | |
| })(); |
apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/dashboard/src/components/commands/ask-ai.tsx (2)
127-127:⚠️ Potential issue | 🟡 Minor
handleFollowUp()returnsvoid, not aPromise.
handleFollowUpinternally callsrunAsynchronously(sendMessage(...))and returnsvoid. WrappingrunAsynchronously(handleFollowUp())wrapsundefined, not a promise. This is a no-op.Either remove the wrapper or make
handleFollowUpreturn the promise:Proposed fix (remove redundant wrapper)
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); - runAsynchronously(handleFollowUp()); + handleFollowUp(); } else if (e.key === "ArrowLeft") {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ask-ai.tsx` at line 127, The call runAsynchronously(handleFollowUp()) is a no-op because handleFollowUp() returns void; either remove the wrapper and call handleFollowUp() directly, or change handleFollowUp to return the promise from runAsynchronously/sendMessage (e.g., have handleFollowUp return runAsynchronously(sendMessage(...)) or be async and return the awaited promise). Update the implementation of handleFollowUp (the function that currently calls runAsynchronously(sendMessage(...))) to return that promise if you need to keep runAsynchronously(...) here, otherwise call handleFollowUp() without wrapping.
239-239:⚠️ Potential issue | 🟡 MinorSame issue: redundant
runAsynchronouslywrapper.Same as above—
handleFollowUp()returnsvoid, sorunAsynchronously(handleFollowUp())wrapsundefined.Proposed fix
- onClick={() => runAsynchronously(handleFollowUp())} + onClick={handleFollowUp}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ask-ai.tsx` at line 239, The onClick currently invokes handleFollowUp immediately (runAsynchronously(handleFollowUp())), passing undefined; change it to pass the function reference instead or call the handler directly: either use onClick={() => runAsynchronously(handleFollowUp)} if you intend runAsynchronously to wrap the handler, or simply use onClick={handleFollowUp} (or onClick={() => handleFollowUp()} if you prefer an arrow) so handleFollowUp is executed on click rather than at render time.
🧹 Nitpick comments (5)
apps/dashboard/src/components/commands/ai-chat-shared.tsx (3)
214-216: Add comment explaining the type assertions for SDK types.Per coding guidelines, when using
ascasts, leave a comment explaining why and how errors would still be flagged. The input/output shapes depend on the tool definition on the backend.Proposed documentation
- // Extract query from input - const input = invocation.input as { query?: string } | undefined; - const queryArg = input?.query; - const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number } | undefined; + // Extract query from input + // Type assertion: invocation.input/output are typed as `unknown` by the AI SDK. + // The actual shape depends on the tool definition (sql-query tool) on the backend. + // Using optional chaining ensures safe access even if the shape differs. + const input = invocation.input as { query?: string } | undefined; + const queryArg = input?.query; + const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 214 - 216, Add an inline comment explaining the use of `as` casts on `invocation.input` and `invocation.output` near the `input`, `queryArg`, and `result` declarations: state that these casts are narrowing the SDK-generic types to the expected shapes produced by the backend tool definition, note that runtime shape mismatches will still surface as errors, and mention where the true canonical shape is defined (the backend tool/schema). Keep the comment succinct and colocated with the `const input = invocation.input as { query?: string } | undefined;` and `const result = invocation.output as { success?: boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;` lines.
52-56: Clean up timeout on unmount to avoid state update on unmounted component.If
CopyButtonunmounts before the 1500ms timeout fires, React will warn about setting state on an unmounted component.Proposed fix using useEffect cleanup
export const CopyButton = memo(function CopyButton({ text, className, size = "sm" }: { text: string, className?: string, size?: "sm" | "xs", }) { const [copied, setCopied] = useState(false); + const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null); + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); const handleCopy = useCallback(async () => { await navigator.clipboard.writeText(text); setCopied(true); - setTimeout(() => setCopied(false), 1500); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 1500); }, [text]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 52 - 56, handleCopy sets a timeout that calls setCopied after 1500ms but never clears it, causing a potential state update on an unmounted component; add a ref (e.g., copiedTimeoutRef) to store the timeout id, clear any existing timeout before creating a new one inside handleCopy, and add a useEffect with a cleanup that clears copiedTimeoutRef.current on unmount to avoid setState on unmounted component; reference the handleCopy function, setCopied state updater, and useCallback so reviewers can find and update the logic.
516-526: Interval continues running after all words are revealed.The interval keeps firing every 15ms even after
displayedWordCountequalstargetWordCount. Consider clearing the interval when reveal completes to reduce unnecessary state updates.Proposed fix
const intervalId = setInterval(() => { setDisplayedWordCount(prev => { if (prev < targetWordCountRef.current) { return prev + 1; } + clearInterval(intervalId); return prev; }); }, 15); return () => clearInterval(intervalId); - }, [hasContent]); + }, [hasContent]);Alternatively, use a ref to track the interval ID and clear it from within the state updater callback.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx` around lines 516 - 526, The interval created in the effect keeps running even after displayedWordCount reaches targetWordCountRef.current; update the logic so the interval is cleared when the reveal completes: store the interval ID (intervalId) in a ref or accessible variable, and inside the setDisplayedWordCount updater check if prev < targetWordCountRef.current and if the next value will reach or exceed the target, call clearInterval(intervalId) before returning the final value; ensure the effect cleanup still clears the interval to cover unmounts (references: setDisplayedWordCount, targetWordCountRef, intervalId).apps/dashboard/src/components/commands/ask-ai.tsx (1)
69-69: Consider usingfindLastinstead ofreverse().find().
findLastis more idiomatic and avoids creating an intermediate reversed array. It's supported in ES2023+ (TypeScript 5+, Node 18+, modern browsers).Proposed refactor
- const lastAssistantMessage = messages.slice(1).reverse().find((m: UIMessage) => m.role === "assistant"); + const lastAssistantMessage = messages.slice(1).findLast((m: UIMessage) => m.role === "assistant");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/commands/ask-ai.tsx` at line 69, The current computation of lastAssistantMessage uses messages.slice(1).reverse().find(...) which allocates an intermediate reversed array; replace it with the ES2023 String.prototype-like array method by calling findLast on the sliced array (e.g., messages.slice(1).findLast(m => m.role === "assistant")) to avoid reversing and reduce allocation; ensure the runtime/TS target supports Array.prototype.findLast or add a polyfill if needed and update the reference to the const lastAssistantMessage accordingly in ask-ai.tsx.apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx (1)
77-77: Same asask-ai.tsx: preferfindLastoverreverse().find().For consistency with the suggested refactor in
ask-ai.tsx:Proposed refactor
- const lastAssistantMessage = messages.slice().reverse().find((m: UIMessage) => m.role === "assistant"); + const lastAssistantMessage = messages.findLast((m: UIMessage) => m.role === "assistant");Note: Unlike
ask-ai.tsx, this doesn't skip the first message, so no.slice(1)needed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx` at line 77, Replace the messages.slice().reverse().find(...) pattern used to compute lastAssistantMessage with Array.prototype.findLast for consistency and clarity: locate the expression assigning lastAssistantMessage in ai-chat-widget.tsx (the variable computed from messages and UIMessage) and change it to use messages.findLast((m: UIMessage) => m.role === "assistant") so it directly returns the last assistant message without creating a reversed copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Around line 19-24: The component AIChatWidget calls React hooks (useState)
before returning early when isActive is false, violating the Rules of Hooks; fix
by moving the early return to the top of AIChatWidget before any hooks or by
refactoring stateful logic into an inner component (e.g., create
AIChatWidgetInner that holds the useState calls and onNewConversation reset
logic) and have AIChatWidget conditionally render that inner component, or
alternatively remove the isActive prop entirely and let the parent unmount
AIChatWidget (stack-companion.tsx already conditionally renders it) so hooks are
only used when mounted.
---
Outside diff comments:
In `@apps/dashboard/src/components/commands/ask-ai.tsx`:
- Line 127: The call runAsynchronously(handleFollowUp()) is a no-op because
handleFollowUp() returns void; either remove the wrapper and call
handleFollowUp() directly, or change handleFollowUp to return the promise from
runAsynchronously/sendMessage (e.g., have handleFollowUp return
runAsynchronously(sendMessage(...)) or be async and return the awaited promise).
Update the implementation of handleFollowUp (the function that currently calls
runAsynchronously(sendMessage(...))) to return that promise if you need to keep
runAsynchronously(...) here, otherwise call handleFollowUp() without wrapping.
- Line 239: The onClick currently invokes handleFollowUp immediately
(runAsynchronously(handleFollowUp())), passing undefined; change it to pass the
function reference instead or call the handler directly: either use onClick={()
=> runAsynchronously(handleFollowUp)} if you intend runAsynchronously to wrap
the handler, or simply use onClick={handleFollowUp} (or onClick={() =>
handleFollowUp()} if you prefer an arrow) so handleFollowUp is executed on click
rather than at render time.
---
Nitpick comments:
In `@apps/dashboard/src/components/commands/ai-chat-shared.tsx`:
- Around line 214-216: Add an inline comment explaining the use of `as` casts on
`invocation.input` and `invocation.output` near the `input`, `queryArg`, and
`result` declarations: state that these casts are narrowing the SDK-generic
types to the expected shapes produced by the backend tool definition, note that
runtime shape mismatches will still surface as errors, and mention where the
true canonical shape is defined (the backend tool/schema). Keep the comment
succinct and colocated with the `const input = invocation.input as { query?:
string } | undefined;` and `const result = invocation.output as { success?:
boolean, result?: unknown[], error?: string, rowCount?: number } | undefined;`
lines.
- Around line 52-56: handleCopy sets a timeout that calls setCopied after 1500ms
but never clears it, causing a potential state update on an unmounted component;
add a ref (e.g., copiedTimeoutRef) to store the timeout id, clear any existing
timeout before creating a new one inside handleCopy, and add a useEffect with a
cleanup that clears copiedTimeoutRef.current on unmount to avoid setState on
unmounted component; reference the handleCopy function, setCopied state updater,
and useCallback so reviewers can find and update the logic.
- Around line 516-526: The interval created in the effect keeps running even
after displayedWordCount reaches targetWordCountRef.current; update the logic so
the interval is cleared when the reveal completes: store the interval ID
(intervalId) in a ref or accessible variable, and inside the
setDisplayedWordCount updater check if prev < targetWordCountRef.current and if
the next value will reach or exceed the target, call clearInterval(intervalId)
before returning the final value; ensure the effect cleanup still clears the
interval to cover unmounts (references: setDisplayedWordCount,
targetWordCountRef, intervalId).
In `@apps/dashboard/src/components/commands/ask-ai.tsx`:
- Line 69: The current computation of lastAssistantMessage uses
messages.slice(1).reverse().find(...) which allocates an intermediate reversed
array; replace it with the ES2023 String.prototype-like array method by calling
findLast on the sliced array (e.g., messages.slice(1).findLast(m => m.role ===
"assistant")) to avoid reversing and reduce allocation; ensure the runtime/TS
target supports Array.prototype.findLast or add a polyfill if needed and update
the reference to the const lastAssistantMessage accordingly in ask-ai.tsx.
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx`:
- Line 77: Replace the messages.slice().reverse().find(...) pattern used to
compute lastAssistantMessage with Array.prototype.findLast for consistency and
clarity: locate the expression assigning lastAssistantMessage in
ai-chat-widget.tsx (the variable computed from messages and UIMessage) and
change it to use messages.findLast((m: UIMessage) => m.role === "assistant") so
it directly returns the last assistant message without creating a reversed copy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6b013c2d-cf9b-4b12-bdf3-35a79bad649c
📒 Files selected for processing (3)
apps/dashboard/src/components/commands/ai-chat-shared.tsxapps/dashboard/src/components/commands/ask-ai.tsxapps/dashboard/src/components/stack-companion/ai-chat-widget.tsx
| export function AIChatWidget({ isActive }: { isActive: boolean }) { | ||
| const [input, setInput] = useState(""); | ||
| const [conversationStarted, setConversationStarted] = useState(false); | ||
| const [conversationKey, setConversationKey] = useState(0); | ||
|
|
||
| if (!isActive) return null; |
There was a problem hiding this comment.
React hooks called before conditional return violates Rules of Hooks.
useState is called on lines 20-22, but line 24 returns null conditionally based on isActive. React requires hooks to be called in the same order on every render. This will cause runtime errors when isActive changes.
Move the early return before any hooks, or move state to the inner component:
Proposed fix: move state into AIChatWidgetInner
export function AIChatWidget({ isActive }: { isActive: boolean }) {
- const [input, setInput] = useState("");
- const [conversationStarted, setConversationStarted] = useState(false);
- const [conversationKey, setConversationKey] = useState(0);
-
if (!isActive) return null;
return (
<AIChatWidgetInner
- key={conversationKey}
- input={input}
- setInput={setInput}
- conversationStarted={conversationStarted}
- setConversationStarted={setConversationStarted}
- onNewConversation={() => {
- setConversationKey(prev => prev + 1);
- setConversationStarted(false);
- setInput("");
- }}
+ key="ai-chat"
/>
);
}
function AIChatWidgetInner({
- input,
- setInput,
- conversationStarted,
- setConversationStarted,
- onNewConversation,
-}: {
- input: string,
- setInput: (v: string) => void,
- conversationStarted: boolean,
- setConversationStarted: (v: boolean) => void,
- onNewConversation: () => void,
-}) {
+}: Record<string, never>) {
+ const [input, setInput] = useState("");
+ const [conversationStarted, setConversationStarted] = useState(false);
const [followUpInput, setFollowUpInput] = useState("");Then update onNewConversation to reset state locally. Alternatively, lift the conditional rendering to the parent (stack-companion.tsx already does {activeItem === 'ask-ai' && <AIChatWidget isActive={true} />}), so the component unmounts when inactive—making the isActive prop and early return unnecessary.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/stack-companion/ai-chat-widget.tsx` around
lines 19 - 24, The component AIChatWidget calls React hooks (useState) before
returning early when isActive is false, violating the Rules of Hooks; fix by
moving the early return to the top of AIChatWidget before any hooks or by
refactoring stateful logic into an inner component (e.g., create
AIChatWidgetInner that holds the useState calls and onNewConversation reset
logic) and have AIChatWidget conditionally render that inner component, or
alternatively remove the isActive prop entirely and let the parent unmount
AIChatWidget (stack-companion.tsx already conditionally renders it) so hooks are
only used when mounted.
Summary by CodeRabbit
New Features
Bug Fixes