@sybilion/uilib 1.3.10 → 1.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +52 -17
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.styl.js +2 -2
  3. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
  4. package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +3 -4
  5. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +20 -6
  6. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +2 -2
  7. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptAttachments.js +11 -0
  8. package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +1 -1
  9. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +4 -1
  10. package/dist/esm/components/ui/Chat/chatAttachmentAccept.js +54 -0
  11. package/dist/esm/components/ui/Chat/chatAttachmentExtract.js +26 -0
  12. package/dist/esm/components/ui/Chat/chatPdfExtract.js +31 -0
  13. package/dist/esm/components/ui/DropZone/DropZone.js +50 -21
  14. package/dist/esm/components/ui/DropZone/DropZone.styl.js +2 -2
  15. package/dist/esm/components/ui/FileChip/FileChip.js +26 -0
  16. package/dist/esm/components/ui/FileChip/FileChip.styl.js +7 -0
  17. package/dist/esm/index.js +2 -0
  18. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +14 -1
  19. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  20. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +9 -2
  21. package/dist/esm/types/src/components/ui/Chat/ChatChrome/index.d.ts +1 -1
  22. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
  23. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.d.ts +8 -0
  24. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +7 -1
  25. package/dist/esm/types/src/components/ui/Chat/chatAttachmentAccept.d.ts +8 -0
  26. package/dist/esm/types/src/components/ui/Chat/chatAttachmentExtract.d.ts +2 -0
  27. package/dist/esm/types/src/components/ui/Chat/chatPdfExtract.d.ts +2 -0
  28. package/dist/esm/types/src/components/ui/Chat/index.d.ts +2 -1
  29. package/dist/esm/types/src/components/ui/DropZone/DropZone.d.ts +2 -0
  30. package/dist/esm/types/src/components/ui/FileChip/FileChip.d.ts +2 -0
  31. package/dist/esm/types/src/components/ui/FileChip/FileChip.types.d.ts +10 -0
  32. package/dist/esm/types/src/components/ui/FileChip/index.d.ts +2 -0
  33. package/dist/esm/types/src/docs/pages/ChatAttachmentsDropzonePage.d.ts +1 -0
  34. package/dist/esm/types/src/docs/pages/FileChipPage.d.ts +1 -0
  35. package/dist/esm/types/src/index.d.ts +1 -0
  36. package/package.json +2 -1
  37. package/src/components/ui/Chat/Chat.types.ts +15 -1
  38. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl +20 -0
  39. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl.d.ts +2 -0
  40. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +94 -4
  41. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +18 -2
  42. package/src/components/ui/Chat/ChatChrome/index.ts +1 -0
  43. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +0 -56
  44. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl.d.ts +0 -5
  45. package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +6 -15
  46. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +15 -16
  47. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl.d.ts +3 -1
  48. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +65 -17
  49. package/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.tsx +34 -0
  50. package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +12 -11
  51. package/src/components/ui/Chat/ChatSheet/ChatSheet.styl.d.ts +13 -13
  52. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +14 -0
  53. package/src/components/ui/Chat/chat-preset-utils.ts +4 -1
  54. package/src/components/ui/Chat/chatAttachmentAccept.ts +70 -0
  55. package/src/components/ui/Chat/chatAttachmentExtract.ts +33 -0
  56. package/src/components/ui/Chat/chatPdfExtract.ts +37 -0
  57. package/src/components/ui/Chat/index.ts +5 -0
  58. package/src/components/ui/DropZone/DropZone.styl +24 -0
  59. package/src/components/ui/DropZone/DropZone.styl.d.ts +3 -0
  60. package/src/components/ui/DropZone/DropZone.tsx +77 -24
  61. package/src/components/ui/FileChip/FileChip.styl +108 -0
  62. package/src/components/ui/FileChip/FileChip.styl.d.ts +12 -0
  63. package/src/components/ui/FileChip/FileChip.tsx +93 -0
  64. package/src/components/ui/FileChip/FileChip.types.ts +11 -0
  65. package/src/components/ui/FileChip/index.ts +2 -0
  66. package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +162 -0
  67. package/src/docs/pages/FileChipPage.tsx +50 -0
  68. package/src/docs/registry.ts +12 -0
  69. package/src/index.ts +1 -0
