{"_id":"@alphanimble/streamhtml","name":"@alphanimble/streamhtml","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@alphanimble/streamhtml","version":"0.1.0","description":"A streaming-optimized HTML renderer for AI responses — handles incomplete HTML gracefully","type":"module","sideEffects":["*.css"],"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./styles.css":"./styles.css"},"scripts":{"build":"tsup","dev":"tsup --watch","prepublishOnly":"npm run build && npm test","test":"vitest run","test:watch":"vitest","demo":"vite --config demo/vite.config.ts","chat":"vite --config demo/vite.config.ts --open /chat/","demo:build":"vite build demo"},"keywords":["html","streaming","ai","react","renderer","llm","chat","dompurify","xss","streamdown","markdown-alternative"],"license":"MIT","author":{"name":"Alphanimble"},"homepage":"https://github.com/Alphanimble/htmlstream#readme","bugs":{"url":"https://github.com/Alphanimble/htmlstream/issues"},"repository":{"type":"git","url":"git+https://github.com/Alphanimble/htmlstream.git"},"engines":{"node":">=18"},"publishConfig":{"access":"public"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react-dom":{"optional":true}},"dependencies":{"clsx":"^2.1.1","isomorphic-dompurify":"^2.22.0"},"devDependencies":{"@testing-library/react":"^16.3.0","@types/react":"^19.0.0","@types/react-dom":"^19.0.0","@vitejs/plugin-react":"^4.3.4","jsdom":"^26.0.0","katex":"^0.16.22","react":"^19.0.0","react-dom":"^19.0.0","tsup":"^8.4.0","typescript":"^5.8.0","vite":"^6.2.0","vitest":"^3.0.0"},"gitHead":"d53ba55787ff7f249c7c20ec1dc025b082f90b4d","_id":"@alphanimble/streamhtml@0.1.0","_nodeVersion":"24.5.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-8eRZ6SJ2Jej4rzvFq9gf+wZABE6OHp8sk9tbdKzXSFJp35lPnBS4RW/7MNINo05KEBxjGMXQSUE/sojI9p8Dgw==","shasum":"12c3d5f888fccf2078d03e3fd1284af6ab06dec7","tarball":"https://registry.npmjs.org/@alphanimble/streamhtml/-/streamhtml-0.1.0.tgz","fileCount":7,"unpackedSize":244232,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCKQsIycawPFfJfRP0YRhq78qco/wEsujT4DamWGEm7rgIhAMNuIBmtEkLYmtJB7wAP9Vjsc3NCSumXYPZ+YFpLQakC"}]},"_npmUser":{"name":"alphanimble","email":"rakshith.g@alphanimble.com"},"directories":{},"maintainers":[{"name":"alphanimble","email":"rakshith.g@alphanimble.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/streamhtml_0.1.0_1779555975486_0.370815197208489"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-23T17:06:15.332Z","0.1.0":"2026-05-23T17:06:15.641Z","modified":"2026-05-23T17:06:15.884Z"},"maintainers":[{"name":"alphanimble","email":"rakshith.g@alphanimble.com"}],"description":"A streaming-optimized HTML renderer for AI responses — handles incomplete HTML gracefully","homepage":"https://github.com/Alphanimble/htmlstream#readme","keywords":["html","streaming","ai","react","renderer","llm","chat","dompurify","xss","streamdown","markdown-alternative"],"repository":{"type":"git","url":"git+https://github.com/Alphanimble/htmlstream.git"},"author":{"name":"Alphanimble"},"bugs":{"url":"https://github.com/Alphanimble/htmlstream/issues"},"license":"MIT","readme":"# StreamHtml\r\n\r\nA streaming-optimized HTML renderer for AI responses — the HTML counterpart to [Streamdown](https://streamdown.ai).\r\n\r\nAI models excel at writing HTML. It's more expressive than markdown: metric dashboards, comparison grids, styled diffs, rich tables, badges, and custom layouts — all in one stream. **StreamHtml** renders that HTML safely while it streams, handling incomplete tags gracefully.\r\n\r\n## Demo\r\n\r\nScreen recording of the included chat demo (`npm run chat`) streaming a live OpenRouter response as HTML.\r\n\r\nGitHub READMEs do not render `<iframe>` or `<video>` embeds ([sanitized HTML only](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#html)). Use the thumbnail below — **click to watch** on Streamable:\r\n\r\n[![Watch the StreamHtml demo on Streamable](https://cdn-cf-east.streamable.com/image/fxnzq6.jpg)](https://streamable.com/fxnzq6)\r\n\r\nDirect link: [streamable.com/fxnzq6](https://streamable.com/fxnzq6)\r\n\r\nWhat you're seeing in the recording:\r\n\r\n- **Incremental HTML repair** — incomplete tags and unclosed elements are handled as tokens arrive, without layout jumps\r\n- **Live DOM patching** — text and table rows append in place instead of replacing the whole message on every chunk\r\n- **Reasoning panel** — model thinking tokens stream in a collapsible block before HTML content starts\r\n- **Streaming caret** — a smooth block cursor tracks the active text insertion point (only while characters are landing)\r\n- **Text fade-in** — each new chunk of streamed text fades from half-opacity to full\r\n- **Stable blocks** — completed sections freeze so earlier content doesn't re-render during the stream\r\n- **Sanitized output** — DOMPurify strips unsafe markup before anything hits the DOM\r\n\r\n## Features\r\n\r\n- **Streaming-first** — Strips incomplete tags, auto-closes open elements, splits stable/live blocks\r\n- **Incremental DOM patching** — Appends text and table rows without full re-renders\r\n- **Reasoning tokens** — Optional collapsible thinking block separate from HTML content\r\n- **Streaming caret** — Smooth block cursor that follows live text insertion\r\n- **Performance** — Memoizes completed blocks so they never re-render during streaming\r\n- **Security** — DOMPurify sanitization with secure defaults (no scripts, no event handlers)\r\n- **Drop-in React component** — Works with AI SDK, any chat UI, or plain streaming text\r\n- **Headless core** — Use `rehtml()` without React for Node or other frameworks\r\n- **Raw-text aware** — Respects `<pre>`, `<code>`, etc. where `<` is literal\r\n\r\n## Install\r\n\r\n```bash\r\nnpm install @alphanimble/streamhtml\r\n```\r\n\r\nRequires **React 18+** (and `react-dom` in browser apps). Import the base styles once:\r\n\r\n```tsx\r\nimport \"@alphanimble/streamhtml/styles.css\";\r\n```\r\n\r\n## Quick start\r\n\r\n```tsx\r\nimport { StreamHtml } from \"@alphanimble/streamhtml\";\r\nimport \"@alphanimble/streamhtml/styles.css\";\r\n\r\nfunction Message({ content, isStreaming }: { content: string; isStreaming: boolean }) {\r\n  return (\r\n    <StreamHtml isStreaming={isStreaming}>\r\n      {content}\r\n    </StreamHtml>\r\n  );\r\n}\r\n```\r\n\r\n### With reasoning tokens\r\n\r\n```tsx\r\n<StreamHtml\r\n  isStreaming={isStreaming}\r\n  reasoning={reasoningText}\r\n  thinkingLabel=\"Thinking\"\r\n>\r\n  {htmlContent}\r\n</StreamHtml>\r\n```\r\n\r\n### With AI SDK\r\n\r\n```tsx\r\nimport { useChat } from \"@ai-sdk/react\";\r\nimport { StreamHtml } from \"@alphanimble/streamhtml\";\r\nimport \"@alphanimble/streamhtml/styles.css\";\r\n\r\nexport function Chat() {\r\n  const { messages, status } = useChat();\r\n\r\n  return (\r\n    <div>\r\n      {messages.map((message) => (\r\n        <div key={message.id}>\r\n          {message.parts.map((part, i) =>\r\n            part.type === \"text\" ? (\r\n              <StreamHtml\r\n                key={i}\r\n                isStreaming={\r\n                  status === \"streaming\" &&\r\n                  message.id === messages.at(-1)?.id\r\n                }\r\n              >\r\n                {part.text}\r\n              </StreamHtml>\r\n            ) : null,\r\n          )}\r\n        </div>\r\n      ))}\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n## How it works\r\n\r\nStreamHtml runs a repair pipeline on every chunk:\r\n\r\n1. **Strip incomplete tags** — `<div class=\"met` → removed until the tag completes\r\n2. **Close open tags** — `<strong>bold` → `<strong>bold</strong>`\r\n3. **Split blocks** — Completed top-level blocks are frozen; only the tail re-renders\r\n4. **Patch incrementally** — Plain text and table rows append to the live DOM\r\n5. **Sanitize** — DOMPurify removes XSS vectors before `dangerouslySetInnerHTML`\r\n\r\n```\r\nStreaming input          Repair                 Render\r\n─────────────────       ──────────             ──────────\r\n<div>A</div><div>B      stable: [<div>A</div>]  memoized ✓\r\n<div>C                  live: <div>C</div>      re-renders each chunk\r\n```\r\n\r\n## API\r\n\r\n### `<StreamHtml />`\r\n\r\n| Prop | Type | Default | Description |\r\n|------|------|---------|-------------|\r\n| `children` | `string` | `\"\"` | HTML content to render |\r\n| `reasoning` | `string` | `\"\"` | Plain-text reasoning / thinking tokens |\r\n| `isStreaming` | `boolean` | `false` | Shows caret, marks streaming state |\r\n| `caret` | `boolean` | `isStreaming` | Toggle streaming caret |\r\n| `repair` | `boolean` | `true` | Run incomplete HTML repair |\r\n| `sanitize` | `boolean` | `true` | DOMPurify sanitization |\r\n| `memoizeBlocks` | `boolean` | `true` | Freeze completed blocks |\r\n| `sanitizeConfig` | `Config` | — | DOMPurify config override |\r\n| `thinkingLabel` | `string` | `\"Thinking\"` | Label for reasoning panel |\r\n| `className` | `string` | — | Root element class |\r\n\r\n### `rehtml(input, options?)`\r\n\r\nHeadless repair function for non-React use:\r\n\r\n```ts\r\nimport { rehtml } from \"@alphanimble/streamhtml\";\r\n\r\nconst { html, stable, live, hadIncompleteTag } = rehtml(partialHtml);\r\n```\r\n\r\n### `sanitizeHtml(html, config?)`\r\n\r\n```ts\r\nimport { sanitizeHtml, configureSanitizer } from \"@alphanimble/streamhtml\";\r\n\r\nconfigureSanitizer({ ALLOWED_TAGS: [\"div\", \"p\", \"span\", \"table\", ...] });\r\nconst safe = sanitizeHtml(untrustedHtml);\r\n```\r\n\r\n## Prompting tips\r\n\r\nTell your model to output semantic HTML with CSS classes:\r\n\r\n```html\r\n<div class=\"metrics\">\r\n  <div class=\"metric good\">\r\n    <div class=\"metric-val\">142ms</div>\r\n    <div class=\"metric-lbl\">avg latency</div>\r\n  </div>\r\n</div>\r\n<table class=\"rt\">\r\n  <thead><tr><th>Endpoint</th><th>Status</th></tr></thead>\r\n  <tbody>...</tbody>\r\n</table>\r\n<div class=\"callout warn\"><strong>Note:</strong> ...</div>\r\n```\r\n\r\nDefine component styles in your app — StreamHtml ships minimal base styles for tables, code, and typography.\r\n\r\n## Development\r\n\r\n```bash\r\ngit clone <your-repo-url>\r\ncd htmlstream\r\nnpm install\r\nnpm test\r\nnpm run build\r\n```\r\n\r\n### Demos\r\n\r\n**Gallery** (static examples):\r\n\r\n```bash\r\nnpm run demo\r\n```\r\n\r\n**Chat** (live OpenRouter streaming):\r\n\r\n```bash\r\ncp .env.example .env\r\n# Add your OPENROUTER_API_KEY and optional MODEL_NAME\r\nnpm run chat\r\n```\r\n\r\nThe chat demo streams HTML from OpenRouter and persists sessions locally in `.chat-sessions/` (gitignored).\r\n\r\n## License\r\n\r\nMIT — see [LICENSE](./LICENSE).\r\n","readmeFilename":"README.md","_rev":"1-e25f69f54a72d39476c6e8a8d4632346"}