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