Feat/1969 ai left panel ai integration#1976
Feat/1969 ai left panel ai integration#1976DSanich wants to merge 8 commits intofeat/1958-ai-left-panel-ui-elementsfrom
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a Google Gemini–backed AI chat: environment variable placeholder, AI SDK dependencies, a streaming /api/chat POST handler, a resizable AI left panel with chat UI and subcomponents, mobile drawer primitives, responsive hook, and related exports/layout integration. Changes
Sequence DiagramsequenceDiagram
participant User as User (UI)
participant Panel as AiLeftPanel
participant ChatHook as useChat Hook
participant API as /api/chat Endpoint
participant Gemini as Google Gemini API
User->>Panel: type message & click Send
Panel->>ChatHook: handleSend(message)
ChatHook->>API: POST /api/chat { messages, modelId }
API->>Gemini: request streaming generation
Gemini-->>API: stream response chunks
API-->>ChatHook: stream UI-formatted chunks
ChatHook->>Panel: update messages incrementally
Panel->>User: render streamed response (auto-scroll)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (9)
packages/epics/src/common/ai-panel/ai-panel-header.tsx (1)
53-83: Add menu state semantics to the model selector trigger/menu.This improves assistive-tech context for expanded/collapsed state and menu items.
Suggested refactor
<button type="button" onClick={() => setShowModelMenu(!showModelMenu)} + aria-haspopup="menu" + aria-expanded={showModelMenu} className="flex min-w-0 items-center gap-1.5 rounded-lg border border-border bg-secondary px-2.5 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" > @@ - <div className="absolute left-0 top-full z-50 mt-1 w-44 animate-in fade-in slide-in-from-top-2 rounded-xl border border-border bg-popover py-1 shadow-2xl duration-200"> + <div + role="menu" + className="absolute left-0 top-full z-50 mt-1 w-44 animate-in fade-in slide-in-from-top-2 rounded-xl border border-border bg-popover py-1 shadow-2xl duration-200" + > @@ <button key={m.id} type="button" + role="menuitem" onClick={() => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 53 - 83, The model selector trigger and menu need proper ARIA semantics: update the trigger button (where setShowModelMenu and showModelMenu are used) to include aria-haspopup="menu" and aria-expanded={showModelMenu} and add an aria-controls pointing to the menu id; give the menu container a stable id (e.g., "model-menu") and role="menu", and mark each mapped item button inside modelOptions (the onClick that calls onModelSelect and setShowModelMenu) with role="menuitem" (and ensure they remain keyboard-focusable). This ties showModelMenu state to assistive tech and exposes the menu and its items with correct roles.packages/epics/src/common/ai-panel/mock-data.ts (1)
4-8: Move shared AI panel types out ofmock-data.tsinto a dedicated types module.
ModelOption/Messageare reused UI contracts; keeping them in a mock fixture file couples component typing to mock runtime data.Suggested refactor
- export type ModelOption = { - id: string - label: string - icon: LucideIcon - } + export type { ModelOption, Message } from './types'// packages/epics/src/common/ai-panel/types.ts import type { LucideIcon } from 'lucide-react' export type ModelOption = { id: string label: string icon: LucideIcon } export type Message = { id: string role: 'user' | 'assistant' content: string timestamp: Date isStreaming?: boolean }Based on learnings: DSanich prefers extracting repeated type declarations into shared modules for reusability and maintainability, particularly for token types that are used across multiple files in the codebase.
Also applies to: 21-27
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/mock-data.ts` around lines 4 - 8, Extract the shared types ModelOption and Message out of mock-data.ts into a dedicated types module (e.g., ai-panel/types) that exports both types; ensure the new module imports LucideIcon from 'lucide-react' and declares ModelOption and Message exactly as used, then update mock-data.ts and any other files to import { ModelOption, Message } from the new types module and remove the duplicate type declarations from mock-data.ts.apps/web/src/app/layout.tsx (1)
137-142: Unnecessary Fragment wrapper.The Fragment (
<>...</>) wrapping the children and Footer is redundant sinceAiLeftPanelLayoutacceptschildren: React.ReactNode, which can already be multiple elements.♻️ Remove unnecessary Fragment
<AiLeftPanelLayout> - <> - <div className="w-full shrink-0 pb-8">{children}</div> - <Footer /> - </> + <div className="w-full shrink-0 pb-8">{children}</div> + <Footer /> </AiLeftPanelLayout>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/layout.tsx` around lines 137 - 142, Remove the redundant React Fragment wrapping the children and Footer inside AiLeftPanelLayout: within the JSX using AiLeftPanelLayout, replace the fragment that contains the div with {children} and <Footer /> by passing those elements directly as children (AiLeftPanelLayout already accepts React.ReactNode), so remove the <>...</> wrapper around the div and Footer.packages/epics/src/common/ai-panel/ai-panel-messages.tsx (3)
8-12: UIMessage type is duplicated.This type is also defined in
ai-panel-message-bubble.tsx. Consider extracting it to a shared types file to ensure consistency and reduce duplication.Based on learnings: DSanich prefers extracting repeated type declarations into shared modules for reusability and maintainability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` around lines 8 - 12, The UIMessage type is duplicated between ai-panel-messages.tsx and ai-panel-message-bubble.tsx; extract the shared type into a new module (e.g., ai-panel-types.ts or index in the common folder), export UIMessage from that file, and update both ai-panel-messages.tsx and ai-panel-message-bubble.tsx to import { UIMessage } from the new shared module; ensure the type shape (id, role, parts) remains identical and update any relative imports/usages of UIMessage to reference the centralized export.
35-44: Consider extracting welcome message to mock-data.The welcome message is hardcoded here while other mock data (like
MOCK_SUGGESTIONS) is inmock-data.ts. Moving this to the mock-data module would improve consistency and make it easier to update or localize.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` around lines 35 - 44, The welcomeMessage object is hardcoded in ai-panel-messages.tsx while other test/mock items (e.g., MOCK_SUGGESTIONS) live in mock-data.ts; move the welcomeMessage definition into the mock-data module and import it where needed. Specifically, extract the constant named welcomeMessage (with id 'welcome', role 'assistant', and its parts array) into mock-data.ts alongside MOCK_SUGGESTIONS, export it, then update ai-panel-messages.tsx to import { welcomeMessage } from the mock-data module and replace the inline object with the imported symbol.
31-33: Auto-scroll triggers on every streaming state change.The
useEffectscrolls into view wheneverisStreamingchanges. During active streaming, this may not be an issue, but ifisStreamingtoggles rapidly, it could cause unnecessary scroll calls. Consider debouncing or only scrolling when new content is added.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` around lines 31 - 33, The useEffect currently scrolls on any isStreaming change which causes unnecessary scrolls; update the effect in ai-panel-messages.tsx (useEffect using endRef, messages, isStreaming) to only trigger when new content is added — e.g., depend on messages.length or compare the last message id/content using a prevMessagesRef, and remove isStreaming from the dependency list (or debounce isStreaming with a short timeout and clear on unmount) so scrollIntoView is called only when messages actually grow/new message appears.packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx (1)
7-16: Consider extracting shared message types to a common module.The
UIMessagePartand message shape types are duplicated here and inai-panel-messages.tsx. Extracting these to a shared types file (e.g.,types.tsin the ai-panel module) would improve maintainability and ensure consistency.Based on learnings: DSanich prefers extracting repeated type declarations into shared modules for reusability and maintainability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx` around lines 7 - 16, Extract the duplicated types into a shared module: create a new ai-panel/types.ts (or similar) exporting UIMessagePart and the message shape used by AiPanelMessageBubbleProps, then update ai-panel-message-bubble.tsx and ai-panel-messages.tsx to import those exported types instead of redeclaring them; ensure you rename or adjust any local type names to match the shared exports (e.g., UIMessagePart, the message interface used in AiPanelMessageBubbleProps) and run TS checks to fix any import paths or references.packages/epics/src/common/ai-panel/ai-panel-suggestions.tsx (1)
16-18: Consider using index-based key if suggestions may contain duplicates.Using
suggestionas the key will cause React key collisions if the suggestions array contains duplicate strings. If duplicates are possible, consider using the array index or a composite key.♻️ Suggested fix using index
- {suggestions.map((suggestion) => ( + {suggestions.map((suggestion, index) => ( <button - key={suggestion} + key={`${index}-${suggestion}`} type="button"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-suggestions.tsx` around lines 16 - 18, The current JSX uses suggestion as the React key inside the suggestions.map rendering (key={suggestion}), which will collide if duplicate strings appear; update the key to a stable unique value such as the map index or a composite (e.g., `${suggestion}-${index}`) in the suggestions.map callback so each button element rendered in the ai-panel-suggestions component has a unique key (refer to suggestions.map, the suggestion variable, and the button's key prop).packages/epics/src/common/ai-left-panel-layout.tsx (1)
98-108: Fixed positioning assumes constant header height.The reopen button uses
top-[4.5rem]which couples to the header height. If the header height changes, this button will misalign. Consider using a CSS variable or calculating the position relative to the header.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-left-panel-layout.tsx` around lines 98 - 108, The reopen button currently hardcodes top-[4.5rem], which breaks if header height changes; update the button (the conditional render controlled by panelOpen and setPanelOpen) to position itself relative to the header using a CSS variable or runtime calculation: replace the fixed top value with something like top-[var(--header-height)] and ensure the header sets --header-height, or compute header.getBoundingClientRect().bottom and set the button.style.top dynamically on mount/resize so the button follows the header height instead of using top-[4.5rem].
🤖 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/web/src/app/api/chat/route.ts`:
- Around line 19-22: In POST, avoid destructuring directly from await
req.json(); instead read the parsed body into a variable and validate it before
using fields: ensure the body is an object, that body.messages exists and is an
array of UIMessage-like objects (check at minimum that each item has the
expected keys or is an object), and coerce or default modelId to
'gemini-2.5-flash'; if validation fails return a 400/422 response. Update the
POST function to perform these checks (use the parsedBody variable instead of
direct destructuring) and early-return error responses for invalid payloads.
- Around line 19-32: The POST handler for /api/chat currently calls getModel,
convertToModelMessages, and streamText without auth or rate limiting; add an
authentication and rate-limit check before those calls: validate the user
session/JWT (e.g., via your existing auth helper or session check) at the top of
POST(req) and return 401 if missing/invalid, then enforce a per-user rate limit
(token bucket or counter) and return 429 when exceeded; alternatively, update
the global middleware matcher to include /api/chat and ensure middleware
enforces auth and rate limits so that POST can assume an authenticated user—make
sure to reference the same model selection (getModel), input conversion
(convertToModelMessages), streaming call (streamText) and response conversion
(toUIMessageStreamResponse) after the checks pass.
In `@packages/epics/src/common/ai-left-panel-layout.tsx`:
- Around line 74-79: The resizer's className string contains a duplicate hover
utility and incorrectly places the 'group' class on the child; remove the
duplicate 'hover:bg-primary/20', move the 'group' utility to the parent
container element (the element wrapping the resizer in the AiLeftPanelLayout
component), and change the child's hover utility to use group-hover (e.g.,
'group-hover:bg-primary/20') while keeping the isDragging conditional
('bg-primary' when isDragging) intact so the resizer reacts to both dragging and
parent hover.
- Around line 27-53: The current onResizeMouseDown creates window listeners
(onMouseMove/onMouseUp) that are never guaranteed to be removed if the component
unmounts; refactor by making the handlers stable and adding a cleanup effect:
extract onMouseMove and onMouseUp into stable refs or useCallback functions
(e.g., onMouseMoveRef / onMouseUpRef or memoized onMouseMove/onMouseUp) so they
can be referenced outside the mousedown handler, then add a useEffect that
removes window event listeners for 'mousemove' and 'mouseup' on cleanup (and
when isDragging changes) to ensure window.removeEventListener('mousemove',
onMouseMoveRef.current) and window.removeEventListener('mouseup',
onMouseUpRef.current) run on unmount; update onResizeMouseDown to attach the
stable handlers instead of inline functions.
In `@packages/epics/src/common/ai-left-panel.tsx`:
- Around line 54-57: handleResetChat must stop any in-flight streaming before
clearing messages to avoid race conditions; update the handleResetChat function
to first cancel/abort the active stream (call the existing stream cancellation
helper such as stopActiveStream(), abortCurrentStream(), or call
abortController.abort() and clear any isStreaming flag/currentStreamRef), then
call setMessages([]) and setShowSuggestions(true). Ensure the cancellation call
occurs synchronously before setMessages to prevent incoming streamed updates
from re-adding messages.
In `@packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx`:
- Around line 43-47: The Enter handler in handleKeyDown currently sends onSend
even while IME composition is active, causing premature submits for CJK users;
update handleKeyDown to ignore Enter when composition is active by either
checking e.nativeEvent.isComposing (if available) or adding composition handlers
(onCompositionStart / onCompositionEnd) that flip an isComposingRef boolean and
having handleKeyDown bail out when isComposingRef.current is true before calling
onSend. Ensure the composition tracking symbol (isComposingRef) and composition
event handlers (onCompositionStart, onCompositionEnd) are defined in the
component and referenced from handleKeyDown so Enter only sends when composition
has finished.
- Line 3: Textarea height isn't recalculated when the input value is updated
externally (e.g., setInput(''), suggestion fill), leaving a stale height; update
the component to recalculate the height whenever the controlled value changes.
Add a useEffect that depends on the input/value state and calls the existing
resize helper (e.g., textareaRef.current and the adjust/resizeTextarea function
or the logic inside onInputChange/onKeyDown) to reset scrollHeight (shrink to 0
then set to scrollHeight) and reapply max/min styling; ensure you reference
textareaRef (or the ref used for the <textarea>) and the same resize helper used
during typing so both typing and external updates use identical height logic.
- Around line 129-133: The Send/Stop button currently calls onStop when
isStreaming is true even if onStop is undefined, so make the button inert when
streaming and no onStop exists: update the disabled logic to include
(isStreaming && !onStop) and ensure onClick only invokes onStop when it is
defined (or otherwise only uses onSend when not streaming); reference the
isStreaming, onStop, onSend symbols and the button JSX to locate and apply this
change.
In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx`:
- Around line 89-97: The Reset button is always rendered and appears clickable
even when no handler is wired; update the JSX in ai-panel-header to
conditionally render the <button> (the element containing the RefreshCw icon)
only when the onResetChat prop/function is truthy (e.g., wrap the button in a
conditional like if (onResetChat) or use && rendering) so it is not shown when
no handler is provided; ensure you reference the onResetChat prop and the button
containing RefreshCw when making this change.
In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx`:
- Around line 54-81: The action buttons in ai-panel-message-bubble.tsx (the
Copy, ThumbsUp, ThumbsDown, and RefreshCw button elements) lack onClick
handlers, so wire each button to the appropriate handler or explicitly mark them
as disabled/placeholder: add onClick props that call well-named functions (e.g.,
handleCopy, handleThumbsUp, handleThumbsDown, handleRefresh) defined in the
component and implement the minimal behavior (copy text to clipboard for
handleCopy, emit events or call props callbacks for the other handlers), or if
not ready, add a TODO comment and set disabled={true} and aria-disabled with a
tooltip; ensure the handlers reference the message data passed into the
component so they act on the correct message.
---
Nitpick comments:
In `@apps/web/src/app/layout.tsx`:
- Around line 137-142: Remove the redundant React Fragment wrapping the children
and Footer inside AiLeftPanelLayout: within the JSX using AiLeftPanelLayout,
replace the fragment that contains the div with {children} and <Footer /> by
passing those elements directly as children (AiLeftPanelLayout already accepts
React.ReactNode), so remove the <>...</> wrapper around the div and Footer.
In `@packages/epics/src/common/ai-left-panel-layout.tsx`:
- Around line 98-108: The reopen button currently hardcodes top-[4.5rem], which
breaks if header height changes; update the button (the conditional render
controlled by panelOpen and setPanelOpen) to position itself relative to the
header using a CSS variable or runtime calculation: replace the fixed top value
with something like top-[var(--header-height)] and ensure the header sets
--header-height, or compute header.getBoundingClientRect().bottom and set the
button.style.top dynamically on mount/resize so the button follows the header
height instead of using top-[4.5rem].
In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx`:
- Around line 53-83: The model selector trigger and menu need proper ARIA
semantics: update the trigger button (where setShowModelMenu and showModelMenu
are used) to include aria-haspopup="menu" and aria-expanded={showModelMenu} and
add an aria-controls pointing to the menu id; give the menu container a stable
id (e.g., "model-menu") and role="menu", and mark each mapped item button inside
modelOptions (the onClick that calls onModelSelect and setShowModelMenu) with
role="menuitem" (and ensure they remain keyboard-focusable). This ties
showModelMenu state to assistive tech and exposes the menu and its items with
correct roles.
In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx`:
- Around line 7-16: Extract the duplicated types into a shared module: create a
new ai-panel/types.ts (or similar) exporting UIMessagePart and the message shape
used by AiPanelMessageBubbleProps, then update ai-panel-message-bubble.tsx and
ai-panel-messages.tsx to import those exported types instead of redeclaring
them; ensure you rename or adjust any local type names to match the shared
exports (e.g., UIMessagePart, the message interface used in
AiPanelMessageBubbleProps) and run TS checks to fix any import paths or
references.
In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx`:
- Around line 8-12: The UIMessage type is duplicated between
ai-panel-messages.tsx and ai-panel-message-bubble.tsx; extract the shared type
into a new module (e.g., ai-panel-types.ts or index in the common folder),
export UIMessage from that file, and update both ai-panel-messages.tsx and
ai-panel-message-bubble.tsx to import { UIMessage } from the new shared module;
ensure the type shape (id, role, parts) remains identical and update any
relative imports/usages of UIMessage to reference the centralized export.
- Around line 35-44: The welcomeMessage object is hardcoded in
ai-panel-messages.tsx while other test/mock items (e.g., MOCK_SUGGESTIONS) live
in mock-data.ts; move the welcomeMessage definition into the mock-data module
and import it where needed. Specifically, extract the constant named
welcomeMessage (with id 'welcome', role 'assistant', and its parts array) into
mock-data.ts alongside MOCK_SUGGESTIONS, export it, then update
ai-panel-messages.tsx to import { welcomeMessage } from the mock-data module and
replace the inline object with the imported symbol.
- Around line 31-33: The useEffect currently scrolls on any isStreaming change
which causes unnecessary scrolls; update the effect in ai-panel-messages.tsx
(useEffect using endRef, messages, isStreaming) to only trigger when new content
is added — e.g., depend on messages.length or compare the last message
id/content using a prevMessagesRef, and remove isStreaming from the dependency
list (or debounce isStreaming with a short timeout and clear on unmount) so
scrollIntoView is called only when messages actually grow/new message appears.
In `@packages/epics/src/common/ai-panel/ai-panel-suggestions.tsx`:
- Around line 16-18: The current JSX uses suggestion as the React key inside the
suggestions.map rendering (key={suggestion}), which will collide if duplicate
strings appear; update the key to a stable unique value such as the map index or
a composite (e.g., `${suggestion}-${index}`) in the suggestions.map callback so
each button element rendered in the ai-panel-suggestions component has a unique
key (refer to suggestions.map, the suggestion variable, and the button's key
prop).
In `@packages/epics/src/common/ai-panel/mock-data.ts`:
- Around line 4-8: Extract the shared types ModelOption and Message out of
mock-data.ts into a dedicated types module (e.g., ai-panel/types) that exports
both types; ensure the new module imports LucideIcon from 'lucide-react' and
declares ModelOption and Message exactly as used, then update mock-data.ts and
any other files to import { ModelOption, Message } from the new types module and
remove the duplicate type declarations from mock-data.ts.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
apps/web/.env.templateapps/web/package.jsonapps/web/src/app/api/chat/route.tsapps/web/src/app/layout.tsxpackage.jsonpackages/epics/package.jsonpackages/epics/src/common/ai-left-panel-layout.tsxpackages/epics/src/common/ai-left-panel.tsxpackages/epics/src/common/ai-panel/ai-panel-chat-bar.tsxpackages/epics/src/common/ai-panel/ai-panel-header.tsxpackages/epics/src/common/ai-panel/ai-panel-message-bubble.tsxpackages/epics/src/common/ai-panel/ai-panel-messages.tsxpackages/epics/src/common/ai-panel/ai-panel-suggestions.tsxpackages/epics/src/common/ai-panel/index.tspackages/epics/src/common/ai-panel/mock-data.tspackages/epics/src/common/index.tspackages/epics/src/hooks/index.tspackages/epics/src/hooks/use-is-mobile.tsxpackages/ui/package.jsonpackages/ui/src/drawer.tsxpackages/ui/src/index.tspackages/ui/src/organisms/footer.tsx
| export async function POST(req: Request) { | ||
| const { messages, modelId = 'gemini-2.5-flash' }: { messages: UIMessage[]; modelId?: string } = | ||
| await req.json() | ||
|
|
There was a problem hiding this comment.
Validate request payload before destructuring.
Direct destructuring from await req.json() can throw or produce invalid messages, causing avoidable 500s for bad client input.
Suggested fix
export async function POST(req: Request) {
- const { messages, modelId = 'gemini-2.5-flash' }: { messages: UIMessage[]; modelId?: string } =
- await req.json()
+ const body = await req.json().catch(() => null)
+ if (!body || !Array.isArray(body.messages)) {
+ return Response.json({ error: 'Invalid request body' }, { status: 400 })
+ }
+ const { messages, modelId = 'gemini-2.5-flash' } = body as {
+ messages: UIMessage[]
+ modelId?: string
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function POST(req: Request) { | |
| const { messages, modelId = 'gemini-2.5-flash' }: { messages: UIMessage[]; modelId?: string } = | |
| await req.json() | |
| export async function POST(req: Request) { | |
| const body = await req.json().catch(() => null) | |
| if (!body || !Array.isArray(body.messages)) { | |
| return Response.json({ error: 'Invalid request body' }, { status: 400 }) | |
| } | |
| const { messages, modelId = 'gemini-2.5-flash' } = body as { | |
| messages: UIMessage[] | |
| modelId?: string | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/app/api/chat/route.ts` around lines 19 - 22, In POST, avoid
destructuring directly from await req.json(); instead read the parsed body into
a variable and validate it before using fields: ensure the body is an object,
that body.messages exists and is an array of UIMessage-like objects (check at
minimum that each item has the expected keys or is an object), and coerce or
default modelId to 'gemini-2.5-flash'; if validation fails return a 400/422
response. Update the POST function to perform these checks (use the parsedBody
variable instead of direct destructuring) and early-return error responses for
invalid payloads.
| export async function POST(req: Request) { | ||
| const { messages, modelId = 'gemini-2.5-flash' }: { messages: UIMessage[]; modelId?: string } = | ||
| await req.json() | ||
|
|
||
| const model = getModel(modelId) | ||
|
|
||
| const result = streamText({ | ||
| model, | ||
| system: SYSTEM_PROMPT, | ||
| messages: await convertToModelMessages(messages), | ||
| }) | ||
|
|
||
| return result.toUIMessageStreamResponse() | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Check middleware files for /api/chat protection ==="
fd -a 'middleware\.(ts|js)$' apps/web 2>/dev/null | while read -r f; do
echo "---- $f"
cat -n "$f" || true
done
echo
echo "=== Check for middleware in root or src directories ==="
fd -a 'middleware\.(ts|js)$' . 2>/dev/null | head -20
echo
echo "=== Inspect apps/web/src/app/api directory structure ==="
find apps/web/src/app/api -type f -name "*.ts" -o -name "*.js" 2>/dev/null | head -30Repository: hypha-dao/hypha-web
Length of output: 2814
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Explicit auth checks in chat route ==="
cat -n apps/web/src/app/api/chat/route.ts
echo
echo "=== Search for auth patterns in apps/web/src/app/api ==="
rg -r '(auth|session|getServerSession|unauthorized|forbidden|jwt|clerk|next-auth|verifyAuth)' apps/web/src/app/api --type ts --type js -l
echo
echo "=== Check for Next.js middleware config ==="
cat -n apps/web/middleware.ts 2>/dev/null || echo "No middleware.ts found"
echo
echo "=== Check next.config files ==="
find . -maxdepth 3 -name 'next.config.*' -o -name 'middleware.config.*' 2>/dev/null | head -10Repository: hypha-dao/hypha-web
Length of output: 1369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for rate limiting in project ==="
rg -i 'rate|throttle|limit' apps/web/src/app/api --type ts --type js -A 2
echo
echo "=== Look for environment protection patterns ==="
rg -i 'process.env|env\.' apps/web/src/app/api/chat --type ts -B 2 -A 2Repository: hypha-dao/hypha-web
Length of output: 8450
Add authentication and rate limiting to /api/chat before calling the LLM provider.
The middleware explicitly excludes /api/ routes (matcher: (?!api|...)), leaving this endpoint unprotected. The handler has no auth checks or rate limiting, allowing unauthenticated users to trigger calls to the paid Gemini API. This exposes the service to cost and quota exhaustion abuse.
Add either:
- Explicit authentication check in the handler (e.g., verify session/JWT), or
- Update middleware matcher to include
/api/chatwith auth enforcement
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/app/api/chat/route.ts` around lines 19 - 32, The POST handler
for /api/chat currently calls getModel, convertToModelMessages, and streamText
without auth or rate limiting; add an authentication and rate-limit check before
those calls: validate the user session/JWT (e.g., via your existing auth helper
or session check) at the top of POST(req) and return 401 if missing/invalid,
then enforce a per-user rate limit (token bucket or counter) and return 429 when
exceeded; alternatively, update the global middleware matcher to include
/api/chat and ensure middleware enforces auth and rate limits so that POST can
assume an authenticated user—make sure to reference the same model selection
(getModel), input conversion (convertToModelMessages), streaming call
(streamText) and response conversion (toUIMessageStreamResponse) after the
checks pass.
| const onResizeMouseDown = useCallback( | ||
| (e: React.MouseEvent) => { | ||
| e.preventDefault(); | ||
| dragStartX.current = e.clientX; | ||
| dragStartWidth.current = panelWidth; | ||
| setIsDragging(true); | ||
|
|
||
| const onMouseMove = (e: MouseEvent) => { | ||
| const delta = e.clientX - dragStartX.current; | ||
| const newWidth = Math.min( | ||
| MAX_WIDTH, | ||
| Math.max(MIN_WIDTH, dragStartWidth.current + delta), | ||
| ); | ||
| setPanelWidth(newWidth); | ||
| }; | ||
|
|
||
| const onMouseUp = () => { | ||
| setIsDragging(false); | ||
| window.removeEventListener('mousemove', onMouseMove); | ||
| window.removeEventListener('mouseup', onMouseUp); | ||
| }; | ||
|
|
||
| window.addEventListener('mousemove', onMouseMove); | ||
| window.addEventListener('mouseup', onMouseUp); | ||
| }, | ||
| [panelWidth], | ||
| ); |
There was a problem hiding this comment.
Event listeners not cleaned up on unmount.
If the component unmounts while dragging (e.g., navigation), the mousemove and mouseup listeners on window won't be removed, causing a memory leak and potential errors.
🐛 Proposed fix using useEffect cleanup
+'use client';
+
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Bot, ChevronsLeftRight, PanelLeftOpen } from 'lucide-react';
+
+// ... other imports ...
+
export function AiLeftPanelLayout({ children }: AiLeftPanelLayoutProps) {
const isMobile = useIsMobile();
const [panelOpen, setPanelOpen] = useState(true);
const [panelWidth, setPanelWidth] = useState(DEFAULT_WIDTH);
const [isDragging, setIsDragging] = useState(false);
const dragStartX = useRef(0);
const dragStartWidth = useRef(DEFAULT_WIDTH);
+ const mouseMoveRef = useRef<((e: MouseEvent) => void) | null>(null);
+ const mouseUpRef = useRef<(() => void) | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (mouseMoveRef.current) {
+ window.removeEventListener('mousemove', mouseMoveRef.current);
+ }
+ if (mouseUpRef.current) {
+ window.removeEventListener('mouseup', mouseUpRef.current);
+ }
+ };
+ }, []);
const onResizeMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
dragStartX.current = e.clientX;
dragStartWidth.current = panelWidth;
setIsDragging(true);
const onMouseMove = (e: MouseEvent) => {
const delta = e.clientX - dragStartX.current;
const newWidth = Math.min(
MAX_WIDTH,
Math.max(MIN_WIDTH, dragStartWidth.current + delta),
);
setPanelWidth(newWidth);
};
const onMouseUp = () => {
setIsDragging(false);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
+ mouseMoveRef.current = null;
+ mouseUpRef.current = null;
};
+ mouseMoveRef.current = onMouseMove;
+ mouseUpRef.current = onMouseUp;
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
},
[panelWidth],
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-left-panel-layout.tsx` around lines 27 - 53, The
current onResizeMouseDown creates window listeners (onMouseMove/onMouseUp) that
are never guaranteed to be removed if the component unmounts; refactor by making
the handlers stable and adding a cleanup effect: extract onMouseMove and
onMouseUp into stable refs or useCallback functions (e.g., onMouseMoveRef /
onMouseUpRef or memoized onMouseMove/onMouseUp) so they can be referenced
outside the mousedown handler, then add a useEffect that removes window event
listeners for 'mousemove' and 'mouseup' on cleanup (and when isDragging changes)
to ensure window.removeEventListener('mousemove', onMouseMoveRef.current) and
window.removeEventListener('mouseup', onMouseUpRef.current) run on unmount;
update onResizeMouseDown to attach the stable handlers instead of inline
functions.
| className={cn( | ||
| 'absolute top-0 right-0 z-20 flex h-full w-1 cursor-col-resize items-center justify-center transition-colors', | ||
| isDragging | ||
| ? 'bg-primary' | ||
| : 'hover:bg-primary/20 group hover:bg-primary/20', | ||
| )} |
There was a problem hiding this comment.
Duplicate CSS class and misplaced group class.
Line 78 contains a duplicate hover:bg-primary/20 and the group class is placed incorrectly. The group class should be on the parent element (line 69) to enable group-hover on the child (line 87).
🐛 Proposed fix
<div
role="separator"
aria-orientation="vertical"
aria-valuenow={panelWidth}
onMouseDown={onResizeMouseDown}
className={cn(
- 'absolute top-0 right-0 z-20 flex h-full w-1 cursor-col-resize items-center justify-center transition-colors',
+ 'group absolute top-0 right-0 z-20 flex h-full w-1 cursor-col-resize items-center justify-center transition-colors',
isDragging
? 'bg-primary'
- : 'hover:bg-primary/20 group hover:bg-primary/20',
+ : 'hover:bg-primary/20',
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className={cn( | |
| 'absolute top-0 right-0 z-20 flex h-full w-1 cursor-col-resize items-center justify-center transition-colors', | |
| isDragging | |
| ? 'bg-primary' | |
| : 'hover:bg-primary/20 group hover:bg-primary/20', | |
| )} | |
| className={cn( | |
| 'group absolute top-0 right-0 z-20 flex h-full w-1 cursor-col-resize items-center justify-center transition-colors', | |
| isDragging | |
| ? 'bg-primary' | |
| : 'hover:bg-primary/20', | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-left-panel-layout.tsx` around lines 74 - 79, The
resizer's className string contains a duplicate hover utility and incorrectly
places the 'group' class on the child; remove the duplicate
'hover:bg-primary/20', move the 'group' utility to the parent container element
(the element wrapping the resizer in the AiLeftPanelLayout component), and
change the child's hover utility to use group-hover (e.g.,
'group-hover:bg-primary/20') while keeping the isDragging conditional
('bg-primary' when isDragging) intact so the resizer reacts to both dragging and
parent hover.
| const handleResetChat = () => { | ||
| setMessages([]); | ||
| setShowSuggestions(true); | ||
| }; |
There was a problem hiding this comment.
Stop active streaming before resetting chat state.
setMessages([]) alone can race with in-flight stream updates, so reset can appear to “undo itself”.
Suggested fix
const handleResetChat = () => {
+ stop();
setMessages([]);
+ setInput('');
setShowSuggestions(true);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-left-panel.tsx` around lines 54 - 57,
handleResetChat must stop any in-flight streaming before clearing messages to
avoid race conditions; update the handleResetChat function to first cancel/abort
the active stream (call the existing stream cancellation helper such as
stopActiveStream(), abortCurrentStream(), or call abortController.abort() and
clear any isStreaming flag/currentStreamRef), then call setMessages([]) and
setShowSuggestions(true). Ensure the cancellation call occurs synchronously
before setMessages to prevent incoming streamed updates from re-adding messages.
| @@ -0,0 +1,156 @@ | |||
| 'use client'; | |||
|
|
|||
| import { useRef } from 'react'; | |||
There was a problem hiding this comment.
Sync textarea auto-resize when value changes externally.
Height recalculation currently misses non-typing updates (setInput(''), suggestion fill, reset), leaving stale height.
Suggested fix
-import { useRef } from 'react';
+import { useLayoutEffect, useRef } from 'react';
@@
const autoResize = () => {
@@
};
+
+ useLayoutEffect(() => {
+ autoResize();
+ }, [value]);Also applies to: 35-41
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx` at line 3, Textarea
height isn't recalculated when the input value is updated externally (e.g.,
setInput(''), suggestion fill), leaving a stale height; update the component to
recalculate the height whenever the controlled value changes. Add a useEffect
that depends on the input/value state and calls the existing resize helper
(e.g., textareaRef.current and the adjust/resizeTextarea function or the logic
inside onInputChange/onKeyDown) to reset scrollHeight (shrink to 0 then set to
scrollHeight) and reapply max/min styling; ensure you reference textareaRef (or
the ref used for the <textarea>) and the same resize helper used during typing
so both typing and external updates use identical height logic.
| const handleKeyDown = (e: React.KeyboardEvent) => { | ||
| if (e.key === 'Enter' && !e.shiftKey) { | ||
| e.preventDefault(); | ||
| onSend(); | ||
| } |
There was a problem hiding this comment.
Prevent Enter-to-send while IME composition is active.
Current key handling can submit partial text for CJK users when Enter confirms composition.
Suggested fix
- if (e.key === 'Enter' && !e.shiftKey) {
+ if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault();
onSend();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| onSend(); | |
| } | |
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { | |
| e.preventDefault(); | |
| onSend(); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx` around lines 43 -
47, The Enter handler in handleKeyDown currently sends onSend even while IME
composition is active, causing premature submits for CJK users; update
handleKeyDown to ignore Enter when composition is active by either checking
e.nativeEvent.isComposing (if available) or adding composition handlers
(onCompositionStart / onCompositionEnd) that flip an isComposingRef boolean and
having handleKeyDown bail out when isComposingRef.current is true before calling
onSend. Ensure the composition tracking symbol (isComposingRef) and composition
event handlers (onCompositionStart, onCompositionEnd) are defined in the
component and referenced from handleKeyDown so Enter only sends when composition
has finished.
| <button | ||
| type="button" | ||
| onClick={isStreaming ? onStop : onSend} | ||
| disabled={!canSend && !isStreaming} | ||
| className={cn( |
There was a problem hiding this comment.
Disable Stop action when no onStop handler exists.
With isStreaming=true and onStop undefined, the button is actionable but does nothing.
Suggested fix
- const canSend = value.trim().length > 0 && !isStreaming;
+ const canSend = value.trim().length > 0 && !isStreaming;
+ const canStop = isStreaming && typeof onStop === 'function';
@@
- disabled={!canSend && !isStreaming}
+ disabled={isStreaming ? !canStop : !canSend}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx` around lines 129 -
133, The Send/Stop button currently calls onStop when isStreaming is true even
if onStop is undefined, so make the button inert when streaming and no onStop
exists: update the disabled logic to include (isStreaming && !onStop) and ensure
onClick only invokes onStop when it is defined (or otherwise only uses onSend
when not streaming); reference the isStreaming, onStop, onSend symbols and the
button JSX to locate and apply this change.
| <button | ||
| type="button" | ||
| onClick={onResetChat} | ||
| className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | ||
| title="Reset chat" | ||
| aria-label="Reset chat" | ||
| > | ||
| <RefreshCw className="h-3.5 w-3.5" /> | ||
| </button> |
There was a problem hiding this comment.
Render Reset button only when onResetChat is provided.
The current control appears clickable even when no handler is wired.
Suggested fix
- <button
- type="button"
- onClick={onResetChat}
- className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
- title="Reset chat"
- aria-label="Reset chat"
- >
- <RefreshCw className="h-3.5 w-3.5" />
- </button>
+ {onResetChat && (
+ <button
+ type="button"
+ onClick={onResetChat}
+ className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
+ title="Reset chat"
+ aria-label="Reset chat"
+ >
+ <RefreshCw className="h-3.5 w-3.5" />
+ </button>
+ )}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| type="button" | |
| onClick={onResetChat} | |
| className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | |
| title="Reset chat" | |
| aria-label="Reset chat" | |
| > | |
| <RefreshCw className="h-3.5 w-3.5" /> | |
| </button> | |
| {onResetChat && ( | |
| <button | |
| type="button" | |
| onClick={onResetChat} | |
| className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | |
| title="Reset chat" | |
| aria-label="Reset chat" | |
| > | |
| <RefreshCw className="h-3.5 w-3.5" /> | |
| </button> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 89 - 97,
The Reset button is always rendered and appears clickable even when no handler
is wired; update the JSX in ai-panel-header to conditionally render the <button>
(the element containing the RefreshCw icon) only when the onResetChat
prop/function is truthy (e.g., wrap the button in a conditional like if
(onResetChat) or use && rendering) so it is not shown when no handler is
provided; ensure you reference the onResetChat prop and the button containing
RefreshCw when making this change.
| <button | ||
| type="button" | ||
| className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | ||
| title="Copy" | ||
| > | ||
| <Copy className="h-3 w-3" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | ||
| title="Thumbs up" | ||
| > | ||
| <ThumbsUp className="h-3 w-3" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | ||
| title="Thumbs down" | ||
| > | ||
| <ThumbsDown className="h-3 w-3" /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" | ||
| title="Refresh" | ||
| > | ||
| <RefreshCw className="h-3 w-3" /> | ||
| </button> |
There was a problem hiding this comment.
Action buttons are missing onClick handlers.
The copy, thumbs up/down, and refresh buttons render but have no onClick handlers, making them non-functional. If these are placeholders for future implementation, consider adding TODO comments or disabling the buttons.
Would you like me to help implement the onClick handlers or open an issue to track this?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx` around lines
54 - 81, The action buttons in ai-panel-message-bubble.tsx (the Copy, ThumbsUp,
ThumbsDown, and RefreshCw button elements) lack onClick handlers, so wire each
button to the appropriate handler or explicitly mark them as
disabled/placeholder: add onClick props that call well-named functions (e.g.,
handleCopy, handleThumbsUp, handleThumbsDown, handleRefresh) defined in the
component and implement the minimal behavior (copy text to clipboard for
handleCopy, emit events or call props callbacks for the other handlers), or if
not ready, add a TODO comment and set disabled={true} and aria-disabled with a
tooltip; ensure the handlers reference the message data passed into the
component so they act on the correct message.
There was a problem hiding this comment.
🧹 Nitpick comments (3)
packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx (1)
7-18: Consider extracting shared message types to a common module.The
UIMessageParttype and similar message shape definitions are duplicated inai-panel-messages.tsx(lines 8-14). Extracting these to a shared types file would improve maintainability and ensure consistency.// e.g., packages/epics/src/common/ai-panel/types.ts export type UIMessagePart = | { type: 'text'; text: string } | { type: string; [k: string]: unknown }; export type UIMessage = { id: string; role: 'user' | 'assistant' | 'system'; parts?: UIMessagePart[]; };Based on learnings: DSanich prefers extracting repeated type declarations into shared modules for reusability and maintainability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx` around lines 7 - 18, Extract the shared message types into a new module (e.g., common/ai-panel/types.ts) and replace the duplicated inline declarations: move UIMessagePart and the message shape (used by AiPanelMessageBubbleProps) into that module as exported types, then import and use them in ai-panel-message-bubble.tsx and ai-panel-messages.tsx (update the AiPanelMessageBubbleProps to reference the imported UIMessage type or UIMessagePart). Ensure symbols UIMessagePart, UIMessage (or the exported types) are exported from the new file and imported where referenced so both AiPanelMessageBubbleProps and the definitions in ai-panel-messages.tsx share the single source of truth.packages/epics/src/common/ai-panel/ai-panel-messages.tsx (2)
37-46: Consider movingwelcomeMessageoutside the component.This constant is recreated on every render. Moving it outside the component or using
useMemowould prevent unnecessary object allocations.💡 Move to module scope
+const WELCOME_MESSAGE: UIMessage = { + id: 'welcome', + role: 'assistant', + parts: [ + { + type: 'text', + text: "Hello! I'm your Hypha AI assistant. I can help you analyze signals, draft proposals, understand community dynamics, and coordinate across spaces. What would you like to explore?", + }, + ], +}; + export function AiPanelMessages({ // ... }: AiPanelMessagesProps) { // ... - const welcomeMessage: UIMessage = { ... }; - - const displayMessages = messages.length > 0 ? messages : [welcomeMessage]; + const displayMessages = messages.length > 0 ? messages : [WELCOME_MESSAGE];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` around lines 37 - 46, The welcomeMessage object is being recreated on every render; move the constant welcomeMessage to module scope (outside the component) or wrap it in useMemo to avoid repeated allocations — locate the welcomeMessage declaration in ai-panel-messages.tsx and either hoist it to the top-level of the module or memoize it where it’s used (referencing the welcomeMessage identifier) so renders reuse the same object.
33-35: Auto-scroll may fire excessively during streaming.The effect depends on
messagesarray reference andisStreaming. During active streaming, if the parent component updates the messages array frequently, this could cause multiple smooth scroll calls. Consider adding a condition to only scroll when a new message is added or streaming completes, rather than on every update.💡 Suggestion to optimize scroll behavior
+import { useEffect, useRef, useMemo } from 'react'; + +// Track message count to scroll only on new messages +const messageCount = messages.length; +const lastMessageId = messages[messages.length - 1]?.id; useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }); -}, [messages, isStreaming]); +}, [messageCount, lastMessageId, isStreaming]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` around lines 33 - 35, The current useEffect with dependencies [messages, isStreaming] calls endRef.current?.scrollIntoView({ behavior: 'smooth' }) on any messages reference change or streaming tick, causing excessive smooth-scroll during streaming; update the effect in useEffect to only call scrollIntoView when a new message is appended or when streaming transitions from true to false (e.g., track previous messages length and previous isStreaming via refs or compare messages.length and prevIsStreaming) so scroll runs only on messages.length increase or on streaming completion, referencing the useEffect, endRef, messages, and isStreaming identifiers to locate and modify the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx`:
- Around line 7-18: Extract the shared message types into a new module (e.g.,
common/ai-panel/types.ts) and replace the duplicated inline declarations: move
UIMessagePart and the message shape (used by AiPanelMessageBubbleProps) into
that module as exported types, then import and use them in
ai-panel-message-bubble.tsx and ai-panel-messages.tsx (update the
AiPanelMessageBubbleProps to reference the imported UIMessage type or
UIMessagePart). Ensure symbols UIMessagePart, UIMessage (or the exported types)
are exported from the new file and imported where referenced so both
AiPanelMessageBubbleProps and the definitions in ai-panel-messages.tsx share the
single source of truth.
In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx`:
- Around line 37-46: The welcomeMessage object is being recreated on every
render; move the constant welcomeMessage to module scope (outside the component)
or wrap it in useMemo to avoid repeated allocations — locate the welcomeMessage
declaration in ai-panel-messages.tsx and either hoist it to the top-level of the
module or memoize it where it’s used (referencing the welcomeMessage identifier)
so renders reuse the same object.
- Around line 33-35: The current useEffect with dependencies [messages,
isStreaming] calls endRef.current?.scrollIntoView({ behavior: 'smooth' }) on any
messages reference change or streaming tick, causing excessive smooth-scroll
during streaming; update the effect in useEffect to only call scrollIntoView
when a new message is appended or when streaming transitions from true to false
(e.g., track previous messages length and previous isStreaming via refs or
compare messages.length and prevIsStreaming) so scroll runs only on
messages.length increase or on streaming completion, referencing the useEffect,
endRef, messages, and isStreaming identifiers to locate and modify the logic.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/src/app/api/chat/route.tspackages/epics/src/common/ai-left-panel.tsxpackages/epics/src/common/ai-panel/ai-panel-message-bubble.tsxpackages/epics/src/common/ai-panel/ai-panel-messages.tsxpackages/epics/src/common/ai-panel/mock-data.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/epics/src/common/ai-panel/mock-data.ts
- apps/web/src/app/api/chat/route.ts
- packages/epics/src/common/ai-left-panel.tsx
Summary by CodeRabbit