@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 +1 -12
- package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +6 -6
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +3 -2
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptComposer.js +1 -1
- package/dist/esm/components/ui/Chat/ChatPrompt/useChatPromptEditor.js +1 -1
- package/dist/esm/contexts/chat-context.js +40 -1
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +3 -0
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
- package/dist/esm/types/src/contexts/chat-context.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/Chat.types.ts +3 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +6 -5
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +8 -2
- package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +5 -1
- package/src/components/ui/Chat/ChatPrompt/ChatPromptComposer.tsx +6 -1
- package/src/components/ui/Chat/ChatPrompt/useChatPromptEditor.ts +1 -1
- package/src/contexts/chat-context.tsx +65 -0
- package/src/docs/pages/ChatSlashCommandsPage.tsx +69 -12
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
|
|
23
|
+
const promptDisabled = isExtractingAttachments;
|
|
24
24
|
const handleAttachmentFiles = useCallback((files) => {
|
|
25
|
-
if (
|
|
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,
|
|
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:
|
|
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:
|
|
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-
|
|
4
|
-
var S = {"root":"ChatMessage_root__6rnsF","text":"ChatMessage_text__Y1XNR","role-user":"ChatMessage_role-user__u4JPV","
|
|
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.
|
|
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
|
@@ -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
|
|
74
|
+
const promptDisabled = isExtractingAttachments;
|
|
75
75
|
|
|
76
76
|
const handleAttachmentFiles = useCallback(
|
|
77
77
|
(files: File[]) => {
|
|
78
|
-
if (
|
|
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,
|
|
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={
|
|
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={
|
|
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
|
+
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}>
|
|
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
|
|
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.
|
|
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
|
|
32
|
-
'Sample command ran via `onSlashItemCommand` — composer cleared, no mention inserted.';
|
|
31
|
+
const SAMPLE_COMMAND_PROGRESS_TEXT = 'Running sample command…';
|
|
33
32
|
|
|
34
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
[
|
|
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
|
|
131
|
-
|
|
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 —
|
|
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>
|