Skip to content

feat(chat): add image preview dialog in message bubble component#22

Merged
Israeltheminer merged 2 commits into
mainfrom
update-ui
Dec 17, 2025
Merged

feat(chat): add image preview dialog in message bubble component#22
Israeltheminer merged 2 commits into
mainfrom
update-ui

Conversation

@Israeltheminer

@Israeltheminer Israeltheminer commented Dec 17, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added interactive image preview modal: Markdown images are now clickable and open a larger preview dialog for easier viewing.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 17, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The MarkdownImage component in message-bubble.tsx has been enhanced with an interactive modal preview feature. The image element is now wrapped in a Dialog component that opens on click, displaying a larger preview of the image. This replaces the previous static inline image rendering and introduces state management (isOpen) and an onOpenChange handler to control the modal's visibility.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Single file modification with localized scope
  • Standard Dialog component pattern implementation
  • Addition of straightforward state management for modal open/close
  • No complex logic or architectural changes

Possibly related PRs


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between d0df5a8 and 579d73a.

📒 Files selected for processing (1)
  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/figma_rules.mdc)

**/*.{tsx,jsx}: Avoid specifying font-['Inter:Regular',_sans-serif] as it should be the default
Only specify font-family when using non-default fonts like font-['Inter:Medium',_sans-serif]
Ensure font-family matches font-weight (Inter:Regular with font-normal, Inter:Medium with font-medium)
Use leading-normal instead of leading-[normal] in Tailwind classes
Use standard font size classes instead of arbitrary values: text-[12px]text-xs, text-[14px]text-sm, text-[16px]text-base, text-[18px]text-lg, text-[20px]text-xl, text-[24px]text-2xl
Use semantic spacing classes: p-[4px]p-1, p-[8px]p-2, m-[4px]m-1, m-[8px]m-2
Convert pixel values to rem using the 16px base for width and height measurements: w-[278px]w-[17.375rem], h-[48px]h-[3rem], min-w-[120px]min-w-[7.5rem], max-w-[400px]max-w-[25rem]
NEVER use hardcoded colors like text-gray-500, bg-gray-100, border-gray-200; ALWAYS use design system semantic colors: text-foreground for primary text, text-muted-foreground for secondary text and icons, bg-background for main backgrounds, bg-muted for subtle backgrounds and hover states, border-border for borders
ALWAYS use the Table component instead of custom flex layouts; use Table, TableHeader, TableBody, TableRow, TableHead, TableCell components with proper column widths using rem units and semantic colors

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
**/*

📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)

Use English only for ALL user-facing content including UI components, labels, buttons, dialogs, forms, toast messages, error messages, success messages, comments, documentation, README files, variable names, function names, and type names

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)

**/*.{ts,tsx,js,jsx}: Use Vercel AI SDK with OpenAI - import from 'ai' and '@ai-sdk/openai', never use raw OpenAI SDK or OpenRouter
Never hallucinate API keys - always use environment variables and existing .env configuration
Use camelCase for function names (e.g., getUserData)
Use SCREAMING_SNAKE_CASE for constants (e.g., API_BASE_URL, MAX_RETRIES)
Use feature flags with enums (TypeScript) or const objects (JavaScript) with UPPERCASE_WITH_UNDERSCORE naming
Implement error handling with try-catch pattern: check for result.error and display descriptive toast messages using result.error as title

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
**/app/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)

In Next.js App Router, use page.tsx as server components by default; use 'use client' only for interactions and state

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/workspace_rules.mdc)

