@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.
Files changed (74) hide show
  1. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +4 -2
  2. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.js +52 -17
  3. package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.styl.js +2 -2
  4. package/dist/esm/components/ui/Chat/ChatMessage/ChatMessage.styl.js +2 -2
  5. package/dist/esm/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.js +3 -4
  6. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.js +6 -4
  7. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPrompt.styl.js +2 -2
  8. package/dist/esm/components/ui/Chat/ChatPrompt/ChatPromptAttachments.js +11 -0
  9. package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +1 -1
  10. package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +4 -1
  11. package/dist/esm/components/ui/Chat/chatAttachmentAccept.js +54 -0
  12. package/dist/esm/components/ui/Chat/chatAttachmentExtract.js +26 -0
  13. package/dist/esm/components/ui/Chat/chatPdfExtract.js +31 -0
  14. package/dist/esm/components/ui/DropZone/DropZone.js +50 -21
  15. package/dist/esm/components/ui/DropZone/DropZone.styl.js +2 -2
  16. package/dist/esm/components/ui/FileChip/FileChip.js +26 -0
  17. package/dist/esm/components/ui/FileChip/FileChip.styl.js +7 -0
  18. package/dist/esm/index.js +2 -0
  19. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
  20. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -0
  21. package/dist/esm/types/src/components/ui/Chat/Chat.types.d.ts +10 -1
  22. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
  23. package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +9 -2
  24. package/dist/esm/types/src/components/ui/Chat/ChatChrome/index.d.ts +1 -1
  25. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPrompt.d.ts +1 -1
  26. package/dist/esm/types/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.d.ts +8 -0
  27. package/dist/esm/types/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.d.ts +7 -1
  28. package/dist/esm/types/src/components/ui/Chat/chatAttachmentAccept.d.ts +8 -0
  29. package/dist/esm/types/src/components/ui/Chat/chatAttachmentExtract.d.ts +2 -0
  30. package/dist/esm/types/src/components/ui/Chat/chatPdfExtract.d.ts +2 -0
  31. package/dist/esm/types/src/components/ui/Chat/index.d.ts +2 -1
  32. package/dist/esm/types/src/components/ui/DropZone/DropZone.d.ts +2 -0
  33. package/dist/esm/types/src/components/ui/FileChip/FileChip.d.ts +2 -0
  34. package/dist/esm/types/src/components/ui/FileChip/FileChip.types.d.ts +10 -0
  35. package/dist/esm/types/src/components/ui/FileChip/index.d.ts +2 -0
  36. package/dist/esm/types/src/docs/pages/ChatAttachmentsDropzonePage.d.ts +1 -0
  37. package/dist/esm/types/src/docs/pages/FileChipPage.d.ts +1 -0
  38. package/dist/esm/types/src/index.d.ts +1 -0
  39. package/package.json +2 -1
  40. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +4 -1
  41. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -0
  42. package/src/components/ui/Chat/Chat.types.ts +11 -1
  43. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl +20 -0
  44. package/src/components/ui/Chat/ChatChrome/ChatChrome.styl.d.ts +2 -0
  45. package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +88 -4
  46. package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +18 -2
  47. package/src/components/ui/Chat/ChatChrome/index.ts +1 -0
  48. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl +0 -56
  49. package/src/components/ui/Chat/ChatMessage/ChatMessage.styl.d.ts +0 -5
  50. package/src/components/ui/Chat/ChatMessage/UserCsvAttachmentBubble.tsx +6 -15
  51. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl +11 -15
  52. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.styl.d.ts +2 -1
  53. package/src/components/ui/Chat/ChatPrompt/ChatPrompt.tsx +17 -8
  54. package/src/components/ui/Chat/ChatPrompt/ChatPromptAttachments.tsx +34 -0
  55. package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +12 -11
  56. package/src/components/ui/Chat/ChatSheet/ChatSheet.styl.d.ts +13 -13
  57. package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +14 -0
  58. package/src/components/ui/Chat/chat-preset-utils.ts +4 -1
  59. package/src/components/ui/Chat/chatAttachmentAccept.ts +70 -0
  60. package/src/components/ui/Chat/chatAttachmentExtract.ts +33 -0
  61. package/src/components/ui/Chat/chatPdfExtract.ts +37 -0
  62. package/src/components/ui/Chat/index.ts +5 -0
  63. package/src/components/ui/DropZone/DropZone.styl +24 -0
  64. package/src/components/ui/DropZone/DropZone.styl.d.ts +3 -0
  65. package/src/components/ui/DropZone/DropZone.tsx +77 -24
  66. package/src/components/ui/FileChip/FileChip.styl +108 -0
  67. package/src/components/ui/FileChip/FileChip.styl.d.ts +12 -0
  68. package/src/components/ui/FileChip/FileChip.tsx +93 -0
  69. package/src/components/ui/FileChip/FileChip.types.ts +11 -0
  70. package/src/components/ui/FileChip/index.ts +2 -0
  71. package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +162 -0
  72. package/src/docs/pages/FileChipPage.tsx +50 -0
  73. package/src/docs/registry.ts +12 -0
  74. package/src/index.ts +1 -0