@@ -57,9 +57,15 @@ export type ScriptCompletePayload = {
57
57
  presetId: string;
58
58
  answers: Record<string, string>;
59
59
  };
60
+ export type ChatAttachmentDropItem = {
61
+ file: File;
62
+ /** UTF-8 text for native text files; PDF yields extracted text. */
63
+ text: string;
64
+ kind: 'text' | 'pdf';
65
+ };
60
66
  export interface ChatPromptProps {
61
67
  className?: string;
62
- onSubmit: (message: string) => void;
68
+ onSubmit: (message: string, attachments?: ChatAttachmentDropItem[]) => void;
63
69
  placeholder?: string;
64
70
  presets?: ChatPreset[];
65
71
  disabled?: boolean;
@@ -68,6 +74,13 @@ export interface ChatPromptProps {
68
74
  prefillMessage?: string | null;
69
75
  /** Disclaimer above composer; default true. ChatChrome sets false when thread has messages. */
70
76
  showNotice?: boolean;
77
+ /** Staged files shown above the composer until send. */
78
+ attachments?: ChatAttachmentDropItem[];
79
+ onRemoveAttachment?: (index: number) => void;
80
+ /** HTML `accept` for the attach file picker; set with `onAttachmentFiles`. */
81
+ attachmentAccept?: string;
82
+ /** Called when the user picks files via the attach button. */
83
+ onAttachmentFiles?: (files: File[]) => void;
71
84
  }
72
85
  export interface ChatMessageProps {
73
86
  role: MessageRole;
@@ -1,2 +1,2 @@
1
1
  import type { ChatChromeProps } from './ChatChrome.types';
2
- export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showBranchActionsRow, showSyntheticBranchButtons, unusedBranchKeys, isScriptComplete, onGenerateDashboard, generatingDashboard, onGenerateDashboardClick, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, scriptContinueLabel, onScriptContinue, renderMessageChart, showBranchActionsRow, showSyntheticBranchButtons, unusedBranchKeys, isScriptComplete, onGenerateDashboard, generatingDashboard, onGenerateDashboardClick, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type { RefObject } from 'react';
2
- import type { Message } from '#uilib/components/ui/Chat/Chat.types';
2
+ import type { ChatAttachmentDropItem, Message } from '#uilib/components/ui/Chat/Chat.types';
3
3
  import type { ChatEmptyStateProps } from '#uilib/components/ui/Chat/ChatEmptyState/ChatEmptyState.types';
4
4
  import type { ChatPresetsLayout } from '#uilib/components/ui/Chat/ChatPresets';
5
5
  import type { ScrollRef } from '@homecode/ui';
@@ -10,6 +10,7 @@ export type ChatChromeResizeHandleConfig = {
10
10
  onDragWidth: (rawPx: number) => void;
11
11
  onDragComplete: (finalRawPx: number) => void;
12
12
  };
13
+ export type { ChatAttachmentDropItem };
13
14
  export interface ChatChromeProps {
14
15
  showResizeHandle: boolean;
15
16
  resizeHandle: ChatChromeResizeHandleConfig | undefined;
@@ -35,10 +36,16 @@ export interface ChatChromeProps {
35
36
  isLastMessageFromUser: boolean;
36
37
  scrollRef: RefObject<ScrollRef | null>;
37
38
  effectiveScopeId: string;
38
- onPromptSubmit: (message: string) => void | Promise<void>;
39
+ onPromptSubmit: (message: string, attachments?: ChatAttachmentDropItem[]) => void | Promise<void>;
39
40
  onChatDeleted: (sessionId: string) => void;
40
41
  /** `?prompt=` deep link text for one-shot composer pre-fill. */
41
42
  promptPrefill?: string | null;
42
43
  footerClassName?: string;
43
44
  emptyState?: ChatEmptyStateProps;
45
+ /** MIME types / extensions (filtered to text-only allowlist). Enables dropzone when non-empty. */
46
+ allowedAttachments?: readonly string[];
47
+ /** When true, PDF files are accepted and parsed to plain text on drop. */
48
+ allowPdfAttachments?: boolean;
49
+ /** Optional hook when attachments are sent with a message. */
50
+ onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
44
51
  }
@@ -1,2 +1,2 @@
1
1
  export { ChatChrome } from './ChatChrome';
2
- export type { ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome.types';
2
+ export type { ChatAttachmentDropItem, ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome.types';
@@ -1,2 +1,2 @@
1
1
  import type { ChatPromptProps } from '../Chat.types';
2
- export declare function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, showNotice, }: ChatPromptProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, attachments, onRemoveAttachment, disabled, attachmentAccept, onAttachmentFiles, }: ChatPromptProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import type { ChatAttachmentDropItem } from '../Chat.types';
2
+ type ChatPromptAttachmentsProps = {
3
+ attachments: ChatAttachmentDropItem[];
4
+ onRemove: (index: number) => void;
5
+ disabled?: boolean;
6
+ };
7
+ export declare function ChatPromptAttachments({ attachments, onRemove, disabled, }: ChatPromptAttachmentsProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { ChatPreset, type ScriptCompletePayload } from '#uilib/components/ui/Chat/Chat.types';
2
2
  import type { ChatChromeProps } from '../ChatChrome';
3
+ import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
3
4
  import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
4
5
  export type UseChatPanelChromeModelInput = {
5
6
  /** When true, skip sidebar chat slot, URL `?chat=`, and portal behavior (e.g. page main content). */
@@ -16,6 +17,11 @@ export type UseChatPanelChromeModelInput = {
16
17
  renderMessageChart?: () => React.ReactNode;
17
18
  /** Forwarded to `ChatChrome` when the thread is empty. */
18
19
  emptyState?: ChatEmptyStateProps;
20
+ /** MIME types / extensions for text-only chat attachments (filtered by uilib allowlist). */
21
+ allowedAttachments?: readonly string[];
22
+ /** When true, PDF drops are accepted and parsed to plain text. */
23
+ allowPdfAttachments?: boolean;
24
+ onAttachmentsDropped?: (items: ChatAttachmentDropItem[]) => void | Promise<void>;
19
25
  };
20
26
  export type UseChatPanelChromeModelResult = {
21
27
  chromeProps: ChatChromeProps;
@@ -25,4 +31,4 @@ export type UseChatPanelChromeModelResult = {
25
31
  newChat: () => void;
26
32
  chatPanelContainer: HTMLElement | null;
27
33
  };
28
- export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
34
+ export declare function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onScriptComplete, onGenerateDashboard, renderMessageChart, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, }: UseChatPanelChromeModelInput): UseChatPanelChromeModelResult;
@@ -0,0 +1,8 @@
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"];
3
+ export declare const PDF_ATTACHMENT_ACCEPT_PARTS: readonly ["application/pdf", ".pdf"];
4
+ /** Keep only tokens from `parts` that appear in the text attachment allowlist. */
5
+ export declare function filterToTextAttachments(parts: readonly string[] | undefined): string[];
6
+ export declare function buildAcceptAttr(filteredTextParts: readonly string[], allowPdf: boolean): string;
7
+ export declare function isPdfFile(file: File): boolean;
8
+ export declare function isAttachmentsDropzoneEnabled(allowedAttachments: readonly string[] | undefined, allowPdfAttachments: boolean | undefined): boolean;
@@ -0,0 +1,2 @@
1
+ import type { ChatAttachmentDropItem } from './Chat.types';
2
+ export declare function extractChatAttachmentItems(files: File[], allowPdfAttachments: boolean): Promise<ChatAttachmentDropItem[]>;
@@ -0,0 +1,2 @@
1
+ /** Best-effort plain text from PDF; pages separated with lightweight markdown headings. */
2
+ export declare function extractPdfFileToText(file: File): Promise<string>;
@@ -2,6 +2,7 @@ export { Chat } from './Chat';
2
2
  export { usedPresetIdsFromMessages } from './chat-preset-utils';
3
3
  export { ChatChrome } from './ChatChrome';
4
4
  export type { ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome';
5
+ export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments, } from './chatAttachmentAccept';
5
6
  export { ChatSheet } from './ChatSheet/ChatSheet';
6
7
  export { useChatPanelChromeModel } from './ChatSheet/useChatPanelChromeModel';
7
8
  export type { ChatSheetActions, ChatSheetProps } from './ChatSheet/ChatSheet';
@@ -9,6 +10,6 @@ export type { UseChatPanelChromeModelInput, UseChatPanelChromeModelResult, } fro
9
10
  export { ChatMessage } from './ChatMessage';
10
11
  export { ChatPrompt } from './ChatPrompt';
11
12
  export { ChatPresets } from './ChatPresets';
12
- export type { Chat as ChatType, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserCsvAttachment, } from './Chat.types';
13
+ export type { Chat as ChatType, ChatAttachmentDropItem, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserCsvAttachment, } from './Chat.types';
13
14
  export { MessageRole } from './Chat.types';
14
15
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -4,6 +4,8 @@ interface DropZoneBaseProps {
4
4
  error?: string | null;
5
5
  disabled?: boolean;
6
6
  ghost?: boolean;
7
+ /** When `container`, drag overlay fills the parent bounds instead of the viewport. */
8
+ overlayScope?: 'viewport' | 'container';
7
9
  id?: string;
8
10
  className?: string;
9
11
  }
@@ -0,0 +1,2 @@
1
+ import type { FileChipProps } from './FileChip.types';
2
+ export declare function FileChip({ name, format, hint, onClick, onRemove, className, }: FileChipProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ export type FileChipFormat = 'csv' | 'pdf' | 'text';
2
+ export type FileChipProps = {
3
+ name: string;
4
+ format: FileChipFormat;
5
+ hint: string;
6
+ onClick?: () => void;
7
+ onRemove?: () => void;
8
+ disabled?: boolean;
9
+ className?: string;
10
+ };
@@ -0,0 +1,2 @@
1
+ export { FileChip } from './FileChip';
2
+ export type { FileChipProps, FileChipFormat } from './FileChip.types';
@@ -0,0 +1 @@
1
+ export default function ChatAttachmentsDropzonePage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export default function FileChipPage(): import("react/jsx-runtime").JSX.Element;
@@ -24,6 +24,7 @@ export * from './components/ui/Dialog';
24
24
  export * from './components/ui/Drawer';
25
25
  export * from './components/ui/DropdownMenu';
26
26
  export * from './components/ui/DropZone/DropZone';
27
+ export * from './components/ui/FileChip';
27
28
  export * from './components/ui/FlickeringGrid';
28
29
  export * from './components/ui/Foldable';
29
30
  export * from './components/ui/Gap/Gap';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -103,6 +103,7 @@
103
103
  "lightweight-charts": "^5.0.9",
104
104
  "lucide-react": "^0.546.0",
105
105
  "motion": "^12.23.12",
106
+ "pdfjs-dist": "^4.10.38",
106
107
  "recharts": "^3.2.1",
107
108
  "standard-version": "^9.5.0",
108
109
  "style-inject": "^0.3.0",
@@ -67,9 +67,16 @@ export type ScriptCompletePayload = {
67
67
  answers: Record<string, string>;
68
68
  };
69
69
 
70
+ export type ChatAttachmentDropItem = {
71
+ file: File;
72
+ /** UTF-8 text for native text files; PDF yields extracted text. */
73
+ text: string;
74
+ kind: 'text' | 'pdf';
75
+ };
76
+
70
77
  export interface ChatPromptProps {
71
78
  className?: string;
72
- onSubmit: (message: string) => void;
79
+ onSubmit: (message: string, attachments?: ChatAttachmentDropItem[]) => void;
73
80
  placeholder?: string;
74
81
  presets?: ChatPreset[];
75
82
  disabled?: boolean;
@@ -78,6 +85,13 @@ export interface ChatPromptProps {
78
85
  prefillMessage?: string | null;
79
86
  /** Disclaimer above composer; default true. ChatChrome sets false when thread has messages. */
80
87
  showNotice?: boolean;
88
+ /** Staged files shown above the composer until send. */
89
+ attachments?: ChatAttachmentDropItem[];
90
+ onRemoveAttachment?: (index: number) => void;
91
+ /** HTML `accept` for the attach file picker; set with `onAttachmentFiles`. */
92
+ attachmentAccept?: string;
93
+ /** Called when the user picks files via the attach button. */
94
+ onAttachmentFiles?: (files: File[]) => void;
81
95
  }
82
96
 
83
97
  export interface ChatMessageProps {
@@ -57,6 +57,11 @@
57
57
  flex-direction column
58
58
  position relative
59
59
 
60
+ .attachmentDropzone
61
+ position absolute
62
+ inset 0
63
+ z-index 200
64
+
60
65
  .scrollWrapper
61
66
  position relative
62
67
  flex 1
@@ -102,6 +107,21 @@
102
107
  border-top 1px solid var(--border)
103
108
  box-shadow 0 0 20px 16px var(--background)
104
109
 
110
+ .notice
111
+ position absolute
112
+ top calc(-1 * var(--p-7))
113
+ left 0
114
+ right 0
115
+ margin-bottom var(--p-1)
116
+
117
+ font-size var(--text-xs)
118
+ text-align center
119
+ color var(--muted-foreground)
120
+ pointer-events none
121
+
122
+ @media (max-width MOBILE)
123
+ font-size 10px
124
+
105
125
  .loader
106
126
  // z-index 1
107
127
  // position absolute
@@ -1,12 +1,14 @@
1
1
  // This file is automatically generated.
2
2
  // Please do not change this file!
3
3
  interface CssExports {
4
+ 'attachmentDropzone': string;
4
5
  'branchBtnWrap': string;
5
6
  'branchRow': string;
6
7
  'chatResizeHandle': string;
7
8
  'content': string;
8
9
  'footer': string;
9
10
  'loader': string;
11
+ 'notice': string;
10
12
  'panelClose': string;
11
13
  'panelHeader': string;
12
14
  'root': string;
@@ -1,5 +1,5 @@
1
1
  import cn from 'classnames';
2
- import { useEffect } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
3
 
4
4
  import {
5
5
  displayLabelForBranchKeyFromMessages,
@@ -10,9 +10,17 @@ import { Scroll } from '@homecode/ui';
10
10
  import { ChartLineIcon, PaperPlaneRightIcon, X } from '@phosphor-icons/react';
11
11
 
12
12
  import { Button } from '../../Button';
13
+ import { DropZone } from '../../DropZone/DropZone';
13
14
  import { PanelResizeHandle } from '../../Sidebar/Sidebar';
14
15
  import SidebarStem from '../../Sidebar/Sidebar.styl';
15
16
  import { Chat } from '../Chat';
17
+ import type { ChatAttachmentDropItem } from '../Chat.types';
18
+ import {
19
+ buildAcceptAttr,
20
+ filterToTextAttachments,
21
+ isAttachmentsDropzoneEnabled,
22
+ } from '../chatAttachmentAccept';
23
+ import { extractChatAttachmentItems } from '../chatAttachmentExtract';
16
24
  import S from './ChatChrome.styl';
17
25
  import type { ChatChromeProps } from './ChatChrome.types';
18
26
 
@@ -45,7 +53,65 @@ export function ChatChrome({
45
53
  promptPrefill,
46
54
  footerClassName,
47
55
  emptyState,
56
+ allowedAttachments,
57
+ allowPdfAttachments = false,
58
+ onAttachmentsDropped,
48
59
  }: ChatChromeProps) {
60
+ const filteredAllowedAttachments = useMemo(
61
+ () => filterToTextAttachments(allowedAttachments),
62
+ [allowedAttachments],
63
+ );
64
+ const attachmentsDropzoneEnabled = isAttachmentsDropzoneEnabled(
65
+ allowedAttachments,
66
+ allowPdfAttachments,
67
+ );
68
+ const attachmentAccept = useMemo(
69
+ () => buildAcceptAttr(filteredAllowedAttachments, allowPdfAttachments),
70
+ [filteredAllowedAttachments, allowPdfAttachments],
71
+ );
72
+ const [pendingAttachments, setPendingAttachments] = useState<
73
+ ChatAttachmentDropItem[]
74
+ >([]);
75
+ const [isExtractingAttachments, setIsExtractingAttachments] = useState(false);
76
+ const promptBusy = isLoading || isExtractingAttachments;
77
+
78
+ const handleAttachmentFiles = useCallback(
79
+ (files: File[]) => {
80
+ if (promptBusy || files.length === 0) return;
81
+
82
+ setIsExtractingAttachments(true);
83
+ void extractChatAttachmentItems(files, allowPdfAttachments)
84
+ .then(items => {
85
+ if (items.length > 0) {
86
+ setPendingAttachments(prev => [...prev, ...items]);
87
+ }
88
+ })
89
+ .finally(() => setIsExtractingAttachments(false));
90
+ },
91
+ [allowPdfAttachments, promptBusy],
92
+ );
93
+
94
+ const handleRemoveAttachment = useCallback((index: number) => {
95
+ setPendingAttachments(prev => prev.filter((_, i) => i !== index));
96
+ }, []);
97
+
98
+ const handlePromptSubmitWithAttachments = useCallback(
99
+ (message: string, submittedAttachments?: ChatAttachmentDropItem[]) => {
100
+ const trimmed = message.trim();
101
+ const attachments = submittedAttachments ?? [];
102
+ if (!trimmed && attachments.length === 0) return;
103
+
104
+ void onPromptSubmit(trimmed, attachments);
105
+
106
+ if (attachments.length > 0 && onAttachmentsDropped) {
107
+ void onAttachmentsDropped(attachments);
108
+ }
109
+
110
+ setPendingAttachments([]);
111
+ },
112
+ [onAttachmentsDropped, onPromptSubmit],
113
+ );
114
+
49
115
  useEffect(() => {
50
116
  if (isEmpty) return;
51
117
 
@@ -91,6 +157,18 @@ export function ChatChrome({
91
157
  ) : null}
92
158
  </div>
93
159
  <div className={S.content}>
160
+ {attachmentsDropzoneEnabled ? (
161
+ <DropZone
162
+ accept={attachmentAccept}
163
+ label="Drop text files to attach"
164
+ multiple
165
+ ghost
166
+ overlayScope="container"
167
+ disabled={promptBusy}
168
+ className={S.attachmentDropzone}
169
+ onFiles={handleAttachmentFiles}
170
+ />
171
+ ) : null}
94
172
  <Chat
95
173
  isEmpty={isEmpty}
96
174
  scopeId={effectiveScopeId}
@@ -195,11 +273,23 @@ export function ChatChrome({
195
273
  )}
196
274
 
197
275
  <div className={cn(S.footer, footerClassName)}>
276
+ {isEmpty ? (
277
+ <div className={S.notice}>
278
+ Forecast Assistant can make mistakes.
279
+ </div>
280
+ ) : null}
198
281
  <Chat.Prompt
199
- onSubmit={onPromptSubmit}
200
- disabled={isLoading}
282
+ onSubmit={handlePromptSubmitWithAttachments}
283
+ disabled={promptBusy}
284
+ attachments={pendingAttachments}
285
+ onRemoveAttachment={handleRemoveAttachment}
201
286
  prefillMessage={promptPrefill ?? undefined}
202
- showNotice={isEmpty}
287
+ attachmentAccept={
288
+ attachmentsDropzoneEnabled ? attachmentAccept : undefined
289
+ }
290
+ onAttachmentFiles={
291
+ attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined
292
+ }
203
293
  />
204
294
  </div>
205
295
  </Chat>
@@ -1,6 +1,9 @@
1
1
  import type { RefObject } from 'react';
2
2
 
3
- import type { Message } from '#uilib/components/ui/Chat/Chat.types';
3
+ import type {
4
+ ChatAttachmentDropItem,
5
+ Message,
6
+ } from '#uilib/components/ui/Chat/Chat.types';
4
7
  import type { ChatEmptyStateProps } from '#uilib/components/ui/Chat/ChatEmptyState/ChatEmptyState.types';
5
8
  import type { ChatPresetsLayout } from '#uilib/components/ui/Chat/ChatPresets';
6
9
  import type { ScrollRef } from '@homecode/ui';
@@ -13,6 +16,8 @@ export type ChatChromeResizeHandleConfig = {
13
16
  onDragComplete: (finalRawPx: number) => void;
14
17
  };
15
18
 
19
+ export type { ChatAttachmentDropItem };
20
+
16
21
  export interface ChatChromeProps {
17
22
  showResizeHandle: boolean;
18
23
  resizeHandle: ChatChromeResizeHandleConfig | undefined;
@@ -40,10 +45,21 @@ export interface ChatChromeProps {
40
45
  isLastMessageFromUser: boolean;
41
46
  scrollRef: RefObject<ScrollRef | null>;
42
47
  effectiveScopeId: string;
43
- onPromptSubmit: (message: string) => void | Promise<void>;
48
+ onPromptSubmit: (
49
+ message: string,
50
+ attachments?: ChatAttachmentDropItem[],
51
+ ) => void | Promise<void>;
44
52
  onChatDeleted: (sessionId: string) => void;
45
53
  /** `?prompt=` deep link text for one-shot composer pre-fill. */
46
54
  promptPrefill?: string | null;
47
55
  footerClassName?: string;
48
56
  emptyState?: ChatEmptyStateProps;
57
+ /** MIME types / extensions (filtered to text-only allowlist). Enables dropzone when non-empty. */
58
+ allowedAttachments?: readonly string[];
59
+ /** When true, PDF files are accepted and parsed to plain text on drop. */
60
+ allowPdfAttachments?: boolean;
61
+ /** Optional hook when attachments are sent with a message. */
62
+ onAttachmentsDropped?: (
63
+ items: ChatAttachmentDropItem[],
64
+ ) => void | Promise<void>;
49
65
  }
@@ -1,5 +1,6 @@
1
1
  export { ChatChrome } from './ChatChrome';
2
2
  export type {
3
+ ChatAttachmentDropItem,
3
4
  ChatChromeProps,
4
5
  ChatChromeResizeHandleConfig,
5
6
  } from './ChatChrome.types';
@@ -35,62 +35,6 @@
35
35
  :global(.dark) &
36
36
  background-color var(--sb-gray-800)
37
37
 
38
- .userCsvCard
39
- appearance none
40
- border 0
41
- margin 0
42
- font inherit
43
- display flex
44
- align-items center
45
- gap var(--p-2)
46
- padding var(--p-3)
47
- padding-right var(--p-4)
48
- background-color var(--background)
49
- box-shadow 0 0 0 1px var(--border)
50
- border-radius var(--p-3)
51
- width fit-content
52
- max-width 100%
53
- text-align left
54
- cursor pointer
55
- transition background-color 150ms
56
-
57
- &:hover
58
- background-color var(--sb-gray-50)
59
-
60
- &:focus-visible
61
- outline 2px solid var(--ring)
62
- outline-offset 2px
63
-
64
- :global(.dark) &
65
- background-color var(--sb-gray-800)
66
-
67
- &:hover
68
- background-color var(--sb-gray-900)
69
-
70
- .userCsvCardIcon
71
- display flex
72
- align-items center
73
- justify-content center
74
- width 32px
75
- height 32px
76
- flex-shrink 0
77
-
78
- .userCsvCardContent
79
- display flex
80
- flex-direction column
81
- flex 1
82
- min-width 0
83
-
84
- .userCsvCardTitle
85
- font-size var(--text-sm)
86
- font-weight 500
87
- line-height 1.4
88
-
89
- .userCsvCardSubtitle
90
- font-size var(--text-xs)
91
- color var(--muted-foreground)
92
- line-height 1.4
93
-
94
38
  .role-system
95
39
  align-items center
96
40
 
@@ -18,11 +18,6 @@ interface CssExports {
18
18
  'scrollHorizontal': string;
19
19
  'text': string;
20
20
  'userColumn': string;
21
- 'userCsvCard': string;
22
- 'userCsvCardContent': string;
23
- 'userCsvCardIcon': string;
24
- 'userCsvCardSubtitle': string;
25
- 'userCsvCardTitle': string;
26
21
  }
27
22
  export const cssExports: CssExports;
28
23
  export default cssExports;
@@ -1,8 +1,7 @@
1
+ import { FileChip } from '#uilib/components/ui/FileChip';
1
2
  import { downloadTextFile } from '#uilib/utils/downloadTextFile';
2
3
 
3
- import { CsvIcon } from '../../../icons/CsvIcon/CsvIcon';
4
4
  import type { UserCsvAttachment } from '../Chat.types';
5
- import S from './ChatMessage.styl';
6
5
 
7
6
  const CSV_DOWNLOAD_HINT = 'Download .CSV file';
8
7
 
@@ -12,10 +11,10 @@ export function UserCsvAttachmentBubble({
12
11
  attachment: UserCsvAttachment;
13
12
  }) {
14
13
  return (
15
- <button
16
- type="button"
17
- className={S.userCsvCard}
18
- aria-label={`${CSV_DOWNLOAD_HINT}: ${attachment.displayName}`}
14
+ <FileChip
15
+ name={attachment.displayName}
16
+ format="csv"
17
+ hint={CSV_DOWNLOAD_HINT}
19
18
  onClick={() =>
20
19
  downloadTextFile(
21
20
  attachment.content,
@@ -23,14 +22,6 @@ export function UserCsvAttachmentBubble({
23
22
  'text/csv;charset=utf-8',
24
23
  )
25
24
  }
26
- >
27
- <div className={S.userCsvCardIcon}>
28
- <CsvIcon size={32} />
29
- </div>
30
- <div className={S.userCsvCardContent}>
31
- <div className={S.userCsvCardTitle}>{attachment.displayName}</div>
32
- <div className={S.userCsvCardSubtitle}>{CSV_DOWNLOAD_HINT}</div>
33
- </div>
34
- </button>
25
+ />
35
26
  );
36
27
  }
@@ -19,20 +19,12 @@ 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)
22
+ .fileInput
23
+ display none
28
24
 
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
25
+ .attachButton
26
+ flex-shrink 0
27
+ align-self flex-end
36
28
 
37
29
  .input
38
30
  flex 1
@@ -76,6 +68,13 @@ INPUT_MAX_HEIGHT = 200px
76
68
  right -100%
77
69
  bottom -100%
78
70
 
79
- .attachButton
80
- background-color var(--page-color)
81
- box-shadow 0 0 20px var(--background)
71
+ .attachments
72
+ display flex
73
+ flex-wrap wrap
74
+ gap var(--p-2)
75
+ margin-bottom var(--p-2)
76
+
77
+ .attachmentItem
78
+ flex 1 1 300px
79
+ max-width 300px
80
+ min-width 0
@@ -2,9 +2,11 @@
2
2
  // Please do not change this file!
3
3
  interface CssExports {
4
4
  'attachButton': string;
5
+ 'attachmentItem': string;
6
+ 'attachments': string;
5
7
  'composer': string;
8
+ 'fileInput': string;
6
9
  'input': string;
7
- 'notice': string;
8
10
  'root': string;
9
11
  'submitColumn': string;
10
12
  }