@filigran/chatbot 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Filigran chat panel — a standalone React + Tailwind chatbot component with SSE
13
13
  - 📥 **Agent-Generated Files** — Renders downloadable file cards from agent output and strips the `[[FILE:id]]` markers from the prose
14
14
  - 📝 **Full Markdown** — Tables (mis-delimited ones repaired), code blocks with copy button, lists, blockquotes, soft line breaks, inline images with a lightbox
15
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
16
+ - 📋 **Message Footer** — Every message says when it was sent; copy any message, rate an answer 👍/👎 (stored by the backend or handed to the host, with an optional comment on a 👎) and have it read aloud — see [Message feedback](#message-feedback)
17
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
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
19
  - 🎙️ **Dictation** — Speech-to-text via the browser's own Web Speech API; no endpoint, no key, hidden where unsupported
@@ -83,7 +83,8 @@ import { ChatPanel } from '@filigran/chatbot';
83
83
  | `onWidthChange` | `(width: number) => void` | — | Called when sidebar width changes during resize |
84
84
  | `onResizeStart` | `() => void` | — | Called when resize drag starts |
85
85
  | `onResizeEnd` | `() => void` | — | Called when resize drag ends |
86
- | `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. |
86
+ | `onMessageFeedback` | `(id, feedback, message) => void` | — | Receives each rating of an answer (`null` clears it); `id` is the message's persisted id when the backend reported one (`done.message_id`, a restored entry's `id`), else the panel's own. On its own it enables 👍/👎 and the panel stores nothing; with `apiEndpoints.feedback` set it is called once the backend has stored the rating. With neither, no thumbs. |
87
+ | `locale` | `string` | browser's | BCP 47 language tag the message times are written in and answers are read aloud in (the platform's language, e.g. `'fr-FR'`) |
87
88
  | `disableImagePreviews` | `boolean` | `false` | Render image attachments as download cards instead of inline previews |
88
89
  | `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). |
89
90
  | `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. |
@@ -175,12 +176,26 @@ Restores conversation history (and, implicitly, resolves the session).
175
176
  {
176
177
  "conversation_id": "uuid-here",
177
178
  "messages": [
178
- { "role": "user", "content": "Hello" },
179
- { "role": "assistant", "content": "Hi! How can I help?" }
179
+ { "id": "msg-1", "role": "user", "content": "Hello", "created_at": "2026-06-10T08:30:00Z" },
180
+ {
181
+ "id": "msg-2",
182
+ "role": "assistant",
183
+ "content": "Hi! How can I help?",
184
+ "created_at": "2026-06-10T08:30:04Z",
185
+ "feedback": { "rating": "positive", "comment": null }
186
+ }
180
187
  ]
181
188
  }
182
189
  ```
183
190
 
191
+ `id`, `created_at` and `feedback` are optional. `id` is the message's
192
+ persisted id, what its rating is stored against (see
193
+ [Message feedback](#message-feedback));
194
+ `created_at` (ISO 8601) is when it was sent, shown in its footer - a message
195
+ without one shows no time. `feedback` is the caller's own rating of an
196
+ assistant answer: the thumbs are then locked on it ("Feedback already
197
+ submitted").
198
+
184
199
  The response **should echo a `conversation_id`**. When the stored
185
200
  `conversation_id` no longer exists (e.g. the platform was reset but the
186
201
  browser kept an old id in `localStorage`), the backend is expected to
@@ -280,9 +295,13 @@ data: {"type": "status", "status": "analyzing"}
280
295
  data: {"type": "status", "status": "streaming"}
281
296
  data: {"type": "stream", "content": "The weather "}
282
297
  data: {"type": "stream", "content": "today is sunny."}
283
- data: {"type": "done", "content": "The weather today is sunny.", "conversation_id": "new-uuid", "tool_names": ["search_web"], "tool_call_count": 1, "iterations": 1, "reasoning": "Let me check the weather data first.", "tool_call_trace": [{"name": "search_web", "input": "{\"query\": \"weather\"}", "output": "Sunny, 24C", "success": true}], "transfer_chain": [{"agent_id": "uuid", "agent_name": "General"}], "is_truncated": false}
298
+ data: {"type": "done", "content": "The weather today is sunny.", "conversation_id": "new-uuid", "message_id": "msg-2", "tool_names": ["search_web"], "tool_call_count": 1, "iterations": 1, "reasoning": "Let me check the weather data first.", "tool_call_trace": [{"name": "search_web", "input": "{\"query\": \"weather\"}", "output": "Sunny, 24C", "success": true}], "transfer_chain": [{"agent_id": "uuid", "agent_name": "General"}], "is_truncated": false}
284
299
  ```
285
300
 
301
+ The optional `message_id` on `done` is the answer's persisted id: without it
302
+ (and without a restore naming the message) the answer cannot be rated through
303
+ `apiEndpoints.feedback`.
304
+
286
305
  The optional `reasoning` field on `done` (and on restored session messages)
287
306
  carries the accumulated model reasoning / pre-tool preamble prose for the
288
307
  turn. When present it is surfaced in the per-message reasoning-details dialog
@@ -530,6 +549,58 @@ abandonment, never the person deciding.
530
549
 
531
550
  REST backend only: `legacy` and `ag-ui` never meet this gate.
532
551
 
552
+ ### Message feedback
553
+
554
+ Every message ends with a footer. An assistant answer (once it has finished
555
+ streaming) shows when it was sent, the "i" reasoning details, **Copy
556
+ response**, 👍 / 👎 and **Read aloud**; a user message shows **Copy message**
557
+ and its time, right-aligned. The time is written in the `locale` prop's
558
+ language (the time alone today, the date and time before; the full date in
559
+ the tooltip). Time and "i" are always visible; the other actions appear when
560
+ the message is hovered or focused, and always on a touch screen. A rating
561
+ given, or the answer being read, stays visible.
562
+
563
+ The thumbs render when `apiEndpoints.feedback` is set **or**
564
+ `onMessageFeedback` is passed. **Opt-in: `feedback` has no default path**
565
+ (the same choice as `approve`): a host names its route, or the panel stores
566
+ nothing.
567
+
568
+ ```
569
+ POST {apiBaseUrl}{apiEndpoints.feedback}/{conversation_id}/messages/{message_id}/feedback
570
+ { "rating": "positive" | "negative", "comment": string | null }
571
+ -> 200 { "rating": ..., "comment": ... }
572
+ DELETE {apiBaseUrl}{apiEndpoints.feedback}/{conversation_id}/messages/{message_id}/feedback
573
+ -> 204
574
+ ```
575
+
576
+ Both are sent with `credentials: 'include'` and your `requestHeaders`
577
+ (XTM One: `feedback: '/chat/conversations'`). `message_id` is the answer's
578
+ persisted id — the `done` event's `message_id`, or the restored entry's
579
+ `id` — so an answer the backend did not identify shows no thumbs.
580
+
581
+ - A thumb is shown as given at once and rolled back, with "Could not submit
582
+ feedback." (or "Could not retract feedback." for a retraction) under the
583
+ message, when the backend refuses it or cannot be reached.
584
+ - Clicking the given thumb again retracts it (`DELETE`).
585
+ - Once a 👎 is stored the panel asks what went wrong: **Save** re-posts the
586
+ rating with the comment (trimmed, at most 2000 characters), **Skip** keeps
587
+ the rating without one.
588
+ - A rating restored with the conversation (`feedback` on a restored entry) is
589
+ final: only its thumb is shown, "Feedback already submitted".
590
+ - `onMessageFeedback`, when passed too, is called once the backend has
591
+ stored the rating. On its own it receives every rating and the panel
592
+ stores nothing (no comment step, since the callback has nowhere to put
593
+ one).
594
+
595
+ REST backend only, and not in `singleEndpoint` mode (there is no per-path
596
+ routing to name a feedback route with).
597
+
598
+ **Read aloud** uses the browser's own speech synthesis — no endpoint, hidden
599
+ where unsupported, and absent on an answer with nothing to say (code only). It
600
+ reads the answer's text without its markdown or file markers, in the `locale`
601
+ prop's language, one answer at a time; switching conversation, sending a
602
+ message or leaving the panel stops it.
603
+
533
604
  ## Customization
534
605
 
535
606
  ### Custom Logo
@@ -671,6 +742,7 @@ Every key the package can ask for, grouped by where it appears:
671
742
  - `'Enter to send now · Esc to stop'`
672
743
  - `'Files uploading...'`
673
744
  - `'Insert prompt template'`
745
+ - `'Listening...'`
674
746
  - `'No prompt matches'`
675
747
  - `'Search prompts...'`
676
748
  - `'Send now'`
@@ -684,16 +756,26 @@ Every key the package can ask for, grouped by where it appears:
684
756
  - `'Copied'`
685
757
  - `'Copied!'`
686
758
  - `'Copy code'`
759
+ - `'Copy message'`
687
760
  - `'Copy response'`
761
+ - `'Could not retract feedback.'`
762
+ - `'Could not submit feedback.'`
688
763
  - `'Download'`
689
764
  - `'Expand image'`
765
+ - `'Feedback already submitted'`
690
766
  - `'Good response'`
691
767
  - `'Image could not be loaded'`
692
768
  - `'Image preview'`
693
769
  - `'Load earlier messages'`
694
770
  - `'Loading image…'`
771
+ - `'Read aloud'`
695
772
  - `'Reasoning details'`
696
773
  - `'Reasoning details — turn limit reached'`
774
+ - `'Save'`
775
+ - `'Skip'`
776
+ - `'Stop reading'`
777
+ - `'Tell us what went wrong (optional)'`
778
+ - `'What could be improved?'`
697
779
 
698
780
  **Agent status**
699
781
 
@@ -786,6 +868,10 @@ Every key the package can ask for, grouped by where it appears:
786
868
  - `'Quota reached'`
787
869
  - `'Usage'`
788
870
  - `'Usage · {period}'`
871
+ - `'this month'`
872
+ - `'this year'`
873
+ - `'today'`
874
+ - `'{quota} (shared across all users)'`
789
875
 
790
876
  **Notices, errors and timestamps**
791
877
 
@@ -822,12 +908,20 @@ backend cannot answer, and there is no mode flag to keep in step.
822
908
  | Endpoint | Default path | Response |
823
909
  | --- | --- | --- |
824
910
  | Prompt library | `GET {apiBaseUrl}/chat/prompts` | `[{ id, title, content, description? }]` (or `{ prompts: [...] }`) |
825
- | Quota status | `GET {apiBaseUrl}/chat/quota` | `{ used: number, limit: number \| null, period: string }` |
911
+ | Quota status | `GET {apiBaseUrl}/chat/quota` | `{ used: number, limit: number \| null, period: string, scope?: 'user' \| 'global' }` |
826
912
  | Agent suggestions | `GET {apiBaseUrl}/chat/suggestions?agent_slug=<slug>` | `["..."]` (or `{ suggestions: [...] }`, or objects with `prompt`/`label`/`text`) |
827
913
 
828
914
  Set either to `null` in `apiEndpoints` to hide it. `limit: null` means no
829
915
  ceiling — the indicator then shows consumption without a bar. The quota is
830
- re-read whenever a turn finishes.
916
+ re-read whenever a turn finishes. The periods XTM One sets a quota for
917
+ (`daily`, `monthly`, `yearly`) are translated (`today`, `this month`,
918
+ `this year`), any other one is shown as sent; `scope: 'global'` says the
919
+ limit is shared across all users.
920
+
921
+ A host behind its own proxy points the routes at it, relative to its
922
+ `apiBaseUrl` — OpenCTI and OpenAEV pass
923
+ `{ prompts: '/prompts', quota: '/quota', feedback: '/conversations' }`
924
+ (`feedback`: see [Message feedback](#message-feedback)).
831
925
 
832
926
  The welcome screen names the selected agent and shows its own suggestions —
833
927
  which is also how switching agent is confirmed: the thread resets to that
@@ -836,10 +930,14 @@ the suggestions route is unavailable the host's `promptSuggestions` prop is used
836
930
  instead, so the section is never empty.
837
931
 
838
932
  Dictation needs no configuration at all: it uses the browser's own Web Speech
839
- API, so the mic button appears wherever the API exists and is simply absent
840
- elsewhere. Finalised phrases are appended to the composer (never replacing a
841
- draft), interim words preview beside the button, and sending stops the mic so
842
- the next words cannot land in a composer the user just emptied.
933
+ API, so the mic button appears inside the text field, next to Send, wherever
934
+ the API exists and is simply absent elsewhere. It is not a toolbar item, so it
935
+ never adds the toolbar row on its own. While it listens the field turns red and
936
+ reads `Listening...`, finalised phrases are appended to the composer (never
937
+ replacing a draft), interim words preview in the line under it, and every send
938
+ (Enter, Send, Send now) stops the mic and drops the phrase still being
939
+ recognised, so no words can land in a composer the user just emptied. Stopping
940
+ the mic by hand keeps that last phrase.
843
941
 
844
942
  ### Context usage
845
943
 
package/dist/index.d.ts CHANGED
@@ -125,6 +125,21 @@ interface ApiEndpoints {
125
125
  * both today's generic suggestions and per-agent (later per-user) ones.
126
126
  */
127
127
  suggestions?: string | null;
128
+ /**
129
+ * Base path of the persisted message feedback resource. A rating is sent as
130
+ * `POST {apiBaseUrl}{feedback}/{conversation_id}/messages/{message_id}/feedback`
131
+ * with `{ rating: 'positive' | 'negative', comment: string | null }` and
132
+ * retracted with a `DELETE` on the same URL. XTM One's platform router
133
+ * serves it at `/chat/conversations`.
134
+ *
135
+ * No default, for the same reason as {@link ApiEndpoints.approve}: a host
136
+ * proxying the chat names the route once it exposes it, and a panel upgraded
137
+ * before its host never rates into a 404. Unset, the thumbs still render
138
+ * when the host passes `onMessageFeedback`, as they always did.
139
+ *
140
+ * REST backend only, and not in `singleEndpoint` mode.
141
+ */
142
+ feedback?: string | null;
128
143
  }
129
144
  /** A reusable prompt the user can insert into the composer. */
130
145
  interface ChatPromptTemplate {
@@ -144,8 +159,18 @@ interface ChatQuotaStatus {
144
159
  used: number;
145
160
  /** null means unlimited — the indicator then shows usage without a bar. */
146
161
  limit: number | null;
147
- /** Human-readable period label, e.g. "monthly". */
162
+ /**
163
+ * The period the limit applies to: `daily`, `monthly` and `yearly` are
164
+ * translated ("today", "this month", "this year"), any other value is shown
165
+ * as sent.
166
+ */
148
167
  period: string;
168
+ /**
169
+ * Whose actions count against the limit: `global` is one allowance shared
170
+ * by every user of the platform, which the indicator says in its tooltip.
171
+ * Absent reads as the user's own.
172
+ */
173
+ scope?: 'user' | 'global';
149
174
  }
150
175
  /**
151
176
  * One tool call the agent wants to make and is waiting on a human to approve.
@@ -345,12 +370,22 @@ interface ChatPanelProps {
345
370
  */
346
371
  onTaskComplete?: (title: string, body: string) => void;
347
372
  /**
348
- * Enables the 👍/👎 affordance on completed assistant messages and receives
349
- * each rating. `feedback` is `null` when the user clears a previous rating.
350
- * Omit to hide the affordance entirely — the chatbot stores nothing itself,
351
- * so a host without a feedback endpoint should not show the buttons.
373
+ * Receives each rating of an assistant answer. `messageId` is the persisted
374
+ * id when the backend reported one (`serverId`), else the panel's own id;
375
+ * `feedback` is `null` when the user clears a previous rating.
376
+ *
377
+ * With {@link ApiEndpoints.feedback} set the panel stores the rating itself
378
+ * and calls this once the backend accepted it. Without it, passing this is
379
+ * what shows the 👍/👎 affordance: the host stores the rating, so a host with
380
+ * neither shows no buttons.
352
381
  */
353
382
  onMessageFeedback?: (messageId: string, feedback: MessageFeedback | null, message: ChatMessage) => void;
383
+ /**
384
+ * The user's language as a BCP 47 tag (`'fr'`, `'en-US'`): formats the time
385
+ * under each message and is the language answers are read aloud in. Default:
386
+ * the browser's own.
387
+ */
388
+ locale?: string;
354
389
  /**
355
390
  * Disable the inline preview of image attachments (they render as ordinary
356
391
  * download cards instead). Previews fetch the image through the host download
@@ -391,10 +426,29 @@ interface ChatToggleButtonProps {
391
426
  icon?: React.ReactNode;
392
427
  }
393
428
  interface ChatMessage {
429
+ /** The panel's own key for the row; stable for the life of the row. */
394
430
  id: string;
431
+ /**
432
+ * The backend's id for the message: the `done` event's `message_id` for a
433
+ * live answer, the entry's `id` for a restored one. What feedback is stored
434
+ * against; absent until the backend reports it.
435
+ */
436
+ serverId?: string;
395
437
  role: 'user' | 'assistant';
396
438
  content: string;
439
+ /** When the message was sent: the entry's `created_at` for a restored one. */
397
440
  timestamp: Date;
441
+ /**
442
+ * True for a restored message whose backend did not say when it was sent:
443
+ * `timestamp` is then the time of the restore, and the footer shows no time
444
+ * rather than that one.
445
+ */
446
+ timestampUnknown?: boolean;
447
+ /**
448
+ * The rating the user gave this answer before the conversation was restored.
449
+ * The panel locks the thumbs on it ("Feedback already submitted").
450
+ */
451
+ feedback?: ChatMessagePersistedFeedback;
398
452
  /**
399
453
  * The agent that produced *this* message, when the backend says so.
400
454
  *
@@ -433,6 +487,11 @@ interface ChatMessage {
433
487
  */
434
488
  isTruncated?: boolean;
435
489
  }
490
+ /** A rating as the feedback endpoint stores it. */
491
+ interface ChatMessagePersistedFeedback {
492
+ rating: 'positive' | 'negative';
493
+ comment: string | null;
494
+ }
436
495
  /** One tool call in the reasoning-details execution trace. */
437
496
  interface ToolCallTraceEntry {
438
497
  name: string;
@@ -516,4 +575,4 @@ interface TransferredAgent {
516
575
  }
517
576
 
518
577
  export { ChatPanel, ChatToggleButton };
519
- export type { ApiEndpoints, BackendType, ChatAttachment, ChatContextBreakdown, ChatContextUsage, ChatConversationSummary, ChatFile, ChatMessage, ChatMode, ChatPanelProps, ChatPromptTemplate, ChatQuotaStatus, ChatToggleButtonProps, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, Translate, XtmAgent };
578
+ export type { ApiEndpoints, BackendType, ChatAttachment, ChatContextBreakdown, ChatContextUsage, ChatConversationSummary, ChatFile, ChatMessage, ChatMessagePersistedFeedback, ChatMode, ChatPanelProps, ChatPromptTemplate, ChatQuotaStatus, ChatToggleButtonProps, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, Translate, XtmAgent };