Skip to content

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Nov 10, 2025

User description

This PR implements a feature where the plant icon (logo.svg) in the header spins counter-clockwise whenever tokens are being generated in the system.

Changes Made:

  • Created a new StreamingContext to track token generation state across the application
  • Updated the BotMessage component to set streaming state based on the pending flag from useStreamableValue
  • Modified the Header component to apply a counter-clockwise spinning animation to the logo when streaming is active
  • Added custom Tailwind CSS animation animate-spin-ccw that rotates the icon -360 degrees over 2 seconds
  • Wrapped the Chat component with StreamingProvider to provide streaming context

Technical Details:

  • The animation is smooth and continuous while tokens are being generated
  • The icon returns to its normal state when generation completes
  • The implementation is efficient and doesn't impact performance

PR Type

Enhancement


Description

  • Add counter-clockwise spinning animation to plant icon during token generation

  • Create StreamingContext to track token generation state globally

  • Update BotMessage component to sync streaming state with context

  • Apply animate-spin-ccw animation to logo in header when streaming

  • Define custom Tailwind animation rotating -360 degrees over 2 seconds


Diagram Walkthrough

flowchart LR
  A["BotMessage<br/>pending state"] -->|"setIsStreaming"| B["StreamingContext<br/>isStreaming"]
  B -->|"useStreaming hook"| C["Header Component<br/>Logo Image"]
  C -->|"apply class"| D["animate-spin-ccw<br/>-360deg rotation"]
Loading

File Walkthrough

Relevant files
Enhancement
streaming-context.tsx
Create streaming context for token generation tracking     

components/streaming-context.tsx

  • Created new React Context to manage global streaming state
  • Implemented useStreaming hook for accessing streaming state
  • Implemented StreamingProvider component to wrap application
  • Provides isStreaming boolean and setIsStreaming setter function
+28/-0   
message.tsx
Sync BotMessage pending state to streaming context             

components/message.tsx

  • Import useStreaming hook and useEffect from React
  • Extract pending state from useStreamableValue hook
  • Sync pending state to global streaming context via setIsStreaming
  • Update streaming state whenever pending changes
+7/-0     
header.tsx
Apply spinning animation to logo when streaming                   

components/header.tsx

  • Import useStreaming hook to access streaming state
  • Get isStreaming value from streaming context
  • Apply animate-spin-ccw class to logo image conditionally
  • Add transition and duration classes for smooth animation
+12/-1   
chat.tsx
Wrap Chat component with StreamingProvider                             

components/chat.tsx

  • Import StreamingProvider component
  • Wrap both mobile and desktop layouts with StreamingProvider
  • Position StreamingProvider as outermost wrapper around MapDataProvider
  • Ensure streaming context is available to all child components
+11/-6   
Configuration changes
tailwind.config.ts
Define counter-clockwise spin animation in Tailwind           

tailwind.config.ts

  • Add spin-ccw keyframe animation rotating from 0deg to -360deg
  • Define spin-ccw animation class with 2s duration and linear timing
  • Set animation to run infinitely while streaming is active
+5/-0     

Summary by CodeRabbit

Release Notes

  • New Features
    • Added visual streaming indicator – the logo now spins with a counter-clockwise animation during active streaming operations, providing real-time feedback on processing status.

@vercel
Copy link

vercel bot commented Nov 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
qcx Ready Ready Preview Comment Nov 17, 2025 11:23am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

Walkthrough

Implements streaming state management via React Context API. Adds StreamingProvider and useStreaming hook for global streaming awareness, updates the header logo to spin counter-clockwise during streaming, integrates streaming detection in message components, and defines the counter-clockwise animation in Tailwind config.

Changes

