Repository navigation
fix(ai-components): replace SpokenMessageText with a standalone useHeardText hook - #3344
Conversation
…useHeardText hook ai-components must work without the core SDK, and SpokenMessageText rendered through the core Message components. Remove SpokenMessageText, SpokenTextProvider, SpokenTextContext and useSpokenTextContext, and keep only useHeardText, which now depends on React alone: it takes the text, a heard(now) progress function (Unicode code points) and an optional subscribe, generating and live, and returns the heard text, whether it is paced, and skip(). A drop-in for the StreamedMessageText slot is a recipe in the docs.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
Size Change: -4.39 kB (-0.37%) Total Size: 1.18 MB 📦 View Changed
ℹ️ View Unchanged
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## release-v14 #3344 +/- ##
===============================================
- Coverage 86.84% 86.47% -0.38%
===============================================
Files 526 541 +15
Lines 16699 17590 +891
Branches 5251 5421 +170
===============================================
+ Hits 14503 15211 +708
- Misses 2196 2379 +183 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
## [14.14.1](v14.14.0...v14.14.1) (2026-10-22) ### Bug Fixes * **ai-components:** replace SpokenMessageText with a standalone useHeardText hook ([#3344](#3344)) ([ad16506](ad16506)), closes [#3343](#3343) [GetStream/getstream.io#761](https://github.com/GetStream/getstream.io/issues/761) [#3343](#3343) [#3339](#3339)
|
🎉 This PR is included in version 14.14.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
…inity as fully heard (#3349) ### Goal Bring the `useHeardText` fixes from the v15 port (#3348) to the v14 line, which shipped the hook in 14.14.1 (#3343, #3344) and shares the same issues. When the same mounted component rendered a new reply, the hook kept the previous reply's reveal length, `skip()` and `live`, so the new text could appear already revealed. `chars: Infinity` revealed nothing, contrary to the type doc. The docs also suggested returning `undefined` for text the voice source does not know yet, which shows the full text before it is heard and defeats pacing. ### Implementation details Cherry-pick of `835c3eb66` from `feat/ai-components-use-heard-text`. The hook and its tests were byte-identical between the two lines before that commit, and the README hunks applied without conflict. - The reveal state now records the text it refers to. When `text` is replaced by a different text, the reveal starts over: the length shown and `skip()` are reset, and `live` is read again. Growing or truncating the text keeps the reveal, capped at the new length. The reset happens during render, so the new text never renders with the previous reveal. - `chars: Infinity` reveals the whole text; NaN, negative and non-numeric counts still reveal nothing. - Docs (type doc and `README.md`): `undefined` is only for a text that is not spoken at all, and a text that will be spoken but is not known yet must return `{ chars: 0, done: false }`. `subscribe` is bound once (remount with a `key` to switch sources) and is the only wake-up for a settled hook. `heard` is called during render, so it must be free of side effects. - 6 new tests in `useHeardText.test.ts` cover the new-text reset, truncation, `skip()` and `live` reset, and `Infinity`. No public API change: the exported types and signature are unchanged. ### UI Changes None. The hook's reveal behaviour changes only for a component that renders a different text, and for `chars: Infinity`.
### Goal Port #3343 and #3344 from `release-v14` to v15. Together they add `useHeardText` to `stream-chat-react/ai-components`: a standalone hook (React only, no chat SDK imports) that reveals a text at the pace a voice AI agent speaks it. Neither PR was ported to `master`. #3344 removes the `SpokenMessageText` / `SpokenTextProvider` / `SpokenTextContext` exports that #3343 added, so `master` never had them, and the port is the net result: the hook, its tests, the export and the README section. A review of the port found issues in the hook itself (shared with v14), which the second commit fixes. ### Implementation details **Commit 1, the port** (`feat(ai-components): add useHeardText for voice-paced text`) - `src/plugins/AIComponents/hooks/useHeardText.ts` and `__tests__/useHeardText.test.ts` are byte-identical to `release-v14`. - `src/plugins/AIComponents/index.ts` exports the hook and its types (`HeardTextProgress`, `HeardTextFunction`, `HeardTextSubscribe`, `UseHeardTextParams`, `UseHeardTextResult`). - README: the `useHeardText` section matches v14; the intro sentence and the export lists mention the hook. The VoiceWave / VoiceGlow / `useAudioLevel` entries that sit next to it on v14 are left to the open port of #3339 (#3347), which edits the same README lines, so whichever lands second has a small conflict there. **Commit 2, fixes from the review** (`fix(ai-components): restart useHeardText for a new text and treat Infinity as fully heard`) - A component that renders the next reply with the same hook instance (for example the README's `SpokenReply` without a `key`) carried over the length already shown, `skip()` and `live`, so the new reply appeared partly or fully revealed before any audio. The reveal now starts over when `text` is replaced by a different text. Growing or truncating the text keeps the reveal, capped at the new length, so a regrowth after a cut does not reuse the old length. The reset happens during render, so the new text never renders with the previous reveal. - `chars: Infinity` revealed nothing, contrary to the type doc ("a value at or above the text's code point length reveals the whole text"). It now reveals the whole text; `NaN`, negative and non-numeric counts still mean nothing heard. - Docs (JSDoc and README): `heard` should return `undefined` only for a text that is not spoken. The old wording also suggested it for a text "not known to the voice source yet", which shows the full text and then never paces it, because the reveal does not move backwards. Also documented: `subscribe` is bound once (remount to switch speech sources) and is the only wake-up for a settled hook, and `heard` is called during render, so it must be free of side effects. - Left as is: re-subscribing on a new `subscribe` identity (it would churn every frame with inline arrows, which the hook explicitly supports), and polling unpaced texts (every non-spoken message would tick forever). Rebuilding the code-point offsets per streamed chunk costs about 20 ms over a whole 4k-character reply in 1k chunks, so it was not optimized. `release-v14` still has the issues fixed in commit 2; they should be ported back. Checks: `yarn test src/plugins/AIComponents` (101 tests, 27 for the hook, 6 of them new), eslint `--max-warnings 0`, prettier, `tsc -p tsconfig.lib.json --noEmit`, `yarn build`, `validate-cjs`, `validate-esm` and `validate-entry-isolation`. The built `useHeardText` module imports only `react`, and the main entry does not include it. ### UI Changes None. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * Added a hook that reveals text in sync with speech progress, with support for skipping the reveal and handling Unicode text. Text is shown immediately when pacing is unavailable and fully revealed when speech or generation is complete. * **Documentation** * Added usage guidance covering the hook’s inputs, results, pacing behavior, and an example. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
🎯 Goal
stream-chat-react/ai-componentsmust work standalone, without the core SDK.SpokenMessageText(added in #3343) rendered through the coreMessagecomponents (MessageText, theStreamedMessageTextfallback, and the message and channel contexts), so it doesn't belong in the plugin. This keeps only the utility hook. The drop-in component becomes a recipe in the docs (GetStream/getstream.io#761).This is the last commit of #3343 (
57e84ca2e), which was pushed just before the PR was merged and didn't make it in.🛠 Implementation details
SpokenMessageText,SpokenTextProvider,SpokenTextContext,useSpokenTextContext, their types and their tests.useHeardTextnow lives insrc/plugins/AIComponents/hooks/useHeardText.ts. Its only import isreact; I checked this in the source, the built ESM and the CJS bundle.useHeardText({ text, heard, subscribe?, generating?, live? }) => { heardText, isPaced, skip }.heard(now)returns{ chars, done } | undefined, wherecharscounts Unicode code points.heardandsubscribeare read through refs, so new function identities on each render don't restart pacing.HeardTextProgress,HeardTextFunction,HeardTextSubscribe,UseHeardTextParams,UseHeardTextResult.floor(chars)code points and never goes backwards; surrogate pairs are never split.requestAnimationFrameonly while paced and not done.done && !generating, whenliveis false, or afterskip().renderHooktests. Full suite 3175/3175 passes, as doyarn types,yarn lint,yarn buildandvalidate-cjs/validate-esm/validate-entry-isolation. In the built output,ai-componentsexportsuseHeardTextand nothingSpoken*, and the main entry exports neither.SpokenMessageTextrecipe on this hook: each voice reply is revealed as its audio plays.🎨 UI Changes
None.