@sybilion/uilib 1.3.40 → 1.3.43

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.
Files changed (24) hide show
  1. package/README.md +1 -12
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +5 -5
  3. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
  4. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +1 -1
  5. package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +1 -1
  6. package/dist/esm/components/ui/RegionCoords/RegionSelector.js +2 -2
  7. package/dist/esm/contexts/chat-context.js +39 -1
  8. package/dist/esm/types/src/components/ui/RegionCoords/RegionSelector.types.d.ts +1 -1
  9. package/dist/esm/types/src/contexts/chat-context.d.ts +8 -0
  10. package/package.json +1 -1
  11. package/src/components/ui/ChartAreaInteractive/AGENT.md +19 -0
  12. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +5 -5
  13. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +8 -2
  14. package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +6 -1
  15. package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +1 -1
  16. package/src/components/ui/RegionCoords/RegionSelector.tsx +20 -18
  17. package/src/components/ui/RegionCoords/RegionSelector.types.ts +1 -1
  18. package/src/components/widgets/AGENT.md +21 -0
  19. package/src/components/widgets/DriverMap/AGENT.md +18 -0
  20. package/src/components/widgets/DriversComparisonChart/AGENT.md +18 -0
  21. package/src/components/widgets/PerformanceChart/AGENT.md +18 -0
  22. package/src/components/widgets/SidebarDatasetsItemsGrouped/AGENT.md +18 -0
  23. package/src/contexts/chat-context.tsx +62 -0
  24. package/src/docs/pages/ChatSlashCommandsPage.tsx +43 -15
package/README.md CHANGED
@@ -108,15 +108,7 @@ Runtime dependencies are listed in `[package.json](./package.json)`.
108
108
  yarn publish:npm # @sybilion/uilib
