@sybilion/uilib 1.2.19 → 1.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +1 -1
  2. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +4 -3
  3. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
  4. package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +11 -0
  5. package/dist/esm/components/ui/Chat/ChatMessage/icons/CsvIcon.js +8 -0
  6. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +12 -18
  7. package/dist/esm/components/ui/Chat/chat-preset-utils.js +12 -3
  8. package/dist/esm/contexts/chat-context.js +69 -10
  9. package/dist/esm/index.js +1 -1
  10. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +14 -0
  11. package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
  12. package/dist/esm/types/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.d.ts +4 -0
  13. package/dist/esm/types/src/components/ui/Chat/ChatMessage/icons/CsvIcon.d.ts +3 -0
  14. package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
  15. package/dist/esm/types/src/contexts/chat-context.d.ts +21 -7
  16. package/dist/esm/types/src/docs/pages/ChatUserCsvAttachmentPage.d.ts +1 -0
  17. package/dist/esm/types/src/utils/downloadTextFile.d.ts +2 -0
  18. package/dist/esm/utils/downloadTextFile.js +14 -0
  19. package/package.json +1 -1
  20. package/src/components/ui/Chat/Chat.types.ts +16 -0
  21. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +1 -0
  22. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +67 -0
  23. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl.d.ts +6 -0
  24. package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +8 -1
  25. package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +36 -0
  26. package/src/components/ui/Chat/ChatMessage/icons/CsvIcon.tsx +7 -0
  27. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +15 -15
  28. package/src/components/ui/Chat/chat-preset-utils.ts +12 -6
  29. package/src/components/ui/Chat/index.ts +3 -1
  30. package/src/contexts/chat-context.tsx +124 -13
  31. package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +171 -0
  32. package/src/docs/registry.ts +6 -0
  33. package/src/utils/downloadTextFile.ts +16 -0
@@ -30,7 +30,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
30
30
  }, [isEmpty, messages.length]);
