@sybilion/uilib 1.3.9 → 1.3.11
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/ChartAreaInteractive/ChartAreaInteractive.js +4 -2
- package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +52 -17
- package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +3 -4
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +6 -4
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptAttachments.js +11 -0
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +1 -1
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +4 -1
- package/dist/esm/components/ui/Chat/chatAttachmentAccept.js +54 -0
- package/dist/esm/components/ui/Chat/chatAttachmentExtract.js +26 -0
- package/dist/esm/components/ui/Chat/chatPdfExtract.js +31 -0
- package/dist/esm/components/ui/DropZone/DropZone.js +50 -21
- package/dist/esm/components/ui/DropZone/DropZone.styl.js +2 -2
- package/dist/esm/components/ui/FileChip/FileChip.js +26 -0
- package/dist/esm/components/ui/FileChip/FileChip.styl.js +7 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +10 -1
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +9 -2
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/index.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.d.ts +8 -0
- package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +7 -1
- package/dist/esm/types/src/components/ui/Chat/chatAttachmentAccept.d.ts +8 -0
- package/dist/esm/types/src/components/ui/Chat/chatAttachmentExtract.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/chatPdfExtract.d.ts +2 -0
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +2 -1
- package/dist/esm/types/src/components/ui/DropZone/DropZone.d.ts +2 -0
- package/dist/esm/types/src/components/ui/FileChip/FileChip.d.ts +2 -0
- package/dist/esm/types/src/components/ui/FileChip/FileChip.types.d.ts +10 -0
- package/dist/esm/types/src/components/ui/FileChip/index.d.ts +2 -0
- package/dist/esm/types/src/docs/pages/ChatAttachmentsDropzonePage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/FileChipPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +1 -0
- package/package.json +2 -1
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +4 -1
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -0
- package/src/components/ui/Chat/Chat.types.ts +11 -1
- package/src/components/ui/Chat/ChatChrome/ChatChrome.styl +20 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.styl.d.ts +2 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +88 -4
- package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +18 -2
- package/src/components/ui/Chat/ChatChrome/index.ts +1 -0
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +0 -56
- package/src/components/ui/Chat/ChatMessage/ChatMessage.styl.d.ts +0 -5
- package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +6 -15
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +11 -15
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl.d.ts +2 -1
- package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +17 -8
- package/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.tsx +34 -0
- package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +12 -11
- package/src/components/ui/Chat/ChatSheet/ChatSheet.styl.d.ts +13 -13
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +14 -0
- package/src/components/ui/Chat/chat-preset-utils.ts +4 -1
- package/src/components/ui/Chat/chatAttachmentAccept.ts +70 -0
- package/src/components/ui/Chat/chatAttachmentExtract.ts +33 -0
- package/src/components/ui/Chat/chatPdfExtract.ts +37 -0
- package/src/components/ui/Chat/index.ts +5 -0
- package/src/components/ui/DropZone/DropZone.styl +24 -0
- package/src/components/ui/DropZone/DropZone.styl.d.ts +3 -0
- package/src/components/ui/DropZone/DropZone.tsx +77 -24
- package/src/components/ui/FileChip/FileChip.styl +108 -0
- package/src/components/ui/FileChip/FileChip.styl.d.ts +12 -0
- package/src/components/ui/FileChip/FileChip.tsx +93 -0
- package/src/components/ui/FileChip/FileChip.types.ts +11 -0
- package/src/components/ui/FileChip/index.ts +2 -0
- package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +162 -0
- package/src/docs/pages/FileChipPage.tsx +50 -0
- package/src/docs/registry.ts +12 -0
- package/src/index.ts +1 -0
|
@@ -19,21 +19,6 @@ INPUT_MAX_HEIGHT = 200px
|
|
|
19
19
|
gap var(--p-3)
|
|
20
20
|
width 100%
|
|
21
21
|
|
|
22
|
-
.notice
|
|
23
|
-
position absolute
|
|
24
|
-
top calc(-1 * var(--p-12))
|
|
25
|
-
left 0
|
|
26
|
-
right 0
|
|
27
|
-
margin-bottom var(--p-1)
|
|
28
|
-
|
|
29
|
-
font-size var(--text-xs)
|
|
30
|
-
text-align center
|
|
31
|
-
color var(--muted-foreground)
|
|
32
|
-
pointer-events none
|
|
33
|
-
|
|
34
|
-
@media (max-width MOBILE)
|
|
35
|
-
font-size 10px
|
|
36
|
-
|
|
37
22
|
.input
|
|
38
23
|
flex 1
|
|
39
24
|
min-width 0
|
|
@@ -79,3 +64,14 @@ INPUT_MAX_HEIGHT = 200px
|
|
|
79
64
|
.attachButton
|
|
80
65
|
background-color var(--page-color)
|
|
81
66
|
box-shadow 0 0 20px var(--background)
|
|
67
|
+
|
|
68
|
+
.attachments
|
|
69
|
+
display flex
|
|
70
|
+
flex-wrap wrap
|
|
71
|
+
gap var(--p-2)
|
|
72
|
+
margin-bottom var(--p-2)
|
|
73
|
+
|
|
74
|
+
.attachmentItem
|
|
75
|
+
flex 1 1 300px
|
|
76
|
+
max-width 300px
|
|
77
|
+
min-width 0
|
|
@@ -9,6 +9,7 @@ import { Input } from '../../Input';
|
|
|
9
9
|
import type { ChatPromptProps } from '../Chat.types';
|
|
10
10
|
import { syncChatPromptTextareaHeight } from './ChatPrompt.helpers';
|
|
11
11
|
import S from './ChatPrompt.styl';
|
|
12
|
+
import { ChatPromptAttachments } from './ChatPromptAttachments';
|
|
12
13
|
|
|
13
14
|
export function ChatPrompt({
|
|
14
15
|
onSubmit,
|
|
@@ -16,7 +17,9 @@ export function ChatPrompt({
|
|
|
16
17
|
className,
|
|
17
18
|
footer,
|
|
18
19
|
prefillMessage,
|
|
19
|
-
|
|
20
|
+
attachments = [],
|
|
21
|
+
onRemoveAttachment,
|
|
22
|
+
disabled = false,
|
|
20
23
|
}: ChatPromptProps) {
|
|
21
24
|
const [message, setMessage] = useState('');
|
|
22
25
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -35,10 +38,11 @@ export function ChatPrompt({
|
|
|
35
38
|
|
|
36
39
|
const handleSubmit = (e: FormEvent | KeyboardEvent) => {
|
|
37
40
|
const trimmedMessage = message.trim();
|
|
41
|
+
const hasAttachments = attachments.length > 0;
|
|
38
42
|
|
|
39
|
-
if (trimmedMessage) {
|
|
43
|
+
if (trimmedMessage || hasAttachments) {
|
|
40
44
|
e.preventDefault();
|
|
41
|
-
onSubmit(trimmedMessage);
|
|
45
|
+
onSubmit(trimmedMessage, hasAttachments ? attachments : undefined);
|
|
42
46
|
setMessage('');
|
|
43
47
|
}
|
|
44
48
|
};
|
|
@@ -56,11 +60,12 @@ export function ChatPrompt({
|
|
|
56
60
|
|
|
57
61
|
return (
|
|
58
62
|
<form onSubmit={handleSubmit} className={cn(S.root, className)}>
|
|
63
|
+
<ChatPromptAttachments
|
|
64
|
+
attachments={attachments}
|
|
65
|
+
onRemove={index => onRemoveAttachment?.(index)}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
/>
|
|
59
68
|
<div className={S.composer}>
|
|
60
|
-
{showNotice ? (
|
|
61
|
-
<div className={S.notice}>Forecast Assistant can make mistakes.</div>
|
|
62
|
-
) : null}
|
|
63
|
-
|
|
64
69
|
<Input
|
|
65
70
|
ref={inputRef}
|
|
66
71
|
type="textarea"
|
|
@@ -72,7 +77,11 @@ export function ChatPrompt({
|
|
|
72
77
|
/>
|
|
73
78
|
|
|
74
79
|
<div className={S.submitColumn}>
|
|
75
|
-
<Button
|
|
80
|
+
<Button
|
|
81
|
+
type="submit"
|
|
82
|
+
size="sm"
|
|
83
|
+
disabled={disabled || (!message.trim() && attachments.length === 0)}
|
|
84
|
+
>
|
|
76
85
|
<SendHorizontalIcon size={16} />
|
|
77
86
|
</Button>
|
|
78
87
|
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FileChip } from '#uilib/components/ui/FileChip';
|
|
2
|
+
|
|
3
|
+
import type { ChatAttachmentDropItem } from '../Chat.types';
|
|
4
|
+
import S from './ChatPrompt.styl';
|
|
5
|
+
|
|
6
|
+
type ChatPromptAttachmentsProps = {
|
|
7
|
+
attachments: ChatAttachmentDropItem[];
|
|
8
|
+
onRemove: (index: number) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ChatPromptAttachments({
|
|
13
|
+
attachments,
|
|
14
|
+
onRemove,
|
|
15
|
+
disabled = false,
|
|
16
|
+
}: ChatPromptAttachmentsProps) {
|
|
17
|
+
if (attachments.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={S.attachments}>
|
|
21
|
+
{attachments.map((item, index) => (
|
|
22
|
+
<FileChip
|
|
23
|
+
key={`${item.file.name}-${index}`}
|
|
24
|
+
className={S.attachmentItem}
|
|
25
|
+
name={item.file.name}
|
|
26
|
+
format={item.kind === 'pdf' ? 'pdf' : 'text'}
|
|
27
|
+
hint={item.kind === 'pdf' ? 'PDF' : 'Text file'}
|
|
28
|
+
onRemove={() => onRemove(index)}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
/>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -87,17 +87,18 @@ export function ChatSelector({
|
|
|
87
87
|
</SelectContent>
|
|
88
88
|
</Select>
|
|
89
89
|
</div>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
{currentChatId && chats.length > 0 && (
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
variant="ghost"
|
|
94
|
+
size="sm"
|
|
95
|
+
className={S.deleteBtn}
|
|
96
|
+
aria-label="Delete chat"
|
|
97
|
+
onClick={handleDeleteChat}
|
|
98
|
+
>
|
|
99
|
+
<Trash2Icon size={16} />
|
|
100
|
+
</Button>
|
|
101
|
+
)}
|
|
101
102
|
</div>
|
|
102
103
|
);
|
|
103
104
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// This file is automatically generated.
|
|
2
2
|
// Please do not change this file!
|
|
3
3
|
interface CssExports {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
branchBtnWrap: string;
|
|
5
|
+
branchRow: string;
|
|
6
|
+
chatResizeHandle: string;
|
|
7
|
+
content: string;
|
|
8
|
+
footer: string;
|
|
9
|
+
loader: string;
|
|
10
|
+
panelClose: string;
|
|
11
|
+
panelHeader: string;
|
|
12
|
+
panelRoot: string;
|
|
13
|
+
scroll: string;
|
|
14
|
+
scrollInner: string;
|
|
15
|
+
scrollWrapper: string;
|
|
16
|
+
scrollbar: string;
|
|
17
17
|
}
|
|
18
18
|
export const cssExports: CssExports;
|
|
19
19
|
export default cssExports;
|
|
@@ -39,6 +39,7 @@ import { ScrollRef } from '@homecode/ui';
|
|
|
39
39
|
import { useSidebar } from '../../Sidebar/Sidebar';
|
|
40
40
|
import { Chat } from '../Chat';
|
|
41
41
|
import type { ChatChromeProps } from '../ChatChrome';
|
|
42
|
+
import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
|
|
42
43
|
import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
|
|
43
44
|
|
|
44
45
|
export type UseChatPanelChromeModelInput = {
|
|
@@ -56,6 +57,13 @@ export type UseChatPanelChromeModelInput = {
|
|
|
56
57
|
renderMessageChart?: () => React.ReactNode;
|
|
57
58
|
/** Forwarded to `ChatChrome` when the thread is empty. */
|
|
58
59
|
emptyState?: ChatEmptyStateProps;
|
|
60
|
+
/** MIME types / extensions for text-only chat attachments (filtered by uilib allowlist). */
|
|
61
|
+
allowedAttachments?: readonly string[];
|
|
62
|
+
/** When true, PDF drops are accepted and parsed to plain text. */
|
|
63
|
+
allowPdfAttachments?: boolean;
|
|
64
|
+
onAttachmentsDropped?: (
|
|
65
|
+
items: ChatAttachmentDropItem[],
|
|
66
|
+
) => void | Promise<void>;
|
|
59
67
|
};
|
|
60
68
|
|
|
61
69
|
export type UseChatPanelChromeModelResult = {
|
|
@@ -101,6 +109,9 @@ export function useChatPanelChromeModel({
|
|
|
101
109
|
onGenerateDashboard,
|
|
102
110
|
renderMessageChart,
|
|
103
111
|
emptyState,
|
|
112
|
+
allowedAttachments,
|
|
113
|
+
allowPdfAttachments,
|
|
114
|
+
onAttachmentsDropped,
|
|
104
115
|
}: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult {
|
|
105
116
|
const effectiveScopeId = scopeId ?? NO_SCOPE_FALLBACK;
|
|
106
117
|
const isMobile = useIsMobile();
|
|
@@ -998,6 +1009,9 @@ export function useChatPanelChromeModel({
|
|
|
998
1009
|
onChatDeleted: endLocalDemoFlow,
|
|
999
1010
|
promptPrefill: promptLinkPrefill,
|
|
1000
1011
|
emptyState,
|
|
1012
|
+
allowedAttachments,
|
|
1013
|
+
allowPdfAttachments,
|
|
1014
|
+
onAttachmentsDropped,
|
|
1001
1015
|
};
|
|
1002
1016
|
|
|
1003
1017
|
const toggleOpen = () => onOpenChange(!isOpen);
|
|
@@ -11,7 +11,10 @@ export function normalizePresetMatchText(s: string): string {
|
|
|
11
11
|
return s.trim().normalize('NFC');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function presetMatchesUserText(
|
|
14
|
+
function presetMatchesUserText(
|
|
15
|
+
presetText: string,
|
|
16
|
+
userTextNorm: string,
|
|
17
|
+
): boolean {
|
|
15
18
|
const presetNorm = normalizePresetMatchText(presetText);
|
|
16
19
|
if (!presetNorm) return false;
|
|
17
20
|
if (userTextNorm === presetNorm) return true;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** MIME types and extensions accepted for chat text attachments. */
|
|
2
|
+
export const TEXT_ATTACHMENT_ACCEPT_PARTS = [
|
|
3
|
+
'text/plain',
|
|
4
|
+
'.txt',
|
|
5
|
+
'text/csv',
|
|
6
|
+
'.csv',
|
|
7
|
+
'text/markdown',
|
|
8
|
+
'.md',
|
|
9
|
+
'.markdown',
|
|
10
|
+
'application/json',
|
|
11
|
+
'.json',
|
|
12
|
+
'text/html',
|
|
13
|
+
'.html',
|
|
14
|
+
'.htm',
|
|
15
|
+
'text/xml',
|
|
16
|
+
'application/xml',
|
|
17
|
+
'.xml',
|
|
18
|
+
'text/yaml',
|
|
19
|
+
'application/yaml',
|
|
20
|
+
'application/x-yaml',
|
|
21
|
+
'.yaml',
|
|
22
|
+
'.yml',
|
|
23
|
+
'text/tab-separated-values',
|
|
24
|
+
'.tsv',
|
|
25
|
+
'text/calendar',
|
|
26
|
+
'.ics',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export const PDF_ATTACHMENT_ACCEPT_PARTS = ['application/pdf', '.pdf'] as const;
|
|
30
|
+
|
|
31
|
+
const TEXT_ATTACHMENT_ACCEPT_SET = new Set<string>(
|
|
32
|
+
TEXT_ATTACHMENT_ACCEPT_PARTS.map(part => part.toLowerCase()),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** Keep only tokens from `parts` that appear in the text attachment allowlist. */
|
|
36
|
+
export function filterToTextAttachments(
|
|
37
|
+
parts: readonly string[] | undefined,
|
|
38
|
+
): string[] {
|
|
39
|
+
if (!parts?.length) return [];
|
|
40
|
+
return parts.filter(part =>
|
|
41
|
+
TEXT_ATTACHMENT_ACCEPT_SET.has(part.trim().toLowerCase()),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildAcceptAttr(
|
|
46
|
+
filteredTextParts: readonly string[],
|
|
47
|
+
allowPdf: boolean,
|
|
48
|
+
): string {
|
|
49
|
+
const parts = [...filteredTextParts];
|
|
50
|
+
if (allowPdf) {
|
|
51
|
+
parts.push(...PDF_ATTACHMENT_ACCEPT_PARTS);
|
|
52
|
+
}
|
|
53
|
+
return parts.join(',');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isPdfFile(file: File): boolean {
|
|
57
|
+
const type = file.type.toLowerCase();
|
|
58
|
+
if (type === 'application/pdf') return true;
|
|
59
|
+
return file.name.toLowerCase().endsWith('.pdf');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isAttachmentsDropzoneEnabled(
|
|
63
|
+
allowedAttachments: readonly string[] | undefined,
|
|
64
|
+
allowPdfAttachments: boolean | undefined,
|
|
65
|
+
): boolean {
|
|
66
|
+
return (
|
|
67
|
+
filterToTextAttachments(allowedAttachments).length > 0 ||
|
|
68
|
+
Boolean(allowPdfAttachments)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ChatAttachmentDropItem } from './Chat.types';
|
|
2
|
+
import { isPdfFile } from './chatAttachmentAccept';
|
|
3
|
+
import { extractPdfFileToText } from './chatPdfExtract';
|
|
4
|
+
|
|
5
|
+
function readTextFile(file: File): Promise<string> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
reader.onload = () => resolve(String(reader.result ?? ''));
|
|
9
|
+
reader.onerror = () =>
|
|
10
|
+
reject(reader.error ?? new Error(`Failed to read ${file.name}`));
|
|
11
|
+
reader.readAsText(file);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function extractChatAttachmentItems(
|
|
16
|
+
files: File[],
|
|
17
|
+
allowPdfAttachments: boolean,
|
|
18
|
+
): Promise<ChatAttachmentDropItem[]> {
|
|
19
|
+
const items = await Promise.all(
|
|
20
|
+
files.map(async file => {
|
|
21
|
+
if (isPdfFile(file)) {
|
|
22
|
+
if (!allowPdfAttachments) return null;
|
|
23
|
+
const text = await extractPdfFileToText(file);
|
|
24
|
+
return { file, text, kind: 'pdf' as const };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = await readTextFile(file);
|
|
28
|
+
return { file, text, kind: 'text' as const };
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return items.filter((item): item is ChatAttachmentDropItem => item != null);
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
let workerConfigured = false;
|
|
2
|
+
|
|
3
|
+
async function configurePdfWorker(
|
|
4
|
+
pdfjs: typeof import('pdfjs-dist/legacy/build/pdf.mjs'),
|
|
5
|
+
): Promise<void> {
|
|
6
|
+
if (workerConfigured) return;
|
|
7
|
+
|
|
8
|
+
const version = pdfjs.version ?? '4.10.38';
|
|
9
|
+
pdfjs.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${version}/legacy/build/pdf.worker.min.mjs`;
|
|
10
|
+
workerConfigured = true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Best-effort plain text from PDF; pages separated with lightweight markdown headings. */
|
|
14
|
+
export async function extractPdfFileToText(file: File): Promise<string> {
|
|
15
|
+
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
16
|
+
await configurePdfWorker(pdfjs);
|
|
17
|
+
|
|
18
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
19
|
+
const doc = await pdfjs.getDocument({ data }).promise;
|
|
20
|
+
const pageTexts: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber += 1) {
|
|
23
|
+
const page = await doc.getPage(pageNumber);
|
|
24
|
+
const content = await page.getTextContent();
|
|
25
|
+
const pageText = content.items
|
|
26
|
+
.map(item => ('str' in item ? item.str : ''))
|
|
27
|
+
.join(' ')
|
|
28
|
+
.replace(/\s+/g, ' ')
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
if (pageText) {
|
|
32
|
+
pageTexts.push(`## Page ${pageNumber}\n\n${pageText}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return pageTexts.join('\n\n');
|
|
37
|
+
}
|
|
@@ -5,6 +5,10 @@ export type {
|
|
|
5
5
|
ChatChromeProps,
|
|
6
6
|
ChatChromeResizeHandleConfig,
|
|
7
7
|
} from './ChatChrome';
|
|
8
|
+
export {
|
|
9
|
+
TEXT_ATTACHMENT_ACCEPT_PARTS,
|
|
10
|
+
filterToTextAttachments,
|
|
11
|
+
} from './chatAttachmentAccept';
|
|
8
12
|
export { ChatSheet } from './ChatSheet/ChatSheet';
|
|
9
13
|
export { useChatPanelChromeModel } from './ChatSheet/useChatPanelChromeModel';
|
|
10
14
|
export type { ChatSheetActions, ChatSheetProps } from './ChatSheet/ChatSheet';
|
|
@@ -17,6 +21,7 @@ export { ChatPrompt } from './ChatPrompt';
|
|
|
17
21
|
export { ChatPresets } from './ChatPresets';
|
|
18
22
|
export type {
|
|
19
23
|
Chat as ChatType,
|
|
24
|
+
ChatAttachmentDropItem,
|
|
20
25
|
ChatSendMessagePayload,
|
|
21
26
|
ChatProps,
|
|
22
27
|
ChatPreset as ChatPresetType,
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
flex-direction column
|
|
6
6
|
gap var(--p-2)
|
|
7
7
|
|
|
8
|
+
.rootContained
|
|
9
|
+
position relative
|
|
10
|
+
width 100%
|
|
11
|
+
height 100%
|
|
12
|
+
|
|
13
|
+
.rootDragging
|
|
14
|
+
pointer-events auto
|
|
15
|
+
|
|
8
16
|
.dropArea
|
|
9
17
|
position relative
|
|
10
18
|
border 2px dashed var(--border)
|
|
@@ -42,8 +50,24 @@
|
|
|
42
50
|
border-color var(--primary-color)
|
|
43
51
|
background-color var(--page-color-alpha-800)
|
|
44
52
|
|
|
53
|
+
&.isDraggingContained
|
|
54
|
+
position absolute
|
|
55
|
+
top 0
|
|
56
|
+
left 0
|
|
57
|
+
width 100%
|
|
58
|
+
height 100%
|
|
59
|
+
z-index 1
|
|
60
|
+
display flex
|
|
61
|
+
align-items center
|
|
62
|
+
justify-content center
|
|
63
|
+
border-radius var(--p-4)
|
|
64
|
+
|
|
65
|
+
border-color var(--primary-color)
|
|
66
|
+
background-color var(--page-color-alpha-800)
|
|
67
|
+
|
|
45
68
|
&:hover:not(.disabled)
|
|
46
69
|
&.isDragging
|
|
70
|
+
&.isDraggingContained
|
|
47
71
|
border 2px dashed var(--muted-border)
|
|
48
72
|
|
|
49
73
|
:global(.dark) &
|
|
@@ -6,8 +6,11 @@ interface CssExports {
|
|
|
6
6
|
'error': string;
|
|
7
7
|
'fileInput': string;
|
|
8
8
|
'isDragging': string;
|
|
9
|
+
'isDraggingContained': string;
|
|
9
10
|
'label': string;
|
|
10
11
|
'root': string;
|
|
12
|
+
'rootContained': string;
|
|
13
|
+
'rootDragging': string;
|
|
11
14
|
}
|
|
12
15
|
export const cssExports: CssExports;
|
|
13
16
|
export default cssExports;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
3
|
|
|
3
4
|
import S from './DropZone.styl';
|
|
@@ -8,6 +9,8 @@ interface DropZoneBaseProps {
|
|
|
8
9
|
error?: string | null;
|
|
9
10
|
disabled?: boolean;
|
|
10
11
|
ghost?: boolean;
|
|
12
|
+
/** When `container`, drag overlay fills the parent bounds instead of the viewport. */
|
|
13
|
+
overlayScope?: 'viewport' | 'container';
|
|
11
14
|
id?: string;
|
|
12
15
|
className?: string;
|
|
13
16
|
}
|
|
@@ -59,39 +62,49 @@ export function DropZone(props: DropZoneProps) {
|
|
|
59
62
|
error,
|
|
60
63
|
disabled = false,
|
|
61
64
|
ghost = false,
|
|
65
|
+
overlayScope = 'viewport',
|
|
62
66
|
id,
|
|
63
67
|
className,
|
|
64
68
|
} = props;
|
|
65
69
|
const [isDragging, setIsDragging] = useState(false);
|
|
70
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
66
71
|
const dropAreaRef = useRef<HTMLDivElement>(null);
|
|
67
72
|
const inputId =
|
|
68
73
|
id || `dropzone-file-input-${Math.random().toString(36).substr(2, 9)}`;
|
|
69
74
|
|
|
70
|
-
const
|
|
71
|
-
(e: React.DragEvent<HTMLDivElement>) => {
|
|
72
|
-
e.preventDefault();
|
|
73
|
-
setIsDragging(false);
|
|
75
|
+
const dropProcessedRef = useRef(false);
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
const emitDroppedFiles = useCallback(
|
|
78
|
+
(files: FileList) => {
|
|
79
|
+
if (disabled || dropProcessedRef.current || files.length === 0) return;
|
|
80
|
+
|
|
81
|
+
const list = Array.from(files).filter(f => matchesAccept(f, accept));
|
|
82
|
+
if (list.length === 0) return;
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
matchesAccept(f, accept),
|
|
79
|
-
);
|
|
84
|
+
dropProcessedRef.current = true;
|
|
80
85
|
|
|
81
86
|
if (props.multiple === true) {
|
|
82
|
-
|
|
83
|
-
props.onFiles(list);
|
|
84
|
-
}
|
|
87
|
+
props.onFiles(list);
|
|
85
88
|
} else {
|
|
86
|
-
|
|
87
|
-
if (file) {
|
|
88
|
-
props.onFile(file);
|
|
89
|
-
}
|
|
89
|
+
props.onFile(list[0]!);
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
[accept, disabled, props],
|
|
93
93
|
);
|
|
94
94
|
|
|
95
|
+
const handleDrop = useCallback(
|
|
96
|
+
(e: React.DragEvent<HTMLDivElement>) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
setIsDragging(false);
|
|
100
|
+
|
|
101
|
+
if (disabled) return;
|
|
102
|
+
|
|
103
|
+
emitDroppedFiles(e.dataTransfer.files);
|
|
104
|
+
},
|
|
105
|
+
[disabled, emitDroppedFiles],
|
|
106
|
+
);
|
|
107
|
+
|
|
95
108
|
const handleFileInput = useCallback(
|
|
96
109
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
97
110
|
if (disabled) return;
|
|
@@ -116,12 +129,20 @@ export function DropZone(props: DropZoneProps) {
|
|
|
116
129
|
);
|
|
117
130
|
|
|
118
131
|
useEffect(() => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
const root = rootRef.current;
|
|
133
|
+
if (!root) return;
|
|
134
|
+
|
|
135
|
+
const targetElement =
|
|
136
|
+
overlayScope === 'container'
|
|
137
|
+
? root.parentElement
|
|
138
|
+
: ((root.closest('[role="dialog"]') as HTMLElement | null) ??
|
|
139
|
+
document.body);
|
|
140
|
+
|
|
141
|
+
if (!targetElement) return;
|
|
122
142
|
|
|
123
143
|
const handleGlobalDragOver = (e: DragEvent) => {
|
|
124
144
|
e.preventDefault();
|
|
145
|
+
dropProcessedRef.current = false;
|
|
125
146
|
setIsDragging(true);
|
|
126
147
|
};
|
|
127
148
|
|
|
@@ -136,30 +157,62 @@ export function DropZone(props: DropZoneProps) {
|
|
|
136
157
|
}
|
|
137
158
|
};
|
|
138
159
|
|
|
139
|
-
const
|
|
160
|
+
const handleGlobalDropPrevent = (e: DragEvent) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleGlobalDropFinalize = (e: DragEvent) => {
|
|
165
|
+
if (overlayScope === 'container' && e.dataTransfer?.files?.length) {
|
|
166
|
+
emitDroppedFiles(e.dataTransfer.files);
|
|
167
|
+
}
|
|
140
168
|
setIsDragging(false);
|
|
141
169
|
};
|
|
142
170
|
|
|
143
171
|
targetElement.addEventListener('dragover', handleGlobalDragOver);
|
|
144
172
|
targetElement.addEventListener('dragleave', handleGlobalDragLeave);
|
|
145
|
-
targetElement.addEventListener('drop',
|
|
173
|
+
targetElement.addEventListener('drop', handleGlobalDropPrevent, true);
|
|
174
|
+
targetElement.addEventListener('drop', handleGlobalDropFinalize);
|
|
146
175
|
|
|
147
176
|
return () => {
|
|
148
177
|
targetElement.removeEventListener('dragover', handleGlobalDragOver);
|
|
149
178
|
targetElement.removeEventListener('dragleave', handleGlobalDragLeave);
|
|
150
|
-
targetElement.removeEventListener('drop',
|
|
179
|
+
targetElement.removeEventListener('drop', handleGlobalDropPrevent, true);
|
|
180
|
+
targetElement.removeEventListener('drop', handleGlobalDropFinalize);
|
|
151
181
|
};
|
|
152
|
-
}, []);
|
|
182
|
+
}, [emitDroppedFiles, overlayScope]);
|
|
153
183
|
|
|
154
184
|
const shouldShowDropArea = !ghost || isDragging;
|
|
155
185
|
const multiple = props.multiple === true;
|
|
156
186
|
|
|
187
|
+
const isContainerOverlay = overlayScope === 'container';
|
|
188
|
+
|
|
157
189
|
return (
|
|
158
|
-
<div
|
|
190
|
+
<div
|
|
191
|
+
ref={rootRef}
|
|
192
|
+
className={cn(
|
|
193
|
+
S.root,
|
|
194
|
+
isContainerOverlay && S.rootContained,
|
|
195
|
+
isDragging && isContainerOverlay && S.rootDragging,
|
|
196
|
+
className,
|
|
197
|
+
)}
|
|
198
|
+
style={
|
|
199
|
+
isContainerOverlay
|
|
200
|
+
? { pointerEvents: isDragging ? 'auto' : 'none' }
|
|
201
|
+
: undefined
|
|
202
|
+
}
|
|
203
|
+
>
|
|
159
204
|
{shouldShowDropArea && (
|
|
160
205
|
<div
|
|
161
206
|
ref={dropAreaRef}
|
|
162
|
-
className={
|
|
207
|
+
className={cn(
|
|
208
|
+
S.dropArea,
|
|
209
|
+
isDragging
|
|
210
|
+
? isContainerOverlay
|
|
211
|
+
? S.isDraggingContained
|
|
212
|
+
: S.isDragging
|
|
213
|
+
: '',
|
|
214
|
+
disabled ? S.disabled : '',
|
|
215
|
+
)}
|
|
163
216
|
onDrop={handleDrop}
|
|
164
217
|
>
|
|
165
218
|
<input
|