[RSM] Show summaries for episodes where available#5276
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an AI-generated episode summary surface to the Episode Details screen by fetching an existing -meta.json sidecar for generated transcripts and displaying it via a new “View summary” banner that opens a bottom sheet.
Changes:
- Extend
TranscriptManager/TranscriptManagerImplwith aloadSummaryText(episodeUuid)API that fetches and parsessummaryfrom the transcript-meta.json. - Add Episode Details UI for summaries: a new ComposeView slot, a “View summary” banner, and a
BaseDialogFragmentbottom sheet to render the summary content. - Add localization strings for the banner and bottom-sheet title.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/transcript/TranscriptManagerImpl.kt | Implements summary loading by locating a generated transcript and fetching/parsing its -meta.json. |
| modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/transcript/TranscriptManager.kt | Adds a new repository API for loading episode summary text. |
| modules/services/localization/src/main/res/values/strings.xml | Adds localized strings for “View summary” and the summary sheet title. |
| modules/features/podcasts/src/main/res/layout/fragment_episode.xml | Adds a new ComposeView (episodeSummary) and moves transcript below it. |
| modules/features/podcasts/src/main/res/layout-land/fragment_episode.xml | Same as portrait layout for landscape. |
| modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/episode/SummaryExcerptBanner.kt | New Compose banner component for the summary call-to-action. |
| modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/episode/SummaryBottomSheet.kt | New bottom sheet dialog that renders a markdown-like summary via HTML. |
| modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/episode/EpisodeFragmentViewModel.kt | Loads summary text alongside transcripts and exposes it as a StateFlow. |
| modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/episode/EpisodeFragment.kt | Wires the banner into the episode details UI and opens the summary bottom sheet on tap. |
|
Instead of two rows I would suggest two collumes as it would take up less space, for example. (ai icon) Summary || (icon*) Transcript |
68420f4 to
477f062
Compare
Thanks for your feedback, this approach is under active discussion with our design team 🙏 |
|
Version |
- Clear stale summary and only mark UUID after successful load so
retries work and old data doesn't flash on episode change
- Reuse loadLocalTranscripts() in loadSummaryText() to keep transcript
availability semantics consistent
- Use removeSuffix(".vtt") instead of replace() for the meta URL
- Wrap ResponseBody in use {} for proper resource cleanup
- Replace org.json.JSONObject with Moshi for unit-test compatibility
- Add unit tests for loadSummaryText: success, no generated transcript,
blank summary, service failure, and timeout
- Fix spotless import ordering
… string, fix merge conflict
8ca8580 to
be80772
Compare
Generated by 🚫 Danger |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (3)
modules/features/podcasts/src/main/res/layout/fragment_episode.xml:307
episodeContentTabsComposeView is always visible in XML now. In the non-AI path the Compose content can be effectively empty (no transcript/chat), but still applies top padding, leaving a blank gap above the show notes. Consider restoringandroid:visibility="gone"and toggling it visible only when there’s content to show (or conditionally omit padding when no children are rendered).
<androidx.compose.ui.platform.ComposeView
android:id="@+id/episodeContentTabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lblDate" />
modules/features/podcasts/src/main/res/layout-land/fragment_episode.xml:254
- Same as portrait:
episodeContentTabsComposeView is always visible. When there’s no transcript/chat to render (non-AI path), the Compose content still reserves padding which can create an empty vertical gap. Consider setting initial visibility togoneand only showing it when needed, or avoid padding when there’s no visible content.
<androidx.compose.ui.platform.ComposeView
android:id="@+id/episodeContentTabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lblDate" />
modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/episode/EpisodeFragment.kt:931
formattedNotes?.let { loadShowNotes(it) }is now called immediately after adding the WebView, butcreateShowNotesWebView()also callsloadShowNotes()again later in the method. This results in duplicateloadDataWithBaseURLcalls when the WebView is first created. Consider removing one of the calls to avoid redundant work.
Timber.e(e)
binding?.webViewLoader?.hide()
val errorMessage = resources.getString(if (e.message?.contains("webview", ignoreCase = true) == true) LR.string.error_webview_not_installed else LR.string.error_loading_show_notes)
binding?.webViewErrorText?.text = errorMessage
|
@geekygecko could you please review this PR as Michal's review is not valid as he doesn't hold write rights on this repo any more |
geekygecko
left a comment
There was a problem hiding this comment.
This looks great. I only had some minor suggestions which I have changed in a PR to help us get through the PRs quicker. #5335
| _selectedContentTab.value = tab | ||
| } | ||
|
|
||
| data class DateDurationInfo(val publishedDate: java.util.Date?, val durationMs: Long) |
There was a problem hiding this comment.
| data class DateDurationInfo(val publishedDate: java.util.Date?, val durationMs: Long) | |
| data class DateDurationInfo(val publishedDate: Date?, val durationMs: Long) |
| enum class EpisodeContentTab { DESCRIPTION, SUMMARY } | ||
|
|
||
| private val _selectedContentTab = MutableStateFlow(EpisodeContentTab.DESCRIPTION) | ||
| val selectedContentTab = _selectedContentTab.asStateFlow() |
There was a problem hiding this comment.
Just an idea but how about having a page state rather than one for the tab and another for the date and duration? If we add more then it will also be easier.
| @StringRes val labelResId: Int, | ||
| val onClick: () -> Unit, | ||
| @DrawableRes val iconResId: Int? = null, | ||
| @DrawableRes val trailingIconResId: Int? = null, |
There was a problem hiding this comment.
These don't seem to be being used. Should we remove them or add them to the preview?
| visible = selectedTab == EpisodeFragmentViewModel.EpisodeContentTab.SUMMARY && summaryText != null, | ||
| enter = BannerEnterTransition, | ||
| exit = BannerExitTransition, | ||
| ) { |
There was a problem hiding this comment.
The content sliding down doesn’t look great, and the similar button tabs on the podcast page don’t use this behaviour either. Could we remove this animation for consistency?
Screen.Recording.2026-05-23.at.7.52.05.am.mov
| ) | ||
| HtmlText( | ||
| html = markdownToHtml(summaryText.orEmpty()), | ||
| color = MaterialTheme.theme.colors.primaryText02, |
There was a problem hiding this comment.
| color = MaterialTheme.theme.colors.primaryText02, | |
| color = MaterialTheme.theme.colors.primaryText01, |
The text on the description page is darker and easier to read on the light theme. Maybe we should make this match.
| } | ||
| }, | ||
| ), | ||
| } |
|
I just realised this might be missing analytics as tapping summary tab didn't trigger anything. This isn't included in my PR. |
|
thanks for the changes @geekygecko , i'll raise new PR with the analytics! |

Description
This PR is part of the Radical Speed Month initiative, Pocket Casts AI project (phcsdm-1j8-p2)
Adds an AI-generated episode summary surface to the episode details screen. When an episode has a generated transcript, we fetch the existing
-meta.jsonsidecar from S3 (no new backend work — the serverlambdapipeline already produces these) and display the summary inline in a tabbed UI alongside Description and Transcript tabs. The summary markdown is rendered as formatted HTML using the existingHtmlTextcomposable. When AI summaries are enabled, an "Ask this episode" input-style banner is shown above the tabs for quick access to the episode chat.In sync with the Chat with Episode story, we agreed to have the following designs: p1779220475409719/1779212859.563619-slack-C05RR9P9RAT
Testing Instructions
Screenshots or Screencast
Screen_recording_20260522_173934.mp4
Checklist
./gradlew spotlessApplyto automatically apply formatting/linting)modules/services/localization/src/main/res/values/strings.xmlI have tested any UI changes...