Cohort / File(s) Summary
Context Infrastructure
components/streaming-context.tsx
New file: Creates StreamingContext, StreamingProvider component, and useStreaming hook to manage and expose streaming state across the app with fallback defaults.
Component Integration
components/header.tsx, components/message.tsx
header.tsx: Imports useStreaming and applies conditional animate-spin-ccw class to logo based on streaming state. message.tsx: Uses useStreaming hook and adds useEffect to sync pending status with streaming context.
Animation Configuration
tailwind.config.ts
Adds spin-ccw keyframes (0deg to -360deg rotation) and registers spin-ccw animation with 2s duration, linear timing, and infinite iteration.
App-Level Setup
app/layout.tsx
Wraps ThemeProvider with StreamingProvider as an outer layer in RootLayout to provide context to all child components.

Sequence Diagram

sequenceDiagram
    participant App as App Root
    participant SP as StreamingProvider
    participant Ctx as Context
    participant Header as Header Component
    participant Message as Message Component

    App->>SP: Render with children
    SP->>Ctx: Initialize isStreaming = false
    SP->>Header: Provide context
    SP->>Message: Provide context
    
    Message->>Ctx: useStreaming()
    Ctx-->>Message: { isStreaming, setIsStreaming }
    
    Message->>Message: useEffect: pending changes
    Message->>Ctx: setIsStreaming(true)
    Ctx->>Header: Context updated
    
    Header->>Ctx: useStreaming()
    Ctx-->>Header: { isStreaming: true }
    Header->>Header: Apply animate-spin-ccw
    
    Note over Message,Header: During streaming lifecycle
    Message->>Ctx: setIsStreaming(false)
    Header->>Header: Remove animation
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Provider nesting in app/layout.tsx: Verify proper wrapping order and no accidental prop propagation issues between StreamingProvider and ThemeProvider.
  • Context hook safety in message.tsx: Ensure useEffect dependency array is correct and that the cleanup logic (if any) is appropriate for streaming state updates.
  • Context fallback behavior: Verify that the default no-op interface in useStreaming handles edge cases where hooks are called outside the provider boundary.
  • Animation application: Confirm cn utility correctly merges transition and conditional animate-spin-ccw classes in header.tsx.

Poem

🐰✨ A spinning logo twirls with glee,
As streaming flows through context tree,
Providers wrap and state flows free,
Counter-clockwise elegantly!
Watch the magic dance with me! 🌀

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 change: adding a counter-clockwise spinning animation to the plant icon (logo) during token generation, which aligns with all file modifications.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/spinning-plant-icon

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ab27d7 and d33cd2f.

📒 Files selected for processing (1)
  • app/layout.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/layout.tsx (2)
components/streaming-context.tsx (1)
  • StreamingProvider (21-29)
components/theme-provider.tsx (1)
  • ThemeProvider (7-9)
🔇 Additional comments (1)
app/layout.tsx (1)

19-19: StreamingProvider placement looks correct; verify there are no remaining nested providers.

Importing StreamingProvider here and wrapping ThemeProvider + the rest of the app ensures Header, Chat, and all descendants share the same streaming context, which should fix the “logo not spinning” issue as long as setIsStreaming is called from within this tree.

One thing to double‑check: make sure StreamingProvider is not still used anywhere else (in particular in components/chat.tsx). Nested StreamingProvider instances would create separate isStreaming states, so the header logo might not reflect the actual streaming state used by the chat.

Also applies to: 64-85


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.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Nov 10, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit logs: The new streaming state changes and UI animation triggers are not logged as critical
actions, but it is unclear whether such actions qualify as auditable events in this
context.

Referred Code
const { setIsStreaming } = useStreaming()

useEffect(() => {
  setIsStreaming(pending)
}, [pending, setIsStreaming])

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Sparse error UI: The BotMessage component sets streaming state based on pending but only renders a generic
"Error" without context or handling for edge cases around streaming state on
errors.

Referred Code
// Currently, sometimes error occurs after finishing the stream.
if (error) return <div>Error</div>

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Console logging: The code logs map drawing context changes via console without indication of redaction or
structure, which may risk leaking sensitive data depending on contents.

Referred Code
console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures);
updateDrawingContext(id, mapData.drawnFeatures);

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Input trust: The streaming context updates depend on external stream state but no explicit validation
or sanitization is added in the new code, though it may be handled elsewhere.