**/*.{ts,tsx}: Use kebab-case for file names (e.g., user-profile.tsx)
Use PascalCase for component names (e.g., UserProfile)
Use descriptive messages as toast title (never generic 'Error'), with optional description for additional context only
Follow component structure: 'use client' directive, imports, interface Props, hooks, effects, event handlers, then render
Prioritize data fetching methods in order: Server Actions (preferred), Route Handlers (when needed), Client-side (minimal use)
Use React.memo for expensive components to optimize performance
Use Next.js Image component for all images instead of native img tags
Use dynamic imports for code splitting

Files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/next-best-practice.mdc:0-0
Timestamp: 2025-07-03T08:43:49.346Z
Learning: Applies to **/*.{tsx,jsx} : Use Next.js Image component for images
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursorrules:0-0
Timestamp: 2025-10-11T11:46:02.452Z
Learning: Applies to **/*.{tsx,jsx} : Use Next.js Image component for all images instead of <img>
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-19T15:29:09.401Z
Learning: Applies to app/components/**/*.{js,jsx,ts,tsx} : Use Next.js Image component for all images.
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:51.424Z
Learning: Applies to **/*.{ts,tsx} : Use Next.js Image component for all images instead of native img tags
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursorrules:0-0
Timestamp: 2025-08-21T14:59:36.874Z
Learning: Applies to {app,components}/**/*.{tsx,jsx} : Use Next.js Image component for all images
📚 Learning: 2025-10-11T11:46:02.452Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursorrules:0-0
Timestamp: 2025-10-11T11:46:02.452Z
Learning: Applies to **/*.{tsx,jsx} : Use Next.js Image component for all images instead of <img>

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-07-19T15:29:09.401Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursorrules:0-0
Timestamp: 2025-07-19T15:29:09.401Z
Learning: Applies to app/components/**/*.{js,jsx,ts,tsx} : Use Next.js Image component for all images.

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-12-02T08:13:51.424Z
Learnt from: CR
Repo: tale-project/tale PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-12-02T08:13:51.424Z
Learning: Applies to **/*.{ts,tsx} : Use Next.js Image component for all images instead of native img tags

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-07-03T08:43:49.346Z
Learnt from: CR
Repo: talecorp/poc PR: 0
File: .cursor/rules/next-best-practice.mdc:0-0
Timestamp: 2025-07-03T08:43:49.346Z
Learning: Applies to **/*.{tsx,jsx} : Use Next.js Image component for images

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-08-21T14:59:36.874Z
Learnt from: CR
Repo: talecorp/lanserhof PR: 0
File: .cursorrules:0-0
Timestamp: 2025-08-21T14:59:36.874Z
Learning: Applies to {app,components}/**/*.{tsx,jsx} : Use Next.js Image component for all images

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Use the `api` object from `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-11-30T03:53:00.316Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-11-30T03:53:00.316Z
Learning: Applies to convex/**/*.ts : Use the `internal` object from `convex/_generated/api.ts` to call internal functions registered with `internalQuery`, `internalMutation`, or `internalAction`

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-10-03T11:34:20.628Z
Learnt from: CR
Repo: talecorp/poc2 PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-10-03T11:34:20.628Z
Learning: Applies to convex/**/*.{ts,js} : Register internal functions with internalQuery, internalMutation, and internalAction (imported from ./_generated/server)

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
📚 Learning: 2025-11-30T12:29:39.745Z
Learnt from: CR
Repo: tale-project/poc2 PR: 0
File: .cursor/rules/workspace_rules.mdc:0-0
Timestamp: 2025-11-30T12:29:39.745Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use Next.js `Image` component for all images instead of native `<img>` tags

Applied to files:

  • services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx
🪛 Biome (2.1.2)
services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx

[error] 468-474: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build Docker Image
🔇 Additional comments (1)
services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx (1)

35-35: LGTM!

The Dialog component imports are appropriate for the new image preview feature.

Comment thread services/platform/app/(app)/dashboard/[id]/chat/components/message-bubble.tsx Outdated
@Israeltheminer Israeltheminer merged commit a0d0243 into main Dec 17, 2025
2 checks passed
@Israeltheminer Israeltheminer deleted the update-ui branch December 17, 2025 12:25
larryro added a commit that referenced this pull request May 17, 2026
Closes #3, #19, #20, #21, #22, #23, #24, #25, #26, #29, #39 — frontend
audio UX + resolver tests.

- `message-bubble.tsx` renders a single stable `<VoiceOutputIndicator>`
  per assistant message instead of three separate mounts (inline-
  streaming + two toolbar copies). The previous shape unmounted the
  inline indicator at streaming-end → triggered `stop()` → mounted a
  fresh toolbar indicator with a `mountTimeRef` captured AFTER all
  chunks were created → auto-play short-circuited and the user heard
  silence at the stream-end boundary. The single mount keeps
  `mountTimeRef` stable across both phases. (#3)
- `use-voice-output.ts` tracks every retry `setTimeout` id in a `Set`
  ref and clears them on unmount + on message change. The prior code
  let the 1.5s backoff timer fire after unmount and re-invoke
  `synthesize` against a dead component. (#19)
- `use-voice-output.ts` caps the synthesis queue at
  `MAX_TTS_QUEUE_DEPTH = 50`. When full, drops the new task and
  surfaces `QUEUE_OVERFLOW` via the error sink so the user sees why
  playback paused. `MAX_IN_FLIGHT` previously throttled concurrent
  dispatch but did not bound queue depth. (#20)
- `use-voice-output.ts` catch branch now falls back to
  `'UNKNOWN_NETWORK'` when `extractConvexErrorCode` returns undefined
  (network drop, action timeout). Previously the only signal was
  `console.error`; the indicator stayed stuck with no actionable
  message. (#21)
- `use-voice-output-player.ts` re-calls `primeAudio(el)` at the start
  of every `play()` invocation and drops the `el.load()` in `stop()`.
  Together these stop iOS Safari from expiring the user-activation
  token between messages of a session. (#22)
- `voice-output-context.tsx` + `prime-audio.ts`: per-provider audio
  element ownership. Each `<VoiceOutputProvider>` constructs its own
  `<audio>` via `useMemo` and exposes it via `useVoiceAudioElement()`.
  The prior module-level singleton meant arena split-view's two
  providers stomped each other's `src` mid-playback. `primeAudio(el?)`
  now takes the element to pre-warm; callers without a provider scope
  (settings page) call it with `undefined` and only the AudioContext
  is banked. (#23)
- `voice-output-indicator.tsx` classifies error codes into
  `retryable | config | terminal`. Config codes (NO_PROVIDER,
  HOST_POLICY, forbidden) render a `<Link>` to Settings → AI
  providers; terminal codes (BUDGET_EXCEEDED, QUEUE_OVERFLOW, char-
  cap) render a non-interactive `<Badge>`. Only retryable codes keep
  the click-to-retry button. Stops the tap→fail→tap→fail loop on
  unrecoverable errors. (#24)
- `voice-output-announcer.tsx` now reads `{ state, errorCode }` from
  the announcer store and speaks the per-code reason on transitions
  into `'error'` (e.g. "Voice provider not configured"). Screen-
  reader users on touch devices — where the indicator's per-code
  tooltip is unreachable — now hear the actionable reason instead of
  the generic "Voice output failed". (#25)
- `personalization-settings.tsx` composes the `providerUnavailable`
  hint into the Switch's `description` prop (a ReactNode) when
  `providerAvailable === false`. The hint now lands in the same
  `aria-describedby` block as the base description, so SR focus on
  the Switch reads it. The duplicate sibling `<Text>` is removed. (#26)
- `voice-output-announcer.tsx` drains announcements through a small
  queue with a 1500ms hold per entry. Rapid transitions
  (playing → blocked → error in <1500ms) no longer clobber the
  previous text mid-utterance; each entry plays in order. (#39)
- `resolve_tts_model.test.ts` adds the missing call-contract assertions
  (tag=text-to-speech, orgSlug propagation, providerName propagation
  on a pinned-provider call) and three failure-path tests that pin
  the resolver's re-throw behaviour for UNKNOWN_MODEL,
  UNKNOWN_PROVIDER, and plain rejections. Without these, a regression
  that hard-coded `tag: 'chat'` or dropped `orgSlug` would have passed
  every prior test silently. (#29)
- i18n: `voiceOutputErrorConfig`, `voiceOutputErrorOpenSettings`,
  `voiceOutputErrorQueueOverflow`, `voiceOutputErrorNetwork` added to
  en/de/fr. The pre-existing orphan `voiceOutputErrorProvider` is
  removed (superseded by `voiceOutputErrorConfig`).
larryro added a commit that referenced this pull request May 17, 2026
Closes #3, #19, #20, #21, #22, #23, #24, #25, #26, #29, #39 — frontend
audio UX + resolver tests.

- `message-bubble.tsx` renders a single stable `<VoiceOutputIndicator>`
  per assistant message instead of three separate mounts (inline-
  streaming + two toolbar copies). The previous shape unmounted the
  inline indicator at streaming-end → triggered `stop()` → mounted a
  fresh toolbar indicator with a `mountTimeRef` captured AFTER all
  chunks were created → auto-play short-circuited and the user heard
  silence at the stream-end boundary. The single mount keeps
  `mountTimeRef` stable across both phases. (#3)
- `use-voice-output.ts` tracks every retry `setTimeout` id in a `Set`
  ref and clears them on unmount + on message change. The prior code
  let the 1.5s backoff timer fire after unmount and re-invoke
  `synthesize` against a dead component. (#19)
- `use-voice-output.ts` caps the synthesis queue at
  `MAX_TTS_QUEUE_DEPTH = 50`. When full, drops the new task and
  surfaces `QUEUE_OVERFLOW` via the error sink so the user sees why
  playback paused. `MAX_IN_FLIGHT` previously throttled concurrent
  dispatch but did not bound queue depth. (#20)
- `use-voice-output.ts` catch branch now falls back to
  `'UNKNOWN_NETWORK'` when `extractConvexErrorCode` returns undefined
  (network drop, action timeout). Previously the only signal was
  `console.error`; the indicator stayed stuck with no actionable
  message. (#21)
- `use-voice-output-player.ts` re-calls `primeAudio(el)` at the start
  of every `play()` invocation and drops the `el.load()` in `stop()`.
  Together these stop iOS Safari from expiring the user-activation
  token between messages of a session. (#22)
- `voice-output-context.tsx` + `prime-audio.ts`: per-provider audio
  element ownership. Each `<VoiceOutputProvider>` constructs its own
  `<audio>` via `useMemo` and exposes it via `useVoiceAudioElement()`.
  The prior module-level singleton meant arena split-view's two
  providers stomped each other's `src` mid-playback. `primeAudio(el?)`
  now takes the element to pre-warm; callers without a provider scope
  (settings page) call it with `undefined` and only the AudioContext
  is banked. (#23)
- `voice-output-indicator.tsx` classifies error codes into
  `retryable | config | terminal`. Config codes (NO_PROVIDER,
  HOST_POLICY, forbidden) render a `<Link>` to Settings → AI
  providers; terminal codes (BUDGET_EXCEEDED, QUEUE_OVERFLOW, char-
  cap) render a non-interactive `<Badge>`. Only retryable codes keep
  the click-to-retry button. Stops the tap→fail→tap→fail loop on
  unrecoverable errors. (#24)
- `voice-output-announcer.tsx` now reads `{ state, errorCode }` from
  the announcer store and speaks the per-code reason on transitions
  into `'error'` (e.g. "Voice provider not configured"). Screen-
  reader users on touch devices — where the indicator's per-code
  tooltip is unreachable — now hear the actionable reason instead of
  the generic "Voice output failed". (#25)
- `personalization-settings.tsx` composes the `providerUnavailable`
  hint into the Switch's `description` prop (a ReactNode) when
  `providerAvailable === false`. The hint now lands in the same
  `aria-describedby` block as the base description, so SR focus on
  the Switch reads it. The duplicate sibling `<Text>` is removed. (#26)
- `voice-output-announcer.tsx` drains announcements through a small
  queue with a 1500ms hold per entry. Rapid transitions
  (playing → blocked → error in <1500ms) no longer clobber the
  previous text mid-utterance; each entry plays in order. (#39)
- `resolve_tts_model.test.ts` adds the missing call-contract assertions
  (tag=text-to-speech, orgSlug propagation, providerName propagation
  on a pinned-provider call) and three failure-path tests that pin
  the resolver's re-throw behaviour for UNKNOWN_MODEL,
  UNKNOWN_PROVIDER, and plain rejections. Without these, a regression
  that hard-coded `tag: 'chat'` or dropped `orgSlug` would have passed
  every prior test silently. (#29)
- i18n: `voiceOutputErrorConfig`, `voiceOutputErrorOpenSettings`,
  `voiceOutputErrorQueueOverflow`, `voiceOutputErrorNetwork` added to
  en/de/fr. The pre-existing orphan `voiceOutputErrorProvider` is
  removed (superseded by `voiceOutputErrorConfig`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant