@filigran/chatbot 3.11.1 โ†’ 3.12.1

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
@@ -682,19 +753,30 @@ Every key the package can ask for, grouped by where it appears:
682
753
  **Messages, markdown and files**
683
754
 
684
755
  - `'Bad response'`
756
+ - `'Conversation transcript'`
685
757
  - `'Copied'`
686
758
  - `'Copied!'`
687
759
  - `'Copy code'`
760
+ - `'Copy message'`
688
761
  - `'Copy response'`
762
+ - `'Could not retract feedback.'`
763
+ - `'Could not submit feedback.'`
689
764
  - `'Download'`
690
765
  - `'Expand image'`
766
+ - `'Feedback already submitted'`
691
767
  - `'Good response'`
692
768
  - `'Image could not be loaded'`
693
769
  - `'Image preview'`
694
770
  - `'Load earlier messages'`
695
771
  - `'Loading imageโ€ฆ'`
772
+ - `'Read aloud'`
696
773
  - `'Reasoning details'`
697
774
  - `'Reasoning details โ€” turn limit reached'`
775
+ - `'Save'`
776
+ - `'Skip'`
777
+ - `'Stop reading'`
778
+ - `'Tell us what went wrong (optional)'`
779
+ - `'What could be improved?'`
698
780
 
699
781
  **Agent status**
700
782
 
@@ -787,6 +869,10 @@ Every key the package can ask for, grouped by where it appears:
787
869
  - `'Quota reached'`
788
870
  - `'Usage'`
789
871
  - `'Usage ยท {period}'`
872
+ - `'this month'`
873
+ - `'this year'`
874
+ - `'today'`
875
+ - `'{quota} (shared across all users)'`
790
876
 
791
877
  **Notices, errors and timestamps**
792
878
 
@@ -823,12 +909,20 @@ backend cannot answer, and there is no mode flag to keep in step.
823
909
  | Endpoint | Default path | Response |
824
910
  | --- | --- | --- |
825
911
  | Prompt library | `GET {apiBaseUrl}/chat/prompts` | `[{ id, title, content, description? }]` (or `{ prompts: [...] }`) |
826
- | Quota status | `GET {apiBaseUrl}/chat/quota` | `{ used: number, limit: number \| null, period: string }` |
912
+ | Quota status | `GET {apiBaseUrl}/chat/quota` | `{ used: number, limit: number \| null, period: string, scope?: 'user' \| 'global' }` |
827
913
  | Agent suggestions | `GET {apiBaseUrl}/chat/suggestions?agent_slug=<slug>` | `["..."]` (or `{ suggestions: [...] }`, or objects with `prompt`/`label`/`text`) |
828
914
 
829
915
  Set either to `null` in `apiEndpoints` to hide it. `limit: null` means no
830
916
  ceiling โ€” the indicator then shows consumption without a bar. The quota is
831
- re-read whenever a turn finishes.
917
+ re-read whenever a turn finishes. The periods XTM One sets a quota for
918
+ (`daily`, `monthly`, `yearly`) are translated (`today`, `this month`,
919
+ `this year`), any other one is shown as sent; `scope: 'global'` says the
920
+ limit is shared across all users.
921
+
922
+ A host behind its own proxy points the routes at it, relative to its
923
+ `apiBaseUrl` โ€” OpenCTI and OpenAEV pass
924
+ `{ prompts: '/prompts', quota: '/quota', feedback: '/conversations' }`
925
+ (`feedback`: see [Message feedback](#message-feedback)).
832
926
 
833
927
  The welcome screen names the selected agent and shows its own suggestions โ€”
834
928
  which is also how switching agent is confirmed: the thread resets to that
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 };