{"_id":"@abundly/widget","_rev":"4-a6329488f531191ee4dead24e6b54882","name":"@abundly/widget","dist-tags":{"beta":"1.1.0-beta","gamma":"1.1.0-gamma","latest":"1.2.0"},"versions":{"1.1.0-beta":{"name":"@abundly/widget","version":"1.1.0-beta","license":"MIT","_id":"@abundly/widget@1.1.0-beta","maintainers":[{"name":"abundly","email":"accounts@abundly.ai"}],"dist":{"shasum":"a38880c628e22d244e127678ceab791f9aca41cf","tarball":"https://registry.npmjs.org/@abundly/widget/-/widget-1.1.0-beta.tgz","fileCount":13,"integrity":"sha512-WxzzefUglrNaLkQ6jCxPNYY/CevaIbCDztN4N9O1o9MRH/Ka93fY8duoux677Tvrw1r7U33ho3L/Ps3M205X/Q==","signatures":[{"sig":"MEUCIQD+wwG1VKmH2kckJYXbKIjTAxwLmvJXakJPX7vRK6GmHgIgC0BA7i6LYycgeJnkA3L593hes8OrHpYvVNzGqurbwzg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":433635},"main":"./dist/index.js","type":"module","_from":"file:abundly-widget-1.1.0-beta.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./iife":"./dist/iife.min.js","./iife/dev":"./dist/iife.js"},"scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup && mkdir -p package && rm -f package/*.tgz && npm pack --pack-destination package","check":"tsc --noEmit","clean":"node scripts/clean-examples.mjs","release":"VERSION=$(node -p \"JSON.parse(require('fs').readFileSync('package.json','utf8')).version\") && NPM_TAG=$(node -e \"const v=JSON.parse(require('fs').readFileSync('package.json','utf8')).version; const d=v.indexOf('-'); process.stdout.write(d===-1?'':v.slice(d+1).split('.')[0])\") && if [ -n \"$NPM_TAG\" ]; then pnpm publish --tag \"$NPM_TAG\"; else pnpm publish; fi && git tag -a \"v$VERSION\" -m \"v$VERSION\" && git push origin \"v$VERSION\"","test:watch":"vitest"},"_npmUser":{"name":"abundly","email":"accounts@abundly.ai"},"_resolved":"/private/var/folders/f0/sw8xhbss3zdcnrbb6t5vdgqm0000gn/T/a8bf8bb5b170f0dc4a872e80302ed223/abundly-widget-1.1.0-beta.tgz","_integrity":"sha512-WxzzefUglrNaLkQ6jCxPNYY/CevaIbCDztN4N9O1o9MRH/Ka93fY8duoux677Tvrw1r7U33ho3L/Ps3M205X/Q==","_npmVersion":"11.6.2","description":"Embeddable Abundly chat widget — headless client + ready-made UI for browser embeds.","directories":{},"sideEffects":["./dist/iife.js","./dist/iife.min.js"],"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","jsdom":"^24.0.0","vitest":"^1.6.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/widget_1.1.0-beta_1779789292184_0.6178638977729067","host":"s3://npm-registry-packages-npm-production"}},"1.1.0-gamma":{"name":"@abundly/widget","version":"1.1.0-gamma","license":"MIT","_id":"@abundly/widget@1.1.0-gamma","maintainers":[{"name":"abundly","email":"accounts@abundly.ai"}],"dist":{"shasum":"2945a265c57b33093d5dc537f14873d50f14ed37","tarball":"https://registry.npmjs.org/@abundly/widget/-/widget-1.1.0-gamma.tgz","fileCount":13,"integrity":"sha512-MeamToQdFySFNmMUpFfl0g27MUGGAr0cH5Ny1yMCoaojcjBOiGC5wApDk/rEj3wZABNGHiRpfObSw2+VJrm/fA==","signatures":[{"sig":"MEUCIQCpkSl+axD3UJlWqd8I4F9K7zA2xlaaBIlpJ7hm+RgAwQIgW3WgxCJjpXIHsF3hGSFDe4+uPXo26rJeg/Eklvdzfes=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":548613},"main":"./dist/index.js","type":"module","_from":"file:abundly-widget-1.1.0-gamma.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./iife":"./dist/iife.min.js","./iife/dev":"./dist/iife.js"},"scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup && mkdir -p package && rm -f package/*.tgz && npm pack --pack-destination package","check":"tsc --noEmit","clean":"node scripts/clean-examples.mjs","release":"VERSION=$(node -p \"JSON.parse(require('fs').readFileSync('package.json','utf8')).version\") && NPM_TAG=$(node -e \"const v=JSON.parse(require('fs').readFileSync('package.json','utf8')).version; const d=v.indexOf('-'); process.stdout.write(d===-1?'':v.slice(d+1).split('.')[0])\") && if [ -n \"$NPM_TAG\" ]; then pnpm publish --tag \"$NPM_TAG\"; else pnpm publish; fi && git tag -a \"v$VERSION\" -m \"v$VERSION\" && git push origin \"v$VERSION\"","test:watch":"vitest"},"_npmUser":{"name":"abundly","email":"accounts@abundly.ai"},"_resolved":"/private/var/folders/f0/sw8xhbss3zdcnrbb6t5vdgqm0000gn/T/68e6843fe8a9a853b1e097d5bdbe9b55/abundly-widget-1.1.0-gamma.tgz","_integrity":"sha512-MeamToQdFySFNmMUpFfl0g27MUGGAr0cH5Ny1yMCoaojcjBOiGC5wApDk/rEj3wZABNGHiRpfObSw2+VJrm/fA==","_npmVersion":"11.6.2","description":"Embeddable Abundly chat widget — headless client + ready-made UI for browser embeds.","directories":{},"sideEffects":["./dist/iife.js","./dist/iife.min.js"],"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"readmeFilename":"README.md","devDependencies":{"tsup":"^8.0.0","jsdom":"^24.0.0","vitest":"^1.6.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/widget_1.1.0-gamma_1779876635826_0.517987877934956","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@abundly/widget","version":"1.1.0","license":"MIT","_id":"@abundly/widget@1.1.0","maintainers":[{"name":"abundly","email":"accounts@abundly.ai"}],"dist":{"shasum":"43237a16f6d58a65179d5d7e3988fb565b9a258e","tarball":"https://registry.npmjs.org/@abundly/widget/-/widget-1.1.0.tgz","fileCount":13,"integrity":"sha512-UvFRPe7RpjUwVojPNQCsB8bsTI27kG1iLWQnbTSG59yEEOc5w/fhPL7kbj4/vJlmgn5SxkhIpLs+JR5xiLQl4g==","signatures":[{"sig":"MEUCIQDhUO0xqkKugXRGXPqOU/fEDERM18bcATemsAbBnU9O5gIgMf3IlGlb8Ye3wUoLaiIKXyPQFZdFO9IJwxWZo9lZqpQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":539911},"main":"./dist/index.js","type":"module","_from":"file:abundly-widget-1.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./iife":"./dist/iife.min.js","./iife/dev":"./dist/iife.js"},"scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup && mkdir -p package && rm -f package/*.tgz && npm pack --pack-destination package","check":"tsc --noEmit","clean":"node scripts/clean-examples.mjs","release":"node scripts/release.mjs","test:watch":"vitest"},"_npmUser":{"name":"abundly","email":"accounts@abundly.ai"},"_resolved":"/private/var/folders/f0/sw8xhbss3zdcnrbb6t5vdgqm0000gn/T/c43270bb6d30a04e04da7a1a9fbf6895/abundly-widget-1.1.0.tgz","_integrity":"sha512-UvFRPe7RpjUwVojPNQCsB8bsTI27kG1iLWQnbTSG59yEEOc5w/fhPL7kbj4/vJlmgn5SxkhIpLs+JR5xiLQl4g==","_npmVersion":"11.6.2","description":"Embeddable Abundly chat widget — headless client + ready-made UI for browser embeds.","directories":{},"sideEffects":["./dist/iife.js","./dist/iife.min.js"],"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","jsdom":"^24.0.0","vitest":"^1.6.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/widget_1.1.0_1779888678917_0.12801696767779114","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@abundly/widget","version":"1.2.0","description":"Embeddable Abundly chat widget — headless client + ready-made UI for browser embeds.","license":"MIT","type":"module","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./iife":"./dist/iife.min.js","./iife/dev":"./dist/iife.js"},"publishConfig":{"access":"public"},"sideEffects":["./dist/iife.js","./dist/iife.min.js"],"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0","vitest":"^1.6.0","jsdom":"^24.0.0","@types/node":"^20.0.0"},"scripts":{"build":"tsup && mkdir -p package && rm -f package/*.tgz && npm pack --pack-destination package","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","check":"tsc --noEmit","clean":"node scripts/clean-examples.mjs","release":"node scripts/release.mjs"},"_id":"@abundly/widget@1.2.0","_integrity":"sha512-drRqgdhoUEMYeHqtviQLeK6zhrf3N74RZMGT32fEK9mAz7bkWjtS8Ui74F9pDZKdDr5jep71IRGfVMg/rK+5ag==","_resolved":"/private/var/folders/f0/sw8xhbss3zdcnrbb6t5vdgqm0000gn/T/57403b6f7150761b3f967cd020e38a08/abundly-widget-1.2.0.tgz","_from":"file:abundly-widget-1.2.0.tgz","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-drRqgdhoUEMYeHqtviQLeK6zhrf3N74RZMGT32fEK9mAz7bkWjtS8Ui74F9pDZKdDr5jep71IRGfVMg/rK+5ag==","shasum":"f74c75fec3c8b8bac9fc98c318e0efefbb9086d4","tarball":"https://registry.npmjs.org/@abundly/widget/-/widget-1.2.0.tgz","fileCount":13,"unpackedSize":543308,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCD+9LuNBQpEpvG/Gswtf0z3nUaXWfisDa70asE3T0pPwIhAJoP46dx9hw6SBLC4IfazhQgIUFxIN5df/g2137AFwG8"}]},"_npmUser":{"name":"abundly","email":"accounts@abundly.ai"},"directories":{},"maintainers":[{"name":"abundly","email":"accounts@abundly.ai"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/widget_1.2.0_1780052672256_0.14648867881172678"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-26T09:54:52.065Z","modified":"2026-05-29T11:04:32.599Z","1.1.0-beta":"2026-05-26T09:54:52.345Z","1.1.0-gamma":"2026-05-27T10:10:35.993Z","1.1.0":"2026-05-27T13:31:19.149Z","1.2.0":"2026-05-29T11:04:32.486Z"},"license":"MIT","description":"Embeddable Abundly chat widget — headless client + ready-made UI for browser embeds.","maintainers":[{"name":"abundly","email":"accounts@abundly.ai"}],"readme":"# @abundly/widget\n\nEmbeddable Abundly chat widget for the [Abundly AI platform](https://app.abundly.ai) — ready-made UI + headless client.\n\nThe widget has two modes of operation:\n\n1. **Ready-made widget** (`mountChatWidget`) — a complete, styled floating chat bubble and panel. Use it as-is, or customize colors and texts through configuration. One function call and you're done.\n2. **Custom UI** (`createClient`) — a headless client that handles backend communication, streaming, URL rewriting, and markdown rendering. You build the UI entirely yourself using the building blocks the client exposes.\n\nBoth modes work from **React** (ESM/CJS import) and from **plain HTML** (script tag). Both share the same backend proxy contract.\n\n## Install\n\n```bash\nnpm install @abundly/widget\n```\n\n## Mode 1: Ready-made widget\n\n`mountChatWidget` gives you a floating chat bubble, a slide-up panel, streaming\nresponses with typing animation, a thinking indicator, and image support — no UI\ncode required.\n\nBoth `mountChatWidget` and `createClient` are exposed as named exports from\n`@abundly/widget` (for bundlers) and as properties on the `Abundly` global (for\nthe IIFE script tag).\n\n### Script tag\n\n```html\n<script src=\"https://app.abundly.ai/widget.js\"></script>\n<script>\n   Abundly.mountChatWidget({\n      backendUrl: \"https://yoursite.com/api/abundly\",\n      textOptions: { headerTitle: \"Chat\" },\n      uiOptions: { bubbleColor: \"#319795\" },\n   });\n</script>\n```\n\nAlternatively, `npm install @abundly/widget` and use `@abundly/widget/iife` (minified) or `@abundly/widget/iife/dev` (readable, for debugging).\n\n### React\n\nMount on a route, unmount on the effect's cleanup so the widget doesn't leak\nwhen the user navigates away:\n\n```tsx\n\"use client\"; // Next.js App Router; omit for plain React\n\nimport { useEffect } from \"react\";\nimport { mountChatWidget } from \"@abundly/widget\";\n\nexport function ChatBubble() {\n   useEffect(() => {\n      const handle = mountChatWidget({\n         backendUrl: \"/api/abundly\",\n         textOptions: { headerTitle: \"Support\" },\n         uiOptions: { bubbleColor: \"#319795\" },\n      });\n      return () => handle.unmount();\n   }, []);\n   return null;\n}\n```\n\n### Handle\n\n`mountChatWidget` returns a handle with two methods:\n\n\n| Method             | What it does                                                                                                                                  |\n| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `handle.reset()`   | Clears conversation history, errors, and suspended state. Restores the welcome message if configured.                                         |\n| `handle.unmount()` | Removes bubble, panel, backdrop, and injected styles. Cancels animations and drops in-flight callbacks. Idempotent, safe to call at any time. |\n\n\n### What the widget handles for you\n\n- Availability check (`/status`) — widget stays hidden if the agent is unavailable\n- SSE streaming with character-by-character reveal animation\n- Thinking indicator (animated dots) during tool/agent processing\n- Markdown rendering with XSS protection\n- URL rewriting (platform file URLs routed through your proxy)\n- Image preloading and authenticated image loading (blob URLs)\n- Mobile soft-keyboard detection and layout adjustment\n- Textarea auto-resize (up to 3 lines)\n- Error display and rate-limit suspension (HTTP 429)\n\n### Config\n\n```ts\ninterface MountChatWidgetConfig {\n   backendUrl: string;                  // required — your proxy URL (path or absolute)\n   textOptions?: {                      // all optional, sensible defaults\n      placeholder?: string;             // input placeholder (\"Type a message…\")\n      bubbleLabel?: string;             // text shown on the floating bubble (\"Chat about this\")\n      headerTitle?: string;             // chat panel title (\"Chat\")\n      welcomeMessage?: string;          // initial assistant message (none by default)\n   };\n   uiOptions?: {                        // all optional, default to brand colors\n      bubbleColor?: string;             // bubble background (color or CSS gradient)\n      bubbleHoverColor?: string;\n      bubbleTextColor?: string;\n      userBubbleColor?: string;         // user message bubble\n      inputBorderColor?: string;        // textarea focus border\n      submitColor?: string;             // send button background\n      submitHoverColor?: string;\n      submitDisabledColor?: string;\n      headerColor?: string;             // chat panel header background\n      headerTextColor?: string;\n      linkColor?: string;               // assistant-message link color\n   };\n   // Optional auth — see the Auth section below for details.\n   headers?:\n      | HeadersInit\n      | (() => HeadersInit | Promise<HeadersInit>);\n   credentials?: RequestCredentials;    // \"omit\" | \"same-origin\" | \"include\"\n}\n```\n\nColors accept any valid CSS value — named colors, hex, rgba, gradients.\n\n---\n\n## Mode 2: Custom UI (headless client)\n\n`createClient` returns the same streaming/protocol engine the ready-made widget\nuses internally, without any DOM or styles. You build the UI entirely yourself —\nReact, Vue, Svelte, or vanilla DOM — using the building blocks the client exposes.\n\n### Creating the client\n\n**React / bundler:**\n\n```ts\nimport { createClient } from \"@abundly/widget\";\nconst client = createClient(\"/api/abundly\");\n```\n\n**Script tag:**\n\n```html\n<script src=\"https://app.abundly.ai/widget.js\"></script>\n<script>\n   const client = Abundly.createClient(\"/api/abundly\");\n</script>\n```\n\nThe optional second argument configures auth (same options as `mountChatWidget`):\n\n```ts\nconst client = createClient(\"/api/abundly\", {\n   headers: async () => ({ Authorization: `Bearer ${await getToken()}` }),\n   credentials: \"include\",  // only needed for cross-origin cookie auth\n});\n```\n\n### Rendering: bundled vs. bring your own\n\nBefore diving into the client API, there's a second choice to make: how to\nrender the markdown that the agent produces. The client gives you two paths:\n\n- **Path A: Bundled renderer** — use `client.renderMarkdown` to get an HTML\nstring, feed it to `dangerouslySetInnerHTML` (React) or `.innerHTML` (vanilla).\nZero extra dependencies. XSS protection built in.\n- **Path B: External library** — use your own markdown library (e.g.\n`react-markdown`, `marked`, `markdown-it`). The streamed text already has URLs\nrewritten, so no renderer-specific hooks are needed.\n\nThis choice only affects how the markdown string is converted into HTML —\nstyling is always your responsibility in either path. The bundled renderer\nemits plain HTML tags (`<p>`, `<ul>`, `<code>`, etc.) with no classes or\ninline styles; you style them with your own CSS.\n\nThis choice also affects which client methods you need. The table below shows which\nmethods are essential, optional, or unused for each path — detailed descriptions\nfollow in the API reference.\n\n\n| Method              | Path A (bundled)                                                                | Path B (external library)                                                                  |\n| ------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| `checkStatus()`     | Essential                                                                       | Essential                                                                                  |\n| `sendMessage()`     | Essential                                                                       | Essential                                                                                  |\n| `resetSession()`    | Call when starting a new conversation                                           | Call when starting a new conversation                                                      |\n| `stripPartialMd()`  | Convenience — cleans up half-formed `![…](` tokens while streaming             | Convenience — same                                                                         |\n| `renderMarkdown()`  | Essential                                                                       | Not used — your library replaces it                                                        |\n| `swapProxiedUrls()` | Header-auth only — rewrites all images in the HTML DOM at once                  | Header-auth only — prefer `loadImage` per-image instead                                    |\n| `loadImage()`       | Header-auth only — prefer `swapProxiedUrls` for bulk                            | Header-auth only — resolve each image URL before passing to your component                 |\n| `releaseImages()`   | On teardown, if you used `swapProxiedUrls` or `loadImage`                       | On teardown, if you used `loadImage`                                                       |\n\n\nSee [Choosing a markdown renderer](#choosing-a-markdown-renderer) further down\nfor a feature comparison and examples of both paths.\n\n### Client API reference\n\n#### `client.checkStatus()`\n\n> Both paths — essential.\n\n```ts\ncheckStatus(): Promise<{ available: boolean }>\n```\n\nCalls `GET {backendUrl}/status`. Returns whether the agent is available. Use this to decide whether to show your chat UI.\n\n#### `client.sendMessage(text, callbacks?)`\n\n> Both paths — essential.\n\n```ts\nsendMessage(\n   text: string,\n   callbacks?: SendMessageCallbacks,\n): Promise<string>\n```\n\nSends a message to `POST {backendUrl}/chat` and streams the response via SSE.\nReturns a promise that resolves to the final accumulated response text.\n\nThe server tracks conversation history automatically via a session ID managed by\nthe client. Just maintain a local `Message[]` for rendering your UI:\n\n```ts\ninterface Message {\n   role: \"user\" | \"assistant\";\n   content: string;\n}\n```\n\nAll text in callbacks and the return value has platform `/api/files/...` URLs\nalready rewritten to point at your backend proxy — hand it straight to any\nmarkdown renderer without additional URL transforms.\n\n**Callbacks** (all optional — the final text is also returned from the promise):\n\n```ts\ninterface SendMessageCallbacks {\n   onChunk?: (accumulated: string, delta: string, isFirstChunk: boolean) => void;\n   onProcessing?: (accumulatedBeforeReset: string) => void;\n   onStreamError?: (errorMessage: string) => void;\n   onDone?: (finalAccumulated: string) => void;\n}\n```\n\n\n| Callback        | When it fires                                                                                                                                                               | What to do                                                                   |\n| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |\n| `onChunk`       | Every SSE chunk. `accumulated` is the full text so far; `delta` is the new slice; `isFirstChunk` is true on the very first chunk after a processing reset or session start. | Update your streaming UI.                                                    |\n| `onProcessing`  | The backend signals a tool/agent step is running and the text buffer resets. `accumulatedBeforeReset` is whatever text had streamed before the reset.                       | Finalize the current partial message (if any) and show a thinking indicator. |\n| `onStreamError` | An error embedded inside the SSE stream (not HTTP-level).                                                                                                                   | Display the error message to the user.                                       |\n| `onDone`        | The stream completes cleanly.                                                                                                                                               | Finalize the message.                                                        |\n\n\n**Error handling:** HTTP-level errors (non-2xx from the chat endpoint) throw an\n`AbundlyWidgetHttpError`:\n\n```ts\ninterface AbundlyWidgetHttpError extends Error {\n   status: number;\n   body: { error?: string; [key: string]: unknown };\n}\n```\n\nBranch on `status` — for example, `429` means rate-limited.\n\n#### `client.resetSession()`\n\n> Both paths — call when starting a new conversation.\n\n```ts\nresetSession(): void\n```\n\nClears the server-side session ID so the next `sendMessage` call starts a new\nconversation. The client manages session continuity automatically — you only need\nthis when you want an explicit \"new conversation\" action (e.g. a reset button).\nClear your local `Message[]` array at the same time.\n\n#### `client.renderMarkdown(text)`\n\n> Path A (bundled) — essential. Path B (external library) — not used.\n\n```ts\nrenderMarkdown(text: string): string\n```\n\nConverts a markdown subset to an HTML string. This is the same renderer the\nready-made widget uses. Dangerous URL schemes (`javascript:`, `data:`,\n`vbscript:`, …) are stripped — only `http(s):`, `mailto:`, and relative URLs\npass through.\n\nSupported syntax: paragraphs, headings, unordered/ordered lists, fenced code\nblocks, tables, bold, inline code, links, images, horizontal rules.\n\nIf you are using your own markdown library (Path B), skip this method entirely —\nyour library replaces it. The streamed text has URLs already rewritten, so no\nrenderer-specific URL hook is needed.\nSee [Choosing a markdown renderer](#choosing-a-markdown-renderer) below.\n\n#### `client.stripPartialMd(text)`\n\n> Both paths — convenience utility.\n\n```ts\nstripPartialMd(text: string): string\n```\n\nCosmetic helper for streaming. While text is still arriving, the accumulated\nstring can end with an incomplete image or link token like `![alt](http…` that\nhasn't closed yet. If you render that as-is, the user briefly sees raw markdown\nsyntax. `stripPartialMd` trims those trailing fragments so the output stays\nclean. Completely optional — everything works without it, you just get a brief\nflash of `[…](…` at the tail end of the stream until the token completes.\n\n#### Image auth: `loadImage`, `swapProxiedUrls`, `releaseImages`\n\n> Skip this section entirely if your proxy uses cookie-based auth or no auth\n> at all — images just work via normal `<img src>` in that case.\n\n**The problem:** when your proxy requires header-based auth (e.g. a Bearer\ntoken), native `<img>` tags can't carry that header. The browser would fire an\nunauthenticated `GET` to the image URL and get a 401. These three methods solve\nthat by fetching images with the same auth as `/chat` and exposing them as\n`blob:` URLs the browser can load without headers.\n\n##### `client.loadImage(url)`\n\n```ts\nloadImage(url: string): Promise<string>\n```\n\nFetches one image with auth, returns a `blob:` URL you can set on `<img src>`.\nResults are cached per-URL. Only URLs pointing at this client's image proxy are\nfetched with auth; external URLs and non-auth setups return the URL unchanged —\nsafe to call on every image unconditionally. Falls back to the original URL on\nfetch failure.\n\nSee [Path B: image auth](#path-b-external-library) for a practical example.\n\n##### `client.swapProxiedUrls(root)`\n\n```ts\nswapProxiedUrls(root: ParentNode): void\n```\n\nWalks `root` and rewrites the `src` of every `<img>` and the `href` of every\n`<a>` that points at the image proxy, swapping them to `blob:` URLs. No-op when\nauth is not configured. Uses `loadImage` under the hood — if an image was\nalready resolved (e.g. from a preload pass during streaming), the swap is\nsynchronous with no flicker.\n\nSee [Path A: image auth](#path-a-bundled-renderer) for a practical example.\n\n##### `client.releaseImages()`\n\n```ts\nreleaseImages(): void\n```\n\nRevokes every `blob:` URL this client has created and clears the cache. Call on\ncomponent teardown if you used `loadImage` or `swapProxiedUrls`. The ready-made\nwidget calls this automatically on `unmount()`.\n\n### Who is responsible for what\n\n\n| Concern                                             | The client handles                                                                                | You handle                                                                |\n| --------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |\n| Backend communication (SSE streaming, status check) | Yes                                                                                               | —                                                                         |\n| URL rewriting (`/api/files/...` → proxy)            | Yes — applied in `sendMessage` before callbacks fire                                              | —                                                                         |\n| Markdown → HTML                                     | Path A: `renderMarkdown`. Path B: not used.                                                       | Choosing a renderer and calling it on the text                            |\n| Streaming partial-markdown cleanup                  | `stripPartialMd` (convenience — optional, prevents half-formed `[…](` flashing during streaming)  | Calling it if you want clean streaming output                             |\n| XSS / dangerous URL schemes                         | Path A: `renderMarkdown` handles it. Path B: your library's responsibility — verify its defaults. | —                                                                         |\n| Image auth (blob URLs)                              | Path A: `swapProxiedUrls`. Path B: `loadImage`.                                                   | Calling the appropriate helper when your proxy requires header-based auth |\n| Blob URL cleanup                                    | `releaseImages`                                                                                   | Calling it on teardown if you used image auth helpers                     |\n| Session management                                  | Yes — persists conversation server-side; call `resetSession()` to start fresh                     | —                                                                         |\n| Conversation history                                | Persisted server-side automatically                                                               | Maintaining a local `Message[]` for rendering your UI                     |\n| UI rendering, layout, styling                       | —                                                                                                 | Everything                                                                |\n\n\n### Minimal example (React)\n\n```tsx\n\"use client\";\n\nimport { useState } from \"react\";\nimport { createClient, type Message } from \"@abundly/widget\";\n\nconst client = createClient(\"/api/abundly\");\n\nexport function MyChat() {\n   const [messages, setMessages] = useState<Message[]>([]);\n   const [streaming, setStreaming] = useState(\"\");\n   const [draft, setDraft] = useState(\"\");\n\n   async function send() {\n      const text = draft.trim();\n      if (!text) return;\n      setDraft(\"\");\n      setMessages((prev) => [...prev, { role: \"user\", content: text }]);\n      setStreaming(\"\");\n      // No need to pass conversation history — the server tracks it via the\n      // session ID that the client manages automatically.\n      const final = await client.sendMessage(text, {\n         onChunk: (accumulated) => setStreaming(accumulated),\n      });\n      setStreaming(\"\");\n      setMessages((prev) => [...prev, { role: \"assistant\", content: final }]);\n   }\n\n   function resetChat() {\n      client.resetSession();\n      setMessages([]);\n   }\n\n   return (\n      <div>\n         {messages.map((m, i) => (\n            <div key={i} className={m.role}>\n               {m.role === \"user\" ? (\n                  m.content\n               ) : (\n                  <div dangerouslySetInnerHTML={{\n                     __html: client.renderMarkdown(m.content)\n                  }} />\n               )}\n            </div>\n         ))}\n         {streaming && (\n            <div className=\"assistant\">\n               <div dangerouslySetInnerHTML={{\n                  __html: client.renderMarkdown(client.stripPartialMd(streaming))\n               }} />\n            </div>\n         )}\n         <input value={draft} onChange={(e) => setDraft(e.target.value)} />\n         <button onClick={send}>Send</button>\n         <button onClick={resetChat}>New chat</button>\n      </div>\n   );\n}\n```\n\n### Choosing a markdown renderer\n\nThe agent emits **standard markdown**. You have two paths to render it, and\nthe choice affects which client methods you use and how you handle images.\n\n#### Path A: Bundled renderer\n\nUse `client.renderMarkdown` to produce HTML, feed it to\n`dangerouslySetInnerHTML` or `.innerHTML`. Zero extra dependencies. XSS\nprotection and scheme filtering built in.\n\n**Methods you need:** `sendMessage`, `checkStatus`, `renderMarkdown`.\nAdd `stripPartialMd` if you want clean streaming output (no half-formed tokens).\nAdd `swapProxiedUrls` + `releaseImages` if your proxy requires header-based auth.\n\n```tsx\n// Streaming render with the bundled renderer\n<div dangerouslySetInnerHTML={{\n   __html: client.renderMarkdown(client.stripPartialMd(streaming))\n}} />\n```\n\nThe `dangerouslySetInnerHTML` is a conscious opt-in — the safety contract is\nthat **the widget** produces the HTML, with scheme filtering baked in. You're\nnot trusting agent output; you're trusting the renderer.\n\n**Image auth with `swapProxiedUrls`:** if your proxy requires header-based auth,\nparse the HTML into an inert `<template>`, call `swapProxiedUrls` to rewrite\nimage URLs to `blob:` URLs, then move the children into the target element.\nApply this to **individual message elements**, not the entire message list —\nreplacing the whole container would re-render all messages and cause images to\nflicker. This is how the ready-made widget handles it internally.\n\n```ts\n// Rendering a single assistant message into its own element:\nfunction renderMessage(el: HTMLElement, text: string) {\n   const tmpl = document.createElement(\"template\");\n   tmpl.innerHTML = client.renderMarkdown(text);\n   client.swapProxiedUrls(tmpl.content);\n   el.replaceChildren(tmpl.content);\n}\n```\n\nCall `client.releaseImages()` on teardown to revoke the blob URLs.\n\n#### Path B: External library\n\nUse your own markdown library (`react-markdown`, `marked`, `markdown-it`, etc.).\nThe streamed text already has URLs rewritten by `sendMessage`, so no\nrenderer-specific URL hook is needed — hand the text straight to your library.\n\n**Methods you need:** `sendMessage`, `checkStatus`.\nAdd `stripPartialMd` if you want clean streaming output (no half-formed tokens).\nAdd `loadImage` + `releaseImages` if your proxy requires header-based auth\n(resolve each image URL before passing it to your `<img>` component). You do\n**not** need `renderMarkdown` or `swapProxiedUrls`.\n\n```tsx\n// Streaming render with react-markdown\n<ReactMarkdown>{client.stripPartialMd(streaming)}</ReactMarkdown>\n```\n\n**Image auth with `loadImage`:** if your proxy requires header-based auth,\n`react-markdown` (or any library) would render a plain\n`<img src=\"/api/abundly/images/...\">` and the browser would try to load it\nwithout auth headers — resulting in a 401. Since you control component rendering\nin Path B, override the `img` component to resolve each URL via `loadImage`\nbefore the browser sees it:\n\n```tsx\nimport ReactMarkdown from \"react-markdown\";\n\nfunction AuthImage({ src, alt }: { src?: string; alt?: string }) {\n   const [resolved, setResolved] = useState(src);\n   useEffect(() => {\n      if (src) client.loadImage(src).then(setResolved);\n   }, [src]);\n   return <img src={resolved} alt={alt ?? \"\"} />;\n}\n\n<ReactMarkdown components={{ img: AuthImage }}>\n   {message.content}\n</ReactMarkdown>\n```\n\nCall `client.releaseImages()` on teardown to revoke the blob URLs.\n\n#### Feature comparison\n\n\n|                             | Path A: bundled renderer                                                    | Path B: external library                                    |\n| --------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------- |\n| Bundle size                 | 0 extra                                                                     | +library size (e.g. ~25 kB for `react-markdown` + `remark`) |\n| `dangerouslySetInnerHTML`?  | yes (intentional — we produce the HTML)                                     | no (React elements)                                         |\n| Markdown features           | headings, lists, bold/italic, inline+fenced code, tables, links, images, hr | whatever the library supports                               |\n| URL rewriting               | done for you (`/api/files/...` already proxied in `sendMessage`)            | done for you (same)                                         |\n| XSS / scheme guard          | ours (`http(s):` / `mailto:` / relative; everything else stripped)          | the library's job (most do it by default — verify)          |\n| Custom components / theming | CSS only; the renderer emits raw HTML tags                                  | full control via React elements                             |\n| Image auth approach         | `swapProxiedUrls` on the DOM fragment                                       | `loadImage` per-image in your component                     |\n\n\nPick **Path A** if you want minimal dependencies and a renderer that matches the\nready-made widget's output. Pick **Path B** if you want full markdown features,\ncustom components, or to match an existing renderer in your app.\n\n## Backend proxy\n\nThe widget never talks to Abundly directly. You run a server-side proxy that holds the API key and forwards requests to Abundly server-to-server:\n\n```\nBrowser  ──▶  Your backend proxy  ──server-to-server──▶  Abundly API\n              (holds API key)                    X-Agent-Access-Key\n```\n\n- Set `backendUrl` to the path or URL of your proxy — a relative path like `/api/abundly` for same-origin setups, or an absolute URL for cross-origin.\n- The **API key stays server-side** — never expose it in frontend code.\n- Same-origin is the simplest setup (cookies flow automatically). For cross-origin, see the [Auth](#auth) section below.\n\nSet these environment variables on your server (values come from the Abundly portal widget settings):\n\n\n| Variable              | Description             |\n| --------------------- | ----------------------- |\n| `ABUNDLY_SERVICE_URL` | Abundly service URL     |\n| `ABUNDLY_AGENT_ID`    | Agent ID                |\n| `ABUNDLY_API_KEY`     | API access key (`ak_…`) |\n\n\nThe widget appends these paths to your `backendUrl`:\n\n\n| Route            | Method | Upstream                                               |\n| ---------------- | ------ | ------------------------------------------------------ |\n| `/status`        | GET    | `{SERVICE_URL}/agents/{AGENT_ID}/widget/status`        |\n| `/chat`          | POST   | `{SERVICE_URL}/agents/{AGENT_ID}/widget/chat` (SSE)    |\n| `/images/<path>` | GET    | `{SERVICE_URL}/agents/{AGENT_ID}/widget/images/<path>` |\n\n\nAll upstream requests use header `X-Agent-Access-Key: {API_KEY}`.\n\nFor ready-to-use backend proxy examples in Node.js, Express, Next.js, and PHP, see the [documentation](https://docs.abundly.ai/features/chat-widget#backend-proxy-examples).\n\n## Auth\n\nIf your proxy is same-origin and protected by a session cookie, you don't need to configure anything — the browser sends cookies on every request out of the box.\n\nFor everything else — Bearer/JWT tokens, custom `X-*` headers, CSRF tokens, API keys, cross-origin cookies — pass `headers` and/or `credentials` on the mount call (or to `createClient`). Both work the same way in `mountChatWidget` and in IIFE script-tag usage.\n\n### Bearer / JWT (rotating token)\n\nUse the function form so the token is re-read on every request — tokens that rotate (Auth0, Clerk, NextAuth, Supabase, Firebase, custom) won't go stale.\n\n```tsx\nmountChatWidget({\n   backendUrl: \"/api/abundly\",\n   headers: async () => ({\n      Authorization: `Bearer ${await getAccessToken()}`,\n   }),\n});\n```\n\n### Static header (API key, tenant, …)\n\nUse the object form when the value doesn't change:\n\n```tsx\nmountChatWidget({\n   backendUrl: \"/api/abundly\",\n   headers: { \"X-API-Key\": \"ak_live_...\" },\n});\n```\n\n### Cross-origin proxy with cookies\n\nIf your proxy lives on a different origin and authenticates via cookies, set `credentials: \"include\"`. The proxy must respond with `Access-Control-Allow-Credentials: true` and an explicit `Access-Control-Allow-Origin`.\n\n```tsx\nmountChatWidget({\n   backendUrl: \"https://api.example.com/abundly\",\n   credentials: \"include\",\n});\n```\n\n### Reference\n\n```ts\n// On MountChatWidgetConfig and createClient's second arg:\nheaders?:\n   | HeadersInit\n   | (() => HeadersInit | Promise<HeadersInit>);\ncredentials?: RequestCredentials;   // \"omit\" | \"same-origin\" | \"include\"\n```\n\n- `headers` is spread onto every request the widget makes (`/status`, `/chat`, and authenticated image loads). User headers are applied **after** the built-in `Content-Type: application/json`, so a user header with the same name wins.\n- `credentials` is forwarded directly to `fetch`. Defaults to the browser default (`\"same-origin\"`) when omitted.\n\n### Images and authenticated proxies\n\nNative `<img>` tags can't carry an `Authorization` header — only cookies. If your `/images` route requires header-based auth, the widget handles this for you: when `headers` or `credentials` is configured, image URLs are fetched with the same auth as `/chat`, exposed as `blob:` URLs, and swapped onto the rendered `<img>` and surrounding `<a href>` tags automatically (so \"open in new tab\" works).\n\nThe bundled `mountChatWidget` does this for you — no extra code. For custom UIs built on `createClient`, two helpers are available:\n\n- `**client.swapProxiedUrls(root)`** — preferred. After you render markdown into HTML and insert it into the DOM, call this on the parent node. It walks every `<img src>` and `<a href>` pointing at the proxy, swaps the URL to a `blob:` URL fetched with your auth, and is a no-op when no auth is configured. To avoid the browser ever firing an unauthenticated request for the original URL, parse the HTML inside a `<template>` first and only attach the children once `swapProxiedUrls` has run:\n  ```tsx\n  const tmpl = document.createElement(\"template\");\n  tmpl.innerHTML = client.renderMarkdown(text);\n  client.swapProxiedUrls(tmpl.content);\n  container.replaceChildren(tmpl.content);\n  ```\n- `**client.loadImage(url)**` — lower-level. Returns a `Promise<string>` (the resolved `blob:` URL when auth is configured, the original URL otherwise). Useful when you're constructing `<img>` elements yourself rather than rendering markdown to HTML.\n\nBoth helpers cache per-URL and fall back to the original URL on fetch failure. The ready-made `mountChatWidget` calls `client.releaseImages()` automatically on `unmount()`; custom UIs should call it on teardown to revoke the blob URLs.\n\n## License\n\nMIT","readmeFilename":"README.md"}