109
109
  ```
110
110
 
111
- runs `yarn build` before `npm publish`.
112
-
113
- **GitHub `github:` consumers:** after changing library **source**, regenerate and commit the prebuilt output:
114
-
115
- ```bash
116
- yarn build
117
- ```
118
-
119
- Then commit the updated `dist/` tree so GitHub installs stay up to date.
111
+ runs `yarn build` before `npm publish`. `dist/` is build output only (not committed); consumers install prebuilt artifacts from the npm registry.
120
112
 
121
113
  ## Integration notes (injected props)
122
114
 
@@ -131,7 +123,6 @@ These areas expect the **app** to supply behavior or data (not vendored from pro
131
123
 
132
124
  ## Scripts (this repo)
133
125
 
134
-
135
126
  | Script | Description |
136
127
  | --------------------- | ------------------------------------------- |
137
128
  | `yarn dev` | Webpack dev server for the docs app |
@@ -143,5 +134,3 @@ These areas expect the **app** to supply behavior or data (not vendored from pro
143
134
  | `yarn ts` | Typecheck (`tsc --noEmit`) |
144
135
  | `yarn tests` | Jest smoke tests |
145
136
  | `yarn release` | `standard-version` (bump changelog + tag) |
146
-
147
-
@@ -20,9 +20,9 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
20
20
  const attachmentAccept = useMemo(() => buildAcceptAttr(filteredAllowedAttachments, allowPdfAttachments), [filteredAllowedAttachments, allowPdfAttachments]);
21
21
  const [pendingAttachments, setPendingAttachments] = useState([]);
22
22
  const [isExtractingAttachments, setIsExtractingAttachments] = useState(false);
23
- const promptBusy = isLoading || isExtractingAttachments;
23
+ const promptDisabled = isExtractingAttachments;
24
24
  const handleAttachmentFiles = useCallback((files) => {
25
- if (promptBusy || files.length === 0)
25
+ if (promptDisabled || files.length === 0)
26
26
  return;
27
27
  setIsExtractingAttachments(true);
28
28
  void extractChatAttachmentItems(files, allowPdfAttachments)
@@ -35,7 +35,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
35
35
  // Extraction failed (parse error, size limit, etc.); skip staging.
36
36
  })
37
37
  .finally(() => setIsExtractingAttachments(false));
38
- }, [allowPdfAttachments, promptBusy]);
38
+ }, [allowPdfAttachments, promptDisabled]);
39
39
  const handleRemoveAttachment = useCallback((index) => {
40
40
  setPendingAttachments(prev => prev.filter((_, i) => i !== index));
41
41
  }, []);
@@ -66,7 +66,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
66
66
  if (inner)
67
67
  setTimeout(scrollToBottom, 100);
68
68
  }, [isEmpty, messages.length]);
69
- return (jsxs("div", { className: S.root, children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptBusy, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, children: [isEmpty ? (jsxs(Fragment, { children: [jsx(Chat.EmptyState, { ...emptyState }), renderPresets('fixed')] })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 180 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
69
+ return (jsxs("div", { className: S.root, children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptDisabled, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, children: [isEmpty ? (jsxs(Fragment, { children: [jsx(Chat.EmptyState, { ...emptyState }), renderPresets('fixed')] })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 180 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
70
70
  const isLast = index === arr.length - 1;
71
71
  return (jsx(Chat.Message, { role: msg.role, text: msg.text, inProgress: msg.inProgress, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
72
72
  ? { label: scriptContinueLabel }
@@ -77,7 +77,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
77
77
  const label = displayLabelForBranchKeyFromMessages(key, messages) ??
78
78
  humanizeBranchKey(key);
79
79
  return (jsx("span", { className: S.branchBtnWrap, children: jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isLoading, onClick: () => onQuickReply(key, label), children: [jsx(PaperPlaneRightIcon, {}), label] }) }, key));
80
- }) })) : null, showInlinePresets && renderPresets('inline'), isLoading && isLastMessageFromUser && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: "Thinking..." }))] }) })), jsxs("div", { className: cn(S.footer, footerClassName), children: [isEmpty ? (jsx("div", { className: S.notice, children: "Forecast Assistant can make mistakes." })) : null, jsx(Chat.Prompt, { onSubmit: handlePromptSubmitWithAttachments, disabled: promptBusy, attachments: pendingAttachments, onRemoveAttachment: handleRemoveAttachment, prefillMessage: promptPrefill ?? undefined, placeholder: promptPlaceholder, slashCommandItems: slashCommandItems, onSlashItemCommand: onSlashItemCommand, attachmentAccept: attachmentsDropzoneEnabled ? attachmentAccept : undefined, onAttachmentFiles: attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined })] })] })] })] }));
80
+ }) })) : null, showInlinePresets && renderPresets('inline'), isLoading && isLastMessageFromUser && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: "Thinking..." }))] }) })), jsxs("div", { className: cn(S.footer, footerClassName), children: [isEmpty ? (jsx("div", { className: S.notice, children: "Forecast Assistant can make mistakes." })) : null, jsx(Chat.Prompt, { onSubmit: handlePromptSubmitWithAttachments, disabled: promptDisabled, attachments: pendingAttachments, onRemoveAttachment: handleRemoveAttachment, prefillMessage: promptPrefill ?? undefined, placeholder: promptPlaceholder, slashCommandItems: slashCommandItems, onSlashItemCommand: onSlashItemCommand, attachmentAccept: attachmentsDropzoneEnabled ? attachmentAccept : undefined, onAttachmentFiles: attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined })] })] })] })] }));
81
81
  }
82
82
 
83
83
  export { ChatChrome };
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".ChatMessage_root__6rnsF{background:var(--bg-secondary);display:flex;flex-direction:column;gap:var(--p-1);padding:var(--p-6)}.ChatMessage_text__Y1XNR{color:var(--text-secondary);font-size:var(--text-sm);max-width:100%;min-width:0;overflow-wrap:anywhere;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content;word-break:break-word}.ChatMessage_role-user__u4JPV{align-items:flex-end;max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_userColumn__cQM6-{align-items:flex-end;display:flex;flex-direction:column;gap:var(--p-2);max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;box-sizing:border-box;overflow:hidden;padding:var(--p-3) var(--p-4);white-space:pre-wrap}.dark .ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-gray-800)}.ChatMessage_role-system__g13OP{align-items:center}.ChatMessage_role-system__g13OP .ChatMessage_text__Y1XNR{color:var(--muted-foreground);font-size:var(--text-xs);width:100%}.ChatMessage_role-assistant__wketE .ChatMessage_text__Y1XNR{width:100%}.ChatMessage_role-assistant__wketE h3{line-height:2.4}.ChatMessage_role-assistant__wketE h4{font-size:1.1em;font-weight:600;line-height:2.2}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq{display:inline-block;margin-left:4px;margin-right:6px}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq:before{color:var(--text-secondary);content:\"•\";display:inline-block}.ChatMessage_role-assistant__wketE .ChatMessage_scrollHorizontal__Rms9n{max-width:100%}.ChatMessage_role-assistant__wketE table{border:1px solid var(--border);border-collapse:collapse;border-radius:var(--p-2);border-spacing:0;margin:var(--p-4) 0;overflow:hidden}.ChatMessage_role-assistant__wketE table td,.ChatMessage_role-assistant__wketE table th{border:1px solid var(--border);min-width:100px;padding:var(--p-1)}.ChatMessage_role-assistant__wketE table th{text-align:left}.ChatMessage_role-assistant__wketE ol,.ChatMessage_role-assistant__wketE ul{padding-left:var(--p-4)}.ChatMessage_role-assistant__wketE ul{list-style-type:disc}.ChatMessage_role-assistant__wketE ol{list-style-type:decimal}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{align-items:center;border:1px dashed var(--sb-slate-300);border-radius:8px;color:var(--foreground);display:inline-flex;font-size:var(--text-xs);gap:6px;margin:1px;max-width:100%;padding:2px 6px 2px 4px;text-decoration:none;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-slate-50);border-color:var(--sb-slate-400);border-style:solid}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{border-color:var(--sb-gray-600)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-gray-900)}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLinkLabel__PMU7e{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ChatMessage_role-assistant__wketE .ChatMessage_quickReplyWrap__1UFyD{display:inline-block;margin:var(--p-1) var(--p-1) var(--p-1) 0;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_downloadButtons__RygM-{display:flex;gap:var(--p-2);margin-top:var(--p-4)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{align-items:center;background-color:var(--background);border-radius:var(--p-3);box-shadow:0 0 0 1px var(--border);cursor:pointer;display:flex;gap:var(--p-4);margin-top:var(--p-3);padding:var(--p-3);padding-right:var(--p-4);transition:all .15s;width:-moz-fit-content;width:fit-content}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-50);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{background-color:var(--sb-gray-900);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-800)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardIcon__jkxDJ{align-items:center;border-radius:var(--p-2);display:flex;flex-shrink:0;height:32px;justify-content:center;width:32px}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardContent__PTPwz{display:flex;flex:1;flex-direction:column;min-width:0}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardTitle__K1wqr{font-size:var(--text-base);font-weight:600;line-height:1.4}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardSubtitle__fVeF2{color:var(--muted-foreground);font-size:var(--text-sm);line-height:1.4}";
4
- var S = {"root":"ChatMessage_root__6rnsF","text":"ChatMessage_text__Y1XNR","role-user":"ChatMessage_role-user__u4JPV","userColumn":"ChatMessage_userColumn__cQM6-","role-system":"ChatMessage_role-system__g13OP","role-assistant":"ChatMessage_role-assistant__wketE","bullet":"ChatMessage_bullet__6vAhq","scrollHorizontal":"ChatMessage_scrollHorizontal__Rms9n","datasetAppLink":"ChatMessage_datasetAppLink__Pxy-T","datasetAppLinkLabel":"ChatMessage_datasetAppLinkLabel__PMU7e","quickReplyWrap":"ChatMessage_quickReplyWrap__1UFyD","downloadButtons":"ChatMessage_downloadButtons__RygM-","downloadCard":"ChatMessage_downloadCard__NsNRa","downloadCardIcon":"ChatMessage_downloadCardIcon__jkxDJ","downloadCardContent":"ChatMessage_downloadCardContent__PTPwz","downloadCardTitle":"ChatMessage_downloadCardTitle__K1wqr","downloadCardSubtitle":"ChatMessage_downloadCardSubtitle__fVeF2"};
3
+ var css_248z = ".ChatMessage_root__6rnsF{background:var(--bg-secondary);display:flex;flex-direction:column;gap:var(--p-1);padding:var(--p-4);padding-bottom:var(--p-1)}.ChatMessage_text__Y1XNR{color:var(--text-secondary);font-size:var(--text-sm);max-width:100%;min-width:0;overflow-wrap:anywhere;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content;word-break:break-word}.ChatMessage_role-assistant__wketE+.ChatMessage_role-assistant__wketE,.ChatMessage_role-system__g13OP+.ChatMessage_role-system__g13OP,.ChatMessage_role-user__u4JPV+.ChatMessage_role-user__u4JPV{padding-top:var(--p-1)}.ChatMessage_role-user__u4JPV{align-items:flex-end;max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_userColumn__cQM6-{align-items:flex-end;display:flex;flex-direction:column;gap:var(--p-2);max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;box-sizing:border-box;overflow:hidden;padding:var(--p-3) var(--p-4);white-space:pre-wrap}.dark .ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-gray-800)}.ChatMessage_role-system__g13OP{align-items:center}.ChatMessage_role-system__g13OP .ChatMessage_text__Y1XNR{color:var(--muted-foreground);width:100%}.ChatMessage_role-assistant__wketE .ChatMessage_text__Y1XNR{width:100%}.ChatMessage_role-assistant__wketE h3{line-height:2.4}.ChatMessage_role-assistant__wketE h4{font-size:1.1em;font-weight:600;line-height:2.2}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq{display:inline-block;margin-left:4px;margin-right:6px}.ChatMessage_role-assistant__wketE .ChatMessage_bullet__6vAhq:before{color:var(--text-secondary);content:\"•\";display:inline-block}.ChatMessage_role-assistant__wketE .ChatMessage_scrollHorizontal__Rms9n{max-width:100%}.ChatMessage_role-assistant__wketE table{border:1px solid var(--border);border-collapse:collapse;border-radius:var(--p-2);border-spacing:0;margin:var(--p-4) 0;overflow:hidden}.ChatMessage_role-assistant__wketE table td,.ChatMessage_role-assistant__wketE table th{border:1px solid var(--border);min-width:100px;padding:var(--p-1)}.ChatMessage_role-assistant__wketE table th{text-align:left}.ChatMessage_role-assistant__wketE ol,.ChatMessage_role-assistant__wketE ul{padding-left:var(--p-4)}.ChatMessage_role-assistant__wketE ul{list-style-type:disc}.ChatMessage_role-assistant__wketE ol{list-style-type:decimal}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{align-items:center;border:1px dashed var(--sb-slate-300);border-radius:8px;color:var(--foreground);display:inline-flex;font-size:var(--text-xs);gap:6px;margin:1px;max-width:100%;padding:2px 6px 2px 4px;text-decoration:none;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-slate-50);border-color:var(--sb-slate-400);border-style:solid}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T{border-color:var(--sb-gray-600)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLink__Pxy-T:hover{background-color:var(--sb-gray-900)}.ChatMessage_role-assistant__wketE .ChatMessage_datasetAppLinkLabel__PMU7e{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ChatMessage_role-assistant__wketE .ChatMessage_quickReplyWrap__1UFyD{display:inline-block;margin:var(--p-1) var(--p-1) var(--p-1) 0;vertical-align:middle}.ChatMessage_role-assistant__wketE .ChatMessage_downloadButtons__RygM-{display:flex;gap:var(--p-2);margin-top:var(--p-4)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{align-items:center;background-color:var(--background);border-radius:var(--p-3);box-shadow:0 0 0 1px var(--border);cursor:pointer;display:flex;gap:var(--p-4);margin-top:var(--p-3);padding:var(--p-3);padding-right:var(--p-4);transition:all .15s;width:-moz-fit-content;width:fit-content}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-50);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa{background-color:var(--sb-gray-900);border-color:var(--border)}.dark .ChatMessage_role-assistant__wketE .ChatMessage_downloadCard__NsNRa:hover{background-color:var(--sb-gray-800)}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardIcon__jkxDJ{align-items:center;border-radius:var(--p-2);display:flex;flex-shrink:0;height:32px;justify-content:center;width:32px}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardContent__PTPwz{display:flex;flex:1;flex-direction:column;min-width:0}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardTitle__K1wqr{font-size:var(--text-base);font-weight:600;line-height:1.4}.ChatMessage_role-assistant__wketE .ChatMessage_downloadCardSubtitle__fVeF2{color:var(--muted-foreground);font-size:var(--text-sm);line-height:1.4}";
4
+ var S = {"root":"ChatMessage_root__6rnsF","text":"ChatMessage_text__Y1XNR","role-user":"ChatMessage_role-user__u4JPV","role-assistant":"ChatMessage_role-assistant__wketE","role-system":"ChatMessage_role-system__g13OP","userColumn":"ChatMessage_userColumn__cQM6-","bullet":"ChatMessage_bullet__6vAhq","scrollHorizontal":"ChatMessage_scrollHorizontal__Rms9n","datasetAppLink":"ChatMessage_datasetAppLink__Pxy-T","datasetAppLinkLabel":"ChatMessage_datasetAppLinkLabel__PMU7e","quickReplyWrap":"ChatMessage_quickReplyWrap__1UFyD","downloadButtons":"ChatMessage_downloadButtons__RygM-","downloadCard":"ChatMessage_downloadCard__NsNRa","downloadCardIcon":"ChatMessage_downloadCardIcon__jkxDJ","downloadCardContent":"ChatMessage_downloadCardContent__PTPwz","downloadCardTitle":"ChatMessage_downloadCardTitle__K1wqr","downloadCardSubtitle":"ChatMessage_downloadCardSubtitle__fVeF2"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -19,7 +19,7 @@ function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, att
19
19
  return (jsxs("div", { className: S.composer, children: [showAttachButton ? (jsxs(Fragment, { children: [jsx("input", { ref: fileInputRef, type: "file", accept: attachmentAccept, multiple: true, className: S.fileInput, disabled: disabled, onChange: handleFileInputChange }), jsx(Button, { type: "button", variant: "ghost", icon: true, size: "sm", className: S.attachButton, "aria-label": "Attach file", disabled: disabled, onClick: e => {
20
20
  e.preventDefault();
21
21
  fileInputRef.current?.click();
22
- }, children: jsx(PaperclipIcon, { size: 16 }) })] })) : null, jsx("div", { className: S.editorWrap, onKeyDown: onComposerKeyDown, children: jsx(EditorContent, { editor: editor, className: S.editorMount }) }), jsx("div", { className: S.submitColumn, children: jsx(Button, { type: "submit", size: "sm", disabled: disabled || !canSubmit, children: jsx(SendHorizontalIcon, { size: 16 }) }) })] }));
22
+ }, children: jsx(PaperclipIcon, { size: 16 }) })] })) : null, jsx("div", { className: S.editorWrap, onKeyDown: onComposerKeyDown, children: jsx(EditorContent, { editor: editor, className: S.editorMount }) }), jsx("div", { className: S.submitColumn, children: jsx(Button, { type: "submit", size: "sm", disabled: disabled || !canSubmit, onMouseDown: e => e.preventDefault(), children: jsx(SendHorizontalIcon, { size: 16 }) }) })] }));
23
23
  }
24
24
 
25
25
  export { ChatPromptComposer };
@@ -136,7 +136,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
136
136
  const resetAfterSend = useCallback(() => {
137
137
  if (!editor)
138
138
  return;
139
- editor.commands.clearContent();
139
+ editor.chain().clearContent().focus().run();
140
140
  queueMicrotask(() => {
141
141
  const dom = chatPromptSafeEditorDom(editor);
142
142
  if (dom)
@@ -6,7 +6,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
6
6
  import { CaretDownIcon } from '@phosphor-icons/react';
7
7
  import S from './RegionSelector.styl.js';
8
8
 
9
- function RegionSelector({ children, onSelect, className, regions: regionCoordinates, }) {
9
+ function RegionSelector({ children, onSelect, className, regions: regionCoordinates = {}, }) {
10
10
  const [open, setOpen] = useState(false);
11
11
  const handleRegionSelect = (regionName) => {
12
12
  const coords = regionCoordinates[regionName];
@@ -16,7 +16,7 @@ function RegionSelector({ children, onSelect, className, regions: regionCoordina
16
16
  }
17
17
  };
18
18
  const regions = Object.keys(regionCoordinates).sort();
19
- return (jsxs("div", { className: cn(S.root, className), children: [children, jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsx(Button, { variant: "outline", size: "sm", className: S.button, children: jsx(CaretDownIcon, { size: 16 }) }) }), jsx(DropdownMenuContent, { align: "end", children: regions.map(region => (jsx(DropdownMenuItem, { onSelect: () => handleRegionSelect(region), children: region }, region))) })] })] }));
19
+ return (jsxs("div", { className: cn(S.root, className), children: [children, regions.length > 0 && (jsxs(DropdownMenu, { open: open, onOpenChange: setOpen, children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsx(Button, { variant: "outline", size: "sm", className: S.button, children: jsx(CaretDownIcon, { size: 16 }) }) }), jsx(DropdownMenuContent, { align: "end", children: regions.map(region => (jsx(DropdownMenuItem, { onSelect: () => handleRegionSelect(region), children: region }, region))) })] }))] }));
20
20
  }
21
21
 
22
22
  export { RegionSelector };
@@ -234,6 +234,42 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
234
234
  return { ...prev, [scopeId]: updatedChats };
235
235
  });
236
236
  }, [userSwitchKey]);
237
+ const updateMessageById = useCallback((scopeId, chatId, messageId, patch) => {
238
+ if (userSwitchKey === null)
239
+ return;
240
+ setChats(prev => {
241
+ const scopeChats = prev[scopeId] ?? [];
242
+ const updatedChats = scopeChats.map(chat => {
243
+ if (chat.session_id !== chatId)
244
+ return chat;
245
+ return {
246
+ ...chat,
247
+ messages: chat.messages.map(message => {
248
+ if (message.id !== messageId)
249
+ return message;
250
+ const next = { ...message };
251
+ if (patch.role != null) {
252
+ next.role = patch.role;
253
+ }
254
+ if (patch.text != null) {
255
+ next.text = stripJsonDashboardFences(patch.text);
256
+ }
257
+ if (patch.inProgress === true) {
258
+ next.inProgress = true;
259
+ }
260
+ else if (patch.inProgress === false ||
261
+ (patch.role != null && patch.role !== MessageRole.SYSTEM)) {
262
+ delete next.inProgress;
263
+ }
264
+ return next;
265
+ }),
266
+ };
267
+ });
268
+ const chatsKey = getChatsKey(scopeId);
269
+ LS.set(chatsKey, updatedChats);
270
+ return { ...prev, [scopeId]: updatedChats };
271
+ });
272
+ }, [userSwitchKey]);
237
273
  const sendMessage = useCallback(async (scopeId, message, chatId) => {
238
274
  const targetChatId = chatId ?? getCurrentChatId(scopeId);
239
275
  if (targetChatId === null || targetChatId === '') {
@@ -310,6 +346,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
310
346
  setCurrentChatId,
311
347
  addMessage,
312
348
  removeMessageById,
349
+ updateMessageById,
313
350
  sendMessage,
314
351
  getChatsForScopeId,
315
352
  getCurrentChatId,
@@ -370,7 +407,7 @@ function useSyncChatPanelBusy(isLoading) {
370
407
  }, [isLoading, acquirePanelBusy, releasePanelBusy]);
371
408
  }
372
409
  function useChatsForScopeId(scopeId) {
373
- const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, sendMessage, deleteChat, } = useChats();
410
+ const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, updateMessageById, sendMessage, deleteChat, } = useChats();
374
411
  const chats = getChatsForScopeId(scopeId);
375
412
  const currentChatId = getCurrentChatId(scopeId);
376
413
  const currentChat = useChat(scopeId, currentChatId ?? undefined);
@@ -384,6 +421,7 @@ function useChatsForScopeId(scopeId) {
384
421
  newChat: () => newChat(scopeId),
385
422
  addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
386
423
  removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
424
+ updateMessageById: (chatId, messageId, patch) => updateMessageById(scopeId, chatId, messageId, patch),
387
425
  sendMessage: (message, chatId) => sendMessage(scopeId, message, chatId),
388
426
  deleteChat: (sessionId) => deleteChat(scopeId, sessionId),
389
427
  };
@@ -6,7 +6,7 @@ export interface RegionSelectorProps {
6
6
  }) => void;
7
7
  className?: string;
8
8
  /** Region name → coordinates (app-defined; replaces app-only drivers catalog). */
9
- regions: Record<string, {
9
+ regions?: Record<string, {
10
10
  longitude: number;
11
11
  latitude: number;
12
12
  }>;
@@ -8,12 +8,18 @@ export type AddChatMessageOptions = {
8
8
  /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
9
9
  inProgress?: boolean;
10
10
  };
11
+ export type UpdateChatMessagePatch = {
12
+ role?: MessageRole;
13
+ text?: string;
14
+ inProgress?: boolean;
15
+ };
11
16
  export interface ChatContextType {
12
17
  /** Returns the new session id, or undefined if no user / not created. */
13
18
  newChat: (scopeId: string) => string | undefined;
14
19
  setCurrentChatId: (currScopeId: string, sessionId: string) => void;
15
20
  addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string | undefined;
16
21
  removeMessageById: (scopeId: string, chatId: string, messageId: string) => void;
22
+ updateMessageById: (scopeId: string, chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
17
23
  sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
18
24
  getChatsForScopeId: (scopeId: string) => Chat[];
19
25
  getCurrentChatId: (scopeId: string) => string | null;
@@ -55,6 +61,7 @@ export declare function useChatsForScopeId(scopeId: string): {
55
61
  newChat: () => string;
56
62
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
57
63
  removeMessageById: (chatId: string, messageId: string) => void;
64
+ updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
58
65
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
59
66
  deleteChat: (sessionId: string) => void;
60
67
  };
@@ -68,6 +75,7 @@ export declare function useChatsForDataset(scopeId: string): {
68
75
  newChat: () => string;
69
76
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
70
77
  removeMessageById: (chatId: string, messageId: string) => void;
78
+ updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
71
79
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
72
80
  deleteChat: (sessionId: string) => void;
73
81
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.40",
3
+ "version": "1.3.43",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -0,0 +1,19 @@
1
+ # ChartAreaInteractive
2
+
3
+ Renders: time-series chart with historical line, forecast lines, optional pin / quantile-band / threshold overlays.
4
+
5
+ Use when: custom forecast UI with overlays or full chart control.
6
+ Not when: packaged performance or drivers-comparison views (use PerformanceChart or DriversComparisonChart).
7
+
8
+ Host provides:
9
+
10
+ - `chartData`, `forecastData` built from API
11
+ - `timeRange` / `onTimeRangeChange` or brush-only range
12
+ - Optional `mode`: pin | intervals | thresholds + overlay state
13
+ - Analysis selector and fetch outside widget
14
+
15
+ Report tile: `dataset_card` — host loads dataset + analysis; chart inside dashboard card.
16
+
17
+ Requires: `chartData`; `forecastData`; `loading`; `toggleLegendSeries` / `ensureAnalysisSeriesVisible` when legend is external.
18
+
19
+ Empty/loading: `loading`, `error`; empty data shows chart empty state via host message props.
@@ -71,11 +71,11 @@ export function ChatChrome({
71
71
  ChatAttachmentDropItem[]
72
72
  >([]);
73
73
  const [isExtractingAttachments, setIsExtractingAttachments] = useState(false);
74
- const promptBusy = isLoading || isExtractingAttachments;
74
+ const promptDisabled = isExtractingAttachments;
75
75
 
76
76
  const handleAttachmentFiles = useCallback(
77
77
  (files: File[]) => {
78
- if (promptBusy || files.length === 0) return;
78
+ if (promptDisabled || files.length === 0) return;
79
79
 
80
80
  setIsExtractingAttachments(true);
81
81
  void extractChatAttachmentItems(files, allowPdfAttachments)
@@ -89,7 +89,7 @@ export function ChatChrome({
89
89
  })
90
90
  .finally(() => setIsExtractingAttachments(false));
91
91
  },
92
- [allowPdfAttachments, promptBusy],
92
+ [allowPdfAttachments, promptDisabled],
93
93
  );
94
94
 
95
95
  const handleRemoveAttachment = useCallback((index: number) => {
@@ -165,7 +165,7 @@ export function ChatChrome({
165
165
  multiple
166
166
  ghost
167
167
  overlayScope="container"
168
- disabled={promptBusy}
168
+ disabled={promptDisabled}
169
169
  className={S.attachmentDropzone}
170
170
  onFiles={handleAttachmentFiles}
171
171
  />
@@ -263,7 +263,7 @@ export function ChatChrome({
263
263
  ) : null}
264
264
  <Chat.Prompt
265
265
  onSubmit={handlePromptSubmitWithAttachments}
266
- disabled={promptBusy}
266
+ disabled={promptDisabled}
267
267
  attachments={pendingAttachments}
268
268
  onRemoveAttachment={handleRemoveAttachment}
269
269
  prefillMessage={promptPrefill ?? undefined}
@@ -3,7 +3,8 @@
3
3
  flex-direction column
4
4
  gap var(--p-1)
5
5
 
6
- padding var(--p-6)
6
+ padding var(--p-4)
7
+ padding-bottom var(--p-1)
7
8
  background var(--bg-secondary)
8
9
 
9
10
  .text
@@ -16,6 +17,12 @@
16
17
  overflow-wrap anywhere
17
18
  word-break break-word
18
19
 
20
+ .role-user
21
+ .role-assistant
22
+ .role-system
23
+ & + &
24
+ padding-top var(--p-1)
25
+
19
26
  .role-user
20
27
  align-items flex-end
21
28
  max-width 100%
@@ -48,7 +55,6 @@
48
55
 
49
56
  .text
50
57
  width 100%
51
- font-size var(--text-xs)
52
58
  color var(--muted-foreground)
53
59
 
54
60
  .role-assistant
@@ -84,7 +84,12 @@ export function ChatPromptComposer({
84
84
  </div>
85
85
 
86
86
  <div className={S.submitColumn}>
87
- <Button type="submit" size="sm" disabled={disabled || !canSubmit}>
87
+ <Button
88
+ type="submit"
89
+ size="sm"
90
+ disabled={disabled || !canSubmit}
91
+ onMouseDown={e => e.preventDefault()}
92
+ >
88
93
  <SendHorizontalIcon size={16} />
89
94
  </Button>
90
95
  </div>
@@ -193,7 +193,7 @@ export function useChatPromptEditor({
193
193
 
194
194
  const resetAfterSend = useCallback(() => {
195
195
  if (!editor) return;
196
- editor.commands.clearContent();
196
+ editor.chain().clearContent().focus().run();
197
197
  queueMicrotask(() => {
198
198
  const dom = chatPromptSafeEditorDom(editor);
199
199
  if (dom) syncChatPromptComposerHeight(dom, '');
@@ -17,7 +17,7 @@ export function RegionSelector({
17
17
  children,
18
18
  onSelect,
19
19
  className,
20
- regions: regionCoordinates,
20
+ regions: regionCoordinates = {},
21
21
  }: RegionSelectorProps) {
22
22
  const [open, setOpen] = useState(false);
23
23
 
@@ -34,23 +34,25 @@ export function RegionSelector({
34
34
  return (
35
35
  <div className={cn(S.root, className)}>
36
36
  {children}
37
- <DropdownMenu open={open} onOpenChange={setOpen}>
38
- <DropdownMenuTrigger asChild>
39
- <Button variant="outline" size="sm" className={S.button}>
40
- <CaretDownIcon size={16} />
41
- </Button>
42
- </DropdownMenuTrigger>
43
- <DropdownMenuContent align="end">
44
- {regions.map(region => (
45
- <DropdownMenuItem
46
- key={region}
47
- onSelect={() => handleRegionSelect(region)}
48
- >
49
- {region}
50
- </DropdownMenuItem>
51
- ))}
52
- </DropdownMenuContent>
53
- </DropdownMenu>
37
+ {regions.length > 0 && (
38
+ <DropdownMenu open={open} onOpenChange={setOpen}>
39
+ <DropdownMenuTrigger asChild>
40
+ <Button variant="outline" size="sm" className={S.button}>
41
+ <CaretDownIcon size={16} />
42
+ </Button>
43
+ </DropdownMenuTrigger>
44
+ <DropdownMenuContent align="end">
45
+ {regions.map(region => (
46
+ <DropdownMenuItem
47
+ key={region}
48
+ onSelect={() => handleRegionSelect(region)}
49
+ >
50
+ {region}
51
+ </DropdownMenuItem>
52
+ ))}
53
+ </DropdownMenuContent>
54
+ </DropdownMenu>
55
+ )}
54
56
  </div>
55
57
  );
56
58
  }
@@ -6,5 +6,5 @@ export interface RegionSelectorProps {
6
6
  ) => void;
7
7
  className?: string;
8
8
  /** Region name → coordinates (app-defined; replaces app-only drivers catalog). */
9
- regions: Record<string, { longitude: number; latitude: number }>;
9
+ regions?: Record<string, { longitude: number; latitude: number }>;
10
10
  }
@@ -0,0 +1,21 @@
1
+ <!-- not a component — authoring guide only -->
2
+
3
+ # Widget `AGENT.md` authoring
4
+
5
+ Each component file ≤18 lines. Signal for LLM prompts — no noise.
6
+
7
+ **Include:** Renders (1 sentence); Use when / Not when; Host provides (3–5 bullets); Report tile (one line or "Not used"); Requires (prop names + role); Empty/loading (one line).
8
+
9
+ **Do not include:** import examples; type/doc/demo/glossary links; page-shell boilerplate; related-components lists unless choosing between exports; implementation/styling/keyboard notes unless binding-relevant; secrets, env vars, API URLs.
10
+
11
+ ```markdown
12
+ # Name
13
+
14
+ Renders: …
15
+ Use when: …
16
+ Not when: …
17
+ Host provides: …
18
+ Report tile: …
19
+ Requires: …
20
+ Empty/loading: …
21
+ ```
@@ -0,0 +1,18 @@
1
+ # DriverMap
2
+
3
+ Renders: world map with regional driver badges, bottom strip for world-level drivers, selection highlight.
4
+
5
+ Use when: geographic driver exploration for one analysis.
6
+ Not when: driver detail metrics card alone (pair with DriverCard) or normalized series chart (use DriversComparisonChart).
7
+
8
+ Host provides:
9
+
10
+ - `drivers` as `DriverData[]` from analysis API
11
+ - Controlled `selectedDriver` + `setSelectedDriver`
12
+ - `isLoading` while fetching drivers
13
+
14
+ Report tile: `drivers_map` — tile resolves analysis id, fetches drivers, passes list + selection (see EmbeddedAnalysisSelector pattern).
15
+
16
+ Requires: `drivers`; `isLoading`; `selectedDriver`; `setSelectedDriver`.
17
+
18
+ Empty/loading: `isLoading` shows overlay; empty `drivers` leaves map without badges.
@@ -0,0 +1,18 @@
1
+ # DriversComparisonChart
2
+
3
+ Renders: target vs drivers backtests chart with ChartAreaInteractive plus table; row click toggles series visibility.
4
+
5
+ Use when: drivers comparison tab with normalized target and driver series from backtests payload.
6
+ Not when: geographic map or performance horizons — use DriverMap or PerformanceChart.
7
+
8
+ Host provides:
9
+
10
+ - `payload`: BacktestsComponentPayload from platform SDK (host fetch per analysis)
11
+ - Optional `datasetHistorical` overlay
12
+ - `seriesInitKey` when selected analysis changes
13
+
14
+ Report tile: Not used in report tiles.
15
+
16
+ Requires: `payload` — target + driver normalized_series; `loading` / `chartLoading` — spinners; `seriesInitKey` — reset visible series on analysis change; `runAnalysisHint` / `statusHint` — empty/error text.
17
+
18
+ Empty/loading: loading props shimmer chart; null `payload` with `runAnalysisHint` prompts to run analysis.
@@ -0,0 +1,18 @@
1
+ # PerformanceChart
2
+
3
+ Renders: forecast performance on ChartAreaInteractive — per-horizon tab (24m window, MAE/MAPE table) and spaghetti tab (all horizons overlaid).
4
+
5
+ Use when: dataset performance tab with horizon selector and error metrics.
6
+ Not when: simple forecast card or driver backtests — use ChartAreaInteractive or DriversComparisonChart.
7
+
8
+ Host provides:
9
+
10
+ - `performanceData` (PerformanceChartPayload) and `historicalData` from performance API
11
+ - Analysis selection and fetch outside widget
12
+ - Optional `forecastData`, `customPerformanceMatrix`, `userSeries` for spaghetti
13
+
14
+ Report tile: Not used in report tiles.
15
+
16
+ Requires: `performanceData` — model/drift forecasts and metrics; `historicalData` — baseline series; `loading` / `chartLoading` / `performanceDataLoading` — spinners; `runAnalysisHint` / `statusHint` — empty states.
17
+
18
+ Empty/loading: loading props show shimmer/spinner; null `performanceData` with `runAnalysisHint` prompts to run analysis.
@@ -0,0 +1,18 @@
1
+ # SidebarDatasetsItemsGrouped
2
+
3
+ Renders: expandable sidebar groups of datasets (by target type, regions, or categories).
4
+
5
+ Use when: app shell needs grouped dataset navigation.
6
+ Not when: in-page or report content (no sidebar slot).
7
+
8
+ Host provides:
9
+
10
+ - `datasets` list (`SidebarDatasetsItemsGroupedDataset`)
11
+ - `groupBy`, `selectedDatasetId`, `onDatasetClick`
12
+ - Optional `preItems` / `postItems`, `defaultExpandedGroupNames`
13
+
14
+ Report tile: Not used in report tiles.
15
+
16
+ Requires: `datasets`; `groupBy`; `onDatasetClick` for navigation.
17
+
18
+ Empty/loading: empty `datasets` renders no groups; loading handled by host before pass-in.
@@ -39,6 +39,12 @@ export type AddChatMessageOptions = {
39
39
  inProgress?: boolean;
40
40
  };
41
41
 
42
+ export type UpdateChatMessagePatch = {
43
+ role?: MessageRole;
44
+ text?: string;
45
+ inProgress?: boolean;
46
+ };
47
+
42
48
  export interface ChatContextType {
43
49
  /** Returns the new session id, or undefined if no user / not created. */
44
50
  newChat: (scopeId: string) => string | undefined;
@@ -55,6 +61,12 @@ export interface ChatContextType {
55
61
  chatId: string,
56
62
  messageId: string,
57
63
  ) => void;
64
+ updateMessageById: (
65
+ scopeId: string,
66
+ chatId: string,
67
+ messageId: string,
68
+ patch: UpdateChatMessagePatch,
69
+ ) => void;
58
70
  sendMessage: (
59
71
  scopeId: string,
60
72
  message: string | ChatSendMessagePayload,
@@ -370,6 +382,49 @@ export function ChatProvider({
370
382
  [userSwitchKey],
371
383
  );
372
384
 
385
+ const updateMessageById = useCallback(
386
+ (
387
+ scopeId: string,
388
+ chatId: string,
389
+ messageId: string,
390
+ patch: UpdateChatMessagePatch,
391
+ ) => {
392
+ if (userSwitchKey === null) return;
393
+ setChats(prev => {
394
+ const scopeChats = prev[scopeId] ?? [];
395
+ const updatedChats = scopeChats.map(chat => {
396
+ if (chat.session_id !== chatId) return chat;
397
+ return {
398
+ ...chat,
399
+ messages: chat.messages.map(message => {
400
+ if (message.id !== messageId) return message;
401
+ const next: Message = { ...message };
402
+ if (patch.role != null) {
403
+ next.role = patch.role;
404
+ }
405
+ if (patch.text != null) {
406
+ next.text = stripJsonDashboardFences(patch.text);
407
+ }
408
+ if (patch.inProgress === true) {
409
+ next.inProgress = true;
410
+ } else if (
411
+ patch.inProgress === false ||
412
+ (patch.role != null && patch.role !== MessageRole.SYSTEM)
413
+ ) {
414
+ delete next.inProgress;
415
+ }
416
+ return next;
417
+ }),
418
+ };
419
+ });
420
+ const chatsKey = getChatsKey(scopeId);
421
+ LS.set(chatsKey, updatedChats);
422
+ return { ...prev, [scopeId]: updatedChats };
423
+ });
424
+ },
425
+ [userSwitchKey],
426
+ );
427
+
373
428
  const sendMessage = useCallback(
374
429
  async (
375
430
  scopeId: string,
@@ -487,6 +542,7 @@ export function ChatProvider({
487
542
  setCurrentChatId,
488
543
  addMessage,
489
544
  removeMessageById,
545
+ updateMessageById,
490
546
  sendMessage,
491
547
  getChatsForScopeId,
492
548
  getCurrentChatId,
@@ -572,6 +628,7 @@ export function useChatsForScopeId(scopeId: string) {
572
628
  newChat,
573
629
  addMessage,
574
630
  removeMessageById,
631
+ updateMessageById,
575
632
  sendMessage,
576
633
  deleteChat,
577
634
  } = useChats();
@@ -595,6 +652,11 @@ export function useChatsForScopeId(scopeId: string) {
595
652
  ) => addMessage(scopeId, chatId, role, text, options),
596
653
  removeMessageById: (chatId: string, messageId: string) =>
597
654
  removeMessageById(scopeId, chatId, messageId),
655
+ updateMessageById: (
656
+ chatId: string,
657
+ messageId: string,
658
+ patch: UpdateChatMessagePatch,
659
+ ) => updateMessageById(scopeId, chatId, messageId, patch),
598
660
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) =>
599
661
  sendMessage(scopeId, message, chatId),
600
662
  deleteChat: (sessionId: string) => deleteChat(scopeId, sessionId),
@@ -28,11 +28,10 @@ const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
28
28
  },
29
29
  ];
30
30
 
31
- const SAMPLE_COMMAND_REPLY_TEXT =
32
- 'Sample command ran via `onSlashItemCommand` — composer cleared, no mention inserted.';
33
-
34
31
  const SAMPLE_COMMAND_PROGRESS_TEXT = 'Running sample command…';
35
32
 
33
+ const SAMPLE_COMMAND_SUCCESS_TEXT = '✅ Sample command complete.';
34
+
36
35
  function makeMessage(
37
36
  role: MessageRole,
38
37
  text: string,
@@ -47,6 +46,33 @@ function makeMessage(
47
46
  };
48
47
  }
49
48
 
49
+ /** Local-state equivalent of chat-context `updateMessageById`. */
50
+ function updateMessageInPlace(
51
+ messages: Message[],
52
+ messageId: string,
53
+ patch: { role?: MessageRole; text?: string; inProgress?: boolean },
54
+ ): Message[] {
55
+ return messages.map(message => {
56
+ if (message.id !== messageId) return message;
57
+ const next: Message = { ...message };
58
+ if (patch.role != null) {
59
+ next.role = patch.role;
60
+ }
61
+ if (patch.text != null) {
62
+ next.text = patch.text;
63
+ }
64
+ if (patch.inProgress === true) {
65
+ next.inProgress = true;
66
+ } else if (
67
+ patch.inProgress === false ||
68
+ (patch.role != null && patch.role !== MessageRole.SYSTEM)
69
+ ) {
70
+ delete next.inProgress;
71
+ }
72
+ return next;
73
+ });
74
+ }
75
+
50
76
  const ASSISTANT_REPLY_TEXT =
51
77
  'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
52
78
 
@@ -88,10 +114,13 @@ export default function ChatSlashCommandsPage() {
88
114
  }
89
115
  replyTimeoutRef.current = setTimeout(() => {
90
116
  replyTimeoutRef.current = null;
91
- setMessages(prev => [
92
- ...prev.filter(m => m.id !== progressId),
93
- makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
94
- ]);
117
+ setMessages(prev =>
118
+ updateMessageInPlace(prev, progressId, {
119
+ role: MessageRole.ASSISTANT,
120
+ text: SAMPLE_COMMAND_SUCCESS_TEXT,
121
+ inProgress: false,
122
+ }),
123
+ );
95
124
  setIsLoading(false);
96
125
  }, 1200);
97
126
  }, []);
@@ -110,7 +139,7 @@ export default function ChatSlashCommandsPage() {
110
139
  const onSubmit = useCallback(
111
140
  (raw: string) => {
112
141
  const text = raw.trim();
113
- if (!text || isLoading) return;
142
+ if (!text) return;
114
143
 
115
144
  if (text === `/${DOCS_SAMPLE_COMMAND_ID}`) {
116
145
  runSampleCommand();
@@ -132,7 +161,7 @@ export default function ChatSlashCommandsPage() {
132
161
  setIsLoading(false);
133
162
  }, 900);
134
163
  },
135
- [isLoading, runSampleCommand],
164
+ [runSampleCommand],
136
165
  );
137
166
 
138
167
  return (
@@ -140,7 +169,7 @@ export default function ChatSlashCommandsPage() {
140
169
  <AppPageHeader
141
170
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
142
171
  title="Chat slash commands"
143
- subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Pass slashCommandItems from the app; optional onSlashItemCommand can clear the composer and run an action instead of inserting a mention. Long-running handlers can append a SYSTEM message with inProgress while work is in flight, then remove it when done.`}
172
+ subheader={`Slash palette uses TipTap Mention with "/" as the trigger. Pass slashCommandItems from the app; optional onSlashItemCommand can clear the composer and run an action instead of inserting a mention. Long-running handlers can append a SYSTEM message with inProgress while work is in flight, then update it in place (e.g. via updateMessageById) to a final assistant message when done.`}
144
173
  actions={
145
174
  <DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
146
175
  }
