Skip to content

feat: add AI chat prototype to journey cards (NES-1554)#9037

Open
jaco-brink wants to merge 60 commits intomainfrom
feature/nes-1554-apologist-chat-slice-1-prototype-base
Open

feat: add AI chat prototype to journey cards (NES-1554)#9037
jaco-brink wants to merge 60 commits intomainfrom
feature/nes-1554-apologist-chat-slice-1-prototype-base

Conversation

@jaco-brink
Copy link
Copy Markdown
Collaborator

@jaco-brink jaco-brink commented Apr 17, 2026

Summary

  • Add AI chat widget prototype to journey cards with streaming chat responses via API routes
  • Implement chat UI components: AiChat drawer, PromptInput, Conversation, Message, InteractionStarter suggestions, and LastCardChatBar pinned on the final card
  • Fix text contrast, layout gaps, and reconcile widget styling to match original prototype (NES-1555)

Related Issues

  • NES-1554 — Apologist Chat Slice 1 Prototype Base
  • NES-1555 — Chat UI fixes (contrast, spacing, pinned prompt bar)

Test plan

  • Verify AI chat button appears on journey cards
  • Open chat drawer and confirm styling matches prototype
  • Send a message and verify streaming response renders correctly
  • Confirm interaction starter suggestions display and are tappable
  • Verify last card shows pinned chat prompt bar
  • Check text contrast is readable in both light/dark contexts
  • Confirm no dead space gap between card content and chat bar

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added AI chat UI: panel & overlay modes, streaming/typewriter assistant responses, send/stop controls, copy-to-clipboard, message rendering, conversation auto-scroll, and responsive footer/chat layout (pinned bar, floating button, drawer).
    • New reusable chat components: AiChat, ChatOverlay, PinnedChatBar, AiChatButton, PromptInput, Message, Conversation, Response, Actions, Drawer.
  • Tests

    • Added API and UI tests validating chat endpoint behavior and feature-flag gating.

jaco-brink and others added 5 commits April 16, 2026 09:42
Add a streaming AI chat feature gated by the showAssistant field on journeys.
Includes chat drawer UI with Radix/Vaul primitives, context extraction from
journey blocks, interaction starters, markdown rendering, and a switchable
provider backend (apologist/gemini/openai).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add AI disclaimer header and update heading to "How would you like to go deeper?"
- Rework quick-action buttons to horizontal pill-style with lucide-react icons
- Add close button to drawer, replace Send with arrow icon button
- Update input placeholder to "Ask me anything", remove extra container padding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On the final journey card, replace the floating sparkle button with a
pinned-open chat bar at 25vh height. Includes Explain and Reflect
interaction starter buttons and an always-visible prompt input. Tapping
a starter or submitting a message opens the full AiChat drawer with the
message pre-loaded.

- New LastCardChatBar component (mobile only, hidden at lg+)
- Conductor conditionally renders LastCardChatBar vs StepFooter
- OverlayContent adjusts bottom spacing for last-card chat bar
- AiChat accepts optional initialMessage prop for pre-loaded messages
- Desktop (lg+) retains standard StepFooter with sparkle button
- TODO placeholder for third interaction starter button (awaiting Aaron)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove fixed 25vh height from LastCardChatBar so it sizes to its content
instead of leaving empty space above the buttons. Remove the border-top
separator and flex-end justification that made the gap visible. Update
OverlayContent last-card bottom margin from 25vh to 120px to match the
actual chat bar content height.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- LastCardChatBar starter buttons: change text to white, border to
  semi-transparent white, add subtle white background tint for
  visibility against the dark card overlay
- LastCardChatBar starter icons: use white instead of purple for
  contrast on dark backgrounds
- Textarea: add explicit color #1a1a1a so typed text is always
  readable against the #f5f5f5 background, regardless of inherited
  theme color (fixes invisible input text on both mobile and desktop)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented Apr 17, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2618ddfb-a118-4f6a-a4f7-76c929190774

📥 Commits

Reviewing files that changed from the base of the PR and between f087f1f and b69c9ad.

📒 Files selected for processing (1)
  • libs/locales/en/libs-journeys-ui.json
✅ Files skipped from review due to trivial changes (1)
  • libs/locales/en/libs-journeys-ui.json

Walkthrough

Adds a feature-flag gated AI chat backend and client UI: a new POST /api/chat endpoint with multi-provider streaming, ~20 new chat-related UI components/hooks/tests/i18n entries, LaunchDarkly client caching changes, and supporting deps and polyfills.

Changes

Cohort / File(s) Summary
API Chat Endpoint & Tests
apps/journeys/pages/api/chat/index.ts, apps/journeys/pages/api/chat/index.spec.ts
New POST-only /api/chat handler gated by apologistChat flag, resolves multiple providers (Gemini/OpenAI/OpenRouter/Apologist), streams responses, logs errors; comprehensive Jest tests for flag gating, method enforcement, and validation.
App bootstrap minor edit
apps/journeys/pages/_app.tsx
Whitespace-only separation of useTranslation hook result and a subsequent useEffect (no logic change).
Jest polyfills
apps/journeys/setupTests.ts
Adds web-streams polyfill hookups for Jest environment when missing.
Conductor & footer integration
apps/journeys/src/components/Conductor/Conductor.tsx, apps/journeys/src/components/Conductor/Conductor.apologistChat.spec.tsx, apps/journeys/src/components/Conductor/Conductor.spec.tsx
Conductor now reads apologistChat flag and computes showPinnedChat; conditional rendering of PinnedChatBar/footers updated; tests added for flag gating and prior FlagsProvider integration tests removed.
Core Chat UI components
libs/journeys/ui/src/components/AiChat/..., libs/journeys/ui/src/components/ChatOverlay/..., libs/journeys/ui/src/components/PinnedChatBar/..., libs/journeys/ui/src/components/AiChatButton/...
Adds AiChat (chat lifecycle, streaming, initialMessage behavior, variants), ChatOverlay (client-only overlay), PinnedChatBar (fixed bottom chat), and AiChatButton (opens overlay).
Message, Conversation, Response
libs/journeys/ui/src/components/Message/..., libs/journeys/ui/src/components/Conversation/..., libs/journeys/ui/src/components/Response/...
New Message component with role/plain styling, Conversation wrapper with auto-scroll hook, and Response component for Markdown rendering.
Input, Actions & Pinned controls
libs/journeys/ui/src/components/PromptInput/..., libs/journeys/ui/src/components/Actions/..., libs/journeys/ui/src/components/PinnedChatBar/...
PromptInput form with Enter/Shift handling and stop/send controls; Actions adds copy-to-clipboard button; PinnedChatBar hosts client AiChat.
StepFooter & footer utilities
libs/journeys/ui/src/components/StepFooter/StepFooter.tsx, libs/journeys/ui/src/components/StepFooter/StepFooter.apologistChat.spec.tsx, libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx, libs/journeys/ui/src/components/Card/utils/getFooterElements/...
StepFooter now conditionally renders AiChatButton based on flags and hasAiChatButton; overlay/footer spacing and footer height logic updated to account for pinned chat; tests added for flag gating.
Drawer & UI barrels
libs/journeys/ui/src/components/Drawer/..., multiple .../index.ts files
Adds bottom-anchored Drawer with context and many barrel exports for new components (AiChat, AiChatButton, Conversation, Message, Response, PromptInput, ChatOverlay, PinnedChatBar, Actions, etc.).
Hooks, GraphQL & utilities
libs/journeys/ui/src/libs/JourneyProvider/journeyFields.tsx, libs/journeys/ui/src/libs/isLastCard/...
Adds showAssistant to Journey fragment and introduces useIsLastCard hook with re-export.
LaunchDarkly client
libs/shared/ui/src/libs/getLaunchDarklyClient/getLaunchDarklyClient.ts
Moves client cache to globalThis to survive HMR, increases init timeout to 10s, and clears global cache on init failure/timeouts to allow retries.
Locales & deps
libs/locales/en/libs-journeys-ui.json, package.json
Adds chat/copy/retry i18n keys and new dependencies: @ai-sdk/openai, @ai-sdk/openai-compatible, @ai-sdk/react, react-markdown, use-stick-to-bottom.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/UI
    participant AiChat as AiChat Component
    participant API as /api/chat Endpoint
    participant Provider as AI Provider (Gemini/OpenAI/OpenRouter/Apologist)

    Browser->>AiChat: User submits message
    AiChat->>API: POST /api/chat (messages + language)
    API->>API: Check getFlags().apologistChat
    alt flag missing/false
        API-->>AiChat: 404 / block
    else flag true
        API->>API: Validate body.messages
        API->>API: Resolve provider from env
        API->>Provider: Start streaming (system prompt + messages)
        Provider-->>API: Streamed chunks
        API-->>AiChat: Stream chunks to client
        AiChat->>Browser: Render assistant messages (typewriter)
    end