31
31
  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 }), jsx("div", { className: S.content, children: 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) => {
32
32
  const isLast = index === arr.length - 1;
33
- return (jsx(Chat.Message, { role: msg.role, text: msg.text, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
33
+ return (jsx(Chat.Message, { role: msg.role, text: msg.text, userCsvAttachment: msg.userCsvAttachment, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
34
34
  ? { label: scriptContinueLabel }
35
35
  : undefined, onScriptContinue: isLast && scriptContinueLabel
36
36
  ? onScriptContinue
@@ -1,14 +1,15 @@
1
- import { jsx } from 'react/jsx-runtime';
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import cn from 'classnames';
3
3
  import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
4
4
  import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
5
5
  import { AgentMessageContent } from './AgentMessageContent.js';
6
6
  import S from './ChatMessage.styl.js';
7
+ import { UserCsvAttachmentBubble } from './UserCsvAttachmentBubble.js';
7
8
 
8
- function ChatMessage({ role, text, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
9
+ function ChatMessage({ role, text, userCsvAttachment, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
9
10
  const isAssistant = role === MessageRole.ASSISTANT;
10
11
  const isSystem = role === MessageRole.SYSTEM;
11
- return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (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 })) : (jsx("div", { className: S.text, children: text })) }));
12
+ return (jsx("div", { className: cn(S.root, S[`role-${role}`]), children: isSystem ? (jsx("div", { className: S.text, children: text === GENERATING_DASHBOARD_SYSTEM_TEXT ? (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: text }), userCsvAttachment ? (jsx(UserCsvAttachmentBubble, { attachment: userCsvAttachment })) : null] })) }));
12
13
  }
13
14
 
14
15
  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%;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content}.ChatMessage_role-user__u4JPV{align-items:flex-end}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;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","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-6)}.ChatMessage_text__Y1XNR{color:var(--text-secondary);font-size:var(--text-sm);max-width:100%;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content}.ChatMessage_role-user__u4JPV{align-items:flex-end}.ChatMessage_role-user__u4JPV .ChatMessage_userColumn__cQM6-{align-items:flex-end;display:flex;flex-direction:column;gap:var(--p-2);max-width:100%}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;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-user__u4JPV .ChatMessage_userCsvCard__D1M7y{align-items:center;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--sb-slate-100);border:0;border-radius:var(--p-4);border-bottom-right-radius:0;box-shadow:0 0 0 1px var(--border);color:var(--sb-green-600);cursor:pointer;display:flex;font:inherit;gap:var(--p-4);margin:0;max-width:100%;padding:var(--p-3);padding-right:var(--p-4);text-align:left;transition:background-color .15s;width:-moz-fit-content;width:fit-content}.ChatMessage_role-user__u4JPV .ChatMessage_userCsvCard__D1M7y:hover{background-color:var(--sb-gray-50)}.ChatMessage_role-user__u4JPV .ChatMessage_userCsvCard__D1M7y:focus-visible{outline:2px solid var(--ring);outline-offset:2px}.dark .ChatMessage_role-user__u4JPV .ChatMessage_userCsvCard__D1M7y{background-color:var(--sb-gray-800);color:var(--sb-green-400)}.dark .ChatMessage_role-user__u4JPV .ChatMessage_userCsvCard__D1M7y:hover{background-color:var(--sb-gray-900)}.ChatMessage_role-user__u4JPV .ChatMessage_userCsvCardIcon__0-KS6{align-items:center;display:flex;flex-shrink:0;height:32px;justify-content:center;width:32px}.ChatMessage_role-user__u4JPV .ChatMessage_userCsvCardContent__LoMGE{display:flex;flex:1;flex-direction:column;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_userCsvCardTitle__9W76E{color:var(--text-secondary);font-size:var(--text-base);font-weight:600;line-height:1.4}.ChatMessage_role-user__u4JPV .ChatMessage_userCsvCardSubtitle__YZeHv{color:var(--muted-foreground);font-size:var(--text-sm);line-height:1.4}.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-","userCsvCard":"ChatMessage_userCsvCard__D1M7y","userCsvCardIcon":"ChatMessage_userCsvCardIcon__0-KS6","userCsvCardContent":"ChatMessage_userCsvCardContent__LoMGE","userCsvCardTitle":"ChatMessage_userCsvCardTitle__9W76E","userCsvCardSubtitle":"ChatMessage_userCsvCardSubtitle__YZeHv","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"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -0,0 +1,11 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { downloadTextFile } from '../../../../utils/downloadTextFile.js';
3
+ import { CsvIcon } from './icons/CsvIcon.js';
4
+ import S from './ChatMessage.styl.js';
5
+
6
+ const CSV_DOWNLOAD_HINT = 'Download .CSV file';
7
+ function UserCsvAttachmentBubble({ attachment, }) {
8
+ return (jsxs("button", { type: "button", className: S.userCsvCard, "aria-label": `${CSV_DOWNLOAD_HINT}: ${attachment.displayName}`, onClick: () => downloadTextFile(attachment.content, attachment.filename, 'text/csv;charset=utf-8'), children: [jsx("div", { className: S.userCsvCardIcon, children: jsx(CsvIcon, { size: 32 }) }), jsxs("div", { className: S.userCsvCardContent, children: [jsx("div", { className: S.userCsvCardTitle, children: attachment.displayName }), jsx("div", { className: S.userCsvCardSubtitle, children: CSV_DOWNLOAD_HINT })] })] }));
9
+ }
10
+
11
+ export { UserCsvAttachmentBubble };
@@ -0,0 +1,8 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { FileSpreadsheet } from 'lucide-react';
3
+
4
+ function CsvIcon({ size = 32 }) {
5
+ return (jsx(FileSpreadsheet, { size: size, "aria-hidden": true, strokeWidth: 1.75, color: "currentColor" }));
6
+ }
7
+
8
+ export { CsvIcon };
@@ -3,7 +3,7 @@ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
4
4
  import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant, isPresetScriptGraph, branchesFromPresetScriptGraph } from '../ChatMessage/presetScript.js';
5
5
  import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
6
- import { useChatsForScopeId, useChat, isChatEmpty } from '../../../../contexts/chat-context.js';
6
+ import { useChatsForScopeId, useChat, useChatOutboundPending, isChatEmpty } from '../../../../contexts/chat-context.js';
7
7
  import useEvent from '../../../../hooks/useEvent.js';
8
8
  import { useIsMobile } from '../../../../hooks/useIsMobile.js';
9
9
  import { useQueryParams } from '../../../../hooks/useQueryParams.js';
