@sybilion/uilib 1.3.12 → 1.3.15
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 +4 -1
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +7 -3
- package/dist/esm/components/ui/Chat/ChatMessage/UserTextFileAttachmentBubble.js +39 -0
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptAttachments.js +7 -1
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSheet.js +4 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +6 -3
- package/dist/esm/components/ui/Chat/buildChatSendMessagePayload.js +70 -0
- package/dist/esm/components/ui/Chat/chatAttachmentAccept.js +20 -1
- package/dist/esm/components/ui/Chat/chatAttachmentExtract.js +11 -1
- package/dist/esm/components/ui/Chat/chatDocxExtract.js +17 -0
- package/dist/esm/components/ui/Chat/chatXlsxExtract.js +34 -0
- package/dist/esm/components/ui/Chat/sanitizeAttachmentFilename.js +14 -0
- package/dist/esm/components/ui/Chat/userTextFileAttachments.js +6 -0
- package/dist/esm/contexts/chat-context.js +8 -3
- package/dist/esm/index.js +2 -0
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +8 -8
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/ChatMessage.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/UserTextFileAttachmentBubble.d.ts +4 -0
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/ChatSheet.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/buildChatSendMessagePayload.d.ts +10 -0
- package/dist/esm/types/src/components/ui/Chat/buildChatSendMessagePayload.test.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chat/chatAttachmentAccept.d.ts +3 -1
- package/dist/esm/types/src/components/ui/Chat/chatAttachmentAccept.test.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chat/chatDocxExtract.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/chatDocxExtract.test.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chat/chatXlsxExtract.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/chatXlsxExtract.test.d.ts +1 -0
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +3 -1
- package/dist/esm/types/src/components/ui/Chat/sanitizeAttachmentFilename.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/userTextFileAttachments.d.ts +3 -0
- package/dist/esm/types/src/contexts/chat-context.d.ts +3 -3
- package/dist/esm/types/tests/setup.d.ts +1 -0
- package/package.json +4 -2
- package/src/components/ui/Chat/Chat.types.ts +8 -8
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +4 -1
- package/src/components/ui/Chat/ChatMessage/ChatMessage.tsx +12 -5
- package/src/components/ui/Chat/ChatMessage/UserTextFileAttachmentBubble.tsx +50 -0
- package/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.tsx +9 -1
- package/src/components/ui/Chat/ChatSheet/ChatSheet.tsx +6 -0
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +10 -3
- package/src/components/ui/Chat/buildChatSendMessagePayload.test.ts +116 -0
- package/src/components/ui/Chat/buildChatSendMessagePayload.ts +82 -0
- package/src/components/ui/Chat/chatAttachmentAccept.test.ts +78 -0
- package/src/components/ui/Chat/chatAttachmentAccept.ts +25 -0
- package/src/components/ui/Chat/chatAttachmentExtract.ts +13 -1
- package/src/components/ui/Chat/chatDocxExtract.test.ts +40 -0
- package/src/components/ui/Chat/chatDocxExtract.ts +19 -0
- package/src/components/ui/Chat/chatXlsxExtract.test.ts +72 -0
- package/src/components/ui/Chat/chatXlsxExtract.ts +43 -0
- package/src/components/ui/Chat/index.ts +7 -1
- package/src/components/ui/Chat/sanitizeAttachmentFilename.ts +15 -0
- package/src/components/ui/Chat/userTextFileAttachments.ts +8 -0
- package/src/contexts/chat-context.tsx +12 -7
- package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +14 -20
- package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +7 -5
- package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +0 -10
- package/dist/esm/types/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.d.ts +0 -4
- package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +0 -27
|
@@ -30,6 +30,9 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
|
|
|
30
30
|
if (items.length > 0) {
|
|
31
31
|
setPendingAttachments(prev => [...prev, ...items]);
|
|
32
32
|
}
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {
|
|
35
|
+
// Extraction failed (parse error, size limit, etc.); skip staging.
|
|
33
36
|
})
|
|
34
37
|
.finally(() => setIsExtractingAttachments(false));
|
|
35
38
|
}, [allowPdfAttachments, promptBusy]);
|
|
@@ -65,7 +68,7 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
|
|
|
65
68
|
}, [isEmpty, messages.length]);
|
|
66
69
|
return (jsxs("div", { className: S.root, children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptBusy, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, children: [isEmpty ? (jsxs(Fragment, { children: [jsx(Chat.EmptyState, { ...emptyState }), renderPresets('fixed')] })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 180 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
|
|
67
70
|
const isLast = index === arr.length - 1;
|
|
68
|
-
return (jsx(Chat.Message, { role: msg.role, text: msg.text,
|
|
71
|
+
return (jsx(Chat.Message, { role: msg.role, text: msg.text, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
|
|
69
72
|
? { label: scriptContinueLabel }
|
|
70
73
|
: undefined, onScriptContinue: isLast && scriptContinueLabel
|
|
71
74
|
? onScriptContinue
|
|
@@ -3,14 +3,18 @@ import cn from 'classnames';
|
|
|
3
3
|
import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.js';
|
|
4
4
|
import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
|
|
5
5
|
import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
|
|
6
|
+
import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments.js';
|
|
6
7
|
import { AgentMessageContent } from './AgentMessageContent.js';
|
|
7
8
|
import S from './ChatMessage.styl.js';
|
|
8
|
-
import {
|
|
9
|
+
import { UserTextFileAttachmentBubble } from './UserTextFileAttachmentBubble.js';
|
|
9
10
|
|
|
10
|
-
function ChatMessage({ role, text,
|
|
11
|
+
function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage = true, scriptContinue, onScriptContinue, renderMessageChart, }) {
|
|
12
|
+
const fileAttachments = userTextFileAttachmentsFromMessage({
|
|
13
|
+
userTextFileAttachments,
|
|
14
|
+
});
|
|
11
15
|
const isAssistant = role === MessageRole.ASSISTANT;
|
|
12
16
|
const isSystem = role === MessageRole.SYSTEM;
|
|
13
|
-
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: jsx(InteractiveContent, { text: text }) }),
|
|
17
|
+
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: jsx(InteractiveContent, { text: text }) }), fileAttachments.map(attachment => (jsx(UserTextFileAttachmentBubble, { attachment: attachment }, `${attachment.displayName}:${attachment.filename}`)))] })) }));
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export { ChatMessage };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { FileChip } from '../../FileChip/FileChip.js';
|
|
3
|
+
import { downloadTextFile } from '../../../../utils/downloadTextFile.js';
|
|
4
|
+
|
|
5
|
+
function formatFromFilename(filename) {
|
|
6
|
+
const lower = filename.toLowerCase();
|
|
7
|
+
if (lower.endsWith('.csv'))
|
|
8
|
+
return 'csv';
|
|
9
|
+
if (lower.endsWith('.pdf'))
|
|
10
|
+
return 'pdf';
|
|
11
|
+
if (lower.endsWith('.xlsx'))
|
|
12
|
+
return 'text';
|
|
13
|
+
return 'text';
|
|
14
|
+
}
|
|
15
|
+
function mimeForFormat(format) {
|
|
16
|
+
if (format === 'csv')
|
|
17
|
+
return 'text/csv;charset=utf-8';
|
|
18
|
+
if (format === 'pdf')
|
|
19
|
+
return 'application/pdf';
|
|
20
|
+
return 'text/plain;charset=utf-8';
|
|
21
|
+
}
|
|
22
|
+
function hintForFormat(format, filename) {
|
|
23
|
+
const lower = filename.toLowerCase();
|
|
24
|
+
if (format === 'csv')
|
|
25
|
+
return 'Download .CSV file';
|
|
26
|
+
if (format === 'pdf')
|
|
27
|
+
return 'Download file';
|
|
28
|
+
if (lower.endsWith('.docx'))
|
|
29
|
+
return 'Download Word document';
|
|
30
|
+
if (lower.endsWith('.xlsx'))
|
|
31
|
+
return 'Download spreadsheet';
|
|
32
|
+
return 'Download text file';
|
|
33
|
+
}
|
|
34
|
+
function UserTextFileAttachmentBubble({ attachment, }) {
|
|
35
|
+
const format = formatFromFilename(attachment.filename);
|
|
36
|
+
return (jsx(FileChip, { name: attachment.displayName, format: format, hint: hintForFormat(format, attachment.filename), onClick: () => downloadTextFile(attachment.content, attachment.filename, mimeForFormat(format)) }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { UserTextFileAttachmentBubble };
|
|
@@ -5,7 +5,13 @@ import S from './ChatPrompt.styl.js';
|
|
|
5
5
|
function ChatPromptAttachments({ attachments, onRemove, disabled = false, }) {
|
|
6
6
|
if (attachments.length === 0)
|
|
7
7
|
return null;
|
|
8
|
-
return (jsx("div", { className: S.attachments, children: attachments.map((item, index) => (jsx(FileChip, { className: S.attachmentItem, name: item.file.name, format: item.kind === 'pdf' ? 'pdf' : 'text', hint: item.kind === 'pdf'
|
|
8
|
+
return (jsx("div", { className: S.attachments, children: attachments.map((item, index) => (jsx(FileChip, { className: S.attachmentItem, name: item.file.name, format: item.kind === 'pdf' ? 'pdf' : 'text', hint: item.kind === 'pdf'
|
|
9
|
+
? 'PDF'
|
|
10
|
+
: item.kind === 'docx'
|
|
11
|
+
? 'Word document'
|
|
12
|
+
: item.kind === 'xlsx'
|
|
13
|
+
? 'Spreadsheet'
|
|
14
|
+
: 'Text file', onRemove: () => onRemove(index), disabled: disabled }, `${item.file.name}-${index}`))) }));
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export { ChatPromptAttachments };
|
|
@@ -4,7 +4,7 @@ import { Button } from '../../Button/Button.js';
|
|
|
4
4
|
import { ChatChrome } from '../ChatChrome/ChatChrome.js';
|
|
5
5
|
import { useChatPanelChromeModel } from './useChatPanelChromeModel.js';
|
|
6
6
|
|
|
7
|
-
function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, inline = false, }) {
|
|
7
|
+
function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, inline = false, }) {
|
|
8
8
|
const model = useChatPanelChromeModel({
|
|
9
9
|
embedAsPage: inline,
|
|
10
10
|
presets,
|
|
@@ -14,6 +14,9 @@ function ChatSheet({ triggerLabel = 'Open Chat', triggerAriaLabel, actionsRef, r
|
|
|
14
14
|
onGenerateDashboard,
|
|
15
15
|
renderMessageChart,
|
|
16
16
|
emptyState,
|
|
17
|
+
allowedAttachments,
|
|
18
|
+
allowPdfAttachments,
|
|
19
|
+
onAttachmentsDropped,
|
|
17
20
|
});
|
|
18
21
|
if (actionsRef) {
|
|
19
22
|
actionsRef.current = {
|
|
@@ -2,6 +2,7 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
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
|
+
import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
|
|
5
6
|
import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
|
|
6
7
|
import { useChatsForScopeId, useChat, useChatOutboundPending, isChatEmpty } from '../../../../contexts/chat-context.js';
|
|
7
8
|
import useEvent from '../../../../hooks/useEvent.js';
|
|
@@ -296,10 +297,11 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
296
297
|
onMessage,
|
|
297
298
|
onScriptComplete,
|
|
298
299
|
]);
|
|
299
|
-
const handlePromptSubmit = useCallback(async (message) => {
|
|
300
|
+
const handlePromptSubmit = useCallback(async (message, attachments) => {
|
|
300
301
|
const chatId = currentChatId;
|
|
301
302
|
if (!chatId)
|
|
302
303
|
return;
|
|
304
|
+
const stagedAttachments = attachments ?? [];
|
|
303
305
|
const quickBranches = quickReplyBranchesByChat[chatId];
|
|
304
306
|
const graphActive = Boolean(quickBranches && Object.keys(quickBranches).length > 0);
|
|
305
307
|
const intake = intakeByChatId[chatId];
|
|
@@ -402,8 +404,9 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
402
404
|
try {
|
|
403
405
|
if (chatId)
|
|
404
406
|
endLocalDemoFlow(chatId);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
+
const payload = buildChatSendMessagePayload(message, stagedAttachments);
|
|
408
|
+
await sendMessage(payload);
|
|
409
|
+
onMessage?.(displayTextFromSendPayload(payload));
|
|
407
410
|
}
|
|
408
411
|
catch (error) {
|
|
409
412
|
logger.error('Error sending chat message:', error);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { sanitizeAttachmentFilename } from './sanitizeAttachmentFilename.js';
|
|
2
|
+
|
|
3
|
+
function defaultExtForAttachment(item) {
|
|
4
|
+
const name = item.file.name.toLowerCase();
|
|
5
|
+
if (item.kind === 'pdf' || name.endsWith('.pdf'))
|
|
6
|
+
return 'pdf';
|
|
7
|
+
if (item.kind === 'docx' || name.endsWith('.docx'))
|
|
8
|
+
return 'docx';
|
|
9
|
+
if (item.kind === 'xlsx' || name.endsWith('.xlsx'))
|
|
10
|
+
return 'xlsx';
|
|
11
|
+
if (name.endsWith('.csv'))
|
|
12
|
+
return 'csv';
|
|
13
|
+
if (name.endsWith('.json'))
|
|
14
|
+
return 'json';
|
|
15
|
+
if (name.endsWith('.md') || name.endsWith('.markdown'))
|
|
16
|
+
return 'md';
|
|
17
|
+
if (name.endsWith('.html') || name.endsWith('.htm'))
|
|
18
|
+
return 'html';
|
|
19
|
+
if (name.endsWith('.xml'))
|
|
20
|
+
return 'xml';
|
|
21
|
+
if (name.endsWith('.yaml') || name.endsWith('.yml'))
|
|
22
|
+
return 'yaml';
|
|
23
|
+
if (name.endsWith('.tsv'))
|
|
24
|
+
return 'tsv';
|
|
25
|
+
if (name.endsWith('.ics'))
|
|
26
|
+
return 'ics';
|
|
27
|
+
return 'txt';
|
|
28
|
+
}
|
|
29
|
+
function dropItemToUserAttachment(item) {
|
|
30
|
+
const ext = defaultExtForAttachment(item);
|
|
31
|
+
return {
|
|
32
|
+
displayName: item.file.name,
|
|
33
|
+
filename: sanitizeAttachmentFilename(item.file.name, ext),
|
|
34
|
+
content: item.text,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function buildApiMessage(displayText, attachments) {
|
|
38
|
+
const parts = [
|
|
39
|
+
displayText,
|
|
40
|
+
...attachments.map(item => item.text.trim()).filter(Boolean),
|
|
41
|
+
].filter(Boolean);
|
|
42
|
+
return parts.join('\n\n');
|
|
43
|
+
}
|
|
44
|
+
/** Resolve file attachments on a send payload. */
|
|
45
|
+
function normalizeUserTextFileAttachments(payload) {
|
|
46
|
+
return payload.userTextFileAttachments ?? [];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build `sendMessage` input from composer text and staged drop items.
|
|
50
|
+
* Returns a plain string when there are no attachments.
|
|
51
|
+
*/
|
|
52
|
+
function buildChatSendMessagePayload(displayText, attachments) {
|
|
53
|
+
const trimmed = displayText.trim();
|
|
54
|
+
if (attachments.length === 0) {
|
|
55
|
+
return trimmed;
|
|
56
|
+
}
|
|
57
|
+
const userTextFileAttachments = attachments.map(dropItemToUserAttachment);
|
|
58
|
+
const resolvedDisplayText = trimmed.length > 0 ? trimmed : userTextFileAttachments[0].displayName;
|
|
59
|
+
return {
|
|
60
|
+
apiMessage: buildApiMessage(resolvedDisplayText, attachments),
|
|
61
|
+
displayText: resolvedDisplayText,
|
|
62
|
+
userTextFileAttachments,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Display text from a string or structured send payload. */
|
|
66
|
+
function displayTextFromSendPayload(message) {
|
|
67
|
+
return typeof message === 'string' ? message : message.displayText;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { buildChatSendMessagePayload, displayTextFromSendPayload, normalizeUserTextFileAttachments };
|
|
@@ -24,6 +24,10 @@ const TEXT_ATTACHMENT_ACCEPT_PARTS = [
|
|
|
24
24
|
'.tsv',
|
|
25
25
|
'text/calendar',
|
|
26
26
|
'.ics',
|
|
27
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
28
|
+
'.docx',
|
|
29
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
30
|
+
'.xlsx',
|
|
27
31
|
];
|
|
28
32
|
const PDF_ATTACHMENT_ACCEPT_PARTS = ['application/pdf', '.pdf'];
|
|
29
33
|
const TEXT_ATTACHMENT_ACCEPT_SET = new Set(TEXT_ATTACHMENT_ACCEPT_PARTS.map(part => part.toLowerCase()));
|
|
@@ -46,9 +50,24 @@ function isPdfFile(file) {
|
|
|
46
50
|
return true;
|
|
47
51
|
return file.name.toLowerCase().endsWith('.pdf');
|
|
48
52
|
}
|
|
53
|
+
function isDocxFile(file) {
|
|
54
|
+
const type = file.type.toLowerCase();
|
|
55
|
+
if (type ===
|
|
56
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return file.name.toLowerCase().endsWith('.docx');
|
|
60
|
+
}
|
|
61
|
+
function isXlsxFile(file) {
|
|
62
|
+
const type = file.type.toLowerCase();
|
|
63
|
+
if (type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return file.name.toLowerCase().endsWith('.xlsx');
|
|
67
|
+
}
|
|
49
68
|
function isAttachmentsDropzoneEnabled(allowedAttachments, allowPdfAttachments) {
|
|
50
69
|
return (filterToTextAttachments(allowedAttachments).length > 0 ||
|
|
51
70
|
Boolean(allowPdfAttachments));
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
export { PDF_ATTACHMENT_ACCEPT_PARTS, TEXT_ATTACHMENT_ACCEPT_PARTS, buildAcceptAttr, filterToTextAttachments, isAttachmentsDropzoneEnabled, isPdfFile };
|
|
73
|
+
export { PDF_ATTACHMENT_ACCEPT_PARTS, TEXT_ATTACHMENT_ACCEPT_PARTS, buildAcceptAttr, filterToTextAttachments, isAttachmentsDropzoneEnabled, isDocxFile, isPdfFile, isXlsxFile };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { isPdfFile } from './chatAttachmentAccept.js';
|
|
1
|
+
import { isPdfFile, isDocxFile, isXlsxFile } from './chatAttachmentAccept.js';
|
|
2
|
+
import { extractDocxFileToText } from './chatDocxExtract.js';
|
|
2
3
|
import { extractPdfFileToText } from './chatPdfExtract.js';
|
|
4
|
+
import { extractXlsxFileToText } from './chatXlsxExtract.js';
|
|
3
5
|
|
|
4
6
|
function readTextFile(file) {
|
|
5
7
|
return new Promise((resolve, reject) => {
|
|
@@ -17,6 +19,14 @@ async function extractChatAttachmentItems(files, allowPdfAttachments) {
|
|
|
17
19
|
const text = await extractPdfFileToText(file);
|
|
18
20
|
return { file, text, kind: 'pdf' };
|
|
19
21
|
}
|
|
22
|
+
if (isDocxFile(file)) {
|
|
23
|
+
const text = await extractDocxFileToText(file);
|
|
24
|
+
return { file, text, kind: 'docx' };
|
|
25
|
+
}
|
|
26
|
+
if (isXlsxFile(file)) {
|
|
27
|
+
const text = await extractXlsxFileToText(file);
|
|
28
|
+
return { file, text, kind: 'xlsx' };
|
|
29
|
+
}
|
|
20
30
|
const text = await readTextFile(file);
|
|
21
31
|
return { file, text, kind: 'text' };
|
|
22
32
|
}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Best-effort plain text from DOCX via mammoth (loaded on demand). */
|
|
2
|
+
async function extractDocxFileToText(file) {
|
|
3
|
+
const mammoth = await import('mammoth');
|
|
4
|
+
const result = await mammoth.extractRawText({
|
|
5
|
+
arrayBuffer: await file.arrayBuffer(),
|
|
6
|
+
});
|
|
7
|
+
const errors = result.messages.filter(m => m.type === 'error');
|
|
8
|
+
if (errors.length > 0) {
|
|
9
|
+
const detail = errors.map(m => m.message).join('; ');
|
|
10
|
+
throw new Error(detail
|
|
11
|
+
? `Failed to read ${file.name}: ${detail}`
|
|
12
|
+
: `Failed to read ${file.name}`);
|
|
13
|
+
}
|
|
14
|
+
return result.value.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { extractDocxFileToText };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
2
|
+
const MAX_SHEETS = 20;
|
|
3
|
+
const MAX_CSV_CHARS_PER_SHEET = 500_000;
|
|
4
|
+
function truncateCsv(csv) {
|
|
5
|
+
if (csv.length <= MAX_CSV_CHARS_PER_SHEET)
|
|
6
|
+
return csv;
|
|
7
|
+
return `${csv.slice(0, MAX_CSV_CHARS_PER_SHEET).trimEnd()}…`;
|
|
8
|
+
}
|
|
9
|
+
/** Best-effort plain text from XLSX; one CSV block per sheet (xlsx loaded on demand). */
|
|
10
|
+
async function extractXlsxFileToText(file) {
|
|
11
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
12
|
+
if (buffer.byteLength > MAX_FILE_BYTES) {
|
|
13
|
+
throw new Error(`${file.name} is too large (max ${MAX_FILE_BYTES / (1024 * 1024)} MB)`);
|
|
14
|
+
}
|
|
15
|
+
const XLSX = await import('xlsx');
|
|
16
|
+
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
17
|
+
const sheetNames = workbook.SheetNames.slice(0, MAX_SHEETS);
|
|
18
|
+
const sheetTexts = [];
|
|
19
|
+
for (const sheetName of sheetNames) {
|
|
20
|
+
const sheet = workbook.Sheets[sheetName];
|
|
21
|
+
if (!sheet)
|
|
22
|
+
continue;
|
|
23
|
+
const csv = truncateCsv(XLSX.utils.sheet_to_csv(sheet, { blankrows: false }).trim());
|
|
24
|
+
if (csv) {
|
|
25
|
+
sheetTexts.push(`## Sheet ${sheetName}\n\n${csv}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (workbook.SheetNames.length > MAX_SHEETS) {
|
|
29
|
+
sheetTexts.push(`_(Only the first ${MAX_SHEETS} of ${workbook.SheetNames.length} sheets were included.)_`);
|
|
30
|
+
}
|
|
31
|
+
return sheetTexts.join('\n\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { extractXlsxFileToText };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Safe download filename from a display title or original file name. */
|
|
2
|
+
function sanitizeAttachmentFilename(displayTitle, defaultExt = 'txt') {
|
|
3
|
+
const cleaned = displayTitle
|
|
4
|
+
.replace(/[/\\?%*:|"<>]/g, '-')
|
|
5
|
+
.replace(/\s+/g, ' ')
|
|
6
|
+
.trim()
|
|
7
|
+
.slice(0, 120);
|
|
8
|
+
const base = cleaned.length > 0 ? cleaned : 'attachment';
|
|
9
|
+
const ext = defaultExt.startsWith('.') ? defaultExt : `.${defaultExt}`;
|
|
10
|
+
const pattern = new RegExp(`${ext.replace('.', '\\.')}$`, 'i');
|
|
11
|
+
return pattern.test(base) ? base : `${base}${ext}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { sanitizeAttachmentFilename };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { createContext, useState, useCallback, useEffect, useContext, useMemo } from 'react';
|
|
3
3
|
import { MessageRole } from '../components/ui/Chat/Chat.types.js';
|
|
4
|
+
import { normalizeUserTextFileAttachments } from '../components/ui/Chat/buildChatSendMessagePayload.js';
|
|
4
5
|
import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
|
|
5
6
|
import { LS } from '@homecode/ui';
|
|
6
7
|
|
|
@@ -181,13 +182,17 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
181
182
|
return undefined;
|
|
182
183
|
addScopeIdToRegistry(scopeId);
|
|
183
184
|
const storedText = stripJsonDashboardFences(text);
|
|
184
|
-
const
|
|
185
|
+
const attachments = role === MessageRole.USER
|
|
186
|
+
? options?.userTextFileAttachments
|
|
187
|
+
: undefined;
|
|
185
188
|
const newMessage = {
|
|
186
189
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
187
190
|
role,
|
|
188
191
|
text: storedText,
|
|
189
192
|
timestamp: Date.now(),
|
|
190
|
-
...(
|
|
193
|
+
...(attachments?.length
|
|
194
|
+
? { userTextFileAttachments: attachments }
|
|
195
|
+
: {}),
|
|
191
196
|
};
|
|
192
197
|
setChats(prev => {
|
|
193
198
|
const scopeChats = prev[scopeId] ?? [];
|
|
@@ -232,7 +237,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
232
237
|
}
|
|
233
238
|
else {
|
|
234
239
|
addMessage(scopeId, targetChatId, MessageRole.USER, message.displayText, {
|
|
235
|
-
|
|
240
|
+
userTextFileAttachments: normalizeUserTextFileAttachments(message),
|
|
236
241
|
});
|
|
237
242
|
}
|
|
238
243
|
const pendingChatSessionId = targetChatId;
|
package/dist/esm/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export { Chat } from './components/ui/Chat/Chat.js';
|
|
|
23
23
|
export { usedPresetIdsFromMessages } from './components/ui/Chat/chat-preset-utils.js';
|
|
24
24
|
export { ChatChrome } from './components/ui/Chat/ChatChrome/ChatChrome.js';
|
|
25
25
|
export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments } from './components/ui/Chat/chatAttachmentAccept.js';
|
|
26
|
+
export { buildChatSendMessagePayload, displayTextFromSendPayload, normalizeUserTextFileAttachments } from './components/ui/Chat/buildChatSendMessagePayload.js';
|
|
27
|
+
export { sanitizeAttachmentFilename } from './components/ui/Chat/sanitizeAttachmentFilename.js';
|
|
26
28
|
export { ChatSheet } from './components/ui/Chat/ChatSheet/ChatSheet.js';
|
|
27
29
|
export { useChatPanelChromeModel } from './components/ui/Chat/ChatSheet/useChatPanelChromeModel.js';
|
|
28
30
|
export { ChatMessage } from './components/ui/Chat/ChatMessage/ChatMessage.js';
|
|
@@ -7,24 +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:
|
|
11
|
-
export type
|
|
10
|
+
/** USER-only: text file attached to a message; shown as downloadable file row(s). */
|
|
11
|
+
export type UserTextFileAttachment = {
|
|
12
12
|
displayName: string;
|
|
13
13
|
filename: string;
|
|
14
14
|
content: string;
|
|
15
15
|
};
|
|
16
|
-
/** Send full text to the chat API while showing `displayText` + optional
|
|
16
|
+
/** Send full text to the chat API while showing `displayText` + optional file attachment(s) in the UI. */
|
|
17
17
|
export type ChatSendMessagePayload = {
|
|
18
18
|
apiMessage: string;
|
|
19
19
|
displayText: string;
|
|
20
|
-
|
|
20
|
+
userTextFileAttachments?: UserTextFileAttachment[];
|
|
21
21
|
};
|
|
22
22
|
export interface Message {
|
|
23
23
|
id: string;
|
|
24
24
|
role: MessageRole;
|
|
25
25
|
text: string;
|
|
26
26
|
timestamp: number;
|
|
27
|
-
|
|
27
|
+
userTextFileAttachments?: UserTextFileAttachment[];
|
|
28
28
|
}
|
|
29
29
|
export interface Chat {
|
|
30
30
|
session_id: string;
|
|
@@ -59,9 +59,9 @@ export type ScriptCompletePayload = {
|
|
|
59
59
|
};
|
|
60
60
|
export type ChatAttachmentDropItem = {
|
|
61
61
|
file: File;
|
|
62
|
-
/** UTF-8 text for native text files; PDF
|
|
62
|
+
/** UTF-8 text for native text files; PDF/DOCX/XLSX yield extracted text. */
|
|
63
63
|
text: string;
|
|
64
|
-
kind: 'text' | 'pdf';
|
|
64
|
+
kind: 'text' | 'pdf' | 'docx' | 'xlsx';
|
|
65
65
|
};
|
|
66
66
|
export interface ChatPromptProps {
|
|
67
67
|
className?: string;
|
|
@@ -85,7 +85,7 @@ export interface ChatPromptProps {
|
|
|
85
85
|
export interface ChatMessageProps {
|
|
86
86
|
role: MessageRole;
|
|
87
87
|
text: string;
|
|
88
|
-
|
|
88
|
+
userTextFileAttachments?: UserTextFileAttachment[];
|
|
89
89
|
onQuickReply?: (branchKey: string, displayLabel: string) => void;
|
|
90
90
|
/** Branch keys already taken (e.g. from chat history); hide quick-reply buttons for these. */
|
|
91
91
|
suppressedQuickReplyKeys?: ReadonlySet<string>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type ChatMessageProps } from '../Chat.types';
|
|
2
|
-
export declare function ChatMessage({ role, text,
|
|
2
|
+
export declare function ChatMessage({ role, text, userTextFileAttachments, onQuickReply, suppressedQuickReplyKeys, quickReplyDisabled, isLastMessage, scriptContinue, onScriptContinue, renderMessageChart, }: ChatMessageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -19,4 +19,4 @@ export interface ChatSheetProps extends Omit<UseChatPanelChromeModelInput, 'embe
|
|
|
19
19
|
*/
|
|
20
20
|
inline?: boolean;
|
|
21
21
|
}
|
|
22
|
-
export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function ChatSheet({ triggerLabel, triggerAriaLabel, actionsRef, renderTrigger, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, inline, }: ChatSheetProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChatAttachmentDropItem, ChatSendMessagePayload, UserTextFileAttachment } from './Chat.types';
|
|
2
|
+
/** Resolve file attachments on a send payload. */
|
|
3
|
+
export declare function normalizeUserTextFileAttachments(payload: ChatSendMessagePayload): UserTextFileAttachment[];
|
|
4
|
+
/**
|
|
5
|
+
* Build `sendMessage` input from composer text and staged drop items.
|
|
6
|
+
* Returns a plain string when there are no attachments.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildChatSendMessagePayload(displayText: string, attachments: readonly ChatAttachmentDropItem[]): string | ChatSendMessagePayload;
|
|
9
|
+
/** Display text from a string or structured send payload. */
|
|
10
|
+
export declare function displayTextFromSendPayload(message: string | ChatSendMessagePayload): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** MIME types and extensions accepted for chat text attachments. */
|
|
2
|
-
export declare const TEXT_ATTACHMENT_ACCEPT_PARTS: readonly ["text/plain", ".txt", "text/csv", ".csv", "text/markdown", ".md", ".markdown", "application/json", ".json", "text/html", ".html", ".htm", "text/xml", "application/xml", ".xml", "text/yaml", "application/yaml", "application/x-yaml", ".yaml", ".yml", "text/tab-separated-values", ".tsv", "text/calendar", ".ics"];
|
|
2
|
+
export declare const TEXT_ATTACHMENT_ACCEPT_PARTS: readonly ["text/plain", ".txt", "text/csv", ".csv", "text/markdown", ".md", ".markdown", "application/json", ".json", "text/html", ".html", ".htm", "text/xml", "application/xml", ".xml", "text/yaml", "application/yaml", "application/x-yaml", ".yaml", ".yml", "text/tab-separated-values", ".tsv", "text/calendar", ".ics", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"];
|
|
3
3
|
export declare const PDF_ATTACHMENT_ACCEPT_PARTS: readonly ["application/pdf", ".pdf"];
|
|
4
4
|
/** Keep only tokens from `parts` that appear in the text attachment allowlist. */
|
|
5
5
|
export declare function filterToTextAttachments(parts: readonly string[] | undefined): string[];
|
|
6
6
|
export declare function buildAcceptAttr(filteredTextParts: readonly string[], allowPdf: boolean): string;
|
|
7
7
|
export declare function isPdfFile(file: File): boolean;
|
|
8
|
+
export declare function isDocxFile(file: File): boolean;
|
|
9
|
+
export declare function isXlsxFile(file: File): boolean;
|
|
8
10
|
export declare function isAttachmentsDropzoneEnabled(allowedAttachments: readonly string[] | undefined, allowPdfAttachments: boolean | undefined): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,8 @@ export { usedPresetIdsFromMessages } from './chat-preset-utils';
|
|
|
3
3
|
export { ChatChrome } from './ChatChrome';
|
|
4
4
|
export type { ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome';
|
|
5
5
|
export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments, } from './chatAttachmentAccept';
|
|
6
|
+
export { buildChatSendMessagePayload, displayTextFromSendPayload, normalizeUserTextFileAttachments, } from './buildChatSendMessagePayload';
|
|
7
|
+
export { sanitizeAttachmentFilename } from './sanitizeAttachmentFilename';
|
|
6
8
|
export { ChatSheet } from './ChatSheet/ChatSheet';
|
|
7
9
|
export { useChatPanelChromeModel } from './ChatSheet/useChatPanelChromeModel';
|
|
8
10
|
export type { ChatSheetActions, ChatSheetProps } from './ChatSheet/ChatSheet';
|
|
@@ -10,6 +12,6 @@ export type { UseChatPanelChromeModelInput, UseChatPanelChromeModelResult, } fro
|
|
|
10
12
|
export { ChatMessage } from './ChatMessage';
|
|
11
13
|
export { ChatPrompt } from './ChatPrompt';
|
|
12
14
|
export { ChatPresets } from './ChatPresets';
|
|
13
|
-
export type { Chat as ChatType, ChatAttachmentDropItem, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message,
|
|
15
|
+
export type { Chat as ChatType, ChatAttachmentDropItem, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserTextFileAttachment, } from './Chat.types';
|
|
14
16
|
export { MessageRole } from './Chat.types';
|
|
15
17
|
export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { type Chat, type ChatSendMessagePayload, MessageRole, type
|
|
2
|
+
import { type Chat, type ChatSendMessagePayload, MessageRole, type UserTextFileAttachment } 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,
|
|
5
|
+
export type { ChatSendMessagePayload, UserTextFileAttachment, } from '#uilib/components/ui/Chat/Chat.types';
|
|
6
6
|
export type AddChatMessageOptions = {
|
|
7
|
-
|
|
7
|
+
userTextFileAttachments?: UserTextFileAttachment[];
|
|
8
8
|
};
|
|
9
9
|
export interface ChatContextType {
|
|
10
10
|
/** Returns the new session id, or undefined if no user / not created. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sybilion/uilib",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.15",
|
|
4
4
|
"description": "Sybilion Design System — React UI components (Webpack + Stylus)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"classnames": "^2.3.2",
|
|
103
103
|
"lightweight-charts": "^5.0.9",
|
|
104
104
|
"lucide-react": "^0.546.0",
|
|
105
|
+
"mammoth": "^1.9.0",
|
|
105
106
|
"motion": "^12.23.12",
|
|
106
107
|
"pdfjs-dist": "^4.10.38",
|
|
107
108
|
"recharts": "^3.2.1",
|
|
@@ -109,7 +110,8 @@
|
|
|
109
110
|
"style-inject": "^0.3.0",
|
|
110
111
|
"tailwindcss": "^4.2.2",
|
|
111
112
|
"tslib": "^2.8.1",
|
|
112
|
-
"vaul": "^1.1.2"
|
|
113
|
+
"vaul": "^1.1.2",
|
|
114
|
+
"xlsx": "^0.18.5"
|
|
113
115
|
},
|
|
114
116
|
"peerDependencies": {
|
|
115
117
|
"@auth0/auth0-react": "^2.3.1",
|