Loading
sequenceDiagram
    participant User as User
    participant Button as AiChatButton
    participant Overlay as ChatOverlay
    participant AiChat as AiChat (in Overlay)

    User->>Button: Click
    Button->>Button: set overlayOpen = true
    Button->>Overlay: Render open=true
    Overlay->>Overlay: Load AiChat (dynamic, ssr:false)
    AiChat->>AiChat: Initialize useChat, focus input
    User->>AiChat: Submit message -> triggers /api/chat flow
    User->>Overlay: Click backdrop or close
    Overlay->>Button: onClose -> set overlayOpen = false
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: adding an AI chat prototype feature to journey cards, with a relevant ticket reference.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/nes-1554-apologist-chat-slice-1-prototype-base

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.

Merge origin/main into feature branch to resolve PR #9037 conflicts:
- package.json: kept vaul dependency from feature branch, took updated
  vercel version (^51.5.1) from main
- pnpm-lock.yaml: accepted main's version and regenerated via pnpm install
  to incorporate both sides' dependency changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

Warnings
⚠️ ❗ Big PR (2088 changes)

(change count - 2088): Pull Request size seems relatively large. If Pull Request contains multiple changes, split each into separate PR will helps faster, easier review.

Generated by 🚫 dangerJS against 4bc2156

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 17, 2026

View your CI Pipeline Execution ↗ for commit 4bc2156

Command Status Duration Result
nx run journeys-admin-e2e:e2e ✅ Succeeded 28s View ↗
nx run watch-e2e:e2e ✅ Succeeded 19s View ↗
nx run journeys-e2e:e2e ✅ Succeeded 22s View ↗
nx run resources-e2e:e2e ✅ Succeeded 21s View ↗
nx run watch-modern-e2e:e2e ✅ Succeeded 3s View ↗
nx run videos-admin-e2e:e2e ✅ Succeeded 5s View ↗
nx run player-e2e:e2e ✅ Succeeded 3s View ↗
nx run short-links-e2e:e2e ✅ Succeeded 3s View ↗
Additional runs (28) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2026-04-24 05:17:03 UTC

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 17, 2026

View your CI Pipeline Execution ↗ for commit 8a76954

Command Status Duration Result
nx run-many --target=deploy --projects=journeys ✅ Succeeded 1m 19s View ↗
nx run-many --target=vercel-alias --projects=jo... ✅ Succeeded 2s View ↗
nx run-many --target=vercel-alias --projects=re... ✅ Succeeded 2s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 9s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 6s View ↗
nx run-many --target=vercel-alias --projects=wa... ✅ Succeeded 2s View ↗
nx run-many --target=deploy --projects=journeys... ✅ Succeeded 44s View ↗
nx run-many --target=deploy --projects=resources ✅ Succeeded 51s View ↗
Additional runs (17) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2026-04-17 06:09:09 UTC

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 17, 2026

View your CI Pipeline Execution ↗ for commit 8a76954


☁️ Nx Cloud last updated this comment at 2026-04-17 06:07:47 UTC

@github-actions github-actions Bot temporarily deployed to Preview - player April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - watch April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - short-links April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - journeys April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - resources April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - watch-modern April 17, 2026 04:39 Inactive
@github-actions github-actions Bot temporarily deployed to Preview - videos-admin April 17, 2026 04:39 Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
short-links ✅ Ready short-links preview Fri Apr 24 17:13:12 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
player ✅ Ready player preview Fri Apr 24 17:12:59 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
docs ✅ Ready docs preview Fri Apr 24 17:13:09 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch-modern ✅ Ready watch-modern preview Fri Apr 24 17:12:59 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys ✅ Ready journeys preview Fri Apr 24 17:13:19 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
videos-admin ✅ Ready videos-admin preview Fri Apr 24 17:13:15 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
resources ✅ Ready resources preview Fri Apr 24 17:13:28 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch ✅ Ready watch preview Fri Apr 24 17:13:52 NZST 2026

jaco-brink and others added 5 commits April 22, 2026 22:40
- Add openrouter provider for dev/QA (avoids apologist token burn)
- Replace server-side smoothStream with client-side typewriter
  (provider-agnostic, avoids OpenRouter chunk-parsing issues)
- Add bouncing-dot typing indicator while awaiting first token
- Hide Copy action until typewriter animation completes
- Remove Retry/regenerate on successful responses
  (fits product goal of capturing unbiased questions)
- Add streamText onError logging

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator Author

@jaco-brink jaco-brink left a comment

Choose a reason for hiding this comment

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

Review summary

Scope-filtered against the World-Cup Apologist Chat Journey project — prompt-injection on language (NES-1582) and rate-limiting/abuse protection (NES-1580 cluster) are already tracked, so not re-raised here.

Critical: 1 — isLastCard guard in OverlayContent is stale after the Conductor refactor; pinned bar overlaps card content on non-last cards.
Concern: 3 — breakpoint overlap at sm/md, missing i18n wrappers, suspicious apologist modelId string.
Nit: 1 — dead isDesktop var in PinnedChatBar.

Draft PR → posting as COMMENT.

Comment thread libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx Outdated
Comment thread libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx Outdated
Comment thread libs/journeys/ui/src/components/PinnedChatBar/PinnedChatBar.tsx Outdated
Comment thread libs/journeys/ui/src/components/PinnedChatBar/PinnedChatBar.tsx Outdated
Comment thread libs/journeys/ui/src/components/PromptInput/PromptInput.tsx Outdated
Comment thread libs/journeys/ui/src/components/Drawer/Drawer.tsx Outdated
Comment thread apps/journeys/pages/api/chat/index.ts Outdated
- Drop isLastCard gate from OverlayContent pinnedChatActive so mb
  reservation aligns with Conductor's per-card showPinnedChat, and
  extend the 120px reservation through sm/md breakpoints where the
  PinnedChatBar is visible.
- Wrap PinnedChatBar drawer title and Drawer close aria-label with
  t('libs-journeys-ui'); remove unused isDesktop/useTheme/useMediaQuery
  from PinnedChatBar.
- Wrap four PromptInput user-facing strings with t(); add new keys
  (Ask me anything, Chat message input, Stop generating, Send message,
  Close chat) to en locale.
- Move apologist modelId to APOLOGIST_MODEL_ID env var (default matches
  the gateway-specific format from scripts/apologist-stream-test.sh).
@jaco-brink
Copy link
Copy Markdown
Collaborator Author

Review feedback addressed (cb0742a)

Fixed:

  • libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx:81 — dropped isLastCard from pinnedChatActive to match Conductor's per-card showPinnedChat (aligns with NES-1556 direction).
  • libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx:87 — extended mb 120px reservation through sm/md breakpoints (where PinnedChatBar is visible); lg returns to 10.
  • libs/journeys/ui/src/components/PinnedChatBar/PinnedChatBar.tsx:88 — wrapped drawer title with t('Chat').
  • libs/journeys/ui/src/components/PinnedChatBar/PinnedChatBar.tsx:28 — removed unused isDesktop, useTheme, useMediaQuery.
  • libs/journeys/ui/src/components/PromptInput/PromptInput.tsx — wrapped all four strings with t('libs-journeys-ui'); added keys Ask me anything, Chat message input, Stop generating, Send message to en locale.
  • libs/journeys/ui/src/components/Drawer/Drawer.tsx:86 — wrapped Close chat aria-label with t('libs-journeys-ui'); added key to en locale.

Fixed (adjusted):

  • apps/journeys/pages/api/chat/index.ts:79 — confirmed the openai/gpt/4o-mini format is gateway-specific (matches scripts/apologist-stream-test.sh:28). Moved to APOLOGIST_MODEL_ID env var (same default, same env var name as the shell script). APOLOGIST_MODEL_ID=openai/gpt/4o-mini added to Doppler. See inline reply on the thread.

jaco-brink and others added 13 commits April 23, 2026 13:38
The on-submit drawer was the primary "widget, not native" signal in both
recordings. AiChat already owns its own PromptInput, Conversation, typing
indicator, error+retry bubble, and stop control — the PinnedChatBar's
duplicate input and Drawer wrapper were redundant.

