@sybilion/uilib 1.3.39 → 1.3.41

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
@@ -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,9 +66,9 @@ 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
- return (jsx(Chat.Message, { role: msg.role, text: msg.text, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
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 }
73
73
  : undefined, onScriptContinue: isLast && scriptContinueLabel
74
74
  ? onScriptContinue
@@ -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 };
@@ -3,19 +3,20 @@ import cn from 'classnames';
3
3
  import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.js';
4
4
  import 'lucide-react';
5
5
  import '../../InteractiveContent/InteractiveContent.styl.js';
6
+ import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
6
7
  import { MessageRole } from '../Chat.types.js';
7
8
  import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments.js';
8
9
  import { AgentMessageContent } from './AgentMessageContent.js';
9
10
  import S from './ChatMessage.styl.js';
10
11
  import { UserTextFileAttachmentBubble } from './UserTextFileAttachmentBubble.js';
11
12
 
12
- function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
13
+ function ChatMessage({ role, text, inProgress, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
13
14
  const fileAttachments = userTextFileAttachmentsFromMessage({
14
15
  userTextFileAttachments,
15
16
  });
16
17
  const isAssistant = role === MessageRole.ASSISTANT;
17
18
  const isSystem = role === MessageRole.SYSTEM;
18
- return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
19
+ return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: inProgress ? jsx(TextShimmer, { as: "span", children: text }) : text })) : isAssistant ? (jsx(AgentMessageContent, { text: text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: quickReplyDisabled, isLastMessage: isLastMessage, scriptContinue: scriptContinue, onScriptContinue: onScriptContinue, renderMessageChart: renderMessageChart })) : (jsxs("div", { className: S.userColumn, children: [jsx("div", { className: S.text, children: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
19
20
  }
20
21
 
21
22
  export { ChatMessage };
@@ -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)
@@ -200,6 +200,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
200
200
  ...(attachments?.length
201
201
  ? { userTextFileAttachments: attachments }
202
202
  : {}),
203
+ ...(options?.inProgress ? { inProgress: true } : {}),
203
204
  };
204
205
  setChats(prev => {
205
206
  const scopeChats = prev[scopeId] ?? [];
@@ -233,6 +234,42 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
233
234
  return { ...prev, [scopeId]: updatedChats };
234
235
  });
235
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]);
236
273
  const sendMessage = useCallback(async (scopeId, message, chatId) => {
237
274
  const targetChatId = chatId ?? getCurrentChatId(scopeId);
238
275
  if (targetChatId === null || targetChatId === '') {
@@ -309,6 +346,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
309
346
  setCurrentChatId,
310
347
  addMessage,
311
348
  removeMessageById,
349
+ updateMessageById,
312
350
  sendMessage,
313
351
  getChatsForScopeId,
314
352
  getCurrentChatId,
@@ -369,7 +407,7 @@ function useSyncChatPanelBusy(isLoading) {
369
407
  }, [isLoading, acquirePanelBusy, releasePanelBusy]);
370
408
  }
371
409
  function useChatsForScopeId(scopeId) {
372
- const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, sendMessage, deleteChat, } = useChats();
410
+ const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, updateMessageById, sendMessage, deleteChat, } = useChats();
373
411
  const chats = getChatsForScopeId(scopeId);
374
412
  const currentChatId = getCurrentChatId(scopeId);
375
413
  const currentChat = useChat(scopeId, currentChatId ?? undefined);
@@ -383,6 +421,7 @@ function useChatsForScopeId(scopeId) {
383
421
  newChat: () => newChat(scopeId),
384
422
  addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
385
423
  removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
424
+ updateMessageById: (chatId, messageId, patch) => updateMessageById(scopeId, chatId, messageId, patch),
386
425
  sendMessage: (message, chatId) => sendMessage(scopeId, message, chatId),
387
426
  deleteChat: (sessionId) => deleteChat(scopeId, sessionId),
388
427
  };
@@ -24,6 +24,8 @@ export interface Message {
24
24
  text: string;
25
25
  timestamp: number;
26
26
  userTextFileAttachments?: UserTextFileAttachment[];
27
+ /** SYSTEM-only: transient progress placeholder while work is in flight. */
28
+ inProgress?: boolean;
27
29
  }
28
30
  export interface Chat {
29
31
  session_id: string;
@@ -88,6 +90,7 @@ export interface ChatPromptProps {
88
90
  export interface ChatMessageProps {
89
91
  role: MessageRole;
90
92
  text: string;
93
+ inProgress?: boolean;
91
94
  userTextFileAttachments?: UserTextFileAttachment[];
92
95
  onQuickReply?: (branchKey: string, displayLabel: string) => void;
93
96
  /** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
@@ -1,2 +1,2 @@
1
1
  import { type ChatMessageProps } from '../Chat.types';
2
- export declare function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatMessage({ role, text, inProgress, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
@@ -5,6 +5,13 @@ export type SendChatMessageFn = (message: string, targetChatId: string) => Promi
5
5
  export type { ChatSendMessagePayload, UserTextFileAttachment, } from '#uilib/components/ui/Chat/Chat.types';
6
6
  export type AddChatMessageOptions = {
7
7
  userTextFileAttachments?: UserTextFileAttachment[];
8
+ /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
9
+ inProgress?: boolean;
10
+ };
11
+ export type UpdateChatMessagePatch = {
12
+ role?: MessageRole;
13
+ text?: string;
14
+ inProgress?: boolean;
8
15
  };
9
16
  export interface ChatContextType {
10
17
  /** Returns the new session id, or undefined if no user / not created. */
@@ -12,6 +19,7 @@ export interface ChatContextType {
12
19
  setCurrentChatId: (currScopeId: string, sessionId: string) => void;
13
20
  addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string | undefined;
14
21
  removeMessageById: (scopeId: string, chatId: string, messageId: string) => void;
22
+ updateMessageById: (scopeId: string, chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
15
23
  sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
16
24
  getChatsForScopeId: (scopeId: string) => Chat[];
17
25
  getCurrentChatId: (scopeId: string) => string | null;
@@ -53,6 +61,7 @@ export declare function useChatsForScopeId(scopeId: string): {
53
61
  newChat: () => string;
54
62
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
55
63
  removeMessageById: (chatId: string, messageId: string) => void;
64
+ updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
56
65
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
57
66
  deleteChat: (sessionId: string) => void;
58
67
  };
@@ -66,6 +75,7 @@ export declare function useChatsForDataset(scopeId: string): {
66
75
  newChat: () => string;
67
76
  addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
68
77
  removeMessageById: (chatId: string, messageId: string) => void;
78
+ updateMessageById: (chatId: string, messageId: string, patch: UpdateChatMessagePatch) => void;
69
79
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
70
80
  deleteChat: (sessionId: string) => void;
71
81
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.39",
3
+ "version": "1.3.41",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -33,6 +33,8 @@ export interface Message {
33
33
  text: string;
34
34
  timestamp: number;
35
35
  userTextFileAttachments?: UserTextFileAttachment[];
36
+ /** SYSTEM-only: transient progress placeholder while work is in flight. */
37
+ inProgress?: boolean;
36
38
  }
37
39
 
38
40
  export interface Chat {
@@ -103,6 +105,7 @@ export interface ChatPromptProps {
103
105
  export interface ChatMessageProps {
104
106
  role: MessageRole;
105
107
  text: string;
108
+ inProgress?: boolean;
106
109
  userTextFileAttachments?: UserTextFileAttachment[];
107
110
  onQuickReply?: (branchKey: string, displayLabel: string) => void;
108
111
  /** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
@@ -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
  />
@@ -199,6 +199,7 @@ export function ChatChrome({
199
199
  key={msg.id}
200
200
  role={msg.role}
201
201
  text={msg.text}
202
+ inProgress={msg.inProgress}
202
203
  userTextFileAttachments={msg.userTextFileAttachments}
203
204
  onQuickReply={onQuickReply}
204
205
  suppressedQuickReplyKeys={suppressedQuickReplyKeys}
@@ -262,7 +263,7 @@ export function ChatChrome({
262
263
  ) : null}
263
264
  <Chat.Prompt
264
265
  onSubmit={handlePromptSubmitWithAttachments}
265
- disabled={promptBusy}
266
+ disabled={promptDisabled}
266
267
  attachments={pendingAttachments}
267
268
  onRemoveAttachment={handleRemoveAttachment}
268
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
@@ -1,6 +1,7 @@
1
1
  import cn from 'classnames';
2
2
 
3
3
  import { InteractiveContent } from '#uilib/components/ui/InteractiveContent';
4
+ import { TextShimmer } from '#uilib/components/ui/TextShimmer';
4
5
 
5
6
  import { type ChatMessageProps, MessageRole } from '../Chat.types';
6
7
  import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments';
@@ -11,6 +12,7 @@ import { UserTextFileAttachmentBubble } from './UserTextFileAttachmentBubble';
11
12
  export function ChatMessage({
12
13
  role,
13
14
  text,
15
+ inProgress,
14
16
  userTextFileAttachments,
15
17
  onQuickReply,
16
18
  suppressedQuickReplyKeys,
@@ -29,7 +31,9 @@ export function ChatMessage({
29
31
  return (
30
32
  <div className={cn(S.root, S[`role-${role}`])}>
31
33
  {isSystem ? (
32
- <div className={S.text}>{text}</div>
34
+ <div className={S.text}>
35
+ {inProgress ? <TextShimmer as="span">{text}</TextShimmer> : text}
36
+ </div>
33
37
  ) : isAssistant ? (
34
38
  <AgentMessageContent
35
39
  text={text}
@@ -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, '');
@@ -35,6 +35,14 @@ const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
35
35
 
36
36
  export type AddChatMessageOptions = {
37
37
  userTextFileAttachments?: UserTextFileAttachment[];
38
+ /** SYSTEM-only: mark as transient progress placeholder (shimmer until removed). */
39
+ inProgress?: boolean;
40
+ };
41
+
42
+ export type UpdateChatMessagePatch = {
43
+ role?: MessageRole;
44
+ text?: string;
45
+ inProgress?: boolean;
38
46
  };
39
47
 
40
48
  export interface ChatContextType {
@@ -53,6 +61,12 @@ export interface ChatContextType {
53
61
  chatId: string,
54
62
  messageId: string,
55
63
  ) => void;
64
+ updateMessageById: (
65
+ scopeId: string,
66
+ chatId: string,
67
+ messageId: string,
68
+ patch: UpdateChatMessagePatch,
69
+ ) => void;
56
70
  sendMessage: (
57
71
  scopeId: string,
58
72
  message: string | ChatSendMessagePayload,
@@ -326,6 +340,7 @@ export function ChatProvider({
326
340
  ...(attachments?.length
327
341
  ? { userTextFileAttachments: attachments }
328
342
  : {}),
343
+ ...(options?.inProgress ? { inProgress: true } : {}),
329
344
  };
330
345
 
331
346
  setChats(prev => {
@@ -367,6 +382,49 @@ export function ChatProvider({
367
382
  [userSwitchKey],
368
383
  );
369
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
+
370
428
  const sendMessage = useCallback(
371
429
  async (
372
430
  scopeId: string,
@@ -484,6 +542,7 @@ export function ChatProvider({
484
542
  setCurrentChatId,
485
543
  addMessage,
486
544
  removeMessageById,
545
+ updateMessageById,
487
546
  sendMessage,
488
547
  getChatsForScopeId,
489
548
  getCurrentChatId,
@@ -569,6 +628,7 @@ export function useChatsForScopeId(scopeId: string) {
569
628
  newChat,
570
629
  addMessage,
571
630
  removeMessageById,
631
+ updateMessageById,
572
632
  sendMessage,
573
633
  deleteChat,
574
634
  } = useChats();
@@ -592,6 +652,11 @@ export function useChatsForScopeId(scopeId: string) {
592
652
  ) => addMessage(scopeId, chatId, role, text, options),
593
653
  removeMessageById: (chatId: string, messageId: string) =>
594
654
  removeMessageById(scopeId, chatId, messageId),
655
+ updateMessageById: (
656
+ chatId: string,
657
+ messageId: string,
658
+ patch: UpdateChatMessagePatch,
659
+ ) => updateMessageById(scopeId, chatId, messageId, patch),
595
660
  sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) =>
596
661
  sendMessage(scopeId, message, chatId),
597
662
  deleteChat: (sessionId: string) => deleteChat(scopeId, sessionId),
@@ -28,21 +28,54 @@ 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.';
31
+ const SAMPLE_COMMAND_PROGRESS_TEXT = 'Running sample command…';
33
32
 
34
- const ASSISTANT_REPLY_TEXT =
35
- 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
33
+ const SAMPLE_COMMAND_SUCCESS_TEXT = '✅ Sample command complete.';
36
34
 
37
- function makeMessage(role: MessageRole, text: string): Message {
35
+ function makeMessage(
36
+ role: MessageRole,
37
+ text: string,
38
+ options?: { inProgress?: boolean },
39
+ ): Message {
38
40
  return {
39
41
  id: crypto.randomUUID(),
40
42
  role,
41
43
  text,
42
44
  timestamp: Date.now(),
45
+ ...(options?.inProgress ? { inProgress: true } : {}),
43
46
  };
44
47
  }
45
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
+
76
+ const ASSISTANT_REPLY_TEXT =
77
+ 'Demo reply for a normal message. Slash picks with a custom handler skip mention insert.';
78
+
46
79
  export default function ChatSlashCommandsPage() {
47
80
  const [messages, setMessages] = useState<Message[]>([]);
48
81
  const [isLoading, setIsLoading] = useState(false);
@@ -63,10 +96,33 @@ export default function ChatSlashCommandsPage() {
63
96
  messages[messages.length - 1]?.role === MessageRole.USER;
64
97
 
65
98
  const runSampleCommand = useCallback(() => {
99
+ const progressId = crypto.randomUUID();
100
+ setIsLoading(true);
66
101
  setMessages(prev => [
67
102
  ...prev,
68
- makeMessage(MessageRole.ASSISTANT, SAMPLE_COMMAND_REPLY_TEXT),
103
+ {
104
+ id: progressId,
105
+ role: MessageRole.SYSTEM,
106
+ text: SAMPLE_COMMAND_PROGRESS_TEXT,
107
+ timestamp: Date.now(),
108
+ inProgress: true,
109
+ },
69
110
  ]);
111
+
112
+ if (replyTimeoutRef.current != null) {
113
+ clearTimeout(replyTimeoutRef.current);
114
+ }
115
+ replyTimeoutRef.current = setTimeout(() => {
116
+ replyTimeoutRef.current = null;
117
+ setMessages(prev =>
118
+ updateMessageInPlace(prev, progressId, {
119
+ role: MessageRole.ASSISTANT,
120
+ text: SAMPLE_COMMAND_SUCCESS_TEXT,
121
+ inProgress: false,
122
+ }),
123
+ );
124
+ setIsLoading(false);
125
+ }, 1200);
70
126
  }, []);
71
127
 
72
128
  const onSlashItemCommand = useCallback(
@@ -83,7 +139,7 @@ export default function ChatSlashCommandsPage() {
83
139
  const onSubmit = useCallback(
84
140
  (raw: string) => {
85
141
  const text = raw.trim();
86
- if (!text || isLoading) return;
142
+ if (!text) return;
87
143
 
88
144
  if (text === `/${DOCS_SAMPLE_COMMAND_ID}`) {
89
145
  runSampleCommand();
@@ -105,7 +161,7 @@ export default function ChatSlashCommandsPage() {
105
161
  setIsLoading(false);
106
162
  }, 900);
107
163
  },
108
- [isLoading, runSampleCommand],
164
+ [runSampleCommand],
109
165
  );
110
166
 
111
167
  return (
@@ -113,7 +169,7 @@ export default function ChatSlashCommandsPage() {
113
169
  <AppPageHeader
114
170
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat slash commands' }]}
115
171
  title="Chat slash commands"
116
- 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.`}
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.`}
117
173
  actions={
118
174
  <DocsHeaderActions slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS} />
119
175
  }
@@ -127,8 +183,9 @@ export default function ChatSlashCommandsPage() {
127
183
  shell below: scrolling history, empty state, disclaimer, composer.
128
184
  Type <kbd className="font-mono">/</kbd> at line start or after a
129
185
  space; pick <kbd className="font-mono">sample-command</kbd> to run the
130
- custom handler, or send{' '}
131
- <kbd className="font-mono">/sample-command</kbd> 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.
132
189
  </p>
133
190
  <ChatChrome
134
191
  showResizeHandle={false}
@@ -156,7 +213,7 @@ export default function ChatSlashCommandsPage() {
156
213
  emptyState={{
157
214
  title: 'Try a slash command',
158
215
  description:
159
- 'Pick sample-command from the palette or send /sample-command — onSlashItemCommand clears the composer and runs the demo action.',
216
+ 'Pick sample-command from the palette or send /sample-command — inProgress placeholder shimmers, then becomes "✅ Sample command complete." in the same slot.',
160
217
  }}
161
218
  />
162
219
  </PageContentSection>