@@ -25,11 +25,13 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
25
25
  const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
26
26
  const isMobile = useIsMobile();
27
27
  const { chatPanelContainer, isOpen: sidebarNavOpen, setOpen: setSidebarNavOpen, chatWidthPx, setChatWidthPx, getShellWidth, setChatPanelOpen, } = useSidebar();
28
+ const [localUiBusy, setLocalUiBusy] = useState(false);
28
29
  const { chats, currentChatId, setCurrentChatId, newChat, sendMessage, addMessage, removeMessageById, } = useChatsForScopeId(effectiveScopeId);
29
30
  const chat = useChat(effectiveScopeId, currentChatId);
31
+ const isOutboundPending = useChatOutboundPending(effectiveScopeId, currentChatId);
32
+ const isLoading = isOutboundPending || localUiBusy;
30
33
  const { searchParams, addSearchParams, removeSearchParams, mutateSearchParams, } = useQueryParams();
31
34
  const chatOpen = searchParams.has(CHAT_QUERY_PARAM);
32
- const [isLoading, setIsLoading] = useState(false);
33
35
  const [isOpen, setIsOpen] = useState(false);
34
36
  /** `?prompt=` deep link text for one-shot composer pre-fill (see ChatPrompt). */
35
37
  const [promptLinkPrefill, setPromptLinkPrefill] = useState(null);
@@ -215,7 +217,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
215
217
  if (quickReplyLockRef.current)
216
218
  return;
217
219
  quickReplyLockRef.current = true;
218
- setIsLoading(true);
220
+ setLocalUiBusy(true);
219
221
  setUsedScriptBranchKeysByChat(prev => ({
220
222
  ...prev,
221
223
  [chatId]: [...new Set([...(prev[chatId] || []), branchKey])],
@@ -266,7 +268,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
266
268
  logger.error('Error resolving preset quick reply:', error);
267
269
  }
268
270
  finally {
269
- setIsLoading(false);
271
+ setLocalUiBusy(false);
270
272
  quickReplyLockRef.current = false;
271
273
  }
272
274
  })();
@@ -274,7 +276,6 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
274
276
  }
275
277
  endLocalDemoFlow(chatId);
276
278
  void (async () => {
277
- setIsLoading(true);
278
279
  try {
279
280
  await sendMessage(displayLabel);
280
281
  onMessage?.(displayLabel);
@@ -282,9 +283,6 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
282
283
  catch (error) {
283
284
  logger.error('Error sending chat message:', error);
284
285
  }
285
- finally {
286
- setIsLoading(false);
287
- }
288
286
  })();
289
287
  }, [
290
288
  currentChatId,
@@ -337,7 +335,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
337
335
  if (quickReplyLockRef.current)
338
336
  return;
339
337
  quickReplyLockRef.current = true;
340
- setIsLoading(true);
338
+ setLocalUiBusy(true);
341
339
  const newAnswers = {
342
340
  ...intake.answers,
343
341
  [intake.scriptStepId]: message,
@@ -381,7 +379,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
381
379
  logger.error('Error advancing freeform preset script:', e);
382
380
  }
383
381
  finally {
384
- setIsLoading(false);
382
+ setLocalUiBusy(false);
385
383
  quickReplyLockRef.current = false;
386
384
  }
387
385
  })();
@@ -401,7 +399,6 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
401
399
  }
402
400
  }
403
401
  }
404
- setIsLoading(true);
405
402
  try {
406
403
  if (chatId)
407
404
  endLocalDemoFlow(chatId);
@@ -411,9 +408,6 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
411
408
  catch (error) {
412
409
  logger.error('Error sending chat message:', error);
413
410
  }
414
- finally {
415
- setIsLoading(false);
416
- }
417
411
  }, [
418
412
  currentChatId,
419
413
  chat?.messages,
@@ -443,7 +437,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
443
437
  await handlePromptSubmit(preset.text);
444
438
  return;
445
439
  }
446
- setIsLoading(true);
440
+ setLocalUiBusy(true);
447
441
  try {
448
442
  if (!currentChatId)
449
443
  return;
@@ -518,7 +512,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
518
512
  logger.error('Error sending chat message:', error);
519
513
  }
520
514
  finally {
521
- setIsLoading(false);
515
+ setLocalUiBusy(false);
522
516
  }
523
517
  };
524
518
  const activeScript = currentChatId
@@ -550,7 +544,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
550
544
  return;
551
545
  const chatId = currentChatId;
552
546
  scriptAdvanceLockRef.current = true;
553
- setIsLoading(true);
547
+ setLocalUiBusy(true);
554
548
  addMessage(chatId, MessageRole.USER, step.buttonLabel);
555
549
  void (async () => {
556
550
  try {
@@ -593,7 +587,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
593
587
  logger.error('Error advancing preset script:', error);
594
588
  }
595
589
  finally {
596
- setIsLoading(false);
590
+ setLocalUiBusy(false);
597
591
  scriptAdvanceLockRef.current = false;
598
592
  }
599
593
  })();
@@ -8,14 +8,23 @@ function formatChatTranscript(messages) {
8
8
  function normalizePresetMatchText(s) {
9
9
  return s.trim().normalize('NFC');
10
10
  }
11
+ function presetMatchesUserText(presetText, userTextNorm) {
12
+ const presetNorm = normalizePresetMatchText(presetText);
13
+ if (!presetNorm)
14
+ return false;
15
+ if (userTextNorm === presetNorm)
16
+ return true;
17
+ const prefix = `${presetNorm} `;
18
+ return userTextNorm.startsWith(prefix);
19
+ }
11
20
  function usedPresetIdsFromMessages(messages, presets) {
12
21
  if (!messages?.length || !presets?.length)
13
22
  return [];
14
- const userTexts = new Set(messages
23
+ const userTexts = messages
15
24
  .filter(m => m.role === MessageRole.USER)
16
- .map(m => normalizePresetMatchText(m.text)));
25
+ .map(m => normalizePresetMatchText(m.text));
17
26
  return presets
18
- .filter(p => userTexts.has(normalizePresetMatchText(p.text)))
27
+ .filter(p => userTexts.some(ut => presetMatchesUserText(p.text, ut)))
19
28
  .map(p => p.id);
20
29
  }
21
30
 
@@ -7,6 +7,10 @@ import { LS } from '@homecode/ui';
7
7
  const CHATS_PREFIX = 'chats-';
8
8
  const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
9
9
  const ChatContext = createContext(undefined);
10
+ /** Stable composite key; avoids collisions if `scopeId` contains `:`. */
11
+ function outboundPendingKey(scopeId, chatSessionId) {
12
+ return `${scopeId}\0${chatSessionId}`;
13
+ }
10
14
  function getCurrentChatIdKey(scopeId) {
11
15
  return `chat-current-id-${scopeId}`;
12
16
  }
@@ -86,6 +90,26 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
86
90
  return {};
87
91
  return loadChatsFromLS(userSwitchKey).currentChatId;
88
92
  });
93
+ const [outboundPendingByKey, setOutboundPendingByKey] = useState({});
94
+ const beginOutboundPending = useCallback((scopeId, chatSessionId) => {
95
+ const key = outboundPendingKey(scopeId, chatSessionId);
96
+ setOutboundPendingByKey(prev => ({
97
+ ...prev,
98
+ [key]: (prev[key] ?? 0) + 1,
99
+ }));
100
+ }, []);
101
+ const endOutboundPending = useCallback((scopeId, chatSessionId) => {
102
+ const key = outboundPendingKey(scopeId, chatSessionId);
103
+ setOutboundPendingByKey(prev => {
104
+ const next = { ...prev };
105
+ const n = (next[key] ?? 0) - 1;
106
+ if (n <= 0)
107
+ delete next[key];
108
+ else
109
+ next[key] = n;
110
+ return next;
111
+ });
112
+ }, []);
89
113
  const getChatsForScopeId = useCallback((scopeId) => chats[scopeId] ?? [], [chats]);
90
114
  const getCurrentChatId = useCallback((scopeId) => {
91
115
  const v = currentChatId[scopeId];
@@ -152,16 +176,18 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
152
176
  return { ...prev, [scopeId]: updatedChats };
153
177
  });
154
178
  }, []);