@@ -154,10 +183,9 @@ export default function ChatSlashCommandsPage() {
154
183
  shell below: scrolling history, empty state, disclaimer, composer.
155
184
  Type <kbd className="font-mono">/</kbd> at line start or after a
156
185
  space; pick <kbd className="font-mono">sample-command</kbd> to run the
157
- custom handler (shows a transient{' '}
158
- <kbd className="font-mono">inProgress</kbd> shimmer while the demo
159
- action runs), or send <kbd className="font-mono">/sample-command</kbd>{' '}
160
- with Enter.
186
+ custom handler (shows an <kbd className="font-mono">inProgress</kbd>{' '}
187
+ shimmer, then updates the same message to a success assistant reply),
188
+ or send <kbd className="font-mono">/sample-command</kbd> with Enter.
161
189
  </p>
162
190
  <ChatChrome
163
191
  showResizeHandle={false}
@@ -185,7 +213,7 @@ export default function ChatSlashCommandsPage() {
185
213
  emptyState={{
186
214
  title: 'Try a slash command',
187
215
  description:
188
- 'Pick sample-command from the palette or send /sample-command — onSlashItemCommand clears the composer, shows an inProgress placeholder, then posts the demo reply.',
216
+ 'Pick sample-command from the palette or send /sample-command — inProgress placeholder shimmers, then becomes "✅ Sample command complete." in the same slot.',
189
217
  }}
190
218
  />
191
219
  </PageContentSection>