Referred Code
const { setIsStreaming } = useStreaming()

useEffect(() => {
  setIsStreaming(pending)
}, [pending, setIsStreaming])

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Nov 10, 2025

PR Code Suggestions ✨

No code suggestions found for the PR.

Copy link

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

  • Header calls useStreaming() but StreamingProvider only wraps Chat, likely causing a runtime crash when Header renders outside the provider.
  • Global streaming state is toggled directly by each BotMessage, which can be incorrect with concurrent streams or on unmount; a ref-counted context is safer.
  • Minor performance/jank risk: the logo’s transition-transform can conflict with the keyframe animation.
  • The Tailwind spin-ccw keyframe/animation looks correct and integrates cleanly with the header logo class toggle.
Additional notes (1)
  • Performance | components/header.tsx:38-41
    The transition-transform duration-1000 on the logo can conflict with and/or add jank when toggling the animate-spin-ccw animation. Since the rotation is handled by keyframes, the transition isn’t needed.
Summary of changes
  • Introduced a new client StreamingProvider/useStreaming context to track token generation state.
  • Wrapped the Chat component’s mobile and desktop layouts with StreamingProvider.
  • Updated BotMessage to set the streaming state via useEffect based on pending from useStreamableValue.
  • Modified Header to consume isStreaming and apply a animate-spin-ccw Tailwind animation to the logo while streaming.
  • Extended Tailwind config with a custom spin-ccw keyframe and animate entry for a 2s linear infinite counter-clockwise rotation.


export const Header = () => {
const { toggleCalendar } = useCalendarToggle()
const { isStreaming } = useStreaming()

Choose a reason for hiding this comment

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

useStreaming() throws if used outside of StreamingProvider, but StreamingProvider is only mounted under Chat. If Header is rendered at a higher level (e.g., in app/layout.tsx), this will crash at runtime. The provider must wrap Header (or the whole app tree), not just the Chat subtree.

Suggestion

Move StreamingProvider to a top-level provider that wraps Header and page content (e.g., via an app/providers.tsx client component used in app/layout.tsx), and remove the per-Chat wrappers to avoid duplication. For example:

  • app/providers.tsx (client):
'use client'
import { StreamingProvider } from '@/components/streaming-context'
export function Providers({ children }: { children: React.ReactNode }) {
  return <StreamingProvider>{children}</StreamingProvider>
}
  • app/layout.tsx (server):
import { Providers } from './providers'
import { Header } from '@/components/header'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>
          <Header />
          {children}
        </Providers>
      </body>
    </html>
  )
}

Then delete the StreamingProvider wrappers added in components/chat.tsx.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor.

Comment on lines +15 to +19
const { setIsStreaming } = useStreaming()

useEffect(() => {
setIsStreaming(pending)
}, [pending, setIsStreaming])

Choose a reason for hiding this comment

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

Using a single boolean setIsStreaming(pending) in each BotMessage can oscillate the global state incorrectly if multiple messages stream concurrently or if a component unmounts mid-stream. This can leave the header stuck spinning or stop it too early.

Suggestion

Refactor the streaming context to track a reference count and expose start()/stop() functions, then call those from BotMessage on pending transitions with an unmount cleanup.

  • streaming-context.tsx:
export interface StreamingContextType {
  isStreaming: boolean
  start: () => void
  stop: () => void
}

export const StreamingProvider = ({ children }: { children: React.ReactNode }) => {
  const [count, setCount] = useState(0)
  const start = () => setCount(c => c + 1)
  const stop = () => setCount(c => Math.max(0, c - 1))
  return (
    <StreamingContext.Provider value={{ isStreaming: count > 0, start, stop }}>
      {children}
    </StreamingContext.Provider>
  )
}
  • components/message.tsx:
const { start, stop } = useStreaming()
useEffect(() => {
  if (pending) start()
  else stop()
  return () => { if (pending) stop() }
}, [pending, start, stop])