155
- const addMessage = useCallback((scopeId, chatId, role, text) => {
179
+ const addMessage = useCallback((scopeId, chatId, role, text, options) => {
156
180
  if (userSwitchKey === null)
157
181
  return undefined;
158
182
  addScopeIdToRegistry(scopeId);
159
183
  const storedText = stripJsonDashboardFences(text);
184
+ const attachment = role === MessageRole.USER ? options?.userCsvAttachment : undefined;
160
185
  const newMessage = {
161
186
  id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
162
187
  role,
163
188
  text: storedText,
164
189
  timestamp: Date.now(),
190
+ ...(attachment ? { userCsvAttachment: attachment } : {}),
165
191
  };
166
192
  setChats(prev => {
167
193
  const scopeChats = prev[scopeId] ?? [];
@@ -200,13 +226,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
200
226
  if (targetChatId === null || targetChatId === '') {
201
227
  throw new Error('No chat selected');
202
228
  }
203
- addMessage(scopeId, targetChatId, MessageRole.USER, message);
229
+ const apiPayload = typeof message === 'string' ? message : message.apiMessage;
230
+ if (typeof message === 'string') {
231
+ addMessage(scopeId, targetChatId, MessageRole.USER, message);
232
+ }
233
+ else {
234
+ addMessage(scopeId, targetChatId, MessageRole.USER, message.displayText, {
235
+ userCsvAttachment: message.userCsvAttachment,
236
+ });
237
+ }
238
+ const pendingChatSessionId = targetChatId;
239
+ beginOutboundPending(scopeId, pendingChatSessionId);
204
240
  try {
205
- const data = await sendChatMessageFn(message, targetChatId);
206
- if (data.session_id && data.session_id !== targetChatId) {
241
+ const data = await sendChatMessageFn(apiPayload, pendingChatSessionId);
242
+ if (data.session_id && data.session_id !== pendingChatSessionId) {
207
243
  setChats(prev => {
208
244
  const scopeChats = prev[scopeId] ?? [];
209
- const updatedChats = scopeChats.map(chat => chat.session_id === targetChatId
245
+ const updatedChats = scopeChats.map(chat => chat.session_id === pendingChatSessionId
210
246
  ? { ...chat, session_id: data.session_id }
211
247
  : chat);
212
248
  const chatsKey = getChatsKey(scopeId);
@@ -215,21 +251,32 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
215
251
  });
216
252
  setCurrentChatId(scopeId, data.session_id);
217
253
  }
218
- addMessage(scopeId, data.session_id ? data.session_id : targetChatId, MessageRole.ASSISTANT, data.response);
254
+ addMessage(scopeId, data.session_id ? data.session_id : pendingChatSessionId, MessageRole.ASSISTANT, data.response);
219
255
  return data.response;
220
256
  }
221
257
  catch (error) {
222
258
  const errorMessage = error instanceof Error
223
259
  ? error.message
224
260
  : 'Sorry, I encountered an error processing your message. Please try again.';
225
- addMessage(scopeId, targetChatId, MessageRole.ASSISTANT, errorMessage);
261
+ addMessage(scopeId, pendingChatSessionId, MessageRole.ASSISTANT, errorMessage);
226
262
  throw error;
227
263
  }
228
- }, [addMessage, getCurrentChatId, sendChatMessageFn, setCurrentChatId]);
264
+ finally {
265
+ endOutboundPending(scopeId, pendingChatSessionId);
266
+ }
267
+ }, [
268
+ addMessage,
269
+ beginOutboundPending,
270
+ endOutboundPending,
271
+ getCurrentChatId,
272
+ sendChatMessageFn,
273
+ setCurrentChatId,
274
+ ]);
229
275
  useEffect(() => {
230
276
  if (userSwitchKey === null) {
231
277
  setChats({});
232
278
  setCurrentChatIdState({});
279
+ setOutboundPendingByKey({});
233
280
  return;
234
281
  }
235
282
  const loaded = loadChatsFromLS(userSwitchKey);
@@ -253,6 +300,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
253
300
  getChatsForScopeId,
254
301
  getCurrentChatId,
255
302
  deleteChat,
303
+ outboundPendingByKey,
256
304
  }, children: children }));
257
305
  }
258
306
  const isChatEmpty = (chat) => chat?.messages.length === 0;
@@ -272,18 +320,29 @@ function useChat(scopeId, chatId) {
272
320
  null);
273
321
  }, [scopeId, chatId, getChatsForScopeId]);
274
322
  }
323
+ function useChatOutboundPending(scopeId, chatSessionId) {
324
+ const { outboundPendingByKey } = useChats();
325
+ return useMemo(() => {
326
+ if (!scopeId || !chatSessionId)
327
+ return false;
328
+ const key = outboundPendingKey(scopeId, chatSessionId);
329
+ return (outboundPendingByKey[key] ?? 0) > 0;
330
+ }, [scopeId, chatSessionId, outboundPendingByKey]);
331
+ }
275
332
  function useChatsForScopeId(scopeId) {
276
333
  const { getChatsForScopeId, getCurrentChatId, setCurrentChatId, newChat, addMessage, removeMessageById, sendMessage, deleteChat, } = useChats();
277
334
  const chats = getChatsForScopeId(scopeId);
278
335
  const currentChatId = getCurrentChatId(scopeId);
279
336
  const currentChat = useChat(scopeId, currentChatId ?? undefined);
337
+ const isOutboundPending = useChatOutboundPending(scopeId, currentChatId);
280
338
  return {
281
339
  chats,
282
340
  currentChat,
283
341
  currentChatId,
342
+ isOutboundPending,
284
343
  setCurrentChatId: (targetId) => setCurrentChatId(scopeId, targetId),
285
344
  newChat: () => newChat(scopeId),
286
- addMessage: (chatId, role, text) => addMessage(scopeId, chatId, role, text),
345
+ addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
287
346
  removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
288
347
  sendMessage: (message, chatId) => sendMessage(scopeId, message, chatId),
289
348
  deleteChat: (sessionId) => deleteChat(scopeId, sessionId),
@@ -299,4 +358,4 @@ function useCurrentChat(scopeId) {
299
358
  return useChat(scopeId, chatId ?? undefined);
300
359
  }
301
360
 
302
- export { ChatContext, ChatProvider, isChatEmpty, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat };
361
+ export { ChatContext, ChatProvider, isChatEmpty, outboundPendingKey, useChat, useChatOutboundPending, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat };
package/dist/esm/index.js CHANGED
@@ -4,7 +4,7 @@ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme.js';
4
4
  export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
5
5
  export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl } from './sybilion-auth/authPaths.js';
6
6
  export { exchangeAuth0AccessTokenForSybilionJwt } from './sybilion-auth/exchangeSybilionToken.js';
7
- export { ChatContext, ChatProvider, isChatEmpty, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat } from './contexts/chat-context.js';
7
+ export { ChatContext, ChatProvider, isChatEmpty, outboundPendingKey, useChat, useChatOutboundPending, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat } from './contexts/chat-context.js';
8
8
  export { AnalysesSelector } from './components/ui/AnalysesSelector/AnalysesSelector.js';
9
9
  export { AnalysisLineIcon } from './components/ui/AnalysisLineIcon/AnalysisLineIcon.js';
10
10
  export { AppHeaderHost, AppHeaderPortal } from './components/ui/AppHeader/AppHeader.js';
@@ -7,11 +7,24 @@ export declare enum MessageRole {
7
7
  }
8
8
  /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
9
9
  export declare const GENERATING_DASHBOARD_SYSTEM_TEXT = "Generating dashboard\u2026";
10
+ /** USER-only: sample CSV attached to a preset message; shown as a file row + client-side download. */
11
+ export type UserCsvAttachment = {
12
+ displayName: string;
13
+ filename: string;
14
+ content: string;
15
+ };
16
+ /** Send full text to the chat API while showing `displayText` + optional CSV attachment in the UI. */
17
+ export type ChatSendMessagePayload = {
18
+ apiMessage: string;
19
+ displayText: string;
20
+ userCsvAttachment?: UserCsvAttachment;
21
+ };
10
22
  export interface Message {
11
23
  id: string;
12
24
  role: MessageRole;
13
25
  text: string;
14
26
  timestamp: number;
27
+ userCsvAttachment?: UserCsvAttachment;
15
28
  }
16
29
  export interface Chat {
17
30
  session_id: string;
@@ -57,6 +70,7 @@ export interface ChatPromptProps {
57
70
  export interface ChatMessageProps {
58
71
  role: MessageRole;
59
72
  text: string;
73
+ userCsvAttachment?: UserCsvAttachment;
60
74
  onQuickReply?: (branchKey: string, displayLabel: string) => void;
61
75
  /** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
62
76
  suppressedQuickReplyKeys?: ReadonlySet<string>;
@@ -1,2 +1,2 @@
1
1
  import { type ChatMessageProps } from '../Chat.types';
2
- export declare function ChatMessage({ role, text, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatMessage({ role, text, userCsvAttachment, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,4 @@
1
+ import type { UserCsvAttachment } from '../Chat.types';
2
+ export declare function UserCsvAttachmentBubble({ attachment, }: {
3
+ attachment: UserCsvAttachment;
4
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,3 @@
1
+ export declare function CsvIcon({ size }: {
2
+ size?: number;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -9,5 +9,5 @@ export type { UseChatPanelChromeModelInput, UseChatPanelChromeModelResult, } fro
9
9
  export { ChatMessage } from './ChatMessage';
10
10
  export { ChatPrompt } from './ChatPrompt';
11
11
  export { ChatPresets } from './ChatPresets';
12
- export type { Chat as ChatType, Message, ChatProps, ChatPreset as ChatPresetType, } from './Chat.types';
12
+ export type { Chat as ChatType, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserCsvAttachment, } from './Chat.types';
13
13
  export { MessageRole } from './Chat.types';
@@ -1,19 +1,30 @@
1
1
  import { ReactNode } from 'react';
2
- import { type Chat, MessageRole } from '#uilib/components/ui/Chat/Chat.types';
2
+ import { type Chat, type ChatSendMessagePayload, MessageRole, type UserCsvAttachment } from '#uilib/components/ui/Chat/Chat.types';
3
3
  import type { ChatResponse } from '#uilib/types/chat-api.types';
4
4
  export type SendChatMessageFn = (message: string, targetChatId: string) => Promise<ChatResponse>;
5
+ export type { ChatSendMessagePayload, UserCsvAttachment, } from '#uilib/components/ui/Chat/Chat.types';
6
+ export type AddChatMessageOptions = {
7
+ userCsvAttachment?: UserCsvAttachment;
8
+ };
5
9
  export interface ChatContextType {
6
10
  /** Returns the new session id, or undefined if no user / not created. */
7
11
  newChat: (scopeId: string) => string | undefined;
8
12
  setCurrentChatId: (currScopeId: string, sessionId: string) => void;
9
- addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string) => string | undefined;
13
+ addMessage: (scopeId: string, chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string | undefined;
10
14
  removeMessageById: (scopeId: string, chatId: string, messageId: string) => void;
11
- sendMessage: (scopeId: string, message: string, chatId?: string) => Promise<string>;
15
+ sendMessage: (scopeId: string, message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
12
16
  getChatsForScopeId: (scopeId: string) => Chat[];
13
17
  getCurrentChatId: (scopeId: string) => string | null;
14
18
  deleteChat: (scopeId: string, sessionId: string) => void;
19
+ /**
20
+ * Ref-count of in-flight `sendChatMessage` requests keyed by
21
+ * `outboundPendingKey(scopeId, chatSessionId)` (session id at send start).
22
+ */
23
+ outboundPendingByKey: Readonly<Record<string, number>>;
15
24
  }
16
25
  declare const ChatContext: import("react").Context<ChatContextType>;
26
+ /** Stable composite key; avoids collisions if `scopeId` contains `:`. */
27
+ export declare function outboundPendingKey(scopeId: string, chatSessionId: string): string;
17
28
  export interface ChatProviderProps {
18
29
  children: ReactNode;
19
30
  /** When null, chat state is cleared (logged out). When set, only LS rows for scopes starting with `${userId}-` are loaded. */
@@ -24,15 +35,17 @@ export declare function ChatProvider({ children, userSwitchKey, sendChatMessage:
24
35
  export declare const isChatEmpty: (chat: Chat | null) => boolean;
25
36
  export declare function useChats(): ChatContextType;
26
37
  export declare function useChat(scopeId: string | undefined, chatId: string | undefined): Chat | null;
38
+ export declare function useChatOutboundPending(scopeId: string | undefined | null, chatSessionId: string | null | undefined): boolean;
27
39
  export declare function useChatsForScopeId(scopeId: string): {
28
40
  chats: Chat[];
29
41
  currentChat: Chat;
30
42
  currentChatId: string;
43
+ isOutboundPending: boolean;
31
44
  setCurrentChatId: (targetId: string) => void;
32
45
  newChat: () => string;
33
- addMessage: (chatId: string, role: MessageRole, text: string) => string;
46
+ addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
34
47
  removeMessageById: (chatId: string, messageId: string) => void;
35
- sendMessage: (message: string, chatId?: string) => Promise<string>;
48
+ sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
36
49
  deleteChat: (sessionId: string) => void;
37
50
  };
38
51
  /** @deprecated Use useChatsForScopeId */
@@ -40,11 +53,12 @@ export declare function useChatsForDataset(scopeId: string): {
40
53
  chats: Chat[];
41
54
  currentChat: Chat;
42
55
  currentChatId: string;
56
+ isOutboundPending: boolean;
43
57
  setCurrentChatId: (targetId: string) => void;
44
58
  newChat: () => string;
45
- addMessage: (chatId: string, role: MessageRole, text: string) => string;
59
+ addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
46
60
  removeMessageById: (chatId: string, messageId: string) => void;
47
- sendMessage: (message: string, chatId?: string) => Promise<string>;
61
+ sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
48
62
  deleteChat: (sessionId: string) => void;
49
63
  };
50
64
  export declare function useCurrentChat(scopeId: string): Chat;
@@ -0,0 +1 @@
1
+ export default function ChatUserCsvAttachmentPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ /** Trigger a browser download for text/binary content (no backend). */
2
+ export declare function downloadTextFile(content: string, filename: string, mimeType: string): void;
@@ -0,0 +1,14 @@
1
+ /** Trigger a browser download for text/binary content (no backend). */
2
+ function downloadTextFile(content, filename, mimeType) {
3
+ const blob = new Blob([content], { type: mimeType });
4
+ const url = URL.createObjectURL(blob);
5
+ const link = document.createElement('a');
6
+ link.href = url;
7
+ link.download = filename;
8
+ document.body.appendChild(link);
9
+ link.click();
10
+ document.body.removeChild(link);
11
+ URL.revokeObjectURL(url);
12
+ }
13
+
14
+ export { downloadTextFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.19",
3
+ "version": "1.2.21",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -11,11 +11,26 @@ export enum MessageRole {
11
11
  /** System placeholder while dashboard generation runs (must match ChatSheet `addMessage` text). */
12
12
  export const GENERATING_DASHBOARD_SYSTEM_TEXT = 'Generating dashboard…';
13
13
 
14
+ /** USER-only: sample CSV attached to a preset message; shown as a file row + client-side download. */
15
+ export type UserCsvAttachment = {
16
+ displayName: string;
17
+ filename: string;
18
+ content: string;
19
+ };
20
+
21
+ /** Send full text to the chat API while showing `displayText` + optional CSV attachment in the UI. */
22
+ export type ChatSendMessagePayload = {
23
+ apiMessage: string;
24
+ displayText: string;
25
+ userCsvAttachment?: UserCsvAttachment;
26
+ };
27
+
14
28
  export interface Message {
15
29
  id: string;
16
30
  role: MessageRole;
17
31
  text: string;
18
32
  timestamp: number;
33
+ userCsvAttachment?: UserCsvAttachment;
19
34
  }
20
35
 
21
36
  export interface Chat {
@@ -66,6 +81,7 @@ export interface ChatPromptProps {
66
81
  export interface ChatMessageProps {
67
82
  role: MessageRole;
68
83
  text: string;
84
+ userCsvAttachment?: UserCsvAttachment;
69
85
  onQuickReply?: (branchKey: string, displayLabel: string) => void;
70
86
  /** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
71
87
  suppressedQuickReplyKeys?: ReadonlySet<string>;
@@ -120,6 +120,7 @@ export function ChatChrome({
120
120
  key={msg.id}
121
121
  role={msg.role}
122
122
  text={msg.text}
123
+ userCsvAttachment={msg.userCsvAttachment}
123
124
  onQuickReply={onQuickReply}
124
125
  suppressedQuickReplyKeys={suppressedQuickReplyKeys}
125
126
  quickReplyDisabled={isLoading}