llm-message-react 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,8 +14,6 @@ A single React component that renders LLM markdown output the way a polished cha
14
14
  npm install llm-message-react
15
15
  ```
16
16
 
17
- `react` and `react-dom` (>=18) are peer dependencies.
18
-
19
17
  ## Usage
20
18
 
21
19
  ```tsx
@@ -114,6 +112,38 @@ Notes:
114
112
 
115
113
  `LLMMessage` repairs partially-streamed markdown/LaTeX by default, so unterminated tokens (`**bold`, `` `code ``, `[label](http`, `$E = mc^2`, `\[ ... `) don't flash as raw delimiters while a response streams in. Disable it with `completePartialTokens={false}`.
116
114
 
115
+ ### Turn repair off once streaming is done
116
+
117
+ The repair only helps _while_ text is still arriving. On a finished message it can do harm: a single trailing `$` (or a stray `\`) that is really part of the content gets read as the start of an unterminated math token and "completed", so a final string like `Pay $12\at the door` is mistaken for LaTeX and mangled.
118
+
119
+ If you know when a message has finished streaming, gate `completePartialTokens` on that. Keep it `true` while streaming, then flip it to `false` once the stream closes so the final, complete text is rendered verbatim:
120
+
121
+ ```tsx
122
+ import { useEffect, useState } from "react";
123
+ import { LLMMessage } from "llm-message-react";
124
+ import "llm-message-react/styles.css";
125
+
126
+ export function StreamedMessage({ messageId }: { messageId: string }) {
127
+ const [content, setContent] = useState("");
128
+ const [isStreaming, setIsStreaming] = useState(true);
129
+
130
+ useEffect(() => {
131
+ const stream = subscribeToMessage(messageId, {
132
+ onToken: (token) => setContent((prev) => prev + token),
133
+ onDone: () => setIsStreaming(false), // stream finished
134
+ });
135
+ return () => stream.close();
136
+ }, [messageId]);
137
+
138
+ // Repair partial tokens while streaming, render verbatim once done.
139
+ return (
140
+ <LLMMessage completePartialTokens={isStreaming}>{content}</LLMMessage>
141
+ );
142
+ }
143
+ ```
144
+
145
+ This way half-streamed tokens are still repaired mid-stream, but the completed message is never "fixed" into something it isn't.
146
+
117
147
  ### Unfinished block math
118
148
 
119
149
  By default, unterminated **block** math (`\[ ... `, `$$ ... `) is rendered _progressively_: the open environments/braces are closed and the largest prefix KaTeX accepts is shown, so a long aligned block reveals itself row by row instead of popping in only once the closing delimiter arrives.
@@ -124,15 +154,32 @@ This convenience has a cost: it runs a synchronous KaTeX parse on every streamed
124
154
  <LLMMessage showUnfinishedLatexBlocks={false}>{content}</LLMMessage>
125
155
  ```
126
156
 
127
- The repair function is also exported if you need it directly, alongside the LaTeX preprocessing helpers:
157
+ ### Smooth reveal
128
158
 
129
- ```ts
130
- import {
131
- completePartialTokens,
132
- preprocessLaTeX,
133
- escapeBrackets,
134
- escapeMhchem,
135
- } from "llm-message-react";
159
+ By default new text pops in as each chunk arrives. Set `smoothReveal` to fade it in instead — character by character for prose, and as a single unit for complex blocks (code, tables, images, rules). Box decorations (a blockquote's border, list bullets, an inline-code background) fade in together with the content they belong to. Block math (`$$ … $$`, `\[ … \]`) is the one exception: KaTeX only produces output once the whole formula has streamed in, so a fade would just be a flash — instead the block appears instantly the moment the wave reaches it:
160
+
161
+ ```tsx
162
+ <LLMMessage smoothReveal>{content}</LLMMessage>
163
+ ```
164
+
165
+ The reveal is purely visual: text is always in the DOM the moment it streams in, it just eases up from transparent as a single travelling wave. The wave is sized so it sweeps through the not-yet-revealed text within a short window (`smoothRevealDuration`, default `300` ms). When a new chunk arrives while the previous one is still fading, the leftover (not-yet-revealed) characters and the new ones reveal together over one fresh window, so the animation stays a single coherent wave that keeps pace with the stream instead of many overlapping fades.
166
+
167
+ ```tsx
168
+ <LLMMessage smoothReveal smoothRevealDuration={200}>
169
+ {content}
170
+ </LLMMessage>
171
+ ```
172
+
173
+ Opacity is computed purely from each character's position relative to the reveal point, so it only ever increases (no flicker) regardless of how the stream re-renders, and it is disabled automatically for users who prefer reduced motion.
174
+
175
+ ### Block memoization
176
+
177
+ While a long message streams in, each new chunk would otherwise re-parse and re-render the entire message — re-running KaTeX and code highlighting over text that has not changed. By default `LLMMessage` splits the message into top-level markdown blocks (paragraphs, headings, code fences, math blocks, lists, tables, …) and memoizes each one, so only the last (currently growing) block re-renders on each chunk. Earlier blocks stay mounted and untouched, which keeps streaming smooth regardless of message length.
178
+
179
+ This is on by default and needs no configuration. The one trade-off is that constructs which resolve across blocks — footnotes and link reference definitions — cannot be split (a definition in one block could not be seen by a reference in another). Those are detected automatically and kept in a single block, but if you render content that relies on them and want to be certain, disable splitting:
180
+
181
+ ```tsx
182
+ <LLMMessage blockMemoization={false}>{content}</LLMMessage>
136
183
  ```
137
184
 
138
185
  ## Theming
@@ -209,6 +256,9 @@ import { MyCheckbox, MyCodeBlock, MyLink } from "./ui";
209
256
  - `highlighter?: CodeHighlighter` — opt-in syntax highlighter for fenced code blocks (see [Syntax highlighting](#syntax-highlighting)). Omitted by default, so no highlighter bundle is pulled in.
210
257
  - `completePartialTokens?: boolean` — repair partially-streamed markdown/LaTeX. Defaults to `true`.
211
258
  - `showUnfinishedLatexBlocks?: boolean` — progressively render unterminated block math while it streams (costs a synchronous KaTeX parse per chunk); set to `false` to hide unfinished blocks until they close and skip that work. Defaults to `true`. Only relevant while `completePartialTokens` is enabled.
259
+ - `smoothReveal?: boolean` — fade newly-streamed text in (per character for prose, whole-unit for code/tables/images; block math snaps in instantly since it can't fade progressively) instead of popping it in. Purely visual and respects `prefers-reduced-motion`. Defaults to `false`.
260
+ - `smoothRevealDuration?: number` — reveal window in milliseconds for each freshly-arrived chunk. Defaults to `300`. Only relevant while `smoothReveal` is enabled.
261
+ - `blockMemoization?: boolean` — split the message into top-level blocks and memoize each so a streaming chunk only re-renders the last block (see [Block memoization](#block-memoization)). Defaults to `true`.
212
262
  - All other `div` props are spread onto the root element.
213
263
 
214
264
  > Pass stable references for `classNames`, `components`, and `highlighter` (define them outside render or memoize them). They are dependencies of an internal `useMemo`, so new object/identity on every render defeats it.
@@ -1,4 +1,4 @@
1
- import { C as CodeHighlighter } from '../types-Dz7GupgB.cjs';
1
+ import { C as CodeHighlighter } from '../types-DETvxTAd.cjs';
2
2
  import 'react';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { C as CodeHighlighter } from '../types-Dz7GupgB.js';
1
+ import { C as CodeHighlighter } from '../types-DETvxTAd.js';
2
2
  import 'react';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { C as CodeHighlighter } from '../types-Dz7GupgB.cjs';
1
+ import { C as CodeHighlighter } from '../types-DETvxTAd.cjs';
2
2
  import 'react';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { C as CodeHighlighter } from '../types-Dz7GupgB.js';
1
+ import { C as CodeHighlighter } from '../types-DETvxTAd.js';
2
2
  import 'react';
3
3
 
4
4
  /**