Reply with "@CharlieHelps yes please" if you'd like me to add a commit implementing this safer ref-count approach.

Comment on lines 88 to 90
<StreamingProvider>
<MapDataProvider> {/* Add Provider */}
<HeaderSearchButton />

Choose a reason for hiding this comment

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

Placing StreamingProvider inside Chat means Header (which uses useStreaming) is very likely outside the provider, leading to runtime errors and/or state desynchronization. After lifting the provider to app/layout.tsx (or another common root), these nested providers should be removed to prevent multiple isolated contexts that won’t share state.

Suggestion

Remove the StreamingProvider wrappers from both the mobile and desktop branches of Chat once the provider is placed at the app root. For example:

// Mobile layout
return (
  <MapDataProvider>
    <HeaderSearchButton />
    {/* ...rest unchanged... */}
  </MapDataProvider>
)

// Desktop layout
return (
  <MapDataProvider>
    <HeaderSearchButton />
    {/* ...rest unchanged... */}
  </MapDataProvider>
)

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that removes these wrappers.

Comment on lines 5 to 27
interface StreamingContextType {
isStreaming: boolean
setIsStreaming: (streaming: boolean) => void
}

const StreamingContext = createContext<StreamingContextType | undefined>(undefined)

export const useStreaming = () => {
const context = useContext(StreamingContext)
if (!context) {
throw new Error('useStreaming must be used within a StreamingProvider')
}
return context
}