@@ -1,3 +1,3 @@
1
1
  import { ChartAreaInteractiveProps } from './ChartAreaInteractive.types';
2
2
  export declare const chartConfig: {};
3
- export declare function ChartAreaInteractive({ className, chartContainerClassName, legendClassName, xAxisClassName, yAxisClassName, timeRange, onTimeRangeChange, pinMonth, onPinMonthChange, onPreviewMonthChange, chartData, forecastData, error, loading, isDarkTheme, footerActions, mode, selectedForecast, upperQuantiles, lowerQuantiles, selectedLowerQuantile, selectedUpperQuantile, onLowerQuantileChange, onUpperQuantileChange, lowerThreshold, upperThreshold, onLowerThresholdChange, onUpperThresholdChange, discreteThresholdValues, headerActions, excludeLegendIds, loadingAnalyses, onLegendClick, onAnalysisSelect, disableTimeRangeSelector, forecastLineStyle, selectedAnalysisId, chartRenderId, toggleLegendSeries, ensureAnalysisSeriesVisible, overlayForecastData, hiddenSeries, disableForecastHistoricalBridge, overlayElements: overlayElementsProp, ...restProps }: ChartAreaInteractiveProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare function ChartAreaInteractive({ className, chartContainerClassName, legendClassName, xAxisClassName, yAxisClassName, timeRange, onTimeRangeChange, pinMonth, onPinMonthChange, onPreviewMonthChange, chartData, forecastData, error, loading, isDarkTheme, footerActions, mode, selectedForecast, upperQuantiles, lowerQuantiles, selectedLowerQuantile, selectedUpperQuantile, onLowerQuantileChange, onUpperQuantileChange, lowerThreshold, upperThreshold, onLowerThresholdChange, onUpperThresholdChange, discreteThresholdValues, headerActions, excludeLegendIds, loadingAnalyses, onLegendClick, onAnalysisSelect, disableTimeRangeSelector, enableTimeRangeBrush, forecastLineStyle, selectedAnalysisId, chartRenderId, toggleLegendSeries, ensureAnalysisSeriesVisible, overlayForecastData, hiddenSeries, disableForecastHistoricalBridge, overlayElements: overlayElementsProp, ...restProps }: ChartAreaInteractiveProps): import("react/jsx-runtime").JSX.Element;
@@ -47,6 +47,8 @@ export interface ChartAreaInteractiveProps extends BaseChartWrapperProps {
47
47
  loadingAnalyses?: Set<string>;
48
48
  onLegendClick?: (data: LegendPayload) => undefined | boolean;
49
49
  disableTimeRangeSelector?: boolean;
50
+ /** When selector hidden (`disableTimeRangeSelector`), still allow drag brush on chart (defaults false). */
51
+ enableTimeRangeBrush?: boolean;
50
52
  forecastLineStyle?: 'dashed' | 'solid';
51
53
  /** When set, ensures this analysis is visible (selected) in chart legend */
52
54
  selectedAnalysisId?: number | null;
@@ -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,9 @@ 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;
71
80
  }
72
81
  export interface ChatMessageProps {
73
82
  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, }: 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.9",
3
+ "version": "1.3.11",
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",
@@ -61,6 +61,7 @@ export function ChartAreaInteractive({
61
61
  onLegendClick,
62
62
  onAnalysisSelect,
63
63
  disableTimeRangeSelector,
64
+ enableTimeRangeBrush = false,
64
65
  forecastLineStyle = 'dashed',
65
66
  selectedAnalysisId,
66
67
  chartRenderId,
@@ -169,7 +170,9 @@ export function ChartAreaInteractive({
169
170
  };
170
171
 
171
172
  const brushEnabled =
172
- !disableTimeRangeSelector && !loading && bridgedChartData.length > 1;
173
+ (!disableTimeRangeSelector || enableTimeRangeBrush) &&
174
+ !loading &&
175
+ bridgedChartData.length > 1;
173
176
 
174
177
  const renderChart = () => {
175
178
  const overlayClassName = cn(chartContainerClassName);
@@ -51,6 +51,8 @@ export interface ChartAreaInteractiveProps extends BaseChartWrapperProps {
51
51
  loadingAnalyses?: Set<string>;
52
52
  onLegendClick?: (data: LegendPayload) => undefined | boolean;
53
53
  disableTimeRangeSelector?: boolean;
54
+ /** When selector hidden (`disableTimeRangeSelector`), still allow drag brush on chart (defaults false). */
55
+ enableTimeRangeBrush?: boolean;
54
56
  forecastLineStyle?: 'dashed' | 'solid';
55
57
  /** When set, ensures this analysis is visible (selected) in chart legend */
56
58
  selectedAnalysisId?: number | null;
@@ -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,9 @@ 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;
81
91
  }
82
92
 
83
93
  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,17 @@ 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}
203
287
  />
204
288
  </div>
205
289
  </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
  }