@sybilion/uilib 1.2.19 → 1.2.20
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/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +1 -1
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +4 -3
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +11 -0
- package/dist/esm/components/ui/Chat/ChatMessage/icons/CsvIcon.js +8 -0
- package/dist/esm/components/ui/Chat/chat-preset-utils.js +12 -3
- package/dist/esm/contexts/chat-context.js +14 -4
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +14 -0
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.d.ts +4 -0
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/icons/CsvIcon.d.ts +3 -0
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
- package/dist/esm/types/src/contexts/chat-context.d.ts +11 -7
- package/dist/esm/types/src/docs/pages/ChatUserCsvAttachmentPage.d.ts +1 -0
- package/dist/esm/types/src/utils/downloadTextFile.d.ts +2 -0
- package/dist/esm/utils/downloadTextFile.js +14 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/Chat.types.ts +16 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +1 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +67 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl.d.ts +6 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +8 -1
- package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +36 -0
- package/src/components/ui/Chat/ChatMessage/icons/CsvIcon.tsx +7 -0
- package/src/components/ui/Chat/chat-preset-utils.ts +12 -6
- package/src/components/ui/Chat/index.ts +3 -1
- package/src/contexts/chat-context.tsx +39 -8
- package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +171 -0
- package/src/docs/registry.ts +6 -0
- 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 };
|
|
@@ -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 =
|
|
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.
|
|
27
|
+
.filter(p => userTexts.some(ut => presetMatchesUserText(p.text, ut)))
|
|
19
28
|
.map(p => p.id);
|
|
20
29
|
}
|
|
21
30
|
|
|
@@ -152,16 +152,18 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
152
152
|
return { ...prev, [scopeId]: updatedChats };
|
|
153
153
|
});
|
|
154
154
|
}, []);
|
|
155
|
-
const addMessage = useCallback((scopeId, chatId, role, text) => {
|
|
155
|
+
const addMessage = useCallback((scopeId, chatId, role, text, options) => {
|
|
156
156
|
if (userSwitchKey === null)
|
|
157
157
|
return undefined;
|
|
158
158
|
addScopeIdToRegistry(scopeId);
|
|
159
159
|
const storedText = stripJsonDashboardFences(text);
|
|
160
|
+
const attachment = role === MessageRole.USER ? options?.userCsvAttachment : undefined;
|
|
160
161
|
const newMessage = {
|
|
161
162
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
162
163
|
role,
|
|
163
164
|
text: storedText,
|
|
164
165
|
timestamp: Date.now(),
|
|
166
|
+
...(attachment ? { userCsvAttachment: attachment } : {}),
|
|
165
167
|
};
|
|
166
168
|
setChats(prev => {
|
|
167
169
|
const scopeChats = prev[scopeId] ?? [];
|
|
@@ -200,9 +202,17 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
200
202
|
if (targetChatId === null || targetChatId === '') {
|
|
201
203
|
throw new Error('No chat selected');
|
|
202
204
|
}
|
|
203
|
-
|
|
205
|
+
const apiPayload = typeof message === 'string' ? message : message.apiMessage;
|
|
206
|
+
if (typeof message === 'string') {
|
|
207
|
+
addMessage(scopeId, targetChatId, MessageRole.USER, message);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
addMessage(scopeId, targetChatId, MessageRole.USER, message.displayText, {
|
|
211
|
+
userCsvAttachment: message.userCsvAttachment,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
204
214
|
try {
|
|
205
|
-
const data = await sendChatMessageFn(
|
|
215
|
+
const data = await sendChatMessageFn(apiPayload, targetChatId);
|
|
206
216
|
if (data.session_id && data.session_id !== targetChatId) {
|
|
207
217
|
setChats(prev => {
|
|
208
218
|
const scopeChats = prev[scopeId] ?? [];
|
|
@@ -283,7 +293,7 @@ function useChatsForScopeId(scopeId) {
|
|
|
283
293
|
currentChatId,
|
|
284
294
|
setCurrentChatId: (targetId) => setCurrentChatId(scopeId, targetId),
|
|
285
295
|
newChat: () => newChat(scopeId),
|
|
286
|
-
addMessage: (chatId, role, text) => addMessage(scopeId, chatId, role, text),
|
|
296
|
+
addMessage: (chatId, role, text, options) => addMessage(scopeId, chatId, role, text, options),
|
|
287
297
|
removeMessageById: (chatId, messageId) => removeMessageById(scopeId, chatId, messageId),
|
|
288
298
|
sendMessage: (message, chatId) => sendMessage(scopeId, message, chatId),
|
|
289
299
|
deleteChat: (sessionId) => deleteChat(scopeId, sessionId),
|
|
@@ -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;
|
|
@@ -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,
|
|
12
|
+
export type { Chat as ChatType, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserCsvAttachment, } from './Chat.types';
|
|
13
13
|
export { MessageRole } from './Chat.types';
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { type Chat, MessageRole } from '#uilib/components/ui/Chat/Chat.types';
|
|
2
|
+
import { type Chat, type ChatSendMessagePayload, type UserCsvAttachment, MessageRole } 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;
|
|
@@ -30,9 +34,9 @@ export declare function useChatsForScopeId(scopeId: string): {
|
|
|
30
34
|
currentChatId: string;
|
|
31
35
|
setCurrentChatId: (targetId: string) => void;
|
|
32
36
|
newChat: () => string;
|
|
33
|
-
addMessage: (chatId: string, role: MessageRole, text: string) => string;
|
|
37
|
+
addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
|
|
34
38
|
removeMessageById: (chatId: string, messageId: string) => void;
|
|
35
|
-
sendMessage: (message: string, chatId?: string) => Promise<string>;
|
|
39
|
+
sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
|
|
36
40
|
deleteChat: (sessionId: string) => void;
|
|
37
41
|
};
|
|
38
42
|
/** @deprecated Use useChatsForScopeId */
|
|
@@ -42,9 +46,9 @@ export declare function useChatsForDataset(scopeId: string): {
|
|
|
42
46
|
currentChatId: string;
|
|
43
47
|
setCurrentChatId: (targetId: string) => void;
|
|
44
48
|
newChat: () => string;
|
|
45
|
-
addMessage: (chatId: string, role: MessageRole, text: string) => string;
|
|
49
|
+
addMessage: (chatId: string, role: MessageRole, text: string, options?: AddChatMessageOptions) => string;
|
|
46
50
|
removeMessageById: (chatId: string, messageId: string) => void;
|
|
47
|
-
sendMessage: (message: string, chatId?: string) => Promise<string>;
|
|
51
|
+
sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) => Promise<string>;
|
|
48
52
|
deleteChat: (sessionId: string) => void;
|
|
49
53
|
};
|
|
50
54
|
export declare function useCurrentChat(scopeId: string): Chat;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function ChatUserCsvAttachmentPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -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
|
@@ -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}
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
.role-user
|
|
18
18
|
align-items flex-end
|
|
19
19
|
|
|
20
|
+
.userColumn
|
|
21
|
+
display flex
|
|
22
|
+
flex-direction column
|
|
23
|
+
align-items flex-end
|
|
24
|
+
gap var(--p-2)
|
|
25
|
+
max-width 100%
|
|
26
|
+
|
|
20
27
|
.text
|
|
21
28
|
padding var(--p-3) var(--p-4)
|
|
22
29
|
|
|
@@ -29,6 +36,66 @@
|
|
|
29
36
|
:global(.dark) &
|
|
30
37
|
background-color var(--sb-gray-800)
|
|
31
38
|
|
|
39
|
+
.userCsvCard
|
|
40
|
+
appearance none
|
|
41
|
+
border 0
|
|
42
|
+
margin 0
|
|
43
|
+
font inherit
|
|
44
|
+
display flex
|
|
45
|
+
align-items center
|
|
46
|
+
gap var(--p-4)
|
|
47
|
+
padding var(--p-3)
|
|
48
|
+
padding-right var(--p-4)
|
|
49
|
+
background-color var(--sb-slate-100)
|
|
50
|
+
box-shadow 0 0 0 1px var(--border)
|
|
51
|
+
border-radius var(--p-4)
|
|
52
|
+
border-bottom-right-radius 0
|
|
53
|
+
width fit-content
|
|
54
|
+
max-width 100%
|
|
55
|
+
text-align left
|
|
56
|
+
cursor pointer
|
|
57
|
+
transition background-color 150ms
|
|
58
|
+
color var(--sb-green-600)
|
|
59
|
+
|
|
60
|
+
&:hover
|
|
61
|
+
background-color var(--sb-gray-50)
|
|
62
|
+
|
|
63
|
+
&:focus-visible
|
|
64
|
+
outline 2px solid var(--ring)
|
|
65
|
+
outline-offset 2px
|
|
66
|
+
|
|
67
|
+
:global(.dark) &
|
|
68
|
+
background-color var(--sb-gray-800)
|
|
69
|
+
color var(--sb-green-400)
|
|
70
|
+
|
|
71
|
+
&:hover
|
|
72
|
+
background-color var(--sb-gray-900)
|
|
73
|
+
|
|
74
|
+
.userCsvCardIcon
|
|
75
|
+
display flex
|
|
76
|
+
align-items center
|
|
77
|
+
justify-content center
|
|
78
|
+
width 32px
|
|
79
|
+
height 32px
|
|
80
|
+
flex-shrink 0
|
|
81
|
+
|
|
82
|
+
.userCsvCardContent
|
|
83
|
+
display flex
|
|
84
|
+
flex-direction column
|
|
85
|
+
flex 1
|
|
86
|
+
min-width 0
|
|
87
|
+
|
|
88
|
+
.userCsvCardTitle
|
|
89
|
+
font-size var(--text-base)
|
|
90
|
+
font-weight 600
|
|
91
|
+
line-height 1.4
|
|
92
|
+
color var(--text-secondary)
|
|
93
|
+
|
|
94
|
+
.userCsvCardSubtitle
|
|
95
|
+
font-size var(--text-sm)
|
|
96
|
+
color var(--muted-foreground)
|
|
97
|
+
line-height 1.4
|
|
98
|
+
|
|
32
99
|
.role-system
|
|
33
100
|
align-items center
|
|
34
101
|
|
|
@@ -17,6 +17,12 @@ interface CssExports {
|
|
|
17
17
|
'root': string;
|
|
18
18
|
'scrollHorizontal': string;
|
|
19
19
|
'text': string;
|
|
20
|
+
'userColumn': string;
|
|
21
|
+
'userCsvCard': string;
|
|
22
|
+
'userCsvCardContent': string;
|
|
23
|
+
'userCsvCardIcon': string;
|
|
24
|
+
'userCsvCardSubtitle': string;
|
|
25
|
+
'userCsvCardTitle': string;
|
|
20
26
|
}
|
|
21
27
|
export const cssExports: CssExports;
|
|
22
28
|
export default cssExports;
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
} from '../Chat.types';
|
|
9
9
|
import { AgentMessageContent } from './AgentMessageContent';
|
|
10
10
|
import S from './ChatMessage.styl';
|
|
11
|
+
import { UserCsvAttachmentBubble } from './UserCsvAttachmentBubble';
|
|
11
12
|
|
|
12
13
|
export function ChatMessage({
|
|
13
14
|
role,
|
|
14
15
|
text,
|
|
16
|
+
userCsvAttachment,
|
|
15
17
|
onQuickReply,
|
|
16
18
|
suppressedQuickReplyKeys,
|
|
17
19
|
quickReplyDisabled,
|
|
@@ -45,7 +47,12 @@ export function ChatMessage({
|
|
|
45
47
|
renderMessageChart={renderMessageChart}
|
|
46
48
|
/>
|
|
47
49
|
) : (
|
|
48
|
-
<div className={S.
|
|
50
|
+
<div className={S.userColumn}>
|
|
51
|
+
<div className={S.text}>{text}</div>
|
|
52
|
+
{userCsvAttachment ? (
|
|
53
|
+
<UserCsvAttachmentBubble attachment={userCsvAttachment} />
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
49
56
|
)}
|
|
50
57
|
</div>
|
|
51
58
|
);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { UserCsvAttachment } from '../Chat.types';
|
|
2
|
+
import { downloadTextFile } from '#uilib/utils/downloadTextFile';
|
|
3
|
+
|
|
4
|
+
import { CsvIcon } from './icons/CsvIcon';
|
|
5
|
+
import S from './ChatMessage.styl';
|
|
6
|
+
|
|
7
|
+
const CSV_DOWNLOAD_HINT = 'Download .CSV file';
|
|
8
|
+
|
|
9
|
+
export function UserCsvAttachmentBubble({
|
|
10
|
+
attachment,
|
|
11
|
+
}: {
|
|
12
|
+
attachment: UserCsvAttachment;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
className={S.userCsvCard}
|
|
18
|
+
aria-label={`${CSV_DOWNLOAD_HINT}: ${attachment.displayName}`}
|
|
19
|
+
onClick={() =>
|
|
20
|
+
downloadTextFile(
|
|
21
|
+
attachment.content,
|
|
22
|
+
attachment.filename,
|
|
23
|
+
'text/csv;charset=utf-8',
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
<div className={S.userCsvCardIcon}>
|
|
28
|
+
<CsvIcon size={32} />
|
|
29
|
+
</div>
|
|
30
|
+
<div className={S.userCsvCardContent}>
|
|
31
|
+
<div className={S.userCsvCardTitle}>{attachment.displayName}</div>
|
|
32
|
+
<div className={S.userCsvCardSubtitle}>{CSV_DOWNLOAD_HINT}</div>
|
|
33
|
+
</div>
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -11,17 +11,23 @@ export function normalizePresetMatchText(s: string): string {
|
|
|
11
11
|
return s.trim().normalize('NFC');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function presetMatchesUserText(presetText: string, userTextNorm: string): boolean {
|
|
15
|
+
const presetNorm = normalizePresetMatchText(presetText);
|
|
16
|
+
if (!presetNorm) return false;
|
|
17
|
+
if (userTextNorm === presetNorm) return true;
|
|
18
|
+
const prefix = `${presetNorm} `;
|
|
19
|
+
return userTextNorm.startsWith(prefix);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export function usedPresetIdsFromMessages(
|
|
15
23
|
messages: Message[] | undefined,
|
|
16
24
|
presets: ChatPreset[] | undefined,
|
|
17
25
|
): string[] {
|
|
18
26
|
if (!messages?.length || !presets?.length) return [];
|
|
19
|
-
const userTexts =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.map(m => normalizePresetMatchText(m.text)),
|
|
23
|
-
);
|
|
27
|
+
const userTexts = messages
|
|
28
|
+
.filter(m => m.role === MessageRole.USER)
|
|
29
|
+
.map(m => normalizePresetMatchText(m.text));
|
|
24
30
|
return presets
|
|
25
|
-
.filter(p => userTexts.
|
|
31
|
+
.filter(p => userTexts.some(ut => presetMatchesUserText(p.text, ut)))
|
|
26
32
|
.map(p => p.id);
|
|
27
33
|
}
|
|
@@ -17,8 +17,10 @@ export { ChatPrompt } from './ChatPrompt';
|
|
|
17
17
|
export { ChatPresets } from './ChatPresets';
|
|
18
18
|
export type {
|
|
19
19
|
Chat as ChatType,
|
|
20
|
-
|
|
20
|
+
ChatSendMessagePayload,
|
|
21
21
|
ChatProps,
|
|
22
22
|
ChatPreset as ChatPresetType,
|
|
23
|
+
Message,
|
|
24
|
+
UserCsvAttachment,
|
|
23
25
|
} from './Chat.types';
|
|
24
26
|
export { MessageRole } from './Chat.types';
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type Chat,
|
|
13
|
+
type ChatSendMessagePayload,
|
|
13
14
|
type Message,
|
|
15
|
+
type UserCsvAttachment,
|
|
14
16
|
MessageRole,
|
|
15
17
|
} from '#uilib/components/ui/Chat/Chat.types';
|
|
16
18
|
import { stripJsonDashboardFences } from '#uilib/lib/dashboard-spec/stripJsonDashboardFences';
|
|
@@ -22,9 +24,15 @@ export type SendChatMessageFn = (
|
|
|
22
24
|
targetChatId: string,
|
|
23
25
|
) => Promise<ChatResponse>;
|
|
24
26
|
|
|
27
|
+
export type { ChatSendMessagePayload, UserCsvAttachment } from '#uilib/components/ui/Chat/Chat.types';
|
|
28
|
+
|
|
25
29
|
const CHATS_PREFIX = 'chats-';
|
|
26
30
|
const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
|
|
27
31
|
|
|
32
|
+
export type AddChatMessageOptions = {
|
|
33
|
+
userCsvAttachment?: UserCsvAttachment;
|
|
34
|
+
};
|
|
35
|
+
|
|
28
36
|
export interface ChatContextType {
|
|
29
37
|
/** Returns the new session id, or undefined if no user / not created. */
|
|
30
38
|
newChat: (scopeId: string) => string | undefined;
|
|
@@ -34,6 +42,7 @@ export interface ChatContextType {
|
|
|
34
42
|
chatId: string,
|
|
35
43
|
role: MessageRole,
|
|
36
44
|
text: string,
|
|
45
|
+
options?: AddChatMessageOptions,
|
|
37
46
|
) => string | undefined;
|
|
38
47
|
removeMessageById: (
|
|
39
48
|
scopeId: string,
|
|
@@ -42,7 +51,7 @@ export interface ChatContextType {
|
|
|
42
51
|
) => void;
|
|
43
52
|
sendMessage: (
|
|
44
53
|
scopeId: string,
|
|
45
|
-
message: string,
|
|
54
|
+
message: string | ChatSendMessagePayload,
|
|
46
55
|
chatId?: string,
|
|
47
56
|
) => Promise<string>;
|
|
48
57
|
getChatsForScopeId: (scopeId: string) => Chat[];
|
|
@@ -239,15 +248,24 @@ export function ChatProvider({
|
|
|
239
248
|
}, []);
|
|
240
249
|
|
|
241
250
|
const addMessage = useCallback(
|
|
242
|
-
(
|
|
251
|
+
(
|
|
252
|
+
scopeId: string,
|
|
253
|
+
chatId: string,
|
|
254
|
+
role: MessageRole,
|
|
255
|
+
text: string,
|
|
256
|
+
options?: AddChatMessageOptions,
|
|
257
|
+
) => {
|
|
243
258
|
if (userSwitchKey === null) return undefined;
|
|
244
259
|
addScopeIdToRegistry(scopeId);
|
|
245
260
|
const storedText = stripJsonDashboardFences(text);
|
|
261
|
+
const attachment =
|
|
262
|
+
role === MessageRole.USER ? options?.userCsvAttachment : undefined;
|
|
246
263
|
const newMessage: Message = {
|
|
247
264
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
248
265
|
role,
|
|
249
266
|
text: storedText,
|
|
250
267
|
timestamp: Date.now(),
|
|
268
|
+
...(attachment ? { userCsvAttachment: attachment } : {}),
|
|
251
269
|
};
|
|
252
270
|
|
|
253
271
|
setChats(prev => {
|
|
@@ -292,7 +310,7 @@ export function ChatProvider({
|
|
|
292
310
|
const sendMessage = useCallback(
|
|
293
311
|
async (
|
|
294
312
|
scopeId: string,
|
|
295
|
-
message: string,
|
|
313
|
+
message: string | ChatSendMessagePayload,
|
|
296
314
|
chatId?: string,
|
|
297
315
|
): Promise<string> => {
|
|
298
316
|
const targetChatId = chatId ?? getCurrentChatId(scopeId);
|
|
@@ -300,10 +318,19 @@ export function ChatProvider({
|
|
|
300
318
|
throw new Error('No chat selected');
|
|
301
319
|
}
|
|
302
320
|
|
|
303
|
-
|
|
321
|
+
const apiPayload =
|
|
322
|
+
typeof message === 'string' ? message : message.apiMessage;
|
|
323
|
+
|
|
324
|
+
if (typeof message === 'string') {
|
|
325
|
+
addMessage(scopeId, targetChatId, MessageRole.USER, message);
|
|
326
|
+
} else {
|
|
327
|
+
addMessage(scopeId, targetChatId, MessageRole.USER, message.displayText, {
|
|
328
|
+
userCsvAttachment: message.userCsvAttachment,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
304
331
|
|
|
305
332
|
try {
|
|
306
|
-
const data = await sendChatMessageFn(
|
|
333
|
+
const data = await sendChatMessageFn(apiPayload, targetChatId);
|
|
307
334
|
|
|
308
335
|
if (data.session_id && data.session_id !== targetChatId) {
|
|
309
336
|
setChats(prev => {
|
|
@@ -430,11 +457,15 @@ export function useChatsForScopeId(scopeId: string) {
|
|
|
430
457
|
currentChatId,
|
|
431
458
|
setCurrentChatId: (targetId: string) => setCurrentChatId(scopeId, targetId),
|
|
432
459
|
newChat: () => newChat(scopeId),
|
|
433
|
-
addMessage: (
|
|
434
|
-
|
|
460
|
+
addMessage: (
|
|
461
|
+
chatId: string,
|
|
462
|
+
role: MessageRole,
|
|
463
|
+
text: string,
|
|
464
|
+
options?: AddChatMessageOptions,
|
|
465
|
+
) => addMessage(scopeId, chatId, role, text, options),
|
|
435
466
|
removeMessageById: (chatId: string, messageId: string) =>
|
|
436
467
|
removeMessageById(scopeId, chatId, messageId),
|
|
437
|
-
sendMessage: (message: string, chatId?: string) =>
|
|
468
|
+
sendMessage: (message: string | ChatSendMessagePayload, chatId?: string) =>
|
|
438
469
|
sendMessage(scopeId, message, chatId),
|
|
439
470
|
deleteChat: (sessionId: string) => deleteChat(scopeId, sessionId),
|
|
440
471
|
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Button } from '#uilib/components/ui/Button';
|
|
4
|
+
import {
|
|
5
|
+
ChatChrome,
|
|
6
|
+
type Message,
|
|
7
|
+
MessageRole,
|
|
8
|
+
} from '#uilib/components/ui/Chat';
|
|
9
|
+
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
10
|
+
import { ScrollRef } from '@homecode/ui';
|
|
11
|
+
|
|
12
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
13
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
14
|
+
|
|
15
|
+
const NO_QUICK_REPLY_KEYS: ReadonlySet<string> = new Set();
|
|
16
|
+
|
|
17
|
+
const SAMPLE_CSV = `Name,Score
|
|
18
|
+
Alice,10
|
|
19
|
+
Bob,20`;
|
|
20
|
+
|
|
21
|
+
const ASSISTANT_ACK =
|
|
22
|
+
'Received your message. Use the file row below the text bubble to download the CSV sample.';
|
|
23
|
+
|
|
24
|
+
const ASSISTANT_REPLY_TYPED =
|
|
25
|
+
'Plain reply for typed messages. Use “Load preset-style…” for the CSV attachment demo.';
|
|
26
|
+
|
|
27
|
+
function makeMessage(role: MessageRole, text: string): Message {
|
|
28
|
+
return {
|
|
29
|
+
id: crypto.randomUUID(),
|
|
30
|
+
role,
|
|
31
|
+
text,
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeUserMessageWithCsv(
|
|
37
|
+
displayText: string,
|
|
38
|
+
displayName: string,
|
|
39
|
+
): Message {
|
|
40
|
+
return {
|
|
41
|
+
id: crypto.randomUUID(),
|
|
42
|
+
role: MessageRole.USER,
|
|
43
|
+
text: displayText,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
userCsvAttachment: {
|
|
46
|
+
displayName,
|
|
47
|
+
filename: 'docs-sample.csv',
|
|
48
|
+
content: SAMPLE_CSV,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default function ChatUserCsvAttachmentPage() {
|
|
54
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
55
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
56
|
+
const replyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
57
|
+
const scrollRef = useRef<ScrollRef>(null);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
return () => {
|
|
61
|
+
if (replyTimeoutRef.current != null) {
|
|
62
|
+
clearTimeout(replyTimeoutRef.current);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const isEmpty = messages.length === 0 && !isLoading;
|
|
68
|
+
const isLastMessageFromUser =
|
|
69
|
+
messages.length > 0 &&
|
|
70
|
+
messages[messages.length - 1]?.role === MessageRole.USER;
|
|
71
|
+
|
|
72
|
+
const addCsvDemo = useCallback(() => {
|
|
73
|
+
if (isLoading) return;
|
|
74
|
+
const displayText =
|
|
75
|
+
'Open procurement buys analysis "Am I overpaying on my open orders?" Open buys (sample)';
|
|
76
|
+
|
|
77
|
+
setMessages(prev => [
|
|
78
|
+
...prev,
|
|
79
|
+
makeUserMessageWithCsv(displayText, 'Open buys (sample)'),
|
|
80
|
+
]);
|
|
81
|
+
setIsLoading(true);
|
|
82
|
+
|
|
83
|
+
if (replyTimeoutRef.current != null) clearTimeout(replyTimeoutRef.current);
|
|
84
|
+
|
|
85
|
+
replyTimeoutRef.current = setTimeout(() => {
|
|
86
|
+
replyTimeoutRef.current = null;
|
|
87
|
+
setMessages(prev => [
|
|
88
|
+
...prev,
|
|
89
|
+
makeMessage(MessageRole.ASSISTANT, ASSISTANT_ACK),
|
|
90
|
+
]);
|
|
91
|
+
setIsLoading(false);
|
|
92
|
+
}, 500);
|
|
93
|
+
}, [isLoading]);
|
|
94
|
+
|
|
95
|
+
const onSubmit = useCallback(
|
|
96
|
+
(raw: string) => {
|
|
97
|
+
const text = raw.trim();
|
|
98
|
+
if (!text || isLoading) return;
|
|
99
|
+
|
|
100
|
+
setMessages(prev => [...prev, makeMessage(MessageRole.USER, text)]);
|
|
101
|
+
setIsLoading(true);
|
|
102
|
+
|
|
103
|
+
if (replyTimeoutRef.current != null) {
|
|
104
|
+
clearTimeout(replyTimeoutRef.current);
|
|
105
|
+
}
|
|
106
|
+
replyTimeoutRef.current = setTimeout(() => {
|
|
107
|
+
replyTimeoutRef.current = null;
|
|
108
|
+
setMessages(prev => [
|
|
109
|
+
...prev,
|
|
110
|
+
makeMessage(MessageRole.ASSISTANT, ASSISTANT_REPLY_TYPED),
|
|
111
|
+
]);
|
|
112
|
+
setIsLoading(false);
|
|
113
|
+
}, 900);
|
|
114
|
+
},
|
|
115
|
+
[isLoading],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<AppPageHeader
|
|
121
|
+
breadcrumbs={[{ label: 'Chat' }, { label: 'User CSV attachment' }]}
|
|
122
|
+
title="Chat — user CSV attachment"
|
|
123
|
+
subheader="USER bubble with separate downloadable CSV row (dashboard preset pattern)."
|
|
124
|
+
actions={<DocsHeaderActions />}
|
|
125
|
+
/>
|
|
126
|
+
<PageContentSection>
|
|
127
|
+
<p style={{ marginBottom: 16 }}>
|
|
128
|
+
<Button
|
|
129
|
+
type="button"
|
|
130
|
+
variant="outline"
|
|
131
|
+
size="sm"
|
|
132
|
+
onClick={addCsvDemo}
|
|
133
|
+
>
|
|
134
|
+
Load preset-style message + CSV attachment
|
|
135
|
+
</Button>
|
|
136
|
+
</p>
|
|
137
|
+
<ChatChrome
|
|
138
|
+
showResizeHandle={false}
|
|
139
|
+
resizeHandle={undefined}
|
|
140
|
+
onClose={undefined}
|
|
141
|
+
isEmpty={isEmpty}
|
|
142
|
+
renderPresets={() => null}
|
|
143
|
+
messages={messages}
|
|
144
|
+
onQuickReply={() => {}}
|
|
145
|
+
suppressedQuickReplyKeys={NO_QUICK_REPLY_KEYS}
|
|
146
|
+
isLoading={isLoading}
|
|
147
|
+
scriptContinueLabel={undefined}
|
|
148
|
+
onScriptContinue={undefined}
|
|
149
|
+
showBranchActionsRow={false}
|
|
150
|
+
showSyntheticBranchButtons={false}
|
|
151
|
+
unusedBranchKeys={[]}
|
|
152
|
+
isScriptComplete={false}
|
|
153
|
+
onGenerateDashboard={undefined}
|
|
154
|
+
generatingDashboard={false}
|
|
155
|
+
onGenerateDashboardClick={() => {}}
|
|
156
|
+
showInlinePresets={false}
|
|
157
|
+
isLastMessageFromUser={isLastMessageFromUser}
|
|
158
|
+
scrollRef={scrollRef}
|
|
159
|
+
effectiveScopeId="docs-chat-user-csv"
|
|
160
|
+
onPromptSubmit={onSubmit}
|
|
161
|
+
onChatDeleted={() => {}}
|
|
162
|
+
emptyState={{
|
|
163
|
+
title: 'CSV attachment demo',
|
|
164
|
+
description:
|
|
165
|
+
'Click the button above or send any message. The CSV row uses the same UI as dashboard presets.',
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</PageContentSection>
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}
|
package/src/docs/registry.ts
CHANGED
|
@@ -96,6 +96,12 @@ export const DOC_REGISTRY: DocEntry[] = [
|
|
|
96
96
|
section: 'Chat',
|
|
97
97
|
load: () => import('./pages/ChatPage'),
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
slug: 'chat-user-csv-attachment',
|
|
101
|
+
title: 'Chat user CSV attachment',
|
|
102
|
+
section: 'Chat',
|
|
103
|
+
load: () => import('./pages/ChatUserCsvAttachmentPage'),
|
|
104
|
+
},
|
|
99
105
|
{
|
|
100
106
|
slug: 'checkbox',
|
|
101
107
|
title: 'Checkbox',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Trigger a browser download for text/binary content (no backend). */
|
|
2
|
+
export function downloadTextFile(
|
|
3
|
+
content: string,
|
|
4
|
+
filename: string,
|
|
5
|
+
mimeType: string,
|
|
6
|
+
): void {
|
|
7
|
+
const blob = new Blob([content], { type: mimeType });
|
|
8
|
+
const url = URL.createObjectURL(blob);
|
|
9
|
+
const link = document.createElement('a');
|
|
10
|
+
link.href = url;
|
|
11
|
+
link.download = filename;
|
|
12
|
+
document.body.appendChild(link);
|
|
13
|
+
link.click();
|
|
14
|
+
document.body.removeChild(link);
|
|
15
|
+
URL.revokeObjectURL(url);
|
|
16
|
+
}
|