@filigran/chatbot 3.7.3 → 3.9.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 +290 -2
- package/dist/index.d.ts +235 -6
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +73 -0
- package/dist/markdown.js +2 -0
- package/dist/markdown.js.map +1 -0
- package/dist/styles.css +1 -1
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -6,11 +6,18 @@ Filigran chat panel — a standalone React + Tailwind chatbot component with SSE
|
|
|
6
6
|
|
|
7
7
|
- 🔄 **SSE Message Streaming** — Real-time response streaming with status indicators
|
|
8
8
|
- ⚡ **Mid-Run Steering** — Send messages while the agent is generating; they are injected into the running agentic loop instead of waiting for the turn to finish
|
|
9
|
-
-
|
|
9
|
+
- ✋ **Tool Approval** — When the agent stops at a tool that needs a human's consent, the turn pauses mid-answer and the reviewer approves, declines with a reason, or approves always — opt-in per host, see [Tool approval](#post-apibaseurlapiendpointsapprove)
|
|
10
|
+
- 🗂️ **Conversation History** — Switch between (and delete) past conversations from a header menu, or from a permanent sidebar in fullscreen mode (collapsible, searchable past 7 entries, rename in place)
|
|
10
11
|
- 🤖 **Multi-Agent Support** — Switch between different AI agents
|
|
11
12
|
- 📎 **File Attachments** — Upload and paste files (PDF, TXT, images)
|
|
12
13
|
- 📥 **Agent-Generated Files** — Renders downloadable file cards from agent output and strips the `[[FILE:id]]` markers from the prose
|
|
13
|
-
- 📝 **Full Markdown** — Tables, code blocks with copy button, lists, blockquotes
|
|
14
|
+
- 📝 **Full Markdown** — Tables (mis-delimited ones repaired), code blocks with copy button, lists, blockquotes, soft line breaks, inline images with a lightbox
|
|
15
|
+
- 🖼️ **Image Previews** — `data:image/*` charts and image attachments render inline, click to expand
|
|
16
|
+
- 📋 **Copy & Rate** — Copy any answer; optional 👍/👎 feedback wired to the host
|
|
17
|
+
- 🧰 **Composer Toolbar** — Prompt library and quota indicator, both driven by whether the host serves the route; plus a slot for the host's own controls
|
|
18
|
+
- 🧠 **Context Gauge** — Ring + percentage showing how full the model's context window is, so a long chat's silent summarising is visible before it happens
|
|
19
|
+
- 🎙️ **Dictation** — Speech-to-text via the browser's own Web Speech API; no endpoint, no key, hidden where unsupported
|
|
20
|
+
- ✍️ **Draft Recovery** — Unsent composer text is kept per conversation and restored when the panel reopens
|
|
14
21
|
- 🎨 **Customizable Theme** — Accent color and logo customization
|
|
15
22
|
- 📱 **3 Display Modes** — Floating, sidebar (resizable), and fullscreen
|
|
16
23
|
- 💾 **Persistence** — Conversation and sidebar width saved to localStorage
|
|
@@ -74,6 +81,10 @@ import { ChatPanel } from '@filigran/chatbot';
|
|
|
74
81
|
| `onWidthChange` | `(width: number) => void` | — | Called when sidebar width changes during resize |
|
|
75
82
|
| `onResizeStart` | `() => void` | — | Called when resize drag starts |
|
|
76
83
|
| `onResizeEnd` | `() => void` | — | Called when resize drag ends |
|
|
84
|
+
| `onMessageFeedback` | `(id, feedback, message) => void` | — | Enables 👍/👎 on completed assistant answers and receives each rating (`null` clears it). Omit to hide the affordance — the panel stores nothing itself. |
|
|
85
|
+
| `disableImagePreviews` | `boolean` | `false` | Render image attachments as download cards instead of inline previews |
|
|
86
|
+
| `contextUsageEnabled` | `boolean` | `true` | Show how full the model's context window is for the current conversation (ring + percentage in the composer toolbar). Data-driven, so it stays absent until the backend reports occupancy — see [Context usage](#context-usage). |
|
|
87
|
+
| `composerToolbar` | `React.ReactNode` | — | Extra controls appended to the composer toolbar. The escape hatch for host-specific affordances (XTM One's session-tool picker) — the package never learns what they are. Pass nothing and the toolbar simply has none. |
|
|
77
88
|
|
|
78
89
|
#### Resizable Sidebar Example
|
|
79
90
|
|
|
@@ -219,6 +230,12 @@ the chat. Set `apiEndpoints.history` to `null` to hide the history menu
|
|
|
219
230
|
entirely, or point it at a dedicated path if your proxy can't route `GET` on
|
|
220
231
|
the sessions path.
|
|
221
232
|
|
|
233
|
+
### `PATCH {apiBaseUrl}/chat/sessions/{conversation_id}`
|
|
234
|
+
|
|
235
|
+
Renames a conversation. Body: `{ "title": "..." }`. Only reached from the
|
|
236
|
+
fullscreen sidebar; a backend without the route simply fails the request and
|
|
237
|
+
the row reverts to its previous title.
|
|
238
|
+
|
|
222
239
|
### `DELETE {apiBaseUrl}/chat/sessions/{conversation_id}`
|
|
223
240
|
|
|
224
241
|
Deletes a conversation from the history menu. Any 2xx response counts as
|
|
@@ -378,6 +395,134 @@ data: {"type": "stream", "content": "Follow-up answer to the steering message"}
|
|
|
378
395
|
data: {"type": "done", "content": "Follow-up answer to the steering message", "conversation_id": "uuid"}
|
|
379
396
|
```
|
|
380
397
|
|
|
398
|
+
### `POST {apiBaseUrl}{apiEndpoints.approve}`
|
|
399
|
+
|
|
400
|
+
Answers a turn that paused because the agent proposed a tool call requiring a
|
|
401
|
+
human's consent. **Opt-in: there is no default path.** The widget advertises
|
|
402
|
+
approval support to the backend only when `apiEndpoints.approve` is set, and
|
|
403
|
+
that flag is a promise — a backend that pauses a turn waits indefinitely for a
|
|
404
|
+
decision, with no timeout. A host that names a path it cannot route would
|
|
405
|
+
receive the pause, POST the decision into a 404, and hang the turn with the
|
|
406
|
+
user watching a spinner. Leave it unset and the backend degrades to an ordinary
|
|
407
|
+
assistant message explaining what it could not run, so an un-updated host keeps
|
|
408
|
+
working untouched.
|
|
409
|
+
|
|
410
|
+
When set, `supports_tool_approval: true` is sent on every `rest` message body.
|
|
411
|
+
|
|
412
|
+
**Server → client**, on the existing SSE stream, alongside `stream` / `status` /
|
|
413
|
+
`done`:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
data: {"type": "approval_required", "conversation_id": "uuid-here", "proposals": [
|
|
417
|
+
{
|
|
418
|
+
"tool_call_id": "call_abc123",
|
|
419
|
+
"tool_name": "opencti_delete_entity",
|
|
420
|
+
"tool_description": "Permanently delete an entity from the platform.",
|
|
421
|
+
"arguments": {"entity_id": "e-123", "cascade": true},
|
|
422
|
+
"input_schema": {"type": "object", "properties": {
|
|
423
|
+
"entity_id": {"type": "string", "description": "Entity to delete"},
|
|
424
|
+
"cascade": {"type": "boolean", "description": "Also delete linked entities"}
|
|
425
|
+
}},
|
|
426
|
+
"source": "integration:opencti"
|
|
427
|
+
}
|
|
428
|
+
]}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The turn is **not** over: no `done` arrives, the stream stays open and silent
|
|
432
|
+
(kept alive by SSE `: keepalive` comment lines, which the reader drops), and the
|
|
433
|
+
rest of the turn continues on it once a decision is sent. The progress bubble is
|
|
434
|
+
replaced by the prompt, which renders each argument next to its description from
|
|
435
|
+
`input_schema` — `cascade: true` is unjudgeable on its own, so a prompt showing
|
|
436
|
+
only names and values would be a rubber stamp.
|
|
437
|
+
|
|
438
|
+
**Client → server**, one decision per proposed call:
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"conversation_id": "uuid-here",
|
|
443
|
+
"decisions": [
|
|
444
|
+
{ "tool_call_id": "call_abc123", "decision": "approve" },
|
|
445
|
+
{ "tool_call_id": "call_ghi789", "decision": "reject", "rejection_reason": "Wrong target environment." },
|
|
446
|
+
{ "tool_call_id": "call_jkl012", "decision": "approve_always" }
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
| `decision` | Effect |
|
|
452
|
+
| ---------------- | ------------------------------------------------------------------- |
|
|
453
|
+
| `approve` | Runs with the arguments exactly as proposed |
|
|
454
|
+
| `reject` | Does not run; the agent receives `rejection_reason` and can adapt |
|
|
455
|
+
| `approve_always` | Runs, **and** saves a standing approval for this user |
|
|
456
|
+
|
|
457
|
+
Every proposed `tool_call_id` must appear exactly once — the backend refuses a
|
|
458
|
+
partial set, because resuming with an undecided call leaves a `tool_use` block
|
|
459
|
+
without its `tool_result`, which the model providers reject outright. The prompt
|
|
460
|
+
therefore submits itself once the last card is decided. A set containing
|
|
461
|
+
`approve_always` waits for an explicit Confirm instead: it is the only verdict
|
|
462
|
+
whose reach outlives the turn (it applies to the user's unattended scheduled
|
|
463
|
+
runs too), so the warning has to be read before it is committed.
|
|
464
|
+
|
|
465
|
+
A decision carries no arguments. Correcting a wrong proposal is what
|
|
466
|
+
`reject` with a reason is for — rewriting a call under the agent's name would
|
|
467
|
+
leave a transcript crediting it with arguments it never chose.
|
|
468
|
+
|
|
469
|
+
On a non-2xx the prompt stays on screen with the failure noted and the controls
|
|
470
|
+
re-armed: the turn is still paused either way, so clearing the prompt would
|
|
471
|
+
strand it with nothing able to answer. A `409` means nothing is waiting any more
|
|
472
|
+
(the turn finished, was cancelled, or was answered elsewhere); the stream ending
|
|
473
|
+
then clears the prompt on its own. Stopping the turn also dismisses it — the
|
|
474
|
+
backend waits indefinitely by design, so abandoning the stream is the reviewer's
|
|
475
|
+
only other way out.
|
|
476
|
+
|
|
477
|
+
**Proxied hosts:** the decision goes through the same fetch path as every other
|
|
478
|
+
endpoint — relative to `apiBaseUrl` and honouring `requestHeaders` — so CSRF
|
|
479
|
+
wrappers and per-request context headers keep working. A proxy in front of the
|
|
480
|
+
chat must forward the request body whole (a proxy rebuilding it from a fixed
|
|
481
|
+
field list silently drops `supports_tool_approval`), never time out the
|
|
482
|
+
streaming turn, and pass SSE keepalives through untouched.
|
|
483
|
+
|
|
484
|
+
#### Recovering a prompt after a page reload
|
|
485
|
+
|
|
486
|
+
`approval_required` is a single event on a stream, so a reload loses it —
|
|
487
|
+
including the `tool_call_id`s a decision has to name — while the turn goes on
|
|
488
|
+
waiting for an answer that can no longer be given. To the user that is a chat
|
|
489
|
+
which simply stopped replying.
|
|
490
|
+
|
|
491
|
+
Set `apiEndpoints.pendingApprovals` (XTM One: `/chat/conversations`) and the
|
|
492
|
+
panel asks once per conversation, on mount and on every conversation switch:
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
GET {apiBaseUrl}{apiEndpoints.pendingApprovals}/{conversation_id}/pending-approvals
|
|
496
|
+
→ {
|
|
497
|
+
"conversation_id": "uuid-here",
|
|
498
|
+
"proposals": [ /* as the event carried */ ],
|
|
499
|
+
"turn": "running" | "idle"
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
An empty `proposals` is the ordinary answer. A non-empty one re-renders the same
|
|
504
|
+
prompt, and the decision is POSTed exactly as before. Like `approve` this has no
|
|
505
|
+
default and is skipped when unset, leaving the live flow untouched.
|
|
506
|
+
|
|
507
|
+
The recovered turn resumes with the reasoning and tool results it had already
|
|
508
|
+
produced — but **not on a stream**: the one it would have reported on died with
|
|
509
|
+
the old page, so the backend persists the answer and suppresses the live `done`
|
|
510
|
+
frame. So after a decision on a recovered prompt the panel shows its ordinary
|
|
511
|
+
working indicator and polls this route every 5s, using `turn` as the stop
|
|
512
|
+
condition: while it reads `running` it keeps waiting, and the moment it reads
|
|
513
|
+
`idle` it re-reads the conversation once — the answer is there. A resumed turn
|
|
514
|
+
that pauses *again* on a second gated call is picked up by the same poll, which
|
|
515
|
+
is the only way that prompt could reach the user with no stream open.
|
|
516
|
+
|
|
517
|
+
One bound applies server-side: after 30 minutes with **no sign of a client** and
|
|
518
|
+
no decision, the turn stops waiting and the pause is discarded. Any request about
|
|
519
|
+
the conversation counts as a sign, so while a recovered prompt is displayed the
|
|
520
|
+
panel re-reads this route every 10 minutes purely to say someone is still there —
|
|
521
|
+
a tab left open makes no requests of its own, and the bound is meant to limit
|
|
522
|
+
abandonment, never the person deciding.
|
|
523
|
+
|
|
524
|
+
REST backend only: `legacy` and `ag-ui` never meet this gate.
|
|
525
|
+
|
|
381
526
|
## Customization
|
|
382
527
|
|
|
383
528
|
### Custom Logo
|
|
@@ -464,6 +609,18 @@ function App() {
|
|
|
464
609
|
- `'Uses AI. Verify results.'`
|
|
465
610
|
- `'How can I help you, '`
|
|
466
611
|
- `'Suggestions'`
|
|
612
|
+
- `'Waiting for your approval…'`
|
|
613
|
+
- `'The agent needs your approval to run a tool:'` / `'The agent needs your approval to run these tools:'`
|
|
614
|
+
- `'Yes'` / `'No'` / `'Yes, always'` / `'Back'` / `'Confirm'` / `'Sending…'` / `'decided'`
|
|
615
|
+
- `'Approved'` / `'Declined'` / `'Always allowed'`
|
|
616
|
+
- `'Decline this call'`
|
|
617
|
+
- `'Why not? The agent sees this and can adapt (optional)'`
|
|
618
|
+
- `'e.g. wrong environment — use staging instead'`
|
|
619
|
+
- `'Also applies to your scheduled runs, until you revoke it'`
|
|
620
|
+
- `'“Yes, always” saves a preference for you. That tool will then run without asking — including on scheduled runs nobody is watching — until you revoke it.'`
|
|
621
|
+
- `'This turn is no longer waiting for a decision.'`
|
|
622
|
+
- `'Could not send your decision. Please try again.'`
|
|
623
|
+
- `'This decision could not be sent. Reload the chat and try again.'`
|
|
467
624
|
- `'Floating'`
|
|
468
625
|
- `'Sidebar'`
|
|
469
626
|
- `'Full screen'`
|
|
@@ -478,11 +635,142 @@ import '@filigran/chatbot/styles.css';
|
|
|
478
635
|
|
|
479
636
|
The component uses Tailwind CSS classes and CSS custom properties for theming. The accent color is applied via `--chat-accent` CSS variable.
|
|
480
637
|
|
|
638
|
+
### Composer toolbar
|
|
639
|
+
|
|
640
|
+
Two toolbar items are **data-driven rather than mode-driven**: they appear only
|
|
641
|
+
when the host serves the route, so the UI can never advertise something the
|
|
642
|
+
backend cannot answer, and there is no mode flag to keep in step.
|
|
643
|
+
|
|
644
|
+
| Endpoint | Default path | Response |
|
|
645
|
+
| --- | --- | --- |
|
|
646
|
+
| Prompt library | `GET {apiBaseUrl}/chat/prompts` | `[{ id, title, content, description? }]` (or `{ prompts: [...] }`) |
|
|
647
|
+
| Quota status | `GET {apiBaseUrl}/chat/quota` | `{ used: number, limit: number \| null, period: string }` |
|
|
648
|
+
| Agent suggestions | `GET {apiBaseUrl}/chat/suggestions?agent_slug=<slug>` | `["..."]` (or `{ suggestions: [...] }`, or objects with `prompt`/`label`/`text`) |
|
|
649
|
+
|
|
650
|
+
Set either to `null` in `apiEndpoints` to hide it. `limit: null` means no
|
|
651
|
+
ceiling — the indicator then shows consumption without a bar. The quota is
|
|
652
|
+
re-read whenever a turn finishes.
|
|
653
|
+
|
|
654
|
+
The welcome screen names the selected agent and shows its own suggestions —
|
|
655
|
+
which is also how switching agent is confirmed: the thread resets to that
|
|
656
|
+
screen, so without it nothing tells you who the next message will reach. When
|
|
657
|
+
the suggestions route is unavailable the host's `promptSuggestions` prop is used
|
|
658
|
+
instead, so the section is never empty.
|
|
659
|
+
|
|
660
|
+
Dictation needs no configuration at all: it uses the browser's own Web Speech
|
|
661
|
+
API, so the mic button appears wherever the API exists and is simply absent
|
|
662
|
+
elsewhere. Finalised phrases are appended to the composer (never replacing a
|
|
663
|
+
draft), interim words preview beside the button, and sending stops the mic so
|
|
664
|
+
the next words cannot land in a composer the user just emptied.
|
|
665
|
+
|
|
666
|
+
### Context usage
|
|
667
|
+
|
|
668
|
+
The composer also carries a context gauge — a small ring plus percentage
|
|
669
|
+
showing how full the model's context window is for the current conversation,
|
|
670
|
+
the affordance Cursor popularised. It answers one question: *is this
|
|
671
|
+
conversation about to get shorter than I think?* Long chats do not fail at the
|
|
672
|
+
window, they get silently summarised, and a user who cannot see that coming
|
|
673
|
+
reads the summary's gaps as the assistant forgetting.
|
|
674
|
+
|
|
675
|
+
Clicking it opens a breakdown: one stacked bar over the window plus a colour
|
|
676
|
+
legend, so "why is this chat 84 % full" has an answer the user can act on —
|
|
677
|
+
usually "the tool results", sometimes "the MCP tools you wired up".
|
|
678
|
+
|
|
679
|
+
Unlike the items above it needs no endpoint of its own. The backend reports the
|
|
680
|
+
occupancy on the frames it already sends:
|
|
681
|
+
|
|
682
|
+
| Frame | Extra keys | When |
|
|
683
|
+
| --- | --- | --- |
|
|
684
|
+
| `status: "thinking"` | `context_tokens`, `context_window`, `context_breakdown?` | Each agent-loop iteration, so the gauge climbs during a long turn |
|
|
685
|
+
| `done` | same | Closing value for the turn — a turn whose last iteration compacted ends lower than it peaked |
|
|
686
|
+
| Restored message (`POST /chat/sessions`) | same | On the newest assistant message, so a reload or conversation switch restores the gauge |
|
|
687
|
+
|
|
688
|
+
`context_tokens` and `context_window` are required together and the window must
|
|
689
|
+
be positive: a token count with no window to measure it against is not a ratio.
|
|
690
|
+
Anything else is ignored, so a backend that reports nothing simply has no gauge —
|
|
691
|
+
and a host on an older backend needs no configuration change.
|
|
692
|
+
|
|
693
|
+
`context_breakdown` is optional and validated independently, so a malformed one
|
|
694
|
+
costs the gauge its detail but never its number. Keys, all optional, in tokens:
|
|
695
|
+
|
|
696
|
+
| Key | Legend row |
|
|
697
|
+
| --- | --- |
|
|
698
|
+
| `system` | System prompt |
|
|
699
|
+
| `tools` | Tool definitions |
|
|
700
|
+
| `dynamic_tools` | MCP & dynamic tools |
|
|
701
|
+
| `summary` | Summarized conversation |
|
|
702
|
+
| `tool_results` | Tool results |
|
|
703
|
+
| `conversation` | Conversation |
|
|
704
|
+
|
|
705
|
+
Only positive values are shown, so a chat with no digest yet simply has no
|
|
706
|
+
"Summarized conversation" row. **The values must sum to `context_tokens`** — the
|
|
707
|
+
popover shows the lines and the headline together, and a breakdown whose parts
|
|
708
|
+
do not add up reads as broken numbers rather than as rounding. A producer doing
|
|
709
|
+
per-bucket integer division has to distribute the remainder rather than drop it.
|
|
710
|
+
|
|
711
|
+
Colours track the backend's own thresholds, not a design choice: neutral below
|
|
712
|
+
80 % (where XTM One's agent loop starts distilling older turns into a summary),
|
|
713
|
+
amber past it, red past 95 % (where it emergency-prunes). `used` is a
|
|
714
|
+
char-derived estimate of what the next call will carry — deliberately a forecast
|
|
715
|
+
rather than a receipt for the turn that just ended, which is why the figures are
|
|
716
|
+
prefixed with `~`.
|
|
717
|
+
|
|
718
|
+
Only the `rest` backend carries the figures today.
|
|
719
|
+
|
|
720
|
+
Anything host-specific goes through `composerToolbar`:
|
|
721
|
+
|
|
722
|
+
```tsx
|
|
723
|
+
<ChatPanel
|
|
724
|
+
composerToolbar={<MySessionToolPicker />}
|
|
725
|
+
{...rest}
|
|
726
|
+
/>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
## Markdown Helpers
|
|
730
|
+
|
|
731
|
+
A host that renders assistant prose with its **own** markdown component (its
|
|
732
|
+
design tokens, its icon set) should still normalise the text the same way the
|
|
733
|
+
panel does, rather than maintaining a divergent copy. These pure
|
|
734
|
+
`string → string` helpers ship from the dedicated **`@filigran/chatbot/markdown`**
|
|
735
|
+
entry point — ~2 kB, no React, no CSS. Import them from there and never from the
|
|
736
|
+
package root, which is the full panel bundle:
|
|
737
|
+
|
|
738
|
+
```tsx
|
|
739
|
+
import {
|
|
740
|
+
hardenNestedCodeFences,
|
|
741
|
+
markdownUrlTransform,
|
|
742
|
+
normalizeImageMarkdown,
|
|
743
|
+
normalizeMarkdownTables,
|
|
744
|
+
wrapBareJson,
|
|
745
|
+
} from '@filigran/chatbot/markdown';
|
|
746
|
+
|
|
747
|
+
const processed = hardenNestedCodeFences(
|
|
748
|
+
normalizeMarkdownTables(wrapBareJson(normalizeImageMarkdown(content))),
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
<ReactMarkdown urlTransform={markdownUrlTransform}>{processed}</ReactMarkdown>;
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
Order matters: alt-text is flattened before anything reads line structure, and
|
|
755
|
+
the JSON wrap must see the raw payload before fences are hardened.
|
|
756
|
+
|
|
757
|
+
| Helper | Fixes |
|
|
758
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
759
|
+
| `normalizeImageMarkdown` | Multi-line `` alt text, which breaks the image into literal paragraphs plus a stray link |
|
|
760
|
+
| `wrapBareJson` | A whole message that is raw JSON — fenced as ```json so it stays readable and copyable |
|
|
761
|
+
| `normalizeMarkdownTables` | A delimiter row whose column count doesn't match the header, including tables nested in blockquotes / list items |
|
|
762
|
+
| `hardenNestedCodeFences` | A ```markdown block containing its own ``` fences, which shatters the snippet into alternating code and prose |
|
|
763
|
+
| `markdownUrlTransform` | react-markdown's default sanitiser stripping `data:image/*` URIs (code-interpreter charts). Still blocks `javascript:` and non-image `data:` |
|
|
764
|
+
|
|
765
|
+
None of them touch content they don't apply to — an already-valid document is
|
|
766
|
+
returned byte-identical.
|
|
767
|
+
|
|
481
768
|
## Peer Dependencies
|
|
482
769
|
|
|
483
770
|
- `react` >= 18
|
|
484
771
|
- `react-dom` >= 18
|
|
485
772
|
- `react-markdown` >= 10
|
|
773
|
+
- `remark-breaks` >= 4
|
|
486
774
|
- `remark-gfm` >= 4
|
|
487
775
|
|
|
488
776
|
---
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { FunctionComponent } from 'react';
|
|
|
2
2
|
|
|
3
3
|
type ChatMode = 'sidebar' | 'floating' | 'fullscreen';
|
|
4
4
|
type BackendType = 'legacy' | 'rest' | 'ag-ui';
|
|
5
|
+
/** A user's rating of one assistant answer. */
|
|
6
|
+
type MessageFeedback = 'up' | 'down';
|
|
5
7
|
/**
|
|
6
8
|
* Custom API endpoint configuration.
|
|
7
9
|
* When using single endpoint mode (like OpenCTI's /chatbot), set singleEndpoint to true
|
|
@@ -23,6 +25,46 @@ interface ApiEndpoints {
|
|
|
23
25
|
* disable mid-run steering entirely.
|
|
24
26
|
*/
|
|
25
27
|
steer?: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Path for submitting tool-approval decisions when the agent pauses on a
|
|
30
|
+
* gated tool call. The widget POSTs
|
|
31
|
+
* `{ conversation_id, decisions: [{ tool_call_id, decision, rejection_reason }] }`
|
|
32
|
+
* and the paused turn resumes on the same stream with the tool result.
|
|
33
|
+
*
|
|
34
|
+
* **No default, unlike every sibling entry, and that is deliberate.** Setting
|
|
35
|
+
* this is what makes the widget advertise `supports_tool_approval` to the
|
|
36
|
+
* backend, and advertising it is a promise to answer: a backend that pauses a
|
|
37
|
+
* turn waits indefinitely for a decision, with no timeout and no safe default
|
|
38
|
+
* action to take on the reviewer's behalf. If this path defaulted to XTM
|
|
39
|
+
* One's own route, a host proxying the chat (OpenCTI, OpenAEV, OpenGRC) could
|
|
40
|
+
* upgrade the widget without adding the matching proxy route, claim support,
|
|
41
|
+
* receive the pause, POST the decision into a 404 — and hang the turn with
|
|
42
|
+
* the user watching a spinner.
|
|
43
|
+
*
|
|
44
|
+
* While unset the widget never claims support and the backend degrades to a
|
|
45
|
+
* plain assistant message explaining what it could not run, which is why an
|
|
46
|
+
* un-updated host keeps working untouched.
|
|
47
|
+
*
|
|
48
|
+
* REST backend only: `legacy` and `ag-ui` never meet this gate.
|
|
49
|
+
*/
|
|
50
|
+
approve?: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Base path for recovering what a paused turn is still waiting on, read as
|
|
53
|
+
* `GET {apiBaseUrl}{pendingApprovals}/{conversation_id}/pending-approvals`
|
|
54
|
+
* (the same base-plus-suffix idiom as {@link ApiEndpoints.download}). XTM
|
|
55
|
+
* One serves it at `/chat/conversations`.
|
|
56
|
+
*
|
|
57
|
+
* `approval_required` is a single event on a stream, so a reload loses it —
|
|
58
|
+
* including the `tool_call_id`s a decision has to name. The turn is left
|
|
59
|
+
* waiting for an answer nobody can give, which reads as a chat that simply
|
|
60
|
+
* stopped replying. The panel therefore asks once per conversation on mount;
|
|
61
|
+
* an empty list is the ordinary answer.
|
|
62
|
+
*
|
|
63
|
+
* No default, for the same reason as {@link ApiEndpoints.approve}: a proxied
|
|
64
|
+
* host has to expose the route before the panel starts calling it. Unset, the
|
|
65
|
+
* live flow still works and only reload recovery is absent.
|
|
66
|
+
*/
|
|
67
|
+
pendingApprovals?: string | null;
|
|
26
68
|
/** Path for fetching agents. Default: '/chat/agents'. Set to null to disable. */
|
|
27
69
|
agents?: string | null;
|
|
28
70
|
/** Path for fetching session history. Default: '/chat/sessions'. Set to null to disable. */
|
|
@@ -51,6 +93,140 @@ interface ApiEndpoints {
|
|
|
51
93
|
* unless this path is set explicitly to a proxy route.
|
|
52
94
|
*/
|
|
53
95
|
download?: string | null;
|
|
96
|
+
/**
|
|
97
|
+
* Path for the prompt library shown in the composer toolbar.
|
|
98
|
+
* Default: '/chat/prompts'. Set to null to hide the affordance.
|
|
99
|
+
*
|
|
100
|
+
* Visibility is data-driven on purpose: a host that does not serve this
|
|
101
|
+
* route simply has no prompt button, so there is no separate "mode" to keep
|
|
102
|
+
* in step with what the backend actually implements.
|
|
103
|
+
*/
|
|
104
|
+
prompts?: string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Path for the quota indicator shown in the composer toolbar.
|
|
107
|
+
* Default: '/chat/quota'. Set to null to hide the affordance.
|
|
108
|
+
*/
|
|
109
|
+
quota?: string | null;
|
|
110
|
+
/**
|
|
111
|
+
* Path for per-agent suggested actions shown on the welcome screen.
|
|
112
|
+
* Default: '/chat/suggestions'. Set to null to always use the host's
|
|
113
|
+
* `promptSuggestions` prop instead.
|
|
114
|
+
*
|
|
115
|
+
* Called as `GET {suggestions}?agent_slug=<slug>`. A backend that ignores
|
|
116
|
+
* the parameter still answers with a generic set, so the same route carries
|
|
117
|
+
* both today's generic suggestions and per-agent (later per-user) ones.
|
|
118
|
+
*/
|
|
119
|
+
suggestions?: string | null;
|
|
120
|
+
}
|
|
121
|
+
/** A reusable prompt the user can insert into the composer. */
|
|
122
|
+
interface ChatPromptTemplate {
|
|
123
|
+
id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
/** The text inserted into the composer when picked. */
|
|
126
|
+
content: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Agentic quota headroom for the current user, as the composer indicator needs
|
|
131
|
+
* it — deliberately just the three numbers it renders. Where the limit comes
|
|
132
|
+
* from (user override, group, platform, licence) is the host platform's own
|
|
133
|
+
* business and has no place on an embedded surface.
|
|
134
|
+
*/
|
|
135
|
+
interface ChatQuotaStatus {
|
|
136
|
+
used: number;
|
|
137
|
+
/** null means unlimited — the indicator then shows usage without a bar. */
|
|
138
|
+
limit: number | null;
|
|
139
|
+
/** Human-readable period label, e.g. "monthly". */
|
|
140
|
+
period: string;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* One tool call the agent wants to make and is waiting on a human to approve.
|
|
144
|
+
*
|
|
145
|
+
* `inputSchema` travels alongside `arguments` so each value can be rendered
|
|
146
|
+
* with the tool's own description of what it means. That pairing is the whole
|
|
147
|
+
* point: `cascade: true` is unjudgeable on its own, while "cascade — also
|
|
148
|
+
* delete linked entities" is a decision someone can actually make. A prompt
|
|
149
|
+
* that shows names and values alone is a rubber stamp wearing the costume of a
|
|
150
|
+
* safety control.
|
|
151
|
+
*/
|
|
152
|
+
interface ToolApprovalProposal {
|
|
153
|
+
/**
|
|
154
|
+
* Identity of the proposed call, and the key every decision is sent back on.
|
|
155
|
+
* Never the tool name: one turn can propose the same tool twice with
|
|
156
|
+
* different arguments.
|
|
157
|
+
*/
|
|
158
|
+
toolCallId: string;
|
|
159
|
+
toolName: string;
|
|
160
|
+
toolDescription?: string;
|
|
161
|
+
arguments: Record<string, unknown>;
|
|
162
|
+
/** JSON Schema the arguments came from, used to label each one. */
|
|
163
|
+
inputSchema?: Record<string, unknown>;
|
|
164
|
+
/** Where the tool comes from, e.g. `integration:opencti`. */
|
|
165
|
+
source?: string;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A reviewer's verdict on one proposed call.
|
|
169
|
+
*
|
|
170
|
+
* There is deliberately no "edit the arguments" verdict. Rewriting a call under
|
|
171
|
+
* the agent's name would leave a transcript crediting it with arguments it
|
|
172
|
+
* never chose, and turn a yes/no judgement into authoring. A wrong proposal is
|
|
173
|
+
* corrected by rejecting it with a reason the agent can act on.
|
|
174
|
+
*/
|
|
175
|
+
type ToolApprovalVerdict = 'approve' | 'approve_always' | 'reject';
|
|
176
|
+
/** One decision, ready to be sent back to the paused turn. */
|
|
177
|
+
interface ToolApprovalDecision {
|
|
178
|
+
toolCallId: string;
|
|
179
|
+
verdict: ToolApprovalVerdict;
|
|
180
|
+
/**
|
|
181
|
+
* Why the call was declined. Optional, and the agent's only signal to correct
|
|
182
|
+
* itself — with argument editing gone, a rejection *is* the correction
|
|
183
|
+
* channel.
|
|
184
|
+
*/
|
|
185
|
+
rejectionReason?: string;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* How full the model's context window is for the current conversation, as the
|
|
189
|
+
* composer gauge needs it — a ratio, so both halves are required.
|
|
190
|
+
*
|
|
191
|
+
* `used` is the backend's own estimate of what the next turn will carry, not a
|
|
192
|
+
* billed token count: it is the figure the agent loop budgets its compaction
|
|
193
|
+
* against, which is what makes the gauge predictive of the summarising the user
|
|
194
|
+
* is about to see rather than a receipt for the turn that just ended.
|
|
195
|
+
*/
|
|
196
|
+
interface ChatContextUsage {
|
|
197
|
+
/** Estimated tokens currently occupying the window. */
|
|
198
|
+
used: number;
|
|
199
|
+
/** The model's context window, in tokens. Always > 0. */
|
|
200
|
+
limit: number;
|
|
201
|
+
/** Where those tokens went, when the backend reports it. */
|
|
202
|
+
breakdown?: ChatContextBreakdown;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* What the context is being spent on, in tokens.
|
|
206
|
+
*
|
|
207
|
+
* Buckets are grouped by what the user can *do* about each: they can start a new
|
|
208
|
+
* chat (`conversation`), they cannot shrink the agent's persona (`system`), and
|
|
209
|
+
* the two the backend manages on their behalf (`summary`, `toolResults`) are
|
|
210
|
+
* what explain a long chat that seems to have forgotten things.
|
|
211
|
+
*
|
|
212
|
+
* Every key is optional and only non-zero ones are reported, so a backend that
|
|
213
|
+
* measures a different set — or none — still renders. Tool definitions are
|
|
214
|
+
* included even though some backends may not count them in their compaction
|
|
215
|
+
* gate; the gauge is meant to reflect what the prompt actually carries.
|
|
216
|
+
*/
|
|
217
|
+
interface ChatContextBreakdown {
|
|
218
|
+
/** The agent's system prompt and any in-run instructions. */
|
|
219
|
+
system?: number;
|
|
220
|
+
/** Schemas of the platform's built-in tools. */
|
|
221
|
+
tools?: number;
|
|
222
|
+
/** Schemas of the agent's own integrations, MCP servers and custom tools. */
|
|
223
|
+
dynamicTools?: number;
|
|
224
|
+
/** Digests of earlier turns produced by the backend's compaction. */
|
|
225
|
+
summary?: number;
|
|
226
|
+
/** The user's and assistant's own messages. */
|
|
227
|
+
conversation?: number;
|
|
228
|
+
/** Output of tool calls still held verbatim in the context. */
|
|
229
|
+
toolResults?: number;
|
|
54
230
|
}
|
|
55
231
|
interface ChatPanelProps {
|
|
56
232
|
mode: ChatMode;
|
|
@@ -138,17 +314,53 @@ interface ChatPanelProps {
|
|
|
138
314
|
/**
|
|
139
315
|
* Notify the user when a long-running turn finishes while they are not
|
|
140
316
|
* watching the chat — away (tab hidden / another window) via a document-title
|
|
141
|
-
* flash and a browser notification (when already granted), or in-app
|
|
142
|
-
*
|
|
317
|
+
* flash and a browser notification (when already granted), or in-app with the
|
|
318
|
+
* chat panel closed/hidden via the `onTaskComplete` host toast. Default: true.
|
|
143
319
|
*/
|
|
144
320
|
notifyOnComplete?: boolean;
|
|
145
321
|
/**
|
|
146
322
|
* Called when a long turn finishes and the user is not actively watching the
|
|
147
|
-
* chat (away, or in-app
|
|
148
|
-
* in-app toast (the chatbot has no toast surface of its own).
|
|
149
|
-
* already-translated `title` and `body`.
|
|
323
|
+
* chat (away, or in-app with the chat panel closed/hidden) so the host can
|
|
324
|
+
* raise its own in-app toast (the chatbot has no toast surface of its own).
|
|
325
|
+
* Receives the already-translated `title` and `body`.
|
|
150
326
|
*/
|
|
151
327
|
onTaskComplete?: (title: string, body: string) => void;
|
|
328
|
+
/**
|
|
329
|
+
* Enables the 👍/👎 affordance on completed assistant messages and receives
|
|
330
|
+
* each rating. `feedback` is `null` when the user clears a previous rating.
|
|
331
|
+
* Omit to hide the affordance entirely — the chatbot stores nothing itself,
|
|
332
|
+
* so a host without a feedback endpoint should not show the buttons.
|
|
333
|
+
*/
|
|
334
|
+
onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;
|
|
335
|
+
/**
|
|
336
|
+
* Disable the inline preview of image attachments (they render as ordinary
|
|
337
|
+
* download cards instead). Previews fetch the image through the host download
|
|
338
|
+
* proxy, so hosts that meter or restrict that endpoint can opt out.
|
|
339
|
+
* Default: false.
|
|
340
|
+
*/
|
|
341
|
+
disableImagePreviews?: boolean;
|
|
342
|
+
/**
|
|
343
|
+
* Show how full the model's context window is for the current conversation
|
|
344
|
+
* — a small ring plus percentage in the composer toolbar, so the user can
|
|
345
|
+
* see a long chat approaching the point where the agent starts summarising
|
|
346
|
+
* older turns and decide to split the work instead. Default: true.
|
|
347
|
+
*
|
|
348
|
+
* A host-level master switch, not a mode flag: the gauge is data-driven and
|
|
349
|
+
* simply absent until the backend reports occupancy (only the XTM One REST
|
|
350
|
+
* backend does today), so leaving this on costs nothing on a backend that
|
|
351
|
+
* says nothing.
|
|
352
|
+
*/
|
|
353
|
+
contextUsageEnabled?: boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Rendered into the composer toolbar, after the built-in controls.
|
|
356
|
+
*
|
|
357
|
+
* The escape hatch for anything the package has no business knowing about —
|
|
358
|
+
* XTM One's session-tool picker (integrations, MCP servers, knowledge bases)
|
|
359
|
+
* being the motivating case. A host that passes nothing gets no extra
|
|
360
|
+
* controls, so this doubles as the "product" toolbar: there is no mode flag
|
|
361
|
+
* to keep in step, only the presence or absence of what a host provides.
|
|
362
|
+
*/
|
|
363
|
+
composerToolbar?: React.ReactNode;
|
|
152
364
|
}
|
|
153
365
|
interface ChatToggleButtonProps {
|
|
154
366
|
isOpen: boolean;
|
|
@@ -162,6 +374,16 @@ interface ChatMessage {
|
|
|
162
374
|
role: 'user' | 'assistant';
|
|
163
375
|
content: string;
|
|
164
376
|
timestamp: Date;
|
|
377
|
+
/**
|
|
378
|
+
* The agent that produced *this* message, when the backend says so.
|
|
379
|
+
*
|
|
380
|
+
* Takes precedence over the panel-wide name, which is the currently selected
|
|
381
|
+
* agent and therefore wrong for history: reopening a thread used to relabel
|
|
382
|
+
* every past answer with whoever happened to be picked in the menu. Optional,
|
|
383
|
+
* because no backend records per-message attribution yet — the panel falls
|
|
384
|
+
* back to the conversation's agent, then to the selected one.
|
|
385
|
+
*/
|
|
386
|
+
agentName?: string;
|
|
165
387
|
files?: ChatFile[];
|
|
166
388
|
/** Agent-generated downloadable files attached to an assistant message. */
|
|
167
389
|
attachments?: ChatAttachment[];
|
|
@@ -247,6 +469,13 @@ interface ChatConversationSummary {
|
|
|
247
469
|
/** ISO timestamp of the last activity, used for the relative-time label. */
|
|
248
470
|
updatedAt?: string;
|
|
249
471
|
messageCount?: number;
|
|
472
|
+
/**
|
|
473
|
+
* The agent this conversation belongs to, when the backend reports it — so
|
|
474
|
+
* the history list can say which agent a thread is with before you open it.
|
|
475
|
+
* Undefined on backends that do not send it, null-ish for conversations that
|
|
476
|
+
* genuinely have no agent.
|
|
477
|
+
*/
|
|
478
|
+
agentName?: string;
|
|
250
479
|
}
|
|
251
480
|
interface XtmAgent {
|
|
252
481
|
id: string;
|
|
@@ -266,4 +495,4 @@ interface TransferredAgent {
|
|
|
266
495
|
}
|
|
267
496
|
|
|
268
497
|
export { ChatPanel, ChatToggleButton };
|
|
269
|
-
export type { ApiEndpoints, BackendType, ChatAttachment, ChatConversationSummary, ChatFile, ChatMessage, ChatMode, ChatPanelProps, ChatToggleButtonProps, TransferredAgent, XtmAgent };
|
|
498
|
+
export type { ApiEndpoints, BackendType, ChatAttachment, ChatContextBreakdown, ChatContextUsage, ChatConversationSummary, ChatFile, ChatMessage, ChatMode, ChatPanelProps, ChatPromptTemplate, ChatQuotaStatus, ChatToggleButtonProps, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, XtmAgent };
|