export const StreamingProvider = ({ children }: { children: ReactNode }) => {
const [isStreaming, setIsStreaming] = useState(false)

return (
<StreamingContext.Provider value={{ isStreaming, setIsStreaming }}>
{children}
</StreamingContext.Provider>
)

Choose a reason for hiding this comment

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

Evolve the streaming context to be ref-count based so it’s resilient to concurrent streams and component unmounts. Right now it only stores a boolean with setIsStreaming, which is not robust across multiple producers. Introducing activeCount with beginStream/endStream avoids flicker and stuck states.

Suggestion

Replace the boolean-only API with a counter-based context. Example:

'use client'
import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from 'react'

interface StreamingContextType {
  isStreaming: boolean
  beginStream: () => void
  endStream: () => void
}

const StreamingContext = createContext<StreamingContextType | undefined>(undefined)

export const useStreaming = () => {
  const ctx = useContext(StreamingContext)
  if (!ctx) throw new Error('useStreaming must be used within a StreamingProvider')
  return ctx
}

export const StreamingProvider = ({ children }: { children: ReactNode }) => {
  const [activeCount, setActiveCount] = useState(0)

  const beginStream = useCallback(() => setActiveCount(c => c + 1), [])
  const endStream = useCallback(() => setActiveCount(c => Math.max(0, c - 1)), [])

  const value = useMemo(
    () => ({ isStreaming: activeCount > 0, beginStream, endStream }),
    [activeCount, beginStream, endStream]
  )

  return <StreamingContext.Provider value={value}>{children}</StreamingContext.Provider>
}

Pair this with the BotMessage hook change to call beginStream/endStream based on pending transitions. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with these changes.

@charliecreates charliecreates bot removed the request for review from CharlieHelps November 10, 2025 07:13
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/chat.tsx (1)

88-116: Remove the inner MapDataProvider from chat.tsx—it creates a state disconnect between Chat's monitoring logic and Mapbox updates.

Chat calls useMapData() at line 78 to consume from the outer provider (injected by page.tsx). Its useEffect (lines 80–85) monitors mapData.drawnFeatures from that outer context. However, the code then renders an inner MapDataProvider at lines 87 and 100, wrapping Mapbox inside it. When Mapbox calls useMapData(), it retrieves the inner provider's isolated state instead of the outer one. This means Mapbox's setMapData() calls update only the inner provider, leaving Chat's outer-context useEffect unaware of map interactions.

Fix: Delete the <MapDataProvider> wrapper at lines 87 and 100 (and corresponding closing tags). Mapbox will then use the outer provider from page.tsx, allowing Chat's useEffect to properly observe drawing changes.

♻️ Duplicate comments (1)
components/chat.tsx (1)

122-154: Same nested MapDataProvider issue in desktop layout.

This has the identical state disconnect problem as the mobile layout (lines 88-116). The inner MapDataProvider (line 123) isolates state from the outer provider that Chat reads from.

Consider removing the inner MapDataProvider from both layouts and relying on the outer provider from page.tsx, or consolidating to a single provider location.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5fb1da and f3fd726.

📒 Files selected for processing (5)
  • components/chat.tsx (4 hunks)
  • components/header.tsx (3 hunks)
  • components/message.tsx (1 hunks)
  • components/streaming-context.tsx (1 hunks)
  • tailwind.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
components/chat.tsx (4)
components/streaming-context.tsx (1)
  • StreamingProvider (20-28)
components/map/map-data-context.tsx (1)
  • MapDataProvider (26-34)
components/header-search-button.tsx (1)
  • HeaderSearchButton (19-108)
app/page.tsx (1)
  • Page (9-18)
tailwind.config.ts (1)
components/ui/spinner.tsx (1)
  • svg (6-19)
components/message.tsx (1)
components/streaming-context.tsx (1)
  • useStreaming (12-18)
components/header.tsx (2)
components/streaming-context.tsx (1)
  • useStreaming (12-18)
lib/utils/index.ts (1)
  • cn (11-13)
🔇 Additional comments (7)
tailwind.config.ts (2)

71-74: LGTM: Counter-clockwise keyframes are correct.

The keyframes correctly define a counter-clockwise rotation using -360deg.


79-79: LGTM: Animation configuration is appropriate.

The 2-second linear infinite animation provides a smooth streaming indicator.

components/streaming-context.tsx (2)

12-18: LGTM: Standard context hook pattern.

The error boundary correctly prevents usage outside the provider.


20-28: LGTM: Clean provider implementation.

The provider correctly initializes streaming state and exposes it to children.

components/header.tsx (2)

5-5: LGTM: Correct streaming context integration.

The header appropriately reads streaming state without modifying it.

Also applies to: 22-22


33-42: LGTM: Smooth animation implementation.

The transition-transform duration-1000 provides a smooth deceleration when streaming stops, which is a nice UX touch.

components/chat.tsx (1)

19-19: LGTM: Correct import for streaming provider.

Comment on lines +17 to +19
useEffect(() => {
setIsStreaming(pending)
}, [pending, setIsStreaming])
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Approve with suggestion: Add explanatory comment.

The implementation correctly tracks streaming state by relying on stable setIsStreaming identity. Only the active streaming message's pending changes will trigger updates.

Consider adding a comment explaining the behavior:

+ // Update global streaming state when this message's streaming status changes.
+ // Only the newest message will have pending=true, older messages remain pending=false.
  useEffect(() => {
    setIsStreaming(pending)
  }, [pending, setIsStreaming])
📝 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
useEffect(() => {
setIsStreaming(pending)
}, [pending, setIsStreaming])
// Update global streaming state when this message's streaming status changes.
// Only the newest message will have pending=true, older messages remain pending=false.
useEffect(() => {
setIsStreaming(pending)
}, [pending, setIsStreaming])
🤖 Prompt for AI Agents
In components/message.tsx around lines 17 to 19, add a brief explanatory comment
above the useEffect explaining that the effect intentionally depends on the
stable setIsStreaming setter and that only changes to the active streaming
message's pending prop should update the isStreaming state (e.g., "Depend on
stable setIsStreaming; only the active message's pending toggles will trigger
updates"). Keep the comment concise and clarify intent so future readers know
this dependency is deliberate.

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: 1

♻️ Duplicate comments (2)
components/streaming-context.tsx (2)

5-8: Concurrent streams will cause animation flicker and incorrect state.

The boolean-based isStreaming state cannot track multiple simultaneous streams. If two BotMessage components stream tokens concurrently and one finishes first, calling setIsStreaming(false) will stop the logo animation even though the second stream is still active. The previous review correctly identified this critical flaw and proposed a ref-count solution (activeCount with beginStream/endStream).

Apply the ref-counting approach suggested in the previous review to handle concurrent streams correctly.


12-19: Inconsistent error handling: should throw when used outside provider.

The codebase pattern for context hooks is to throw an error when used outside a provider (see useMapLoading, useMapToggle, and useProfileToggle in the relevant snippets). Returning silent defaults can hide bugs where a component is incorrectly placed outside the StreamingProvider. The SSR comment is also misleading since the 'use client' directive ensures this only runs client-side.

Apply this diff to align with the established pattern:

 export const useStreaming = () => {
   const context = useContext(StreamingContext)
-  if (!context) {
-    // Return default values if used outside provider (e.g., during SSR)
-    return { isStreaming: false, setIsStreaming: () => {} }
-  }
+  if (!context) {
+    throw new Error('useStreaming must be used within a StreamingProvider')
+  }
   return context
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3fd726 and 5ab27d7.

📒 Files selected for processing (1)
  • components/streaming-context.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
components/streaming-context.tsx (4)
components/map-loading-context.tsx (2)
  • children (11-18)
  • context (20-26)
components/map-toggle-context.tsx (1)
  • context (36-42)
components/map/map-context.tsx (1)
  • children (14-22)
components/profile-toggle-context.tsx (2)
  • ProfileToggleProviderProps (20-22)
  • context (38-44)

Comment on lines +21 to +29
export const StreamingProvider = ({ children }: { children: ReactNode }) => {
const [isStreaming, setIsStreaming] = useState(false)

return (
<StreamingContext.Provider value={{ isStreaming, setIsStreaming }}>
{children}
</StreamingContext.Provider>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Memoize the context value to prevent unnecessary re-renders.

The context value object is recreated on every render, causing all consumers (Header, BotMessage) to re-render even when isStreaming hasn't changed. While setIsStreaming from useState is stable, the wrapper object is not.

Apply this diff to memoize the value:

-import { createContext, useContext, useState, ReactNode } from 'react'
+import { createContext, useContext, useState, ReactNode, useMemo } from 'react'

 export const StreamingProvider = ({ children }: { children: ReactNode }) => {
   const [isStreaming, setIsStreaming] = useState(false)

+  const value = useMemo(
+    () => ({ isStreaming, setIsStreaming }),
+    [isStreaming]
+  )
+
   return (
-    <StreamingContext.Provider value={{ isStreaming, setIsStreaming }}>
+    <StreamingContext.Provider value={value}>
       {children}
     </StreamingContext.Provider>
   )
 }
🤖 Prompt for AI Agents
In components/streaming-context.tsx around lines 21 to 29, the context value
object is recreated every render causing unnecessary consumer re-renders; fix by
memoizing the provider value: import and use React.useMemo to create the value
object ({ isStreaming, setIsStreaming }) and only recompute when isStreaming or
setIsStreaming change (depend on isStreaming is sufficient since setIsStreaming
is stable), then pass that memoized value to StreamingContext.Provider.

@ngoiyaeric
Copy link
Collaborator Author

its not spinning

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the streaming context implementation
cat components/streaming-context.tsx

Length of output: 808


🏁 Script executed:

#!/bin/bash
# Read the Header component implementation
cat components/header.tsx

Length of output: 2576


🏁 Script executed:

#!/bin/bash
# Read the BotMessage implementation in message.tsx
cat components/message.tsx

Length of output: 1704


🏁 Script executed:

#!/bin/bash
# Read the Chat component to see StreamingProvider usage
cat components/chat.tsx

Length of output: 5136


🏁 Script executed:

#!/bin/bash
# Check the Tailwind config for the animation
cat tailwind.config.ts

Length of output: 2543


🏁 Script executed:

#!/bin/bash
# Find where Header is actually rendered - check layout files
fd -e tsx -e ts "layout" --type f | head -20

Length of output: 14


🏁 Script executed:

#!/bin/bash
# Check the main app layout
cat app/layout.tsx

Length of output: 2562


🏁 Script executed:

#!/bin/bash
# Check if there's a root layout or other layout files
find . -name "layout.tsx" -o -name "layout.ts" 2>/dev/null | grep -v node_modules

Length of output: 16


@ngoiyaeric I found the issue! The problem is that the Header component cannot access the streaming state because it's rendered outside the StreamingProvider scope.

The Problem

  • Header is rendered in app/layout.tsx (the root layout)
  • StreamingProvider is only wrapping content inside components/chat.tsx
  • These are separate parts of the component tree, so Header gets the default isStreaming: false value

The Solution

Move the StreamingProvider up to app/layout.tsx so both Header and Chat components can share the same streaming context.

Update app/layout.tsx:

import type { Metadata, Viewport } from 'next'
import { Inter as FontSans } from 'next/font/google'
import './globals.css'
import 'katex/dist/katex.min.css';
import { cn } from '@/lib/utils'
import { ThemeProvider } from '@/components/theme-provider'
import Header from '@/components/header'
import Footer from '@/components/footer'
import { Sidebar } from '@/components/sidebar'
import { Analytics } from "@vercel/analytics/next"
import { SpeedInsights } from "@vercel/speed-insights/next"
import { Toaster } from '@/components/ui/sonner'
import { MapToggleProvider } from '@/components/map-toggle-context'
import { ProfileToggleProvider } from '@/components/profile-toggle-context'
import { CalendarToggleProvider } from '@/components/calendar-toggle-context'
import { MapLoadingProvider } from '@/components/map-loading-context';
import ConditionalLottie from '@/components/conditional-lottie';
import { MapProvider } from '@/components/map/map-context'
import { StreamingProvider } from '@/components/streaming-context' // Add this import

const fontSans = FontSans({
  subsets: ['latin'],
  variable: '--font-sans'
})

const title = 'Beta'
const description =
  'language to Maps'

export const metadata: Metadata = {
  metadataBase: new URL('https://www.qcx.world'),
  title,
  description,
  openGraph: {
    title,
    description
  },
  twitter: {
    title,
    description,
    card: 'summary_large_image',
    creator: '@queueLab'
  }
}

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  minimumScale: 1,
  maximumScale: 1
}

export default function RootLayout({
  children
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={cn('font-sans antialiased', fontSans.variable)}>
        <StreamingProvider> {/* Add StreamingProvider here */}
          <CalendarToggleProvider>
            <MapToggleProvider>
              <ProfileToggleProvider>
                <ThemeProvider
                  attribute="class"
                  defaultTheme="earth"
                  enableSystem
                  disableTransitionOnChange
                  themes={['light', 'dark', 'earth']}
                >
                  <MapProvider>
                    <MapLoadingProvider>
                      <Header />
                      <ConditionalLottie />
                      {children}
                      <Sidebar />
                      <Footer />
                      <Toaster />
                    </MapLoadingProvider>
                  </MapProvider>
                </ThemeProvider>
              </ProfileToggleProvider>
            </MapToggleProvider>
          </CalendarToggleProvider>
        </StreamingProvider> {/* Close StreamingProvider here */}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

Then remove the StreamingProvider from components/chat.tsx since it's now at the layout level:

// In chat.tsx - Remove StreamingProvider wrapper from both mobile and desktop layouts
// Remove this import:
// import { StreamingProvider } from './streaming-context'

// Mobile layout - remove StreamingProvider wrapper:
if (isMobile) {
  return (
    <MapDataProvider>
      <HeaderSearchButton />
      {/* rest of mobile layout */}
    </MapDataProvider>
  );
}

// Desktop layout - remove StreamingProvider wrapper:
return (
  <MapDataProvider>
    <HeaderSearchButton />
    {/* rest of desktop layout */}
  </MapDataProvider>
);

After these changes, the Header component will be able to access the streaming state, and the logo should spin counter-clockwise while tokens are being generated! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants