Skip to content

Feat/1969 ai left panel ai integration#1976

Open
DSanich wants to merge 8 commits intofeat/1958-ai-left-panel-ui-elementsfrom
feat/1969-ai-left-panel-ai-integration
Open

Feat/1969 ai left panel ai integration#1976
DSanich wants to merge 8 commits intofeat/1958-ai-left-panel-ui-elementsfrom
feat/1969-ai-left-panel-ai-integration

Conversation

@DSanich
Copy link
Member

@DSanich DSanich commented Mar 3, 2026

Summary by CodeRabbit

  • New Features
    • Resizable AI left sidebar (collapsible on desktop, drawer on mobile) with integrated AI chat UI
    • Streaming chat with send/stop controls, message bubbles, suggestions, and model selection
    • Chat accessible from site layout (panel integrated into main layout) and updated footer placement
    • Added environment placeholder for AI API key and required client UI primitives (drawer)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Env & Top-level deps
apps/web/.env.template, package.json
Added GOOGLE_GENERATIVE_AI_API_KEY placeholder and bumped zod version.
Web app deps
apps/web/package.json
Added AI SDK deps: @ai-sdk/google, @ai-sdk/react, ai.
Epics package deps
packages/epics/package.json
Added @ai-sdk/react and ai to epics package dependencies.
API: chat endpoint
apps/web/src/app/api/chat/route.ts
New POST route implementing streaming chat integration, model selection (default gemini-2.5-flash), system prompt, and streaming response conversion.
App layout integration
apps/web/src/app/layout.tsx
Reworked layout to use AiLeftPanelLayout, moved Footer inside layout, updated imports to include AiLeftPanelLayout.
AI left panel & layout components
packages/epics/src/common/ai-left-panel.tsx, packages/epics/src/common/ai-left-panel-layout.tsx, packages/epics/src/common/index.ts
New AiLeftPanel and AiLeftPanelLayout: desktop drag-to-resize, mobile drawer, panel open/close state; re-exported from common.
AI panel subcomponents
packages/epics/src/common/ai-panel/...
ai-panel-header.tsx, ai-panel-messages.tsx, ai-panel-chat-bar.tsx, ai-panel-message-bubble.tsx, ai-panel-suggestions.tsx, index.ts, mock-data.ts
New composable header, messages list (auto-scroll), chat input (auto-resize, send/stop), message bubble (actions), suggestions grid, mock data, and barrel exports.
Responsive hook
packages/epics/src/hooks/use-is-mobile.tsx, packages/epics/src/hooks/index.ts
Added useIsMobile hook and exported it from hooks index for responsive behavior.
Drawer UI & export
packages/ui/src/drawer.tsx, packages/ui/src/index.ts
New Drawer component suite wrapping vaul primitives (Content, Overlay, Header, Footer, Title, Description) and re-exported from UI index.
Footer tweak
packages/ui/src/organisms/footer.tsx
Minor layout change: added w-full to outer container.
Web app .env template
apps/web/.env.template
Added comment and GOOGLE_GENERATIVE_AI_API_KEY= placeholder for Gemini key.

Sequence Diagram

sequenceDiagram
    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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #1087 — Overlaps layout changes and ConnectedButtonProfile usage in apps/web/src/app/layout.tsx.
  • #1276 — Edits the Footer component in packages/ui/src/organisms/footer.tsx, touching the same UI file.
  • #1004 — Also modifies apps/web/src/app/layout.tsx structure; related to layout integration.

Suggested reviewers

  • sergey3bv
  • evgenibir

Poem

🐰 Whiskers twitch at streaming light,

Gemini hums through day and night.
A left panel grows, it chats and plays,
Resizes, suggests, and lights our ways.
Hop on—let conversations take flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being added: an AI left panel with AI integration, which is reflected throughout the changeset with new AI components, hooks, and API integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/1969-ai-left-panel-ai-integration

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 of mock-data.ts into a dedicated types module.

ModelOption/Message are 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 since AiLeftPanelLayout accepts children: 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 in mock-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 useEffect scrolls into view whenever isStreaming changes. During active streaming, this may not be an issue, but if isStreaming toggles 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 UIMessagePart and message shape types are duplicated here and in ai-panel-messages.tsx. Extracting these to a shared types file (e.g., types.ts in 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 suggestion as 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1a432be and 98c986d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • apps/web/.env.template
  • apps/web/package.json
  • apps/web/src/app/api/chat/route.ts
  • apps/web/src/app/layout.tsx
  • package.json
  • packages/epics/package.json
  • packages/epics/src/common/ai-left-panel-layout.tsx
  • packages/epics/src/common/ai-left-panel.tsx
  • packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx
  • packages/epics/src/common/ai-panel/ai-panel-header.tsx
  • packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx
  • packages/epics/src/common/ai-panel/ai-panel-messages.tsx
  • packages/epics/src/common/ai-panel/ai-panel-suggestions.tsx
  • packages/epics/src/common/ai-panel/index.ts
  • packages/epics/src/common/ai-panel/mock-data.ts
  • packages/epics/src/common/index.ts
  • packages/epics/src/hooks/index.ts
  • packages/epics/src/hooks/use-is-mobile.tsx
  • packages/ui/package.json
  • packages/ui/src/drawer.tsx
  • packages/ui/src/index.ts
  • packages/ui/src/organisms/footer.tsx

Comment on lines +19 to +22
export async function POST(req: Request) {
const { messages, modelId = 'gemini-2.5-flash' }: { messages: UIMessage[]; modelId?: string } =
await req.json()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +19 to +32
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()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -30

Repository: 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 -10

Repository: 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 2

Repository: 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/chat with 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.

Comment on lines +27 to +53
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],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +74 to +79
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',
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +54 to +57
const handleResetChat = () => {
setMessages([]);
setShowSuggestions(true);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +43 to +47
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSend();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +129 to +133
<button
type="button"
onClick={isStreaming ? onStop : onSend}
disabled={!canSend && !isStreaming}
className={cn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +89 to +97
<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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested 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>
{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.

Comment on lines +54 to +81
<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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 UIMessagePart type and similar message shape definitions are duplicated in ai-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 moving welcomeMessage outside the component.

This constant is recreated on every render. Moving it outside the component or using useMemo would 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 messages array reference and isStreaming. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 98c986d and 52256d7.

📒 Files selected for processing (5)
  • apps/web/src/app/api/chat/route.ts
  • packages/epics/src/common/ai-left-panel.tsx
  • packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx
  • packages/epics/src/common/ai-panel/ai-panel-messages.tsx
  • packages/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

@DSanich DSanich changed the base branch from main to feat/1958-ai-left-panel-ui-elements March 3, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant