@sybilion/uilib 1.3.14 → 1.3.16
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/Chat.styl.js +1 -1
- package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +3 -0
- package/dist/esm/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.js +6 -3
- package/dist/esm/components/ui/Chat/ChatMessage/AgentMessageContent.js +2 -1
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.js +2 -0
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +1 -1
- package/dist/esm/components/ui/Chat/ChatMessage/UserTextFileAttachmentBubble.js +9 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptAttachments.js +7 -1
- package/dist/esm/components/ui/Chat/buildChatSendMessagePayload.js +4 -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/InteractiveContent/AutolinkUrl.js +34 -0
- package/dist/esm/components/ui/InteractiveContent/InteractiveContent.styl.js +2 -2
- package/dist/esm/index.js +1 -0
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +2 -2
- 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/InteractiveContent/AutolinkUrl.d.ts +11 -0
- package/dist/esm/types/src/components/ui/InteractiveContent/index.d.ts +1 -0
- package/dist/esm/types/tests/setup.d.ts +1 -0
- package/package.json +4 -2
- package/src/components/ui/Chat/Chat.styl +1 -0
- package/src/components/ui/Chat/Chat.types.ts +2 -2
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +3 -0
- package/src/components/ui/Chat/ChatMessage/AgentMessageContent.helpers.tsx +35 -7
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +8 -0
- package/src/components/ui/Chat/ChatMessage/UserTextFileAttachmentBubble.tsx +6 -2
- package/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.tsx +9 -1
- package/src/components/ui/Chat/buildChatSendMessagePayload.test.ts +15 -1
- package/src/components/ui/Chat/buildChatSendMessagePayload.ts +2 -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/InteractiveContent/AutolinkUrl.tsx +53 -0
- package/src/components/ui/InteractiveContent/InteractiveContent.styl +60 -9
- package/src/components/ui/InteractiveContent/InteractiveContent.styl.d.ts +4 -0
- package/src/components/ui/InteractiveContent/index.ts +1 -0
- package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +14 -20
- package/src/docs/pages/InteractiveContentPage.tsx +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".Chat_root__IWt99{background-color:var(--background);display:flex;flex-direction:column;height:100%;min-height:0}.Chat_header__ZjwP-{align-items:center;display:flex;flex-shrink:0;min-height:64px;padding:var(--p-2) var(--p-6) 0}.Chat_isEmpty__b4ViB{padding-bottom:170px}";
|
|
3
|
+
var css_248z = ".Chat_root__IWt99{background-color:var(--background);display:flex;flex-direction:column;height:100%;min-height:0}.Chat_header__ZjwP-{align-items:center;display:flex;flex-shrink:0;min-height:64px;padding:var(--p-2) var(--p-6) 0;padding-right:var(--p-12)}.Chat_isEmpty__b4ViB{padding-bottom:170px}";
|
|
4
4
|
var S = {"root":"Chat_root__IWt99","header":"Chat_header__ZjwP-","isEmpty":"Chat_isEmpty__b4ViB"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
@@ -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]);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { shouldRenderAutolinkPill, AutolinkUrl } from '../../InteractiveContent/AutolinkUrl.js';
|
|
2
3
|
import logger from '../../../../lib/logger.js';
|
|
3
4
|
import S from './ChatMessage.styl.js';
|
|
5
|
+
import S$1 from '../../InteractiveContent/InteractiveContent.styl.js';
|
|
4
6
|
|
|
5
7
|
const injectHeaders = (content) => {
|
|
6
8
|
// Match #, ##, ###, or #### headers at start of line
|
|
@@ -320,7 +322,7 @@ const injectMarkdownLink = (content) => {
|
|
|
320
322
|
const { target, rel } = linkTargetRelForHref(href, undefined);
|
|
321
323
|
const label = matches[1];
|
|
322
324
|
return {
|
|
323
|
-
elem: (jsx("a", { href: href, target: target, rel: rel, children: runFormattingPipeline(label, linkLabelInjectors) })),
|
|
325
|
+
elem: shouldRenderAutolinkPill(label, href) ? (jsx(AutolinkUrl, { href: href, display: label, target: target, rel: rel })) : (jsx("a", { href: href, target: target, rel: rel, className: S$1.textLink, children: runFormattingPipeline(label, linkLabelInjectors) })),
|
|
324
326
|
index: matches.index,
|
|
325
327
|
length: matches[0].length,
|
|
326
328
|
};
|
|
@@ -366,7 +368,7 @@ const injectAutolinkUrl = (content) => {
|
|
|
366
368
|
}
|
|
367
369
|
const { target, rel } = linkTargetRelForHref(c.href, undefined);
|
|
368
370
|
return {
|
|
369
|
-
elem: (jsx(
|
|
371
|
+
elem: (jsx(AutolinkUrl, { href: c.href, display: c.display, target: target, rel: rel })),
|
|
370
372
|
index: c.index,
|
|
371
373
|
length: c.length,
|
|
372
374
|
};
|
|
@@ -393,8 +395,9 @@ const injectAnchor = (content) => {
|
|
|
393
395
|
return null;
|
|
394
396
|
}
|
|
395
397
|
const { target, rel } = linkTargetRelForHref(href, targetRaw || undefined);
|
|
398
|
+
const innerTrimmed = inner.trim();
|
|
396
399
|
return {
|
|
397
|
-
elem: (jsx("a", { href: href, target: target, rel: rel, children: runFormattingPipeline(inner, linkLabelInjectors) })),
|
|
400
|
+
elem: shouldRenderAutolinkPill(innerTrimmed, href) ? (jsx(AutolinkUrl, { href: href, display: innerTrimmed, target: target, rel: rel })) : (jsx("a", { href: href, target: target, rel: rel, className: S$1.textLink, children: runFormattingPipeline(inner, linkLabelInjectors) })),
|
|
398
401
|
index: matches.index,
|
|
399
402
|
length: matches[0].length,
|
|
400
403
|
};
|
|
@@ -2,10 +2,11 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
2
2
|
import { useState, useMemo } from 'react';
|
|
3
3
|
import { Button } from '../../Button/Button.js';
|
|
4
4
|
import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.js';
|
|
5
|
+
import { Presentation } from 'lucide-react';
|
|
6
|
+
import '../../InteractiveContent/InteractiveContent.styl.js';
|
|
5
7
|
import { parseDatasetAppToken } from '../../../../utils/datasetApplicationLink.js';
|
|
6
8
|
import { Scroll } from '@homecode/ui';
|
|
7
9
|
import { PaperPlaneRightIcon } from '@phosphor-icons/react';
|
|
8
|
-
import { Presentation } from 'lucide-react';
|
|
9
10
|
import { convertMarkdownTableToHTML } from './AgentMessageContent.helpers.js';
|
|
10
11
|
import { ChatDatasetApplicationLink } from './ChatDatasetApplicationLink.js';
|
|
11
12
|
import S from './ChatMessage.styl.js';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import cn from 'classnames';
|
|
3
3
|
import { InteractiveContent } from '../../InteractiveContent/InteractiveContent.js';
|
|
4
|
+
import 'lucide-react';
|
|
5
|
+
import '../../InteractiveContent/InteractiveContent.styl.js';
|
|
4
6
|
import { TextShimmer } from '../../TextShimmer/TextShimmer.js';
|
|
5
7
|
import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
|
|
6
8
|
import { userTextFileAttachmentsFromMessage } from '../userTextFileAttachments.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
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%;min-width:0;overflow-wrap:anywhere;-webkit-user-select:text;-moz-user-select:text;user-select:text;width:-moz-fit-content;width:fit-content;word-break:break-word}.ChatMessage_role-user__u4JPV{align-items:flex-end;max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_userColumn__cQM6-{align-items:flex-end;display:flex;flex-direction:column;gap:var(--p-2);max-width:100%;min-width:0}.ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-slate-100);border-radius:var(--p-4);border-bottom-right-radius:0;box-sizing:border-box;overflow:hidden;padding:var(--p-3) var(--p-4);white-space:pre-wrap}.dark .ChatMessage_role-user__u4JPV .ChatMessage_text__Y1XNR{background-color:var(--sb-gray-800)}.ChatMessage_role-system__g13OP{align-items:center}.ChatMessage_role-system__g13OP .ChatMessage_text__Y1XNR{color:var(--muted-foreground);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
4
|
var S = {"root":"ChatMessage_root__6rnsF","text":"ChatMessage_text__Y1XNR","role-user":"ChatMessage_role-user__u4JPV","userColumn":"ChatMessage_userColumn__cQM6-","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
|
|
|
@@ -8,6 +8,8 @@ function formatFromFilename(filename) {
|
|
|
8
8
|
return 'csv';
|
|
9
9
|
if (lower.endsWith('.pdf'))
|
|
10
10
|
return 'pdf';
|
|
11
|
+
if (lower.endsWith('.xlsx'))
|
|
12
|
+
return 'text';
|
|
11
13
|
return 'text';
|
|
12
14
|
}
|
|
13
15
|
function mimeForFormat(format) {
|
|
@@ -17,16 +19,21 @@ function mimeForFormat(format) {
|
|
|
17
19
|
return 'application/pdf';
|
|
18
20
|
return 'text/plain;charset=utf-8';
|
|
19
21
|
}
|
|
20
|
-
function hintForFormat(format) {
|
|
22
|
+
function hintForFormat(format, filename) {
|
|
23
|
+
const lower = filename.toLowerCase();
|
|
21
24
|
if (format === 'csv')
|
|
22
25
|
return 'Download .CSV file';
|
|
23
26
|
if (format === 'pdf')
|
|
24
27
|
return 'Download file';
|
|
28
|
+
if (lower.endsWith('.docx'))
|
|
29
|
+
return 'Download Word document';
|
|
30
|
+
if (lower.endsWith('.xlsx'))
|
|
31
|
+
return 'Download spreadsheet';
|
|
25
32
|
return 'Download text file';
|
|
26
33
|
}
|
|
27
34
|
function UserTextFileAttachmentBubble({ attachment, }) {
|
|
28
35
|
const format = formatFromFilename(attachment.filename);
|
|
29
|
-
return (jsx(FileChip, { name: attachment.displayName, format: format, hint: hintForFormat(format), onClick: () => downloadTextFile(attachment.content, attachment.filename, mimeForFormat(format)) }));
|
|
36
|
+
return (jsx(FileChip, { name: attachment.displayName, format: format, hint: hintForFormat(format, attachment.filename), onClick: () => downloadTextFile(attachment.content, attachment.filename, mimeForFormat(format)) }));
|
|
30
37
|
}
|
|
31
38
|
|
|
32
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,6 +4,10 @@ function defaultExtForAttachment(item) {
|
|
|
4
4
|
const name = item.file.name.toLowerCase();
|
|
5
5
|
if (item.kind === 'pdf' || name.endsWith('.pdf'))
|
|
6
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';
|
|
7
11
|
if (name.endsWith('.csv'))
|
|
8
12
|
return 'csv';
|
|
9
13
|
if (name.endsWith('.json'))
|
|
@@ -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,34 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { LinkIcon } from 'lucide-react';
|
|
3
|
+
import S from './InteractiveContent.styl.js';
|
|
4
|
+
|
|
5
|
+
function displayUrlForAutolink(display) {
|
|
6
|
+
if (display.startsWith('https://'))
|
|
7
|
+
return display.slice(8);
|
|
8
|
+
if (display.startsWith('http://'))
|
|
9
|
+
return display.slice(7);
|
|
10
|
+
return display;
|
|
11
|
+
}
|
|
12
|
+
function shouldRenderAutolinkPill(label, href) {
|
|
13
|
+
const normalizedLabel = label.trim();
|
|
14
|
+
const normalizedHref = href.trim();
|
|
15
|
+
if (!normalizedLabel || !normalizedHref)
|
|
16
|
+
return false;
|
|
17
|
+
const labelAsHref = normalizeWwwToHttps(normalizedLabel);
|
|
18
|
+
if (labelAsHref === normalizedHref)
|
|
19
|
+
return true;
|
|
20
|
+
return (/^https?:\/\//i.test(normalizedLabel) ||
|
|
21
|
+
/^www\./i.test(normalizedLabel) ||
|
|
22
|
+
normalizedLabel === displayUrlForAutolink(normalizedHref));
|
|
23
|
+
}
|
|
24
|
+
const normalizeWwwToHttps = (raw) => {
|
|
25
|
+
const t = raw.trim();
|
|
26
|
+
if (/^www\./i.test(t))
|
|
27
|
+
return `https://${t}`;
|
|
28
|
+
return t;
|
|
29
|
+
};
|
|
30
|
+
function AutolinkUrl({ href, display, target, rel }) {
|
|
31
|
+
return (jsxs("a", { href: href, target: target, rel: rel, className: S.autolinkUrl, children: [jsx(LinkIcon, { size: 12, "aria-hidden": true, className: S.autolinkUrlIcon }), jsx("span", { className: S.autolinkUrlLabel, title: displayUrlForAutolink(display), children: displayUrlForAutolink(display) })] }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { AutolinkUrl, S as autolinkStyles, displayUrlForAutolink, shouldRenderAutolinkPill };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".InteractiveContent_root__FHnlY strong{font-weight:600}.InteractiveContent_root__FHnlY em{font-style:italic}.
|
|
4
|
-
var S = {"root":"InteractiveContent_root__FHnlY"};
|
|
3
|
+
var css_248z = ".InteractiveContent_root__FHnlY{box-sizing:border-box;display:inline-block;max-width:100%;min-width:0;overflow-wrap:anywhere;word-break:break-word}.InteractiveContent_root__FHnlY strong{font-weight:600}.InteractiveContent_root__FHnlY em{font-style:italic}.InteractiveContent_textLink__Ubh4i{color:var(--sb-green-600);text-decoration:underline;text-underline-offset:2px}.InteractiveContent_textLink__Ubh4i:hover{color:var(--sb-green-700)}.dark .InteractiveContent_textLink__Ubh4i{color:var(--sb-green-400)}.dark .InteractiveContent_textLink__Ubh4i:hover{color:var(--sb-green-300)}.InteractiveContent_autolinkUrl__W0tQH{align-items:center;border-radius:var(--p-2);box-sizing:border-box;color:var(--link-color);display:inline-flex;font-size:var(--text-xs);gap:6px;line-height:1.4;margin:0 1px;max-width:100%;padding:2px 8px 2px 6px;position:relative;text-decoration:none;vertical-align:middle}.InteractiveContent_autolinkUrl__W0tQH:before{background-color:var(--link-color);border-radius:inherit;content:\"\";display:inline-block;height:100%;left:0;opacity:.1;position:absolute;top:0;width:100%}.InteractiveContent_autolinkUrl__W0tQH:hover:before{opacity:.2}.InteractiveContent_autolinkUrlIcon__QoqlQ{flex-shrink:0}.InteractiveContent_autolinkUrlLabel__CcJOG{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}";
|
|
4
|
+
var S = {"root":"InteractiveContent_root__FHnlY","textLink":"InteractiveContent_textLink__Ubh4i","autolinkUrl":"InteractiveContent_autolinkUrl__W0tQH","autolinkUrlIcon":"InteractiveContent_autolinkUrlIcon__QoqlQ","autolinkUrlLabel":"InteractiveContent_autolinkUrlLabel__CcJOG"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { S as default };
|
package/dist/esm/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export { Image } from './components/ui/Image/Image.js';
|
|
|
47
47
|
export { ImageWithFallback } from './components/ui/ImageWithFallback/ImageWithFallback.js';
|
|
48
48
|
export { Input } from './components/ui/Input/Input.js';
|
|
49
49
|
export { InteractiveContent } from './components/ui/InteractiveContent/InteractiveContent.js';
|
|
50
|
+
export { AutolinkUrl, displayUrlForAutolink } from './components/ui/InteractiveContent/AutolinkUrl.js';
|
|
50
51
|
export { Label } from './components/ui/Label/Label.js';
|
|
51
52
|
export { LabeledInput } from './components/ui/LabeledInput/LabeledInput.js';
|
|
52
53
|
export { AltKeyProvider } from './components/ui/LabelWithId/AltKeyProvider.js';
|
|
@@ -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;
|
|
@@ -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 {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import S from './InteractiveContent.styl';
|
|
2
|
+
type AutolinkUrlProps = {
|
|
3
|
+
href: string;
|
|
4
|
+
display: string;
|
|
5
|
+
target?: string;
|
|
6
|
+
rel?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function displayUrlForAutolink(display: string): string;
|
|
9
|
+
export declare function shouldRenderAutolinkPill(label: string, href: string): boolean;
|
|
10
|
+
export declare function AutolinkUrl({ href, display, target, rel }: AutolinkUrlProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export { S as autolinkStyles };
|
|
@@ -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.16",
|
|
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",
|
|
@@ -69,9 +69,9 @@ export type ScriptCompletePayload = {
|
|
|
69
69
|
|
|
70
70
|
export type ChatAttachmentDropItem = {
|
|
71
71
|
file: File;
|
|
72
|
-
/** UTF-8 text for native text files; PDF
|
|
72
|
+
/** UTF-8 text for native text files; PDF/DOCX/XLSX yield extracted text. */
|
|
73
73
|
text: string;
|
|
74
|
-
kind: 'text' | 'pdf';
|
|
74
|
+
kind: 'text' | 'pdf' | 'docx' | 'xlsx';
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
export interface ChatPromptProps {
|
|
@@ -86,6 +86,9 @@ export function ChatChrome({
|
|
|
86
86
|
setPendingAttachments(prev => [...prev, ...items]);
|
|
87
87
|
}
|
|
88
88
|
})
|
|
89
|
+
.catch(() => {
|
|
90
|
+
// Extraction failed (parse error, size limit, etc.); skip staging.
|
|
91
|
+
})
|
|
89
92
|
.finally(() => setIsExtractingAttachments(false));
|
|
90
93
|
},
|
|
91
94
|
[allowPdfAttachments, promptBusy],
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
AutolinkUrl,
|
|
5
|
+
autolinkStyles,
|
|
6
|
+
shouldRenderAutolinkPill,
|
|
7
|
+
} from '#uilib/components/ui/InteractiveContent/AutolinkUrl';
|
|
3
8
|
import logger from '#uilib/lib/logger';
|
|
4
9
|
|
|
5
10
|
import S from './ChatMessage.styl';
|
|
@@ -397,8 +402,15 @@ const injectMarkdownLink: Injector = (content: string) => {
|
|
|
397
402
|
const label = matches[1];
|
|
398
403
|
|
|
399
404
|
return {
|
|
400
|
-
elem: (
|
|
401
|
-
<
|
|
405
|
+
elem: shouldRenderAutolinkPill(label, href) ? (
|
|
406
|
+
<AutolinkUrl href={href} display={label} target={target} rel={rel} />
|
|
407
|
+
) : (
|
|
408
|
+
<a
|
|
409
|
+
href={href}
|
|
410
|
+
target={target}
|
|
411
|
+
rel={rel}
|
|
412
|
+
className={autolinkStyles.textLink}
|
|
413
|
+
>
|
|
402
414
|
{runFormattingPipeline(label, linkLabelInjectors)}
|
|
403
415
|
</a>
|
|
404
416
|
),
|
|
@@ -459,9 +471,12 @@ const injectAutolinkUrl: Injector = (content: string) => {
|
|
|
459
471
|
|
|
460
472
|
return {
|
|
461
473
|
elem: (
|
|
462
|
-
<
|
|
463
|
-
{c.
|
|
464
|
-
|
|
474
|
+
<AutolinkUrl
|
|
475
|
+
href={c.href}
|
|
476
|
+
display={c.display}
|
|
477
|
+
target={target}
|
|
478
|
+
rel={rel}
|
|
479
|
+
/>
|
|
465
480
|
),
|
|
466
481
|
index: c.index,
|
|
467
482
|
length: c.length,
|
|
@@ -500,10 +515,23 @@ const injectAnchor: Injector = (content: string) => {
|
|
|
500
515
|
}
|
|
501
516
|
|
|
502
517
|
const { target, rel } = linkTargetRelForHref(href, targetRaw || undefined);
|
|
518
|
+
const innerTrimmed = inner.trim();
|
|
503
519
|
|
|
504
520
|
return {
|
|
505
|
-
elem: (
|
|
506
|
-
<
|
|
521
|
+
elem: shouldRenderAutolinkPill(innerTrimmed, href) ? (
|
|
522
|
+
<AutolinkUrl
|
|
523
|
+
href={href}
|
|
524
|
+
display={innerTrimmed}
|
|
525
|
+
target={target}
|
|
526
|
+
rel={rel}
|
|
527
|
+
/>
|
|
528
|
+
) : (
|
|
529
|
+
<a
|
|
530
|
+
href={href}
|
|
531
|
+
target={target}
|
|
532
|
+
rel={rel}
|
|
533
|
+
className={autolinkStyles.textLink}
|
|
534
|
+
>
|
|
507
535
|
{runFormattingPipeline(inner, linkLabelInjectors)}
|
|
508
536
|
</a>
|
|
509
537
|
),
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
.text
|
|
10
10
|
max-width 100%
|
|
11
|
+
min-width 0
|
|
11
12
|
font-size var(--text-sm)
|
|
12
13
|
color var(--text-secondary)
|
|
13
14
|
width fit-content
|
|
14
15
|
user-select text
|
|
16
|
+
overflow-wrap anywhere
|
|
17
|
+
word-break break-word
|
|
15
18
|
|
|
16
19
|
.role-user
|
|
17
20
|
align-items flex-end
|
|
21
|
+
max-width 100%
|
|
22
|
+
min-width 0
|
|
18
23
|
|
|
19
24
|
.userColumn
|
|
20
25
|
display flex
|
|
@@ -22,6 +27,7 @@
|
|
|
22
27
|
align-items flex-end
|
|
23
28
|
gap var(--p-2)
|
|
24
29
|
max-width 100%
|
|
30
|
+
min-width 0
|
|
25
31
|
|
|
26
32
|
.text
|
|
27
33
|
padding var(--p-3) var(--p-4)
|
|
@@ -31,6 +37,8 @@
|
|
|
31
37
|
border-bottom-right-radius 0
|
|
32
38
|
|
|
33
39
|
white-space pre-wrap
|
|
40
|
+
overflow hidden
|
|
41
|
+
box-sizing border-box
|
|
34
42
|
|
|
35
43
|
:global(.dark) &
|
|
36
44
|
background-color var(--sb-gray-800)
|
|
@@ -7,6 +7,7 @@ function formatFromFilename(filename: string): FileChipFormat {
|
|
|
7
7
|
const lower = filename.toLowerCase();
|
|
8
8
|
if (lower.endsWith('.csv')) return 'csv';
|
|
9
9
|
if (lower.endsWith('.pdf')) return 'pdf';
|
|
10
|
+
if (lower.endsWith('.xlsx')) return 'text';
|
|
10
11
|
return 'text';
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -16,9 +17,12 @@ function mimeForFormat(format: FileChipFormat): string {
|
|
|
16
17
|
return 'text/plain;charset=utf-8';
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function hintForFormat(format: FileChipFormat): string {
|
|
20
|
+
function hintForFormat(format: FileChipFormat, filename: string): string {
|
|
21
|
+
const lower = filename.toLowerCase();
|
|
20
22
|
if (format === 'csv') return 'Download .CSV file';
|
|
21
23
|
if (format === 'pdf') return 'Download file';
|
|
24
|
+
if (lower.endsWith('.docx')) return 'Download Word document';
|
|
25
|
+
if (lower.endsWith('.xlsx')) return 'Download spreadsheet';
|
|
22
26
|
return 'Download text file';
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -33,7 +37,7 @@ export function UserTextFileAttachmentBubble({
|
|
|
33
37
|
<FileChip
|
|
34
38
|
name={attachment.displayName}
|
|
35
39
|
format={format}
|
|
36
|
-
hint={hintForFormat(format)}
|
|
40
|
+
hint={hintForFormat(format, attachment.filename)}
|
|
37
41
|
onClick={() =>
|
|
38
42
|
downloadTextFile(
|
|
39
43
|
attachment.content,
|
|
@@ -24,7 +24,15 @@ export function ChatPromptAttachments({
|
|
|
24
24
|
className={S.attachmentItem}
|
|
25
25
|
name={item.file.name}
|
|
26
26
|
format={item.kind === 'pdf' ? 'pdf' : 'text'}
|
|
27
|
-
hint={
|
|
27
|
+
hint={
|
|
28
|
+
item.kind === 'pdf'
|
|
29
|
+
? 'PDF'
|
|
30
|
+
: item.kind === 'docx'
|
|
31
|
+
? 'Word document'
|
|
32
|
+
: item.kind === 'xlsx'
|
|
33
|
+
? 'Spreadsheet'
|
|
34
|
+
: 'Text file'
|
|
35
|
+
}
|
|
28
36
|
onRemove={() => onRemove(index)}
|
|
29
37
|
disabled={disabled}
|
|
30
38
|
/>
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
function makeDropItem(
|
|
9
9
|
name: string,
|
|
10
10
|
text: string,
|
|
11
|
-
kind: '
|
|
11
|
+
kind: ChatAttachmentDropItem['kind'] = 'text',
|
|
12
12
|
): ChatAttachmentDropItem {
|
|
13
13
|
return {
|
|
14
14
|
file: { name } as File,
|
|
@@ -50,6 +50,20 @@ describe('buildChatSendMessagePayload', () => {
|
|
|
50
50
|
expect(result.userTextFileAttachments?.[0].filename).toMatch(/\.pdf$/i);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it('uses docx and xlsx extensions from kind', () => {
|
|
54
|
+
const docx = buildChatSendMessagePayload('', [
|
|
55
|
+
makeDropItem('brief.docx', 'word body', 'docx'),
|
|
56
|
+
]);
|
|
57
|
+
const xlsx = buildChatSendMessagePayload('', [
|
|
58
|
+
makeDropItem('sheet.xlsx', 'a,b', 'xlsx'),
|
|
59
|
+
]);
|
|
60
|
+
if (typeof docx === 'string' || typeof xlsx === 'string') {
|
|
61
|
+
throw new Error('expected payload object');
|
|
62
|
+
}
|
|
63
|
+
expect(docx.userTextFileAttachments?.[0].filename).toMatch(/\.docx$/i);
|
|
64
|
+
expect(xlsx.userTextFileAttachments?.[0].filename).toMatch(/\.xlsx$/i);
|
|
65
|
+
});
|
|
66
|
+
|
|
53
67
|
it('maps multiple attachments', () => {
|
|
54
68
|
const result = buildChatSendMessagePayload('Hi', [
|
|
55
69
|
makeDropItem('one.txt', 'first'),
|
|
@@ -8,6 +8,8 @@ import { sanitizeAttachmentFilename } from './sanitizeAttachmentFilename';
|
|
|
8
8
|
function defaultExtForAttachment(item: ChatAttachmentDropItem): string {
|
|
9
9
|
const name = item.file.name.toLowerCase();
|
|
10
10
|
if (item.kind === 'pdf' || name.endsWith('.pdf')) return 'pdf';
|
|
11
|
+
if (item.kind === 'docx' || name.endsWith('.docx')) return 'docx';
|
|
12
|
+
if (item.kind === 'xlsx' || name.endsWith('.xlsx')) return 'xlsx';
|
|
11
13
|
if (name.endsWith('.csv')) return 'csv';
|
|
12
14
|
if (name.endsWith('.json')) return 'json';
|
|
13
15
|
if (name.endsWith('.md') || name.endsWith('.markdown')) return 'md';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TEXT_ATTACHMENT_ACCEPT_PARTS,
|
|
3
|
+
buildAcceptAttr,
|
|
4
|
+
filterToTextAttachments,
|
|
5
|
+
isDocxFile,
|
|
6
|
+
isPdfFile,
|
|
7
|
+
isXlsxFile,
|
|
8
|
+
} from './chatAttachmentAccept';
|
|
9
|
+
|
|
10
|
+
function makeFile(name: string, type = ''): File {
|
|
11
|
+
return { name, type } as File;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('TEXT_ATTACHMENT_ACCEPT_PARTS', () => {
|
|
15
|
+
it('includes docx and xlsx MIME types and extensions', () => {
|
|
16
|
+
expect(TEXT_ATTACHMENT_ACCEPT_PARTS).toContain('.docx');
|
|
17
|
+
expect(TEXT_ATTACHMENT_ACCEPT_PARTS).toContain('.xlsx');
|
|
18
|
+
expect(TEXT_ATTACHMENT_ACCEPT_PARTS).toContain(
|
|
19
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
20
|
+
);
|
|
21
|
+
expect(TEXT_ATTACHMENT_ACCEPT_PARTS).toContain(
|
|
22
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('isDocxFile', () => {
|
|
28
|
+
it('detects by extension and MIME type', () => {
|
|
29
|
+
expect(
|
|
30
|
+
isDocxFile(
|
|
31
|
+
makeFile(
|
|
32
|
+
'notes.docx',
|
|
33
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
).toBe(true);
|
|
37
|
+
expect(isDocxFile(makeFile('notes.docx'))).toBe(true);
|
|
38
|
+
expect(isDocxFile(makeFile('notes.txt'))).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('isXlsxFile', () => {
|
|
43
|
+
it('detects by extension and MIME type', () => {
|
|
44
|
+
expect(
|
|
45
|
+
isXlsxFile(
|
|
46
|
+
makeFile(
|
|
47
|
+
'data.xlsx',
|
|
48
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
).toBe(true);
|
|
52
|
+
expect(isXlsxFile(makeFile('data.xlsx'))).toBe(true);
|
|
53
|
+
expect(isXlsxFile(makeFile('data.csv'))).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('isPdfFile', () => {
|
|
58
|
+
it('does not treat docx or xlsx as pdf', () => {
|
|
59
|
+
expect(isPdfFile(makeFile('file.docx'))).toBe(false);
|
|
60
|
+
expect(isPdfFile(makeFile('file.xlsx'))).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('filterToTextAttachments', () => {
|
|
65
|
+
it('keeps docx and xlsx tokens from the allowlist', () => {
|
|
66
|
+
expect(
|
|
67
|
+
filterToTextAttachments(['.docx', '.xlsx', 'application/pdf']),
|
|
68
|
+
).toEqual(['.docx', '.xlsx']);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('buildAcceptAttr', () => {
|
|
73
|
+
it('includes filtered text parts and optional pdf', () => {
|
|
74
|
+
expect(buildAcceptAttr(['.docx', '.txt'], false)).toBe('.docx,.txt');
|
|
75
|
+
expect(buildAcceptAttr(['.docx'], true)).toContain('.docx');
|
|
76
|
+
expect(buildAcceptAttr(['.docx'], true)).toContain('.pdf');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -24,6 +24,10 @@ export 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
|
] as const;
|
|
28
32
|
|
|
29
33
|
export const PDF_ATTACHMENT_ACCEPT_PARTS = ['application/pdf', '.pdf'] as const;
|
|
@@ -59,6 +63,27 @@ export function isPdfFile(file: File): boolean {
|
|
|
59
63
|
return file.name.toLowerCase().endsWith('.pdf');
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
export function isDocxFile(file: File): boolean {
|
|
67
|
+
const type = file.type.toLowerCase();
|
|
68
|
+
if (
|
|
69
|
+
type ===
|
|
70
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
71
|
+
) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return file.name.toLowerCase().endsWith('.docx');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isXlsxFile(file: File): boolean {
|
|
78
|
+
const type = file.type.toLowerCase();
|
|
79
|
+
if (
|
|
80
|
+
type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
81
|
+
) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return file.name.toLowerCase().endsWith('.xlsx');
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
export function isAttachmentsDropzoneEnabled(
|
|
63
88
|
allowedAttachments: readonly string[] | undefined,
|
|
64
89
|
allowPdfAttachments: boolean | undefined,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ChatAttachmentDropItem } from './Chat.types';
|
|
2
|
-
import { isPdfFile } from './chatAttachmentAccept';
|
|
2
|
+
import { isDocxFile, isPdfFile, isXlsxFile } from './chatAttachmentAccept';
|
|
3
|
+
import { extractDocxFileToText } from './chatDocxExtract';
|
|
3
4
|
import { extractPdfFileToText } from './chatPdfExtract';
|
|
5
|
+
import { extractXlsxFileToText } from './chatXlsxExtract';
|
|
4
6
|
|
|
5
7
|
function readTextFile(file: File): Promise<string> {
|
|
6
8
|
return new Promise((resolve, reject) => {
|
|
@@ -24,6 +26,16 @@ export async function extractChatAttachmentItems(
|
|
|
24
26
|
return { file, text, kind: 'pdf' as const };
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
if (isDocxFile(file)) {
|
|
30
|
+
const text = await extractDocxFileToText(file);
|
|
31
|
+
return { file, text, kind: 'docx' as const };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (isXlsxFile(file)) {
|
|
35
|
+
const text = await extractXlsxFileToText(file);
|
|
36
|
+
return { file, text, kind: 'xlsx' as const };
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
const text = await readTextFile(file);
|
|
28
40
|
return { file, text, kind: 'text' as const };
|
|
29
41
|
}),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { extractDocxFileToText } from './chatDocxExtract';
|
|
2
|
+
|
|
3
|
+
const extractRawText = jest.fn();
|
|
4
|
+
|
|
5
|
+
jest.mock('mammoth', () => ({
|
|
6
|
+
extractRawText: (...args: unknown[]) => extractRawText(...args),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('extractDocxFileToText', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
extractRawText.mockReset();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns trimmed text from mammoth', async () => {
|
|
15
|
+
extractRawText.mockResolvedValue({
|
|
16
|
+
value: ' Hello from Word ',
|
|
17
|
+
messages: [],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const file = new File([new Uint8Array(8)], 'doc.docx', {
|
|
21
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await expect(extractDocxFileToText(file)).resolves.toBe('Hello from Word');
|
|
25
|
+
expect(extractRawText).toHaveBeenCalledTimes(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('throws when mammoth reports errors', async () => {
|
|
29
|
+
extractRawText.mockResolvedValue({
|
|
30
|
+
value: '',
|
|
31
|
+
messages: [{ type: 'error', message: 'corrupt file' }],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const file = new File([new Uint8Array(8)], 'bad.docx', {
|
|
35
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await expect(extractDocxFileToText(file)).rejects.toThrow(/corrupt file/i);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Best-effort plain text from DOCX via mammoth (loaded on demand). */
|
|
2
|
+
export async function extractDocxFileToText(file: File): Promise<string> {
|
|
3
|
+
const mammoth = await import('mammoth');
|
|
4
|
+
const result = await mammoth.extractRawText({
|
|
5
|
+
arrayBuffer: await file.arrayBuffer(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const errors = result.messages.filter(m => m.type === 'error');
|
|
9
|
+
if (errors.length > 0) {
|
|
10
|
+
const detail = errors.map(m => m.message).join('; ');
|
|
11
|
+
throw new Error(
|
|
12
|
+
detail
|
|
13
|
+
? `Failed to read ${file.name}: ${detail}`
|
|
14
|
+
: `Failed to read ${file.name}`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result.value.trim();
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as XLSX from 'xlsx';
|
|
2
|
+
|
|
3
|
+
import { extractXlsxFileToText } from './chatXlsxExtract';
|
|
4
|
+
|
|
5
|
+
function makeXlsxFile(
|
|
6
|
+
sheets: Record<string, (string | number)[][]>,
|
|
7
|
+
name = 'test.xlsx',
|
|
8
|
+
): File {
|
|
9
|
+
const workbook = XLSX.utils.book_new();
|
|
10
|
+
for (const [sheetName, rows] of Object.entries(sheets)) {
|
|
11
|
+
XLSX.utils.book_append_sheet(
|
|
12
|
+
workbook,
|
|
13
|
+
XLSX.utils.aoa_to_sheet(rows),
|
|
14
|
+
sheetName,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const buffer = new Uint8Array(
|
|
18
|
+
XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }),
|
|
19
|
+
);
|
|
20
|
+
return new File([buffer], name, {
|
|
21
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('extractXlsxFileToText', () => {
|
|
26
|
+
it('extracts non-empty sheets as CSV with headings', async () => {
|
|
27
|
+
const file = makeXlsxFile({
|
|
28
|
+
Data: [
|
|
29
|
+
['name', 'value'],
|
|
30
|
+
['a', 1],
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const text = await extractXlsxFileToText(file);
|
|
35
|
+
expect(text).toContain('## Sheet Data');
|
|
36
|
+
expect(text).toContain('name,value');
|
|
37
|
+
expect(text).toContain('a,1');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('skips empty sheets', async () => {
|
|
41
|
+
const workbook = XLSX.utils.book_new();
|
|
42
|
+
XLSX.utils.book_append_sheet(
|
|
43
|
+
workbook,
|
|
44
|
+
XLSX.utils.aoa_to_sheet([]),
|
|
45
|
+
'Empty',
|
|
46
|
+
);
|
|
47
|
+
XLSX.utils.book_append_sheet(
|
|
48
|
+
workbook,
|
|
49
|
+
XLSX.utils.aoa_to_sheet([['x']]),
|
|
50
|
+
'Filled',
|
|
51
|
+
);
|
|
52
|
+
const buffer = new Uint8Array(
|
|
53
|
+
XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }),
|
|
54
|
+
);
|
|
55
|
+
const file = new File([buffer], 'mixed.xlsx', {
|
|
56
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const text = await extractXlsxFileToText(file);
|
|
60
|
+
expect(text).not.toContain('## Sheet Empty');
|
|
61
|
+
expect(text).toContain('## Sheet Filled');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects files over the size limit', async () => {
|
|
65
|
+
const huge = new Uint8Array(10 * 1024 * 1024 + 1);
|
|
66
|
+
const file = new File([huge], 'huge.xlsx', {
|
|
67
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await expect(extractXlsxFileToText(file)).rejects.toThrow(/too large/i);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
2
|
+
const MAX_SHEETS = 20;
|
|
3
|
+
const MAX_CSV_CHARS_PER_SHEET = 500_000;
|
|
4
|
+
|
|
5
|
+
function truncateCsv(csv: string): string {
|
|
6
|
+
if (csv.length <= MAX_CSV_CHARS_PER_SHEET) return csv;
|
|
7
|
+
return `${csv.slice(0, MAX_CSV_CHARS_PER_SHEET).trimEnd()}…`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Best-effort plain text from XLSX; one CSV block per sheet (xlsx loaded on demand). */
|
|
11
|
+
export async function extractXlsxFileToText(file: File): Promise<string> {
|
|
12
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
13
|
+
if (buffer.byteLength > MAX_FILE_BYTES) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`${file.name} is too large (max ${MAX_FILE_BYTES / (1024 * 1024)} MB)`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const XLSX = await import('xlsx');
|
|
20
|
+
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
21
|
+
const sheetNames = workbook.SheetNames.slice(0, MAX_SHEETS);
|
|
22
|
+
const sheetTexts: string[] = [];
|
|
23
|
+
|
|
24
|
+
for (const sheetName of sheetNames) {
|
|
25
|
+
const sheet = workbook.Sheets[sheetName];
|
|
26
|
+
if (!sheet) continue;
|
|
27
|
+
|
|
28
|
+
const csv = truncateCsv(
|
|
29
|
+
XLSX.utils.sheet_to_csv(sheet, { blankrows: false }).trim(),
|
|
30
|
+
);
|
|
31
|
+
if (csv) {
|
|
32
|
+
sheetTexts.push(`## Sheet ${sheetName}\n\n${csv}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (workbook.SheetNames.length > MAX_SHEETS) {
|
|
37
|
+
sheetTexts.push(
|
|
38
|
+
`_(Only the first ${MAX_SHEETS} of ${workbook.SheetNames.length} sheets were included.)_`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return sheetTexts.join('\n\n');
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { LinkIcon } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
import S from './InteractiveContent.styl';
|
|
4
|
+
|
|
5
|
+
type AutolinkUrlProps = {
|
|
6
|
+
href: string;
|
|
7
|
+
display: string;
|
|
8
|
+
target?: string;
|
|
9
|
+
rel?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function displayUrlForAutolink(display: string): string {
|
|
13
|
+
if (display.startsWith('https://')) return display.slice(8);
|
|
14
|
+
if (display.startsWith('http://')) return display.slice(7);
|
|
15
|
+
return display;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldRenderAutolinkPill(label: string, href: string): boolean {
|
|
19
|
+
const normalizedLabel = label.trim();
|
|
20
|
+
const normalizedHref = href.trim();
|
|
21
|
+
if (!normalizedLabel || !normalizedHref) return false;
|
|
22
|
+
|
|
23
|
+
const labelAsHref = normalizeWwwToHttps(normalizedLabel);
|
|
24
|
+
if (labelAsHref === normalizedHref) return true;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
/^https?:\/\//i.test(normalizedLabel) ||
|
|
28
|
+
/^www\./i.test(normalizedLabel) ||
|
|
29
|
+
normalizedLabel === displayUrlForAutolink(normalizedHref)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalizeWwwToHttps = (raw: string): string => {
|
|
34
|
+
const t = raw.trim();
|
|
35
|
+
if (/^www\./i.test(t)) return `https://${t}`;
|
|
36
|
+
return t;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function AutolinkUrl({ href, display, target, rel }: AutolinkUrlProps) {
|
|
40
|
+
return (
|
|
41
|
+
<a href={href} target={target} rel={rel} className={S.autolinkUrl}>
|
|
42
|
+
<LinkIcon size={12} aria-hidden className={S.autolinkUrlIcon} />
|
|
43
|
+
<span
|
|
44
|
+
className={S.autolinkUrlLabel}
|
|
45
|
+
title={displayUrlForAutolink(display)}
|
|
46
|
+
>
|
|
47
|
+
{displayUrlForAutolink(display)}
|
|
48
|
+
</span>
|
|
49
|
+
</a>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { S as autolinkStyles };
|
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
.root
|
|
2
|
+
display inline-block
|
|
3
|
+
max-width 100%
|
|
4
|
+
min-width 0
|
|
5
|
+
box-sizing border-box
|
|
6
|
+
overflow-wrap anywhere
|
|
7
|
+
word-break break-word
|
|
8
|
+
|
|
2
9
|
strong
|
|
3
10
|
font-weight 600
|
|
4
11
|
|
|
5
12
|
em
|
|
6
13
|
font-style italic
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
.textLink
|
|
16
|
+
color var(--sb-green-600)
|
|
17
|
+
text-decoration underline
|
|
18
|
+
text-underline-offset 2px
|
|
19
|
+
|
|
20
|
+
&:hover
|
|
21
|
+
color var(--sb-green-700)
|
|
22
|
+
|
|
23
|
+
:global(.dark) &
|
|
24
|
+
color var(--sb-green-400)
|
|
12
25
|
|
|
13
26
|
&:hover
|
|
14
|
-
color var(--sb-green-
|
|
27
|
+
color var(--sb-green-300)
|
|
28
|
+
|
|
29
|
+
.autolinkUrl
|
|
30
|
+
position relative
|
|
31
|
+
display inline-flex
|
|
32
|
+
align-items center
|
|
33
|
+
gap 6px
|
|
34
|
+
|
|
35
|
+
max-width 100%
|
|
36
|
+
box-sizing border-box
|
|
37
|
+
vertical-align middle
|
|
38
|
+
|
|
39
|
+
padding 2px 8px 2px 6px
|
|
40
|
+
margin 0 1px
|
|
41
|
+
|
|
42
|
+
border-radius var(--p-2)
|
|
43
|
+
|
|
44
|
+
color var(--link-color)
|
|
45
|
+
font-size var(--text-xs)
|
|
46
|
+
line-height 1.4
|
|
47
|
+
text-decoration none
|
|
48
|
+
|
|
49
|
+
&::before
|
|
50
|
+
position absolute
|
|
51
|
+
top 0
|
|
52
|
+
left 0
|
|
53
|
+
content ''
|
|
54
|
+
display inline-block
|
|
55
|
+
width 100%
|
|
56
|
+
height 100%
|
|
57
|
+
background-color var(--link-color)
|
|
58
|
+
border-radius inherit
|
|
59
|
+
opacity .1
|
|
60
|
+
|
|
61
|
+
&:hover::before
|
|
62
|
+
opacity .2
|
|
15
63
|
|
|
16
|
-
|
|
17
|
-
|
|
64
|
+
.autolinkUrlIcon
|
|
65
|
+
flex-shrink 0
|
|
18
66
|
|
|
19
|
-
|
|
20
|
-
|
|
67
|
+
.autolinkUrlLabel
|
|
68
|
+
min-width 0
|
|
69
|
+
overflow hidden
|
|
70
|
+
text-overflow ellipsis
|
|
71
|
+
white-space nowrap
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// This file is automatically generated.
|
|
2
2
|
// Please do not change this file!
|
|
3
3
|
interface CssExports {
|
|
4
|
+
'autolinkUrl': string;
|
|
5
|
+
'autolinkUrlIcon': string;
|
|
6
|
+
'autolinkUrlLabel': string;
|
|
4
7
|
'root': string;
|
|
8
|
+
'textLink': string;
|
|
5
9
|
}
|
|
6
10
|
export const cssExports: CssExports;
|
|
7
11
|
export default cssExports;
|
|
@@ -97,16 +97,16 @@ export default function ChatAttachmentsDropzonePage() {
|
|
|
97
97
|
<AppPageHeader
|
|
98
98
|
breadcrumbs={[{ label: 'Chat' }, { label: 'Attachments dropzone' }]}
|
|
99
99
|
title="Chat — attachments dropzone"
|
|
100
|
-
subheader="Drop text files onto the chat shell; they appear on the prompt until you send."
|
|
100
|
+
subheader="Drop text, Office, or PDF files onto the chat shell; they appear on the prompt until you send."
|
|
101
101
|
actions={<DocsHeaderActions />}
|
|
102
102
|
/>
|
|
103
103
|
<PageContentSection>
|
|
104
104
|
<p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
|
|
105
|
-
Drop a <code>.txt</code>, <code>.csv</code>, <code>.md</code>,
|
|
106
|
-
<code>.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<code>allowPdfAttachments</code>.
|
|
105
|
+
Drop a <code>.txt</code>, <code>.csv</code>, <code>.md</code>,{' '}
|
|
106
|
+
<code>.docx</code>, <code>.xlsx</code>, or <code>.pdf</code> file
|
|
107
|
+
anywhere on the chat panel. The file shows above the composer; press
|
|
108
|
+
send to post it. Office and PDF parsers load on demand when you attach
|
|
109
|
+
those types; PDF also requires <code>allowPdfAttachments</code>.
|
|
110
110
|
</p>
|
|
111
111
|
<ChatChrome
|
|
112
112
|
showResizeHandle={false}
|
|
@@ -133,25 +133,19 @@ export default function ChatAttachmentsDropzonePage() {
|
|
|
133
133
|
effectiveScopeId="docs-chat-attachments-dropzone"
|
|
134
134
|
onPromptSubmit={onPromptSubmit}
|
|
135
135
|
onChatDeleted={() => {}}
|
|
136
|
-
allowedAttachments={
|
|
137
|
-
'text/plain',
|
|
138
|
-
'.txt',
|
|
139
|
-
'text/csv',
|
|
140
|
-
'.csv',
|
|
141
|
-
'text/markdown',
|
|
142
|
-
'.md',
|
|
143
|
-
'application/json',
|
|
144
|
-
'.json',
|
|
145
|
-
]}
|
|
136
|
+
allowedAttachments={TEXT_ATTACHMENT_ACCEPT_PARTS}
|
|
146
137
|
allowPdfAttachments
|
|
147
138
|
emptyState={{
|
|
148
|
-
title: 'Drop a text
|
|
139
|
+
title: 'Drop a text, Office, or PDF file',
|
|
149
140
|
description:
|
|
150
|
-
'Drag a file onto this panel, review it above the composer, then send.',
|
|
141
|
+
'Drag a file onto this panel, review it above the composer, then send. DOCX and XLSX are parsed in the browser.',
|
|
151
142
|
additionalContent: (
|
|
152
143
|
<p style={{ fontSize: 13, opacity: 0.85 }}>
|
|
153
|
-
|
|
154
|
-
{
|
|
144
|
+
Accepted types include <code>.txt</code>, <code>.csv</code>,{' '}
|
|
145
|
+
<code>.md</code>, <code>.json</code>, <code>.docx</code>,{' '}
|
|
146
|
+
<code>.xlsx</code>, and more (
|
|
147
|
+
{TEXT_ATTACHMENT_ACCEPT_PARTS.length} entries in the text
|
|
148
|
+
allowlist).
|
|
155
149
|
</p>
|
|
156
150
|
),
|
|
157
151
|
}}
|
|
@@ -4,7 +4,8 @@ import { PageContentSection } from '#uilib/components/ui/Page';
|
|
|
4
4
|
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
5
5
|
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
6
6
|
|
|
7
|
-
const SAMPLE =
|
|
7
|
+
const SAMPLE =
|
|
8
|
+
'Use **bold** and *italic* in formatted text. See https://www.theguardian.com/business/ng-interactive/2026/may/14/us-stock-market-war-inflation-tariffs-trump for context.';
|
|
8
9
|
|
|
9
10
|
export default function InteractiveContentPage() {
|
|
10
11
|
return (
|