PinnedChatBar now renders AiChat directly in an absolute-positioned
container anchored to the card bottom, capped at min(70vh, 100%). Height
is content-driven up to the cap; Conversation's overflow-y: auto takes
over beyond it. No more Chat/× chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Once messages push the chat surface to its cap, there was no way back to
the card behind it. Added a subtle pill-shaped handle at the top of the
chat surface (visible only when messages exist) that toggles a collapsed
state. Collapsed hides Conversation via display:none — preserves scroll
position, useChat message state, and in-flight streaming — so re-opening
resumes exactly where it left off.

iOS sheet pattern over a × button: signals "this can slide away" rather
than "close this widget". Re-uses the Drawer component's 48x4 pill for
visual consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If the chat is collapsed when the user sends a new message or retries
after an error, the response would stream in invisibly. Force-expand on
the three user-driven entry points (handleSubmit, handleRetry, and the
initialMessage effect) so answers always land in view. Leaves iOS-sheet
mental model intact otherwise — background content changes don't reopen
a dismissed surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1570)

Three UX fixes on wider viewports:

- Drag handle is now a mobile affordance only (xs). On sm+ it would
  duplicate desktop dismiss controls and implies a gesture mouse users
  don't make.
- AiChat gains a top-right close/expand icon on sm+ that collapses the
  conversation in place — same semantics as the mobile handle but
  expressed as a click target. New `collapsible` prop lets callers that
  wrap AiChat in their own dismissible container (Drawer) opt out.
- Conversation and PromptInput columns cap at 48rem centered on sm+,
  matching ChatGPT's readable line-length on wide screens.

Drawer chrome gets the same treatment: its pill handle hides on sm+,
its top corners square off so it reads as a surface edge rather than
a floating widget, and the Paper caps at 48rem centered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ransform (NES-1570)

translateX(-50%) on the Drawer Paper collided with MuiDrawer's own
transform-based slide-up transition, leaving the panel stranded in the
bottom-right corner. Centre via positional CSS instead — left:
max(0px, calc(50% - 24rem)) gives a centered 48rem panel on sm+ and
clamps to full-width below that, without touching transform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dChatBar (NES-1570)

Desktop chat drops the Drawer panel and adopts a ChatGPT-style ambient
overlay: a floating capsule prompt input centred at the bottom of the
viewport, with plain-prose assistant responses streaming above it and a
small close button tucked above the column. The card behind stays fully
interactive until the user actually clicks into the input.

- New `ChatOverlay` component replaces the Drawer branch in
  `AiChatButton`. Fixed-position layer, pointer-events routed only to
  the input column and close button.
- `AiChat` gains a `variant` prop (`panel` default, `overlay` for
  desktop) that propagates to `Message.plain` and
  `PromptInput.variant='floating'`. Panel callers (PinnedChatBar on
  mobile) are unchanged.
- `Message` learns a `plain` mode: assistant messages render as plain
  prose with no bubble; user messages keep their pill.
- `PromptInput` gains a `floating` variant: rounded capsule, shadow,
  no top border — designed to stand alone over a card.
- `Conductor` shifts breakpoints so PinnedChatBar is xs-only and the
  StepFooter (with AiChatButton → ChatOverlay) appears from sm+. This
  gives mobile its existing inline UX and desktop the new overlay UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erlay (NES-1570)

The transparent overlay rendered dark assistant prose directly over the
dark card background, leaving the response unreadable. ChatGPT's
equivalent works because their entire app is dark; ours lives over
variable card imagery, so the overlay needs its own colour context.

- ChatOverlay now paints a full-viewport dark scrim
  (rgba(10,10,15,0.55) + 6px backdrop blur) behind the column. Clicking
  the scrim closes the overlay, matching standard modal behaviour.
- Message `plain` mode switches assistant text to light
  (rgba(255,255,255,0.92)) for contrast against the scrim. User pills
  and bubble-mode messages (mobile) stay dark-on-light.
- Actions (Copy) learns a `plain` prop so the action row picks up the
  same light treatment. Retry button colour also branches on the
  overlay variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…m (NES-1570)

Three cosmetic follow-ups on the desktop ChatOverlay:

- Scrim now derives from theme palette (alpha(grey[900], 0.7)) instead
  of a hard-coded near-black, so it reads as the app's own dark tone.
- Conversation scrollbar picks up thin width and a rounded grey thumb
  (rgba(128,128,128,0.5)) that stays visible on both the white mobile
  panel and the dark desktop scrim.
- PromptInput in floating mode now nests a matching rounded input
  (radius 9999, transparent bg) inside the capsule. The previous 3-unit
  inner radius and grey.50 fill left a visible seam at the top-left
  corner where the inner rectangle met the outer pill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integrates the UX iteration on the apologist chat surface:
- Mobile: PinnedChatBar renders AiChat inline with a drag handle.
- Desktop: new ambient ChatOverlay replaces the drawer — floating
  capsule input over a theme-driven scrim, plain assistant prose, X
  button tucked above the column, rounded scrollbar.
- Breakpoint split moved from lg to sm so mobile and desktop UX diverge
  at the phone/tablet boundary.
Copy link
Copy Markdown
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: 9

🧹 Nitpick comments (14)
libs/journeys/ui/src/components/Drawer/Drawer.tsx (3)

55-69: Hard-coded colors bypass the MUI theme.

color: '#1a1a1a', bgcolor: '#e0e0e0' (Line 87), and color: '#666' (Line 94) won't respond to theme changes (e.g., dark mode) and duplicate values across the component. Since the PR description mentions contrast fixes and "hard-coded light colors on chat surfaces," this appears intentional for the prototype — just flagging for a follow-up pass to migrate to theme.palette tokens (e.g., text.primary, divider, text.secondary) once the prototype stabilizes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Drawer/Drawer.tsx` around lines 55 - 69,
Replace hard-coded hex colors in Drawer.tsx with MUI theme palette tokens:
update the PaperProps.sx color: '#1a1a1a' to use 'text.primary', replace the
bgcolor '#e0e0e0' usage (the other Paper/box instance at the noted location)
with an appropriate token like 'background.paper' or 'divider', and change the
'#666' usage to 'text.secondary'; make these changes in the sx objects
(PaperProps and the other style blocks referenced) so the component responds to
dark mode and centralized theme changes and keep the token choices consistent
across the component.

90-97: Chat-specific aria-label inside a generic Drawer component.

aria-label={t('Close chat')} hard-codes a chat use case into what is otherwise a reusable Drawer/DrawerContent primitive. If this component is reused outside the AI chat surface (the barrel export at Drawer/index.ts makes it generally importable), the screen-reader announcement will be misleading.

Consider making the close label configurable (falling back to a generic default) so the component stays reusable:

♻️ Proposed refactor
 interface DrawerContentProps {
   children: ReactNode
   title: string
+  closeLabel?: string
 }

 export function DrawerContent({
   children,
-  title
+  title,
+  closeLabel
 }: DrawerContentProps): ReactElement {
   const { t } = useTranslation('libs-journeys-ui')
   ...
         <IconButton
           onClick={() => onOpenChange(false)}
-          aria-label={t('Close chat')}
+          aria-label={closeLabel ?? t('Close')}
           size="small"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Drawer/Drawer.tsx` around lines 90 - 97, The
IconButton in the Drawer/DrawerContent component uses a chat-specific aria-label
(aria-label={t('Close chat')}); change the component API to accept a
configurable close label prop (e.g., closeLabel or closeAriaLabel) with a
sensible default (e.g., t('Close')) and use that prop for aria-label on the
IconButton; update the component props/interface (and any defaultProps or
destructuring in the Drawer/DrawerContent function) so consumers can override
the label when reusing Drawer outside the chat surface while preserving the
current translation-based default.

81-89: Visual-only drag handle.

The pill on mobile looks like a drag-to-dismiss affordance but there's no swipe/drag handler wired up — users may try to drag it and get no feedback. Fine as a visual cue for the prototype; consider either adding swipe-to-close via SwipeableDrawer later or making this clearly decorative. No action required now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Drawer/Drawer.tsx` around lines 81 - 89, The
pill element in Drawer (the Box inside the Drawer component) appears to be a
drag handle but has no swipe behavior; either make it clearly decorative by
marking the Box as non-interactive (add aria-hidden="true", role="presentation"
and sx={{ pointerEvents: 'none' }}) and update its styling comment to indicate
it's purely visual, or replace the mobile Drawer with MUI's SwipeableDrawer and
wire the existing onClose/onOpen handlers so the pill actually supports
swipe-to-close; locate the Box in Drawer.tsx (the Box with width:48 height:4
borderRadius:9999) and apply one of these two changes.
libs/journeys/ui/src/libs/isLastCard/useIsLastCard.ts (1)

7-9: Optional: tighten the TreeBlock<StepFields> cast.

blockHistory is typed as TreeBlock[] (any BlockFields), so the cast to TreeBlock<StepFields> is unchecked. In practice history only contains StepBlocks, but a small runtime guard would make the cast sound and avoid silently feeding a non-step into getNextBlock.

♻️ Optional narrowing
-  const activeBlock = blockHistory[blockHistory.length - 1] as
-    | TreeBlock<StepFields>
-    | undefined
-  if (activeBlock == null || treeBlocks.length === 0) return false
+  const lastHistoryBlock = blockHistory[blockHistory.length - 1]
+  if (lastHistoryBlock == null || treeBlocks.length === 0) return false
+  if (lastHistoryBlock.__typename !== 'StepBlock') return false
+  const activeBlock = lastHistoryBlock as TreeBlock<StepFields>
   return getNextBlock({ activeBlock }) === undefined
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/libs/isLastCard/useIsLastCard.ts` around lines 7 - 9,
The current unchecked cast of activeBlock to TreeBlock<StepFields> can allow
non-step blocks into getNextBlock; add a runtime narrowing guard before using
activeBlock as a Step block: check the block kind/fields (e.g., activeBlock &&
activeBlock.fields?.type === 'step' or use an existing isStepBlock type guard)
and only call getNextBlock or treat it as TreeBlock<StepFields> when that check
passes, otherwise return/handle the non-step case early; update the variable use
in useIsLastCard (the activeBlock binding) so the cast is removed and the
narrowed branch uses a proper StepFields-typed variable.
libs/journeys/ui/src/components/Response/Response.tsx (1)

11-16: Consider a typed components override and sanitization policy for AI/user content.

react-markdown v10 escapes HTML and blocks javascript: URLs by default, so raw XSS via markdown alone is unlikely. However, rendering arbitrary AI output as markdown with no components, remarkPlugins, or urlTransform overrides means:

  • Links (including from model output) render with target=_self and no rel="noopener noreferrer"; external links from a hallucinated model could be a phishing vector.
  • Images (![alt](url)) will trigger network requests to arbitrary hosts, which can be a privacy/SSRF-adjacent concern for logged-in users.

For a prototype this is fine, but worth tightening before exposing widely — e.g., override a to force target="_blank" rel="noopener noreferrer ugc" and disable or allow-list img.

react-markdown v10 default urlTransform and HTML handling defaults
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Response/Response.tsx` around lines 11 - 16,
The Response component renders unmodified markdown via the Markdown element (in
Response.tsx) which can produce unsafe links/images; update Response to pass a
typed components override and sanitization: provide a custom components.a that
forces target="_blank" and rel="noopener noreferrer ugc", implement urlTransform
or use rehype-sanitize to block javascript: and other unsafe schemes, and
replace or disable the image renderer (components.img) to either disallow remote
images or validate/allowlist sources; ensure the props/types for the Markdown
components override are strongly typed so the change is compile-time safe.
libs/journeys/ui/src/components/Message/Message.tsx (1)

36-57: Hard-coded colors bypass the MUI theme.

Colors like #6D28D9, #f5f5f5, #1a1a1a and rgba literals aren't resolvable by theming (dark mode, white-label, etc.) and duplicate design tokens across components. If the prototype color parity is intentional short-term (PR description alludes to this), consider at minimum extracting the palette into a shared constants module so the values can be swapped in one place later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Message/Message.tsx` around lines 36 - 57,
The Box in Message.tsx uses hard-coded color literals in its sx prop (bgcolor,
color) which bypass the MUI theme; replace those literals by reading from the
theme palette or a shared design token module: update the sx to be a function
receiving theme and use theme.palette.<semanticKey>.main/contrastText (or import
shared constants like MESSAGE_TOKENS.userBg/assistantBg/plainAssistantBg) and
swap '#6D28D9', '#f5f5f5', '#1a1a1a' and the rgba value with those theme/token
references so dark mode and white‑labeling work consistently; create or reuse a
small constants file (e.g., MESSAGE_TOKENS) if palette keys aren’t yet available
and use those tokens in the sx logic that currently branches on isUser and
isPlainAssistant.
libs/journeys/ui/src/components/Actions/Actions.tsx (1)

6-15: Rename component to better reflect its chat-message context.

Actions is too generic for a message-level copy action bar inside the chat feature. Per the naming conventions used elsewhere in the codebase (%UiType%%ComponentFunction%), consider renaming to MessageActions or ChatMessageActions to be more descriptive at call sites and reduce collision risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Actions/Actions.tsx` around lines 6 - 15,
Rename the generic Actions component to a more descriptive chat message name:
update the interface ActionsProps to MessageActionsProps (or
ChatMessageActionsProps) and rename the exported function Actions to
MessageActions (or ChatMessageActions), adjust the props destructuring signature
accordingly, and update all local exports and any import sites that reference
Actions/ActionsProps so they import the new symbol name; ensure any tests,
storybook entries, and type annotations are updated to the new names to avoid
breaking references.
libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx (1)

76-81: Reuse hasAiChatButton to avoid duplicated gating logic.

The variant and showAssistant checks duplicate the logic in hasAiChatButton (libs/journeys/ui/src/components/Card/utils/getFooterElements/getFooterElements.ts), which is already used alongside flags.apologistChat in StepFooter.tsx. Reusing the helper keeps the pinned-chat visibility condition in one place.

♻️ Proposed refactor
-import { getFooterMobileSpacing } from '../utils/getFooterElements'
+import {
+  getFooterMobileSpacing,
+  hasAiChatButton
+} from '../utils/getFooterElements'
...
-  const flags = useFlags()
-  const pinnedChatActive =
-    flags.apologistChat === true &&
-    journey?.showAssistant === true &&
-    variant !== 'admin' &&
-    variant !== 'embed'
+  const flags = useFlags()
+  const pinnedChatActive =
+    flags.apologistChat === true && hasAiChatButton({ journey, variant })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx`
around lines 76 - 81, The pinnedChatActive computation duplicates gating
logic—replace the inline checks in OverlayContent (the useFlags +
pinnedChatActive block) with the existing helper hasAiChatButton from
getFooterElements so visibility logic is centralized; specifically, import and
call hasAiChatButton(journey, variant) and combine it with flags.apologistChat
(i.e. pinnedChatActive = flags.apologistChat === true && hasAiChatButton(...))
instead of rechecking journey?.showAssistant and variant here so behavior
matches StepFooter.tsx.
libs/journeys/ui/src/components/AiChatButton/AiChatButton.tsx (1)

30-39: Redundant keyboard handling on MUI IconButton.

IconButton renders a native <button>, which is already focusable and triggers onClick on Enter/Space. Both tabIndex={0} and the onKeyDown handler are redundant and add maintenance overhead.

♻️ Proposed simplification
       <IconButton
         onClick={handleClick}
         aria-label={t('Open AI chat')}
-        tabIndex={0}
-        onKeyDown={(e) => {
-          if (e.key === 'Enter' || e.key === ' ') {
-            e.preventDefault()
-            handleClick()
-          }
-        }}
         sx={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/AiChatButton/AiChatButton.tsx` around lines
30 - 39, The IconButton is handling keyboard activation redundantly—remove the
explicit tabIndex={0} and the onKeyDown handler from the IconButton so the
native button behavior (Enter/Space activating onClick) is used; keep
onClick={handleClick} and aria-label intact. Locate the IconButton usage in
AiChatButton.tsx and delete the tabIndex and the onKeyDown prop while leaving
handleClick and aria-label unchanged to preserve accessibility and simplify the
component.
libs/journeys/ui/src/components/PromptInput/PromptInput.tsx (2)

111-157: Hardcoded hex colors bypass the theme palette

Colors like #1a1a1a, #9e9e9e, #6D28D9, #5B21B6, etc. are inlined rather than sourced from the MUI theme. The PR description notes this is intentional "hard-coded light colors on chat surfaces" for prototype parity, so flagging only as a follow-up — worth migrating to theme.palette tokens (or a dedicated chat palette extension) before this graduates out of prototype, so the chat surface can respect theme modes / RTL color swaps and be restyled without code changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/PromptInput/PromptInput.tsx` around lines 111
- 157, The component PromptInput.tsx currently uses hardcoded hex colors in the
sx props and nested selectors (e.g., '& .MuiInputBase-input', '&
.MuiInputBase-input::placeholder', and the IconButton sx blocks for the
submit/stop buttons); replace those literal color strings (like '#1a1a1a',
'#9e9e9e', '#6D28D9', '#5B21B6', '#e0e0e0', '#999', '#666') with values resolved
from the MUI theme (e.g., theme.palette.* tokens or a dedicated chat palette
extension) by accessing theme in the sx callbacks or using classes that map to
palette tokens so the InputBase placeholder, input text color, button
background, hover and disabled states all come from theme.palette rather than
hardcoded hex values.

38-38: textareaRef is unused

textareaRef is created and wired to TextField.inputRef but never read from anywhere else in the component. Unless you're planning to use it (e.g. to focus the input after send or when the prompt bar mounts), the ref and the useRef import can be dropped.

♻️ Proposed cleanup
-import {
-  FormEvent,
-  KeyboardEvent,
-  ReactElement,
-  useCallback,
-  useRef
-} from 'react'
+import { FormEvent, KeyboardEvent, ReactElement, useCallback } from 'react'
...
-  const { t } = useTranslation('libs-journeys-ui')
-  const textareaRef = useRef<HTMLTextAreaElement>(null)
+  const { t } = useTranslation('libs-journeys-ui')
...
-      <TextField
-        inputRef={textareaRef}
-        value={input}
+      <TextField
+        value={input}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/PromptInput/PromptInput.tsx` at line 38, The
textareaRef created with useRef<HTMLTextAreaElement>(null) is never read; remove
the unused textareaRef and its wiring to TextField.inputRef and also remove the
unused useRef import from the module, or alternatively actually use textareaRef
(for example to call textareaRef.current?.focus() after send/mount) — update the
component to either eliminate textareaRef and the inputRef prop on TextField or
implement the focus/DOM access logic where needed (reference symbols:
textareaRef, TextField.inputRef, useRef).
apps/journeys/src/components/Conductor/Conductor.tsx (1)

76-80: Variant gating is duplicated in PinnedChatBar

PinnedChatBar already short-circuits to null when variant === 'admin' || variant === 'embed' (see libs/journeys/ui/src/components/PinnedChatBar/PinnedChatBar.tsx lines 25-27), so the variant !== 'admin' && variant !== 'embed' clauses here are redundant. Either drop the duplicate check in Conductor (letting PinnedChatBar own the rule) or drop it in PinnedChatBar — keeping it in both means future variant rules have to be kept in sync in two places. Not blocking; defensive duplication is fine if intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/journeys/src/components/Conductor/Conductor.tsx` around lines 76 - 80,
The variant gating for admin/embed is duplicated: remove the redundant checks
from Conductor by updating the showPinnedChat calculation (remove the "variant
!== 'admin' && variant !== 'embed'" clause) so that PinnedChatBar retains
responsibility for short-circuiting; keep the rest of the predicate
(apologistChatEnabled && journey?.showAssistant === true) and rely on
PinnedChatBar to enforce variant-based visibility (see showPinnedChat and
PinnedChatBar).
apps/journeys/pages/api/chat/index.ts (1)

96-110: No auth or rate limiting on this endpoint

Once apologistChat is true, /api/chat is world-reachable, unauthenticated, and each successful POST triggers an upstream LLM stream (billable). For a prototype that's acceptable with a tight feature-flag audience, but worth tracking before this rolls out broadly — at minimum a per-IP/per-visitor rate limit and a request-size cap would protect against cost abuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/journeys/pages/api/chat/index.ts` around lines 96 - 110, The handler
currently allows unauthenticated, unlimited access once getFlags().apologistChat
is true; add authentication and rate/size protections at the start of the
exported handler: validate the requester (e.g., call a verifyAuth or
validateApiKey function) and reject 401/403 when missing/invalid, enforce a
per-IP or per-user rate limit (e.g., via an applyRateLimit or checkRateLimit
helper that returns 429 when exceeded), and enforce a request body size cap
(e.g., bodySizeLimit or validatePayloadSize before parsing) to return 413 for
oversized requests; keep these checks before any upstream LLM call and reuse
clear identifiers from this file (handler, getFlags, apologistChat) so the
changes are easy to locate and test.
libs/journeys/ui/src/components/AiChat/AiChat.tsx (1)

72-92: RAF keeps running after reveal completes.

Once !isStreaming && revealed >= target.length, the effect still schedules a new requestAnimationFrame every frame with a no-op setRevealed. Cheap, but unnecessary for every open assistant bubble. Consider breaking the loop when the reveal is caught up and streaming has ended (add isStreaming to deps and re-arm when it flips back true).

♻️ Sketch
   useEffect(() => {
     if (!enabled) return
     let raf = 0
     let last = performance.now()
     const tick = (now: number): void => {
       const deltaSec = (now - last) / 1000
       last = now
-      setRevealed((prev) => {
-        const targetLength = targetRef.current.length
-        if (prev >= targetLength) return prev
-        const step = Math.max(
-          1,
-          Math.floor(TYPEWRITER_CHARS_PER_SEC * deltaSec)
-        )
-        return Math.min(targetLength, prev + step)
-      })
-      raf = requestAnimationFrame(tick)
+      let caughtUp = false
+      setRevealed((prev) => {
+        const targetLength = targetRef.current.length
+        if (prev >= targetLength) {
+          caughtUp = true
+          return prev
+        }
+        const step = Math.max(1, Math.floor(TYPEWRITER_CHARS_PER_SEC * deltaSec))
+        return Math.min(targetLength, prev + step)
+      })
+      if (caughtUp && !isStreaming) return
+      raf = requestAnimationFrame(tick)
     }
     raf = requestAnimationFrame(tick)
     return () => cancelAnimationFrame(raf)
-  }, [enabled])
+  }, [enabled, isStreaming])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/AiChat/AiChat.tsx` around lines 72 - 92, The
RAF loop in the useEffect (tick/requestAnimationFrame) keeps running even after
the typewriter reveal is complete; modify the effect for the component using
targetRef, setRevealed and TYPEWRITER_CHARS_PER_SEC to stop re-arming when the
content is fully revealed and streaming has ended: add isStreaming to the effect
deps, and inside tick check the current revealed/targetLength and isStreaming —
if revealed >= targetLength and !isStreaming, cancel the RAF and return without
scheduling a new requestAnimationFrame; keep the existing cleanup that cancels
raf so when isStreaming flips back to true the effect will re-arm and resume.
🤖 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/journeys/pages/api/chat/index.ts`:
- Around line 177-190: The system prompt builder (function buildSystemMessage)
directly interpolates the request body language (ChatRequestBody.language),
allowing prompt-injection; validate the language before concatenation by
checking it against a strict allow-list of supported languages or a BCP-47 regex
and only append the "Respond in the following language" line when the value
passes validation; if validation fails, either omit the language line or fall
back to a safe default (e.g., "en") and ensure the validation logic is
implemented where ChatRequestBody.language is consumed so buildSystemMessage
receives only vetted values.
- Around line 112-126: Validate and sanitize req.body before using it: replace
the unchecked cast to ChatRequestBody with runtime validation (e.g., a Zod
schema) that enforces messages as an array of objects with required
role/content/parts shapes, a capped messages.length (e.g., MAX_MESSAGES), a
maximum total character count across messages (e.g., MAX_TOTAL_CHARS), and a
constrained language string; perform this validation in the API handler before
calling resolveChatModel/buildSystemMessage/convertToModelMessages and return
400 on schema violations. Additionally, add simple rate limiting by IP/visitor
in this route (memory store or existing middleware) to prevent abuse while
apologistChat is enabled, and ensure error responses include clear validation
failure messages.

In `@libs/journeys/ui/src/components/Actions/Actions.tsx`:
- Around line 18-20: The handleCopy useCallback currently calls
navigator.clipboard.writeText(content) without error handling; wrap that call in
a try/catch inside handleCopy to catch rejected promises, and on success/failure
update or invoke a feedback mechanism (e.g., set a local success/error state,
call a provided onCopy callback or show a toast) so the user gets notified of
copy outcome; ensure handleCopy remains memoized and still depends on content.

In `@libs/journeys/ui/src/components/AiChat/AiChat.tsx`:
- Around line 175-183: The keyboard handler handleHandleKeyDown duplicates
native button behavior and causes double-toggle: remove the onKeyDown handler
and delete the handleHandleKeyDown function (and its KeyboardEvent import if
unused) so IconButton relies on its native onClick which calls
handleToggleCollapse and setCollapsed; also rename any remaining occurrences to
a descriptive name if needed and ensure no other code references
handleHandleKeyDown (repeat the same removal for the similar block around lines
267-281).
- Around line 126-157: The TypingIndicator component uses a hard-coded
aria-label ("Assistant is typing"); replace it with a translated string via the
existing t() translation function (e.g., aria-label={t('aiChat.typing')}),
ensuring you import/use the same translation hook used elsewhere in AiChat.tsx
and add the new key (aiChat.typing) to the libs-journeys-ui en locale file with
the appropriate English value.

In
`@libs/journeys/ui/src/components/Card/utils/getFooterElements/getFooterElements.ts`:
- Around line 47-53: hasAiChatButton currently only checks journey.showAssistant
and variant, causing getFooterMobileSpacing/getFooterMobileHeight to reserve
space when flags.apologistChat is false; change hasAiChatButton({ journey,
variant }: JourneyInfoProps) to accept a resolved boolean (e.g., apologistChat:
boolean) or the full flags object and include apologistChat in its return
(return variant !== 'admin' && variant !== 'embed' && journey?.showAssistant ===
true && apologistChat === true), then update callers (StepFooter and the spacing
helpers getFooterMobileSpacing/getFooterMobileHeight) to pass the apologistChat
flag (or the pre-computed boolean) so spacing is computed from the same boolean
that actually controls button rendering.

In `@libs/journeys/ui/src/components/ChatOverlay/ChatOverlay.tsx`:
- Around line 24-88: ChatOverlay is rendering a modal-like overlay with Boxes
but lacks Escape-to-close and focus management; update ChatOverlay to use MUI
Modal or Dialog (wrapping the existing outer Box and backdrop) so you get
built-in aria-modal, escape-key handling, focus trap, and scroll lock, and pass
onClose as the Modal/Dialog onClose prop; ensure the IconButton close still
calls onClose and that AiChat remains inside the Modal/Dialog so initial focus
is trapped on open and returns to the opener when closed.

In `@libs/shared/ui/src/libs/getLaunchDarklyClient/getLaunchDarklyClient.ts`:
- Around line 38-49: The timeout timer created inside the Promise.race leaks
because the setTimeout callback isn’t cleared when
launchDarklyClient.waitForInitialization resolves; fix by capturing the timer id
in an outer-scoped variable (e.g., let initTimer: NodeJS.Timeout | null), assign
it inside the new Promise that calls setTimeout, and after the Promise.race
await (or in a finally block) call clearTimeout(initTimer) (and null it) so the
timer is cancelled; reference the existing
launchDarklyClient.waitForInitialization call and INIT_TIMEOUT_SECONDS constant
when making this change.
- Around line 31-36: The cached LaunchDarkly client at
globalForLD.__launchDarklyClient can remain in a failed or stuck state after
init() times out; modify getLaunchDarklyClient so that when
waitForInitialization() rejects or times out you explicitly clear
globalForLD.__launchDarklyClient (set to undefined/null) before returning the
fallback stub or throwing, allowing subsequent calls to create a fresh client;
specifically wrap the waitForInitialization() call on the launchDarklyClient
instance with try/catch/finally and in the failure path clear
globalForLD.__launchDarklyClient and then proceed to return the stub or
propagate the error.

---

Nitpick comments:
In `@apps/journeys/pages/api/chat/index.ts`:
- Around line 96-110: The handler currently allows unauthenticated, unlimited
access once getFlags().apologistChat is true; add authentication and rate/size
protections at the start of the exported handler: validate the requester (e.g.,
call a verifyAuth or validateApiKey function) and reject 401/403 when
missing/invalid, enforce a per-IP or per-user rate limit (e.g., via an
applyRateLimit or checkRateLimit helper that returns 429 when exceeded), and
enforce a request body size cap (e.g., bodySizeLimit or validatePayloadSize
before parsing) to return 413 for oversized requests; keep these checks before
any upstream LLM call and reuse clear identifiers from this file (handler,
getFlags, apologistChat) so the changes are easy to locate and test.

In `@apps/journeys/src/components/Conductor/Conductor.tsx`:
- Around line 76-80: The variant gating for admin/embed is duplicated: remove
the redundant checks from Conductor by updating the showPinnedChat calculation
(remove the "variant !== 'admin' && variant !== 'embed'" clause) so that
PinnedChatBar retains responsibility for short-circuiting; keep the rest of the
predicate (apologistChatEnabled && journey?.showAssistant === true) and rely on
PinnedChatBar to enforce variant-based visibility (see showPinnedChat and
PinnedChatBar).

In `@libs/journeys/ui/src/components/Actions/Actions.tsx`:
- Around line 6-15: Rename the generic Actions component to a more descriptive
chat message name: update the interface ActionsProps to MessageActionsProps (or
ChatMessageActionsProps) and rename the exported function Actions to
MessageActions (or ChatMessageActions), adjust the props destructuring signature
accordingly, and update all local exports and any import sites that reference
Actions/ActionsProps so they import the new symbol name; ensure any tests,
storybook entries, and type annotations are updated to the new names to avoid
breaking references.

In `@libs/journeys/ui/src/components/AiChat/AiChat.tsx`:
- Around line 72-92: The RAF loop in the useEffect (tick/requestAnimationFrame)
keeps running even after the typewriter reveal is complete; modify the effect
for the component using targetRef, setRevealed and TYPEWRITER_CHARS_PER_SEC to
stop re-arming when the content is fully revealed and streaming has ended: add
isStreaming to the effect deps, and inside tick check the current
revealed/targetLength and isStreaming — if revealed >= targetLength and
!isStreaming, cancel the RAF and return without scheduling a new
requestAnimationFrame; keep the existing cleanup that cancels raf so when
isStreaming flips back to true the effect will re-arm and resume.

In `@libs/journeys/ui/src/components/AiChatButton/AiChatButton.tsx`:
- Around line 30-39: The IconButton is handling keyboard activation
redundantly—remove the explicit tabIndex={0} and the onKeyDown handler from the
IconButton so the native button behavior (Enter/Space activating onClick) is
used; keep onClick={handleClick} and aria-label intact. Locate the IconButton
usage in AiChatButton.tsx and delete the tabIndex and the onKeyDown prop while
leaving handleClick and aria-label unchanged to preserve accessibility and
simplify the component.

In `@libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx`:
- Around line 76-81: The pinnedChatActive computation duplicates gating
logic—replace the inline checks in OverlayContent (the useFlags +
pinnedChatActive block) with the existing helper hasAiChatButton from
getFooterElements so visibility logic is centralized; specifically, import and
call hasAiChatButton(journey, variant) and combine it with flags.apologistChat
(i.e. pinnedChatActive = flags.apologistChat === true && hasAiChatButton(...))
instead of rechecking journey?.showAssistant and variant here so behavior
matches StepFooter.tsx.

In `@libs/journeys/ui/src/components/Drawer/Drawer.tsx`:
- Around line 55-69: Replace hard-coded hex colors in Drawer.tsx with MUI theme
palette tokens: update the PaperProps.sx color: '#1a1a1a' to use 'text.primary',
replace the bgcolor '#e0e0e0' usage (the other Paper/box instance at the noted
location) with an appropriate token like 'background.paper' or 'divider', and
change the '#666' usage to 'text.secondary'; make these changes in the sx
objects (PaperProps and the other style blocks referenced) so the component
responds to dark mode and centralized theme changes and keep the token choices
consistent across the component.
- Around line 90-97: The IconButton in the Drawer/DrawerContent component uses a
chat-specific aria-label (aria-label={t('Close chat')}); change the component
API to accept a configurable close label prop (e.g., closeLabel or
closeAriaLabel) with a sensible default (e.g., t('Close')) and use that prop for
aria-label on the IconButton; update the component props/interface (and any
defaultProps or destructuring in the Drawer/DrawerContent function) so consumers
can override the label when reusing Drawer outside the chat surface while
preserving the current translation-based default.
- Around line 81-89: The pill element in Drawer (the Box inside the Drawer
component) appears to be a drag handle but has no swipe behavior; either make it
clearly decorative by marking the Box as non-interactive (add
aria-hidden="true", role="presentation" and sx={{ pointerEvents: 'none' }}) and
update its styling comment to indicate it's purely visual, or replace the mobile
Drawer with MUI's SwipeableDrawer and wire the existing onClose/onOpen handlers
so the pill actually supports swipe-to-close; locate the Box in Drawer.tsx (the
Box with width:48 height:4 borderRadius:9999) and apply one of these two
changes.

In `@libs/journeys/ui/src/components/Message/Message.tsx`:
- Around line 36-57: The Box in Message.tsx uses hard-coded color literals in
its sx prop (bgcolor, color) which bypass the MUI theme; replace those literals
by reading from the theme palette or a shared design token module: update the sx
to be a function receiving theme and use
theme.palette.<semanticKey>.main/contrastText (or import shared constants like
MESSAGE_TOKENS.userBg/assistantBg/plainAssistantBg) and swap '#6D28D9',
'#f5f5f5', '#1a1a1a' and the rgba value with those theme/token references so
dark mode and white‑labeling work consistently; create or reuse a small
constants file (e.g., MESSAGE_TOKENS) if palette keys aren’t yet available and
use those tokens in the sx logic that currently branches on isUser and
isPlainAssistant.

In `@libs/journeys/ui/src/components/PromptInput/PromptInput.tsx`:
- Around line 111-157: The component PromptInput.tsx currently uses hardcoded
hex colors in the sx props and nested selectors (e.g., '& .MuiInputBase-input',
'& .MuiInputBase-input::placeholder', and the IconButton sx blocks for the
submit/stop buttons); replace those literal color strings (like '#1a1a1a',
'#9e9e9e', '#6D28D9', '#5B21B6', '#e0e0e0', '#999', '#666') with values resolved
from the MUI theme (e.g., theme.palette.* tokens or a dedicated chat palette
extension) by accessing theme in the sx callbacks or using classes that map to
palette tokens so the InputBase placeholder, input text color, button
background, hover and disabled states all come from theme.palette rather than
hardcoded hex values.
- Line 38: The textareaRef created with useRef<HTMLTextAreaElement>(null) is
never read; remove the unused textareaRef and its wiring to TextField.inputRef
and also remove the unused useRef import from the module, or alternatively
actually use textareaRef (for example to call textareaRef.current?.focus() after
send/mount) — update the component to either eliminate textareaRef and the
inputRef prop on TextField or implement the focus/DOM access logic where needed
(reference symbols: textareaRef, TextField.inputRef, useRef).

In `@libs/journeys/ui/src/components/Response/Response.tsx`:
- Around line 11-16: The Response component renders unmodified markdown via the
Markdown element (in Response.tsx) which can produce unsafe links/images; update
Response to pass a typed components override and sanitization: provide a custom
components.a that forces target="_blank" and rel="noopener noreferrer ugc",
implement urlTransform or use rehype-sanitize to block javascript: and other
unsafe schemes, and replace or disable the image renderer (components.img) to
either disallow remote images or validate/allowlist sources; ensure the
props/types for the Markdown components override are strongly typed so the
change is compile-time safe.

In `@libs/journeys/ui/src/libs/isLastCard/useIsLastCard.ts`:
- Around line 7-9: The current unchecked cast of activeBlock to
TreeBlock<StepFields> can allow non-step blocks into getNextBlock; add a runtime
narrowing guard before using activeBlock as a Step block: check the block
kind/fields (e.g., activeBlock && activeBlock.fields?.type === 'step' or use an
existing isStepBlock type guard) and only call getNextBlock or treat it as
TreeBlock<StepFields> when that check passes, otherwise return/handle the
non-step case early; update the variable use in useIsLastCard (the activeBlock
binding) so the cast is removed and the narrowed branch uses a proper
StepFields-typed variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d54435a1-3b51-497f-a8cb-1adb131b2cc2

📥 Commits

Reviewing files that changed from the base of the PR and between 762cfe0 and 95d6871.

⛔ Files ignored due to path filters (5)
  • apps/journeys/__generated__/GetJourney.ts is excluded by !**/__generated__/**
  • apps/journeys/__generated__/JourneyFields.ts is excluded by !**/__generated__/**
  • libs/journeys/ui/src/libs/JourneyProvider/__generated__/JourneyFields.ts is excluded by !**/__generated__/**
  • libs/journeys/ui/src/libs/useJourneyQuery/__generated__/GetJourney.ts is excluded by !**/__generated__/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (38)
  • apps/journeys/pages/_app.tsx
  • apps/journeys/pages/api/chat/index.spec.ts
  • apps/journeys/pages/api/chat/index.ts
  • apps/journeys/setupTests.ts
  • apps/journeys/src/components/Conductor/Conductor.apologistChat.spec.tsx
  • apps/journeys/src/components/Conductor/Conductor.spec.tsx
  • apps/journeys/src/components/Conductor/Conductor.tsx
  • libs/journeys/ui/src/components/Actions/Actions.tsx
  • libs/journeys/ui/src/components/Actions/index.ts
  • libs/journeys/ui/src/components/AiChat/AiChat.tsx
  • libs/journeys/ui/src/components/AiChat/index.ts
  • libs/journeys/ui/src/components/AiChatButton/AiChatButton.tsx
  • libs/journeys/ui/src/components/AiChatButton/index.ts
  • libs/journeys/ui/src/components/Card/OverlayContent/OverlayContent.tsx
  • libs/journeys/ui/src/components/Card/utils/getFooterElements/getFooterElements.ts
  • libs/journeys/ui/src/components/Card/utils/getFooterElements/index.ts
  • libs/journeys/ui/src/components/ChatOverlay/ChatOverlay.tsx
  • libs/journeys/ui/src/components/ChatOverlay/index.ts
  • libs/journeys/ui/src/components/Conversation/Conversation.tsx
  • libs/journeys/ui/src/components/Conversation/index.ts
  • libs/journeys/ui/src/components/Drawer/Drawer.tsx
  • libs/journeys/ui/src/components/Drawer/index.ts
  • libs/journeys/ui/src/components/Message/Message.tsx
  • libs/journeys/ui/src/components/Message/index.ts
  • libs/journeys/ui/src/components/PinnedChatBar/PinnedChatBar.tsx
  • libs/journeys/ui/src/components/PinnedChatBar/index.ts
  • libs/journeys/ui/src/components/PromptInput/PromptInput.tsx
  • libs/journeys/ui/src/components/PromptInput/index.ts
  • libs/journeys/ui/src/components/Response/Response.tsx
  • libs/journeys/ui/src/components/Response/index.ts
  • libs/journeys/ui/src/components/StepFooter/StepFooter.apologistChat.spec.tsx
  • libs/journeys/ui/src/components/StepFooter/StepFooter.tsx
  • libs/journeys/ui/src/libs/JourneyProvider/journeyFields.tsx
  • libs/journeys/ui/src/libs/isLastCard/index.ts
  • libs/journeys/ui/src/libs/isLastCard/useIsLastCard.ts
  • libs/locales/en/libs-journeys-ui.json
  • libs/shared/ui/src/libs/getLaunchDarklyClient/getLaunchDarklyClient.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/journeys/src/components/Conductor/Conductor.spec.tsx

Comment thread apps/journeys/pages/api/chat/index.ts
Comment thread apps/journeys/pages/api/chat/index.ts
Comment thread libs/journeys/ui/src/components/Actions/Actions.tsx
Comment thread libs/journeys/ui/src/components/AiChat/AiChat.tsx
Comment thread libs/journeys/ui/src/components/AiChat/AiChat.tsx Outdated
Comment thread libs/journeys/ui/src/components/ChatOverlay/ChatOverlay.tsx
Copy link
Copy Markdown
Contributor

@edmonday edmonday left a comment

Choose a reason for hiding this comment

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

Review summary

Focused review on the two Critical items in /api/chat. Leaving the Concern-level items (LaunchDarkly timer/timeout, pervasive hardcoded hex colors, unused useIsLastCard, Conductor breakpoint duplication) for a follow-up pass — CodeRabbit has already raised several of them.

Critical (please address before flipping the flag on in prod):

  • apps/journeys/pages/api/chat/index.ts:112 — no runtime validation of req.body; unbounded LLM spend + client-injected system role.
  • apps/journeys/pages/api/chat/index.ts:186language is unvalidated user input concatenated into the system prompt; prompt-injection is trivial once the flag is on.

Both are public via POST — the route has no auth, middleware excludes /api/*, and the journeys app only has anonymous Firebase visitors. The flag gate is the only thing between an attacker and an LLM bill.

@jaco-brink
Copy link
Copy Markdown
Collaborator Author

Thanks @edmonday — responding to the focused review.

Critical (both addressed as planned-and-tracked):

  • apps/journeys/pages/api/chat/index.ts:112 (req.body validation + unbounded spend + client-injected system role) → NES-1579 (zod schema, maxTokens, message-count + char caps) and NES-1580 (Upstash per-IP/per-uid rate-limit via middleware.ts matcher).
  • apps/journeys/pages/api/chat/index.ts:186 (unvalidated language in system prompt) → NES-1579 constrains the shape; NES-1582 (server-derived contextText — client sends journeyId/blockId only) kills the broader prompt-injection surface.

All three are M4 tickets — the full M4 'Public-launch safety floor' milestone lands before the apologistChat flag flips public in M5. Agreed the flag gate is currently the only thing between an attacker and the bill; that's why M1 ships internal-only (Jaco / Sway / Aaron) and the flag stays off for anonymous visitors until M4 is complete. Inline replies posted on both threads and resolved.

Concerns (parked, not this PR):

  • LaunchDarkly timer leak + failed-client cache → CodeRabbit raised both; no Linear ticket yet. Will file under Cleanup & Tech Debt.
  • Hardcoded hex colors, unused useIsLastCard, Conductor breakpoint duplication → will triage into Cleanup & Tech Debt after this merges so the backlog is visible.

Happy to block merge on any of these if you'd prefer — otherwise keeping Slice 1 tight and rolling the safety + cleanup work forward as planned.

…nt (NES-1554)

- Actions: wrap clipboard writeText in try/catch so permission/focus
  rejections don't surface as unhandled promise rejections.
- AiChat: translate TypingIndicator aria-label via t(), add
  "Assistant is typing" to libs-journeys-ui en locale.
- AiChat: remove handleHandleKeyDown — MUI IconButton already fires
  onClick on Space/Enter, so the extra onKeyDown caused a double-toggle
  that left mobile keyboard users unable to collapse the chat.
- getLaunchDarklyClient: clear the cached client on init failure so
  subsequent calls can retry with a fresh instance rather than re-racing
  the same stuck instance and paying the 10s timeout on every request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jaco-brink
Copy link
Copy Markdown
Collaborator Author

Review feedback addressed (f087f1f)

All 9 unresolved threads now closed — all 9 resolved, 4 fixed in this PR, 5 deferred to planned/cleanup tickets with explicit links.

Fixed in this PR:

  • libs/journeys/ui/src/components/Actions/Actions.tsx — wrapped navigator.clipboard.writeText in try/catch with console.warn on failure.
  • libs/journeys/ui/src/components/AiChat/AiChat.tsxTypingIndicator aria-label now translated via t('Assistant is typing'); key added to libs-journeys-ui en locale.
  • libs/journeys/ui/src/components/AiChat/AiChat.tsx — removed handleHandleKeyDown + onKeyDown prop on mobile drag handle (was net-zero double-toggling, breaking mobile keyboard collapse). Dropped unused KeyboardEvent import.
  • libs/shared/ui/src/libs/getLaunchDarklyClient/getLaunchDarklyClient.ts — clear globalForLD.__launchDarklyClient on init failure so subsequent calls can retry with a fresh client.

Deferred to planned safety work (M4 — Public-launch safety floor):

Rationale: apologist chat is gated behind internal-only apologistChat flag (Jaco/Sway/Aaron) for Slice 1. Full M4 safety floor lands before the flag flips public in M5.

Deferred to Cleanup & Tech Debt tickets:

  • Footer spacing reserves button space when apologistChat flag off → NES-1603 (plus coordination note on NES-1556 since per-card showAssistant refactor touches the same surface).
  • ChatOverlay missing Escape/focus-trap/aria-modalNES-1604. Rationale: M2 UX iteration with Aaron still active; MUI Modal swap carries regression risk against validated behaviour, so it gets a focused PR with deliberate QA rather than a drive-by change here.
  • getLaunchDarklyClient setTimeout leak → NES-1605. Shared infra; deserves its own PR with a spec exercising success + timeout paths. (Companion cache-on-failure issue is fixed here — 1-line change, unambiguous impact.)

Note:

  • Pre-existing lint (22) CI failure is unrelated to this commit — will look at separately.
  • Upstream branch already had a merge from main (95d6871b7). My fix commit rebased cleanly on top.

Copy link
Copy Markdown
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 (1)
libs/journeys/ui/src/components/AiChat/AiChat.tsx (1)

62-97: Cancel the rAF loop once the typewriter reaches the target.

The effect keeps scheduling requestAnimationFrame(tick) for every frame as long as enabled is true, even after revealed >= targetRef.current.length and streaming has stopped. setRevealed bails out of re-renders (same value), but the rAF scheduling itself continues for the lifetime of the last assistant bubble — wasteful CPU/battery for an otherwise idle chat. Stop the loop when fully revealed and streaming is done, and resume only if the target grows again.

♻️ Proposed refactor
-  useEffect(() => {
-    if (!enabled) return
-    let raf = 0
-    let last = performance.now()
-    const tick = (now: number): void => {
-      const deltaSec = (now - last) / 1000
-      last = now
-      setRevealed((prev) => {
-        const targetLength = targetRef.current.length
-        if (prev >= targetLength) return prev
-        const step = Math.max(
-          1,
-          Math.floor(TYPEWRITER_CHARS_PER_SEC * deltaSec)
-        )
-        return Math.min(targetLength, prev + step)
-      })
-      raf = requestAnimationFrame(tick)
-    }
-    raf = requestAnimationFrame(tick)
-    return () => cancelAnimationFrame(raf)
-  }, [enabled])
+  useEffect(() => {
+    if (!enabled) return
+    let raf = 0
+    let last = performance.now()
+    const tick = (now: number): void => {
+      const deltaSec = (now - last) / 1000
+      last = now
+      let done = false
+      setRevealed((prev) => {
+        const targetLength = targetRef.current.length
+        if (prev >= targetLength) {
+          done = !isStreaming
+          return prev
+        }
+        const step = Math.max(
+          1,
+          Math.floor(TYPEWRITER_CHARS_PER_SEC * deltaSec)
+        )
+        return Math.min(targetLength, prev + step)
+      })
+      if (done) return
+      raf = requestAnimationFrame(tick)
+    }
+    raf = requestAnimationFrame(tick)
+    return () => cancelAnimationFrame(raf)
+  }, [enabled, isStreaming])

Adding isStreaming to deps re-starts the loop if new chunks arrive after a pause.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/journeys/ui/src/components/AiChat/AiChat.tsx` around lines 62 - 97, The
useTypewriter effect currently keeps scheduling requestAnimationFrame even after
revealed >= target length and streaming has stopped; modify the effect in
useTypewriter so the tick function checks current target length and isStreaming
and calls cancelAnimationFrame(raf) / avoids requesting a new frame when
revealed >= targetLength && !isStreaming (i.e., stop the loop once fully
revealed and not streaming), and update the effect dependency array to include
isStreaming and target (so the loop restarts when streaming resumes or the
target grows); reference the revealed state, targetRef.current, isStreaming
param, and the raf variable/requestAnimationFrame/cancelAnimationFrame when
making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@libs/journeys/ui/src/components/AiChat/AiChat.tsx`:
- Around line 62-97: The useTypewriter effect currently keeps scheduling
requestAnimationFrame even after revealed >= target length and streaming has
stopped; modify the effect in useTypewriter so the tick function checks current
target length and isStreaming and calls cancelAnimationFrame(raf) / avoids
requesting a new frame when revealed >= targetLength && !isStreaming (i.e., stop
the loop once fully revealed and not streaming), and update the effect
dependency array to include isStreaming and target (so the loop restarts when
streaming resumes or the target grows); reference the revealed state,
targetRef.current, isStreaming param, and the raf
variable/requestAnimationFrame/cancelAnimationFrame when making this change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 80c629d1-47c9-4704-831a-784f2a0c21b6

📥 Commits

Reviewing files that changed from the base of the PR and between 95d6871 and f087f1f.

📒 Files selected for processing (4)
  • libs/journeys/ui/src/components/Actions/Actions.tsx
  • libs/journeys/ui/src/components/AiChat/AiChat.tsx
  • libs/locales/en/libs-journeys-ui.json
  • libs/shared/ui/src/libs/getLaunchDarklyClient/getLaunchDarklyClient.ts
✅ Files skipped from review due to trivial changes (1)
  • libs/locales/en/libs-journeys-ui.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • libs/shared/ui/src/libs/getLaunchDarklyClient/getLaunchDarklyClient.ts
  • libs/journeys/ui/src/components/Actions/Actions.tsx

@blacksmith-sh

This comment has been minimized.

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