@databricks/appkit-ui 0.15.0 → 0.17.0
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/cli/commands/plugin/add-resource/add-resource.js +10 -4
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js +10 -16
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js +44 -26
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/manifest-resolve.js +57 -0
- package/dist/cli/commands/plugin/manifest-resolve.js.map +1 -0
- package/dist/cli/commands/plugin/sync/sync.js +121 -71
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/trusted-js-manifest.js +28 -0
- package/dist/cli/commands/plugin/trusted-js-manifest.js.map +1 -0
- package/dist/cli/commands/plugin/validate/validate.js +32 -14
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/js/arrow/arrow-client.d.ts.map +1 -1
- package/dist/js/arrow/index.d.ts +3 -0
- package/dist/js/arrow/lazy-arrow.d.ts +0 -1
- package/dist/js/arrow/lazy-arrow.d.ts.map +1 -1
- package/dist/js/constants.d.ts.map +1 -1
- package/dist/js/index.d.ts +2 -0
- package/dist/js/sse/connect-sse.d.ts +0 -1
- package/dist/js/sse/connect-sse.d.ts.map +1 -1
- package/dist/js/sse/types.d.ts +1 -3
- package/dist/js/sse/types.d.ts.map +1 -1
- package/dist/react/charts/area/index.d.ts +2 -3
- package/dist/react/charts/area/index.d.ts.map +1 -1
- package/dist/react/charts/bar/index.d.ts +2 -3
- package/dist/react/charts/bar/index.d.ts.map +1 -1
- package/dist/react/charts/base.d.ts +2 -2
- package/dist/react/charts/base.d.ts.map +1 -1
- package/dist/react/charts/base.js.map +1 -1
- package/dist/react/charts/chart-error-boundary.js.map +1 -1
- package/dist/react/charts/constants.d.ts.map +1 -1
- package/dist/react/charts/create-chart.d.ts +2 -3
- package/dist/react/charts/create-chart.d.ts.map +1 -1
- package/dist/react/charts/create-chart.js.map +1 -1
- package/dist/react/charts/empty.js.map +1 -1
- package/dist/react/charts/error.js.map +1 -1
- package/dist/react/charts/heatmap/index.d.ts +2 -3
- package/dist/react/charts/heatmap/index.d.ts.map +1 -1
- package/dist/react/charts/index.d.ts +18 -0
- package/dist/react/charts/line/index.d.ts +2 -3
- package/dist/react/charts/line/index.d.ts.map +1 -1
- package/dist/react/charts/loading.js.map +1 -1
- package/dist/react/charts/normalize.d.ts +0 -1
- package/dist/react/charts/normalize.d.ts.map +1 -1
- package/dist/react/charts/options.d.ts.map +1 -1
- package/dist/react/charts/pie/index.d.ts +3 -4
- package/dist/react/charts/pie/index.d.ts.map +1 -1
- package/dist/react/charts/radar/index.d.ts +2 -3
- package/dist/react/charts/radar/index.d.ts.map +1 -1
- package/dist/react/charts/scatter/index.d.ts +2 -3
- package/dist/react/charts/scatter/index.d.ts.map +1 -1
- package/dist/react/charts/theme.d.ts +0 -1
- package/dist/react/charts/theme.d.ts.map +1 -1
- package/dist/react/charts/types.d.ts.map +1 -1
- package/dist/react/charts/utils.d.ts.map +1 -1
- package/dist/react/charts/wrapper.d.ts +2 -2
- package/dist/react/charts/wrapper.d.ts.map +1 -1
- package/dist/react/charts/wrapper.js.map +1 -1
- package/dist/react/genie/genie-chat-input.d.ts +2 -2
- package/dist/react/genie/genie-chat-input.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-input.js.map +1 -1
- package/dist/react/genie/genie-chat-message-list.d.ts +9 -3
- package/dist/react/genie/genie-chat-message-list.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message-list.js +92 -17
- package/dist/react/genie/genie-chat-message-list.js.map +1 -1
- package/dist/react/genie/genie-chat-message.d.ts +2 -2
- package/dist/react/genie/genie-chat-message.d.ts.map +1 -1
- package/dist/react/genie/genie-chat-message.js.map +1 -1
- package/dist/react/genie/genie-chat.d.ts +2 -2
- package/dist/react/genie/genie-chat.d.ts.map +1 -1
- package/dist/react/genie/genie-chat.js +4 -2
- package/dist/react/genie/genie-chat.js.map +1 -1
- package/dist/react/genie/index.d.ts +7 -0
- package/dist/react/genie/types.d.ts +8 -1
- package/dist/react/genie/types.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.d.ts +0 -1
- package/dist/react/genie/use-genie-chat.d.ts.map +1 -1
- package/dist/react/genie/use-genie-chat.js +119 -41
- package/dist/react/genie/use-genie-chat.js.map +1 -1
- package/dist/react/hooks/index.d.ts +3 -0
- package/dist/react/hooks/types.d.ts.map +1 -1
- package/dist/react/hooks/use-analytics-query.d.ts +0 -1
- package/dist/react/hooks/use-analytics-query.d.ts.map +1 -1
- package/dist/react/hooks/use-chart-data.d.ts.map +1 -1
- package/dist/react/index.d.ts +5 -0
- package/dist/react/lib/utils.d.ts.map +1 -1
- package/dist/react/portal-container-context.d.ts +0 -1
- package/dist/react/portal-container-context.d.ts.map +1 -1
- package/dist/react/portal-container-context.js.map +1 -1
- package/dist/react/table/data-table.d.ts +2 -3
- package/dist/react/table/data-table.d.ts.map +1 -1
- package/dist/react/table/data-table.js.map +1 -1
- package/dist/react/table/empty.js.map +1 -1
- package/dist/react/table/error.js.map +1 -1
- package/dist/react/table/index.d.ts +1 -0
- package/dist/react/table/loading.js.map +1 -1
- package/dist/react/table/table-wrapper.js.map +1 -1
- package/dist/react/table/types.d.ts +0 -1
- package/dist/react/table/types.d.ts.map +1 -1
- package/dist/react/ui/accordion.d.ts.map +1 -1
- package/dist/react/ui/accordion.js.map +1 -1
- package/dist/react/ui/alert-dialog.d.ts +12 -12
- package/dist/react/ui/alert-dialog.d.ts.map +1 -1
- package/dist/react/ui/alert-dialog.js.map +1 -1
- package/dist/react/ui/alert.d.ts +4 -4
- package/dist/react/ui/alert.d.ts.map +1 -1
- package/dist/react/ui/alert.js.map +1 -1
- package/dist/react/ui/aspect-ratio.d.ts +2 -2
- package/dist/react/ui/aspect-ratio.d.ts.map +1 -1
- package/dist/react/ui/aspect-ratio.js.map +1 -1
- package/dist/react/ui/avatar.d.ts +4 -4
- package/dist/react/ui/avatar.d.ts.map +1 -1
- package/dist/react/ui/avatar.js.map +1 -1
- package/dist/react/ui/badge.d.ts +2 -2
- package/dist/react/ui/badge.d.ts.map +1 -1
- package/dist/react/ui/badge.js.map +1 -1
- package/dist/react/ui/breadcrumb.d.ts +8 -8
- package/dist/react/ui/breadcrumb.d.ts.map +1 -1
- package/dist/react/ui/breadcrumb.js.map +1 -1
- package/dist/react/ui/button-group.d.ts +6 -6
- package/dist/react/ui/button-group.d.ts.map +1 -1
- package/dist/react/ui/button-group.js.map +1 -1
- package/dist/react/ui/button.d.ts +4 -4
- package/dist/react/ui/button.d.ts.map +1 -1
- package/dist/react/ui/button.js.map +1 -1
- package/dist/react/ui/calendar.d.ts +3 -3
- package/dist/react/ui/calendar.d.ts.map +1 -1
- package/dist/react/ui/calendar.js.map +1 -1
- package/dist/react/ui/card.d.ts +8 -8
- package/dist/react/ui/card.d.ts.map +1 -1
- package/dist/react/ui/card.js.map +1 -1
- package/dist/react/ui/carousel.d.ts +6 -6
- package/dist/react/ui/carousel.d.ts.map +1 -1
- package/dist/react/ui/carousel.js.map +1 -1
- package/dist/react/ui/chart.d.ts +19 -19
- package/dist/react/ui/chart.d.ts.map +1 -1
- package/dist/react/ui/chart.js.map +1 -1
- package/dist/react/ui/checkbox.d.ts +2 -2
- package/dist/react/ui/checkbox.d.ts.map +1 -1
- package/dist/react/ui/checkbox.js.map +1 -1
- package/dist/react/ui/collapsible.d.ts +4 -4
- package/dist/react/ui/collapsible.d.ts.map +1 -1
- package/dist/react/ui/collapsible.js.map +1 -1
- package/dist/react/ui/command.d.ts +10 -10
- package/dist/react/ui/command.d.ts.map +1 -1
- package/dist/react/ui/command.js.map +1 -1
- package/dist/react/ui/context-menu.d.ts +16 -16
- package/dist/react/ui/context-menu.d.ts.map +1 -1
- package/dist/react/ui/context-menu.js.map +1 -1
- package/dist/react/ui/dialog.d.ts +11 -11
- package/dist/react/ui/dialog.d.ts.map +1 -1
- package/dist/react/ui/dialog.js.map +1 -1
- package/dist/react/ui/drawer.d.ts +11 -11
- package/dist/react/ui/drawer.d.ts.map +1 -1
- package/dist/react/ui/drawer.js.map +1 -1
- package/dist/react/ui/dropdown-menu.d.ts +16 -16
- package/dist/react/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/react/ui/dropdown-menu.js.map +1 -1
- package/dist/react/ui/empty.d.ts +9 -9
- package/dist/react/ui/empty.d.ts.map +1 -1
- package/dist/react/ui/empty.js.map +1 -1
- package/dist/react/ui/field.d.ts +13 -13
- package/dist/react/ui/field.d.ts.map +1 -1
- package/dist/react/ui/field.js.map +1 -1
- package/dist/react/ui/form.d.ts +9 -9
- package/dist/react/ui/form.d.ts.map +1 -1
- package/dist/react/ui/form.js.map +1 -1
- package/dist/react/ui/hover-card.d.ts +4 -4
- package/dist/react/ui/hover-card.d.ts.map +1 -1
- package/dist/react/ui/hover-card.js.map +1 -1
- package/dist/react/ui/index.d.ts +53 -0
- package/dist/react/ui/input-group.d.ts +10 -10
- package/dist/react/ui/input-group.d.ts.map +1 -1
- package/dist/react/ui/input-group.js.map +1 -1
- package/dist/react/ui/input-otp.d.ts +5 -5
- package/dist/react/ui/input-otp.d.ts.map +1 -1
- package/dist/react/ui/input-otp.js.map +1 -1
- package/dist/react/ui/input.d.ts +2 -2
- package/dist/react/ui/input.d.ts.map +1 -1
- package/dist/react/ui/input.js.map +1 -1
- package/dist/react/ui/item.d.ts +14 -14
- package/dist/react/ui/item.d.ts.map +1 -1
- package/dist/react/ui/item.js.map +1 -1
- package/dist/react/ui/kbd.d.ts +3 -3
- package/dist/react/ui/kbd.d.ts.map +1 -1
- package/dist/react/ui/kbd.js.map +1 -1
- package/dist/react/ui/label.d.ts +2 -2
- package/dist/react/ui/label.d.ts.map +1 -1
- package/dist/react/ui/label.js.map +1 -1
- package/dist/react/ui/menubar.d.ts +17 -17
- package/dist/react/ui/menubar.d.ts.map +1 -1
- package/dist/react/ui/menubar.js.map +1 -1
- package/dist/react/ui/navigation-menu.d.ts +11 -11
- package/dist/react/ui/navigation-menu.d.ts.map +1 -1
- package/dist/react/ui/navigation-menu.js.map +1 -1
- package/dist/react/ui/pagination.d.ts +8 -8
- package/dist/react/ui/pagination.d.ts.map +1 -1
- package/dist/react/ui/pagination.js.map +1 -1
- package/dist/react/ui/popover.d.ts +5 -5
- package/dist/react/ui/popover.d.ts.map +1 -1
- package/dist/react/ui/popover.js.map +1 -1
- package/dist/react/ui/progress.d.ts +2 -2
- package/dist/react/ui/progress.d.ts.map +1 -1
- package/dist/react/ui/progress.js.map +1 -1
- package/dist/react/ui/radio-group.d.ts +3 -3
- package/dist/react/ui/radio-group.d.ts.map +1 -1
- package/dist/react/ui/radio-group.js.map +1 -1
- package/dist/react/ui/resizable.d.ts +4 -4
- package/dist/react/ui/resizable.d.ts.map +1 -1
- package/dist/react/ui/resizable.js.map +1 -1
- package/dist/react/ui/scroll-area.d.ts +3 -3
- package/dist/react/ui/scroll-area.d.ts.map +1 -1
- package/dist/react/ui/scroll-area.js.map +1 -1
- package/dist/react/ui/select.d.ts +11 -11
- package/dist/react/ui/select.d.ts.map +1 -1
- package/dist/react/ui/select.js.map +1 -1
- package/dist/react/ui/separator.d.ts +2 -2
- package/dist/react/ui/separator.d.ts.map +1 -1
- package/dist/react/ui/separator.js.map +1 -1
- package/dist/react/ui/sheet.d.ts +9 -9
- package/dist/react/ui/sheet.d.ts.map +1 -1
- package/dist/react/ui/sheet.js.map +1 -1
- package/dist/react/ui/sidebar.d.ts +38 -55
- package/dist/react/ui/sidebar.d.ts.map +1 -1
- package/dist/react/ui/sidebar.js.map +1 -1
- package/dist/react/ui/skeleton.d.ts +2 -2
- package/dist/react/ui/skeleton.d.ts.map +1 -1
- package/dist/react/ui/skeleton.js.map +1 -1
- package/dist/react/ui/slider.d.ts +2 -2
- package/dist/react/ui/slider.d.ts.map +1 -1
- package/dist/react/ui/slider.js.map +1 -1
- package/dist/react/ui/sonner.d.ts +2 -2
- package/dist/react/ui/sonner.d.ts.map +1 -1
- package/dist/react/ui/sonner.js.map +1 -1
- package/dist/react/ui/spinner.d.ts +2 -2
- package/dist/react/ui/spinner.d.ts.map +1 -1
- package/dist/react/ui/spinner.js.map +1 -1
- package/dist/react/ui/switch.d.ts +2 -2
- package/dist/react/ui/switch.d.ts.map +1 -1
- package/dist/react/ui/switch.js.map +1 -1
- package/dist/react/ui/table.d.ts +9 -9
- package/dist/react/ui/table.d.ts.map +1 -1
- package/dist/react/ui/table.js.map +1 -1
- package/dist/react/ui/tabs.d.ts +5 -5
- package/dist/react/ui/tabs.d.ts.map +1 -1
- package/dist/react/ui/tabs.js.map +1 -1
- package/dist/react/ui/textarea.d.ts +2 -2
- package/dist/react/ui/textarea.d.ts.map +1 -1
- package/dist/react/ui/textarea.js.map +1 -1
- package/dist/react/ui/toggle-group.d.ts +3 -3
- package/dist/react/ui/toggle-group.d.ts.map +1 -1
- package/dist/react/ui/toggle-group.js.map +1 -1
- package/dist/react/ui/toggle.d.ts +4 -4
- package/dist/react/ui/toggle.d.ts.map +1 -1
- package/dist/react/ui/toggle.js.map +1 -1
- package/dist/react/ui/tooltip.d.ts +5 -5
- package/dist/react/ui/tooltip.d.ts.map +1 -1
- package/dist/react/ui/tooltip.js.map +1 -1
- package/dist/shared/src/cache.d.ts +1 -0
- package/dist/shared/src/execute.d.ts +1 -0
- package/dist/shared/src/genie.d.ts +6 -0
- package/dist/shared/src/genie.d.ts.map +1 -1
- package/dist/shared/src/index.d.ts +7 -0
- package/dist/shared/src/plugin.d.ts +2 -0
- package/dist/shared/src/sql/helpers.d.ts +0 -1
- package/dist/shared/src/sql/helpers.d.ts.map +1 -1
- package/dist/shared/src/sql/types.d.ts.map +1 -1
- package/dist/shared/src/tunnel.d.ts +1 -0
- package/docs/api/appkit-ui/genie/GenieChatMessageList.md +7 -5
- package/docs/development/project-setup.md +1 -1
- package/docs/plugins/plugin-management.md +16 -2
- package/package.json +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.js","names":[],"sources":["../../../src/react/charts/wrapper.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useChartData } from \"../hooks/use-chart-data\";\nimport { ChartErrorBoundary } from \"./chart-error-boundary\";\nimport { EmptyState } from \"./empty\";\nimport { ErrorState } from \"./error\";\nimport { LoadingSkeleton } from \"./loading\";\nimport type { ChartData, DataFormat } from \"./types\";\nimport { isArrowTable } from \"./types\";\n\n// ============================================================================\n// Props Types\n// ============================================================================\n\ninterface ChartWrapperQueryProps {\n /** Analytics query key */\n queryKey: string;\n /** Query parameters */\n parameters?: Record<string, unknown>;\n /** Data format preference */\n format?: DataFormat;\n /** Transform data after fetching */\n transformer?: <T>(data: T) => T;\n /** Direct data - not used in query mode */\n data?: never;\n}\n\ninterface ChartWrapperDataProps {\n /** Direct data (Arrow Table or JSON array) */\n data: ChartData;\n /** Not used in data mode */\n queryKey?: never;\n parameters?: never;\n format?: never;\n transformer?: never;\n}\n\ninterface CommonProps {\n /** Chart height in pixels */\n height?: number;\n /** Additional CSS classes */\n className?: string;\n /** Accessibility label */\n ariaLabel?: string;\n /** Test ID for automated testing */\n testId?: string;\n /** Render function receiving the chart data */\n children: (data: ChartData) => ReactNode;\n}\n\nexport type ChartWrapperProps = CommonProps &\n (ChartWrapperQueryProps | ChartWrapperDataProps);\n\n// ============================================================================\n// Query Mode Content\n// ============================================================================\n\nfunction QueryModeContent({\n queryKey,\n parameters,\n format,\n transformer,\n height,\n className,\n ariaLabel,\n testId,\n children,\n}: CommonProps & ChartWrapperQueryProps) {\n const { data, loading, error, isEmpty } = useChartData({\n queryKey,\n parameters,\n format,\n transformer,\n });\n\n if (loading) return <LoadingSkeleton height={height ?? 300} />;\n if (error) return <ErrorState error={error} />;\n if (isEmpty || !data) return <EmptyState />;\n\n return (\n <ChartErrorBoundary\n fallback={<ErrorState error=\"Failed to render chart\" />}\n >\n <div\n className={className}\n style={{ height }}\n aria-label={ariaLabel}\n data-testid={testId}\n role=\"img\"\n >\n {children(data)}\n </div>\n </ChartErrorBoundary>\n );\n}\n\n// ============================================================================\n// Data Mode Content\n// ============================================================================\n\nfunction DataModeContent({\n data,\n height,\n className,\n ariaLabel,\n testId,\n children,\n}: CommonProps & ChartWrapperDataProps) {\n const isEmpty = isArrowTable(data)\n ? data.numRows === 0\n : !Array.isArray(data) || data.length === 0;\n\n if (isEmpty) return <EmptyState />;\n\n return (\n <ChartErrorBoundary\n fallback={<ErrorState error=\"Failed to render chart\" />}\n >\n <div\n className={className}\n style={{ height }}\n aria-label={ariaLabel}\n data-testid={testId}\n role=\"img\"\n >\n {children(data)}\n </div>\n </ChartErrorBoundary>\n );\n}\n\n// ============================================================================\n// Main Wrapper Component\n// ============================================================================\n\n/**\n * Wrapper component for charts.\n * Handles data fetching (query mode) or direct data injection (data mode).\n *\n * @example Query mode - fetches data from analytics endpoint\n * ```tsx\n * <ChartWrapper\n * queryKey=\"spend_data\"\n * parameters={{ limit: 100 }}\n * format=\"auto\"\n * >\n * {(data) => <MyChart data={data} />}\n * </ChartWrapper>\n * ```\n *\n * @example Data mode - uses provided data directly\n * ```tsx\n * <ChartWrapper data={myArrowTable}>\n * {(data) => <MyChart data={data} />}\n * </ChartWrapper>\n * ```\n */\nexport function ChartWrapper(props: ChartWrapperProps) {\n const { height = 300, className, ariaLabel, testId, children } = props;\n\n // Data mode: use provided data directly\n if (\"data\" in props && props.data !== undefined) {\n return (\n <DataModeContent\n data={props.data}\n height={height}\n className={className}\n ariaLabel={ariaLabel}\n testId={testId}\n >\n {children}\n </DataModeContent>\n );\n }\n\n // Query mode: fetch data from analytics endpoint\n if (\"queryKey\" in props && props.queryKey !== undefined) {\n return (\n <QueryModeContent\n queryKey={props.queryKey}\n parameters={props.parameters}\n format={props.format}\n transformer={props.transformer}\n height={height}\n className={className}\n ariaLabel={ariaLabel}\n testId={testId}\n >\n {children}\n </QueryModeContent>\n );\n }\n\n // Should never reach here due to TypeScript, but safety fallback\n return <ErrorState error=\"Chart requires either 'queryKey' or 'data' prop\" />;\n}\n"],"mappings":";;;;;;;;;AAwDA,SAAS,iBAAiB,EACxB,UACA,YACA,QACA,aACA,QACA,WACA,WACA,QACA,YACuC;CACvC,MAAM,EAAE,MAAM,SAAS,OAAO,YAAY,aAAa;EACrD;EACA;EACA;EACA;EACD,CAAC;AAEF,KAAI,QAAS,QAAO,oBAAC,
|
|
1
|
+
{"version":3,"file":"wrapper.js","names":[],"sources":["../../../src/react/charts/wrapper.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useChartData } from \"../hooks/use-chart-data\";\nimport { ChartErrorBoundary } from \"./chart-error-boundary\";\nimport { EmptyState } from \"./empty\";\nimport { ErrorState } from \"./error\";\nimport { LoadingSkeleton } from \"./loading\";\nimport type { ChartData, DataFormat } from \"./types\";\nimport { isArrowTable } from \"./types\";\n\n// ============================================================================\n// Props Types\n// ============================================================================\n\ninterface ChartWrapperQueryProps {\n /** Analytics query key */\n queryKey: string;\n /** Query parameters */\n parameters?: Record<string, unknown>;\n /** Data format preference */\n format?: DataFormat;\n /** Transform data after fetching */\n transformer?: <T>(data: T) => T;\n /** Direct data - not used in query mode */\n data?: never;\n}\n\ninterface ChartWrapperDataProps {\n /** Direct data (Arrow Table or JSON array) */\n data: ChartData;\n /** Not used in data mode */\n queryKey?: never;\n parameters?: never;\n format?: never;\n transformer?: never;\n}\n\ninterface CommonProps {\n /** Chart height in pixels */\n height?: number;\n /** Additional CSS classes */\n className?: string;\n /** Accessibility label */\n ariaLabel?: string;\n /** Test ID for automated testing */\n testId?: string;\n /** Render function receiving the chart data */\n children: (data: ChartData) => ReactNode;\n}\n\nexport type ChartWrapperProps = CommonProps &\n (ChartWrapperQueryProps | ChartWrapperDataProps);\n\n// ============================================================================\n// Query Mode Content\n// ============================================================================\n\nfunction QueryModeContent({\n queryKey,\n parameters,\n format,\n transformer,\n height,\n className,\n ariaLabel,\n testId,\n children,\n}: CommonProps & ChartWrapperQueryProps) {\n const { data, loading, error, isEmpty } = useChartData({\n queryKey,\n parameters,\n format,\n transformer,\n });\n\n if (loading) return <LoadingSkeleton height={height ?? 300} />;\n if (error) return <ErrorState error={error} />;\n if (isEmpty || !data) return <EmptyState />;\n\n return (\n <ChartErrorBoundary\n fallback={<ErrorState error=\"Failed to render chart\" />}\n >\n <div\n className={className}\n style={{ height }}\n aria-label={ariaLabel}\n data-testid={testId}\n role=\"img\"\n >\n {children(data)}\n </div>\n </ChartErrorBoundary>\n );\n}\n\n// ============================================================================\n// Data Mode Content\n// ============================================================================\n\nfunction DataModeContent({\n data,\n height,\n className,\n ariaLabel,\n testId,\n children,\n}: CommonProps & ChartWrapperDataProps) {\n const isEmpty = isArrowTable(data)\n ? data.numRows === 0\n : !Array.isArray(data) || data.length === 0;\n\n if (isEmpty) return <EmptyState />;\n\n return (\n <ChartErrorBoundary\n fallback={<ErrorState error=\"Failed to render chart\" />}\n >\n <div\n className={className}\n style={{ height }}\n aria-label={ariaLabel}\n data-testid={testId}\n role=\"img\"\n >\n {children(data)}\n </div>\n </ChartErrorBoundary>\n );\n}\n\n// ============================================================================\n// Main Wrapper Component\n// ============================================================================\n\n/**\n * Wrapper component for charts.\n * Handles data fetching (query mode) or direct data injection (data mode).\n *\n * @example Query mode - fetches data from analytics endpoint\n * ```tsx\n * <ChartWrapper\n * queryKey=\"spend_data\"\n * parameters={{ limit: 100 }}\n * format=\"auto\"\n * >\n * {(data) => <MyChart data={data} />}\n * </ChartWrapper>\n * ```\n *\n * @example Data mode - uses provided data directly\n * ```tsx\n * <ChartWrapper data={myArrowTable}>\n * {(data) => <MyChart data={data} />}\n * </ChartWrapper>\n * ```\n */\nexport function ChartWrapper(props: ChartWrapperProps) {\n const { height = 300, className, ariaLabel, testId, children } = props;\n\n // Data mode: use provided data directly\n if (\"data\" in props && props.data !== undefined) {\n return (\n <DataModeContent\n data={props.data}\n height={height}\n className={className}\n ariaLabel={ariaLabel}\n testId={testId}\n >\n {children}\n </DataModeContent>\n );\n }\n\n // Query mode: fetch data from analytics endpoint\n if (\"queryKey\" in props && props.queryKey !== undefined) {\n return (\n <QueryModeContent\n queryKey={props.queryKey}\n parameters={props.parameters}\n format={props.format}\n transformer={props.transformer}\n height={height}\n className={className}\n ariaLabel={ariaLabel}\n testId={testId}\n >\n {children}\n </QueryModeContent>\n );\n }\n\n // Should never reach here due to TypeScript, but safety fallback\n return <ErrorState error=\"Chart requires either 'queryKey' or 'data' prop\" />;\n}\n"],"mappings":";;;;;;;;;AAwDA,SAAS,iBAAiB,EACxB,UACA,YACA,QACA,aACA,QACA,WACA,WACA,QACA,YACuC;CACvC,MAAM,EAAE,MAAM,SAAS,OAAO,YAAY,aAAa;EACrD;EACA;EACA;EACA;EACD,CAAC;AAEF,KAAI,QAAS,QAAO,oBAAC,mBAAgB,QAAQ,UAAU,MAAO;AAC9D,KAAI,MAAO,QAAO,oBAAC,cAAkB,QAAS;AAC9C,KAAI,WAAW,CAAC,KAAM,QAAO,oBAAC,eAAa;AAE3C,QACE,oBAAC;EACC,UAAU,oBAAC,cAAW,OAAM,2BAA2B;YAEvD,oBAAC;GACY;GACX,OAAO,EAAE,QAAQ;GACjB,cAAY;GACZ,eAAa;GACb,MAAK;aAEJ,SAAS,KAAK;IACX;GACa;;AAQzB,SAAS,gBAAgB,EACvB,MACA,QACA,WACA,WACA,QACA,YACsC;AAKtC,KAJgB,aAAa,KAAK,GAC9B,KAAK,YAAY,IACjB,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,WAAW,EAE/B,QAAO,oBAAC,eAAa;AAElC,QACE,oBAAC;EACC,UAAU,oBAAC,cAAW,OAAM,2BAA2B;YAEvD,oBAAC;GACY;GACX,OAAO,EAAE,QAAQ;GACjB,cAAY;GACZ,eAAa;GACb,MAAK;aAEJ,SAAS,KAAK;IACX;GACa;;;;;;;;;;;;;;;;;;;;;;;;AA8BzB,SAAgB,aAAa,OAA0B;CACrD,MAAM,EAAE,SAAS,KAAK,WAAW,WAAW,QAAQ,aAAa;AAGjE,KAAI,UAAU,SAAS,MAAM,SAAS,OACpC,QACE,oBAAC;EACC,MAAM,MAAM;EACJ;EACG;EACA;EACH;EAEP;GACe;AAKtB,KAAI,cAAc,SAAS,MAAM,aAAa,OAC5C,QACE,oBAAC;EACC,UAAU,MAAM;EAChB,YAAY,MAAM;EAClB,QAAQ,MAAM;EACd,aAAa,MAAM;EACX;EACG;EACA;EACH;EAEP;GACgB;AAKvB,QAAO,oBAAC,cAAW,OAAM,oDAAoD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
2
|
|
|
3
3
|
//#region src/react/genie/genie-chat-input.d.ts
|
|
4
4
|
interface GenieChatInputProps {
|
|
@@ -17,7 +17,7 @@ declare function GenieChatInput({
|
|
|
17
17
|
disabled,
|
|
18
18
|
placeholder,
|
|
19
19
|
className
|
|
20
|
-
}: GenieChatInputProps):
|
|
20
|
+
}: GenieChatInputProps): react_jsx_runtime0.JSX.Element;
|
|
21
21
|
//#endregion
|
|
22
22
|
export { GenieChatInput };
|
|
23
23
|
//# sourceMappingURL=genie-chat-input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-input.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"
|
|
1
|
+
{"version":3,"file":"genie-chat-input.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"mappings":";;;UAIiB,mBAAA;;EAEf,MAAA,GAAS,OAAA;EAFM;EAIf,QAAA;;EAEA,WAAA;EAJA;EAMA,SAAA;AAAA;;iBAIc,cAAA,CAAA;EACd,MAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,mBAAA,GAAmB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-input.js","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"sourcesContent":["import { type KeyboardEvent, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\n\nexport interface GenieChatInputProps {\n /** Callback fired when the user submits a message */\n onSend: (content: string) => void;\n /** Disable the input and send button */\n disabled?: boolean;\n /** Placeholder text shown in the textarea */\n placeholder?: string;\n /** Additional CSS class for the container */\n className?: string;\n}\n\n/** Auto-expanding textarea input with a send button for chat messages. Submits on Enter (Shift+Enter for newline). */\nexport function GenieChatInput({\n onSend,\n disabled = false,\n placeholder = \"Ask a question...\",\n className,\n}: GenieChatInputProps) {\n const [value, setValue] = useState(\"\");\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleSubmit = () => {\n const trimmed = value.trim();\n if (!trimmed || disabled) return;\n onSend(trimmed);\n setValue(\"\");\n if (textareaRef.current) {\n textareaRef.current.style.height = \"auto\";\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n const MAX_HEIGHT = 200;\n\n const handleInput = () => {\n const textarea = textareaRef.current;\n if (textarea) {\n textarea.style.height = \"auto\";\n const clamped = Math.min(textarea.scrollHeight, MAX_HEIGHT);\n textarea.style.height = `${clamped}px`;\n textarea.style.overflowY =\n textarea.scrollHeight > MAX_HEIGHT ? \"auto\" : \"hidden\";\n }\n };\n\n return (\n <div className={cn(\"flex gap-2 p-4 border-t shrink-0\", className)}>\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n disabled={disabled}\n rows={1}\n className={cn(\n \"flex-1 resize-none overflow-hidden rounded-md border border-input bg-background px-3 py-2\",\n \"text-sm placeholder:text-muted-foreground\",\n \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n )}\n />\n <Button\n onClick={handleSubmit}\n disabled={disabled || !value.trim()}\n size=\"default\"\n className=\"self-end\"\n >\n Send\n </Button>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,eAAe,EAC7B,QACA,WAAW,OACX,cAAc,qBACd,aACsB;CACtB,MAAM,CAAC,OAAO,YAAY,SAAS,GAAG;CACtC,MAAM,cAAc,OAA4B,KAAK;CAErD,MAAM,qBAAqB;EACzB,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,WAAW,SAAU;AAC1B,SAAO,QAAQ;AACf,WAAS,GAAG;AACZ,MAAI,YAAY,QACd,aAAY,QAAQ,MAAM,SAAS;;CAIvC,MAAM,iBAAiB,MAA0C;AAC/D,MAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,KAAE,gBAAgB;AAClB,iBAAc;;;CAIlB,MAAM,aAAa;CAEnB,MAAM,oBAAoB;EACxB,MAAM,WAAW,YAAY;AAC7B,MAAI,UAAU;AACZ,YAAS,MAAM,SAAS;GACxB,MAAM,UAAU,KAAK,IAAI,SAAS,cAAc,WAAW;AAC3D,YAAS,MAAM,SAAS,GAAG,QAAQ;AACnC,YAAS,MAAM,YACb,SAAS,eAAe,aAAa,SAAS;;;AAIpD,QACE,qBAAC
|
|
1
|
+
{"version":3,"file":"genie-chat-input.js","names":[],"sources":["../../../src/react/genie/genie-chat-input.tsx"],"sourcesContent":["import { type KeyboardEvent, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\n\nexport interface GenieChatInputProps {\n /** Callback fired when the user submits a message */\n onSend: (content: string) => void;\n /** Disable the input and send button */\n disabled?: boolean;\n /** Placeholder text shown in the textarea */\n placeholder?: string;\n /** Additional CSS class for the container */\n className?: string;\n}\n\n/** Auto-expanding textarea input with a send button for chat messages. Submits on Enter (Shift+Enter for newline). */\nexport function GenieChatInput({\n onSend,\n disabled = false,\n placeholder = \"Ask a question...\",\n className,\n}: GenieChatInputProps) {\n const [value, setValue] = useState(\"\");\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const handleSubmit = () => {\n const trimmed = value.trim();\n if (!trimmed || disabled) return;\n onSend(trimmed);\n setValue(\"\");\n if (textareaRef.current) {\n textareaRef.current.style.height = \"auto\";\n }\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n };\n\n const MAX_HEIGHT = 200;\n\n const handleInput = () => {\n const textarea = textareaRef.current;\n if (textarea) {\n textarea.style.height = \"auto\";\n const clamped = Math.min(textarea.scrollHeight, MAX_HEIGHT);\n textarea.style.height = `${clamped}px`;\n textarea.style.overflowY =\n textarea.scrollHeight > MAX_HEIGHT ? \"auto\" : \"hidden\";\n }\n };\n\n return (\n <div className={cn(\"flex gap-2 p-4 border-t shrink-0\", className)}>\n <textarea\n ref={textareaRef}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n onInput={handleInput}\n placeholder={placeholder}\n disabled={disabled}\n rows={1}\n className={cn(\n \"flex-1 resize-none overflow-hidden rounded-md border border-input bg-background px-3 py-2\",\n \"text-sm placeholder:text-muted-foreground\",\n \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n )}\n />\n <Button\n onClick={handleSubmit}\n disabled={disabled || !value.trim()}\n size=\"default\"\n className=\"self-end\"\n >\n Send\n </Button>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,eAAe,EAC7B,QACA,WAAW,OACX,cAAc,qBACd,aACsB;CACtB,MAAM,CAAC,OAAO,YAAY,SAAS,GAAG;CACtC,MAAM,cAAc,OAA4B,KAAK;CAErD,MAAM,qBAAqB;EACzB,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,CAAC,WAAW,SAAU;AAC1B,SAAO,QAAQ;AACf,WAAS,GAAG;AACZ,MAAI,YAAY,QACd,aAAY,QAAQ,MAAM,SAAS;;CAIvC,MAAM,iBAAiB,MAA0C;AAC/D,MAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,KAAE,gBAAgB;AAClB,iBAAc;;;CAIlB,MAAM,aAAa;CAEnB,MAAM,oBAAoB;EACxB,MAAM,WAAW,YAAY;AAC7B,MAAI,UAAU;AACZ,YAAS,MAAM,SAAS;GACxB,MAAM,UAAU,KAAK,IAAI,SAAS,cAAc,WAAW;AAC3D,YAAS,MAAM,SAAS,GAAG,QAAQ;AACnC,YAAS,MAAM,YACb,SAAS,eAAe,aAAa,SAAS;;;AAIpD,QACE,qBAAC;EAAI,WAAW,GAAG,oCAAoC,UAAU;aAC/D,oBAAC;GACC,KAAK;GACE;GACP,WAAW,MAAM,SAAS,EAAE,OAAO,MAAM;GACzC,WAAW;GACX,SAAS;GACI;GACH;GACV,MAAM;GACN,WAAW,GACT,6FACA,6CACA,2EACA,kDACD;IACD,EACF,oBAAC;GACC,SAAS;GACT,UAAU,YAAY,CAAC,MAAM,MAAM;GACnC,MAAK;GACL,WAAU;aACX;IAEQ;GACL"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GenieChatStatus, GenieMessageItem } from "./types.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/react/genie/genie-chat-message-list.d.ts
|
|
5
5
|
interface GenieChatMessageListProps {
|
|
@@ -9,13 +9,19 @@ interface GenieChatMessageListProps {
|
|
|
9
9
|
status: GenieChatStatus;
|
|
10
10
|
/** Additional CSS class for the scroll area */
|
|
11
11
|
className?: string;
|
|
12
|
+
/** Whether a previous page of older messages exists */
|
|
13
|
+
hasPreviousPage?: boolean;
|
|
14
|
+
/** Callback to fetch the previous page of messages */
|
|
15
|
+
onFetchPreviousPage?: () => void;
|
|
12
16
|
}
|
|
13
17
|
/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */
|
|
14
18
|
declare function GenieChatMessageList({
|
|
15
19
|
messages,
|
|
16
20
|
status,
|
|
17
|
-
className
|
|
18
|
-
|
|
21
|
+
className,
|
|
22
|
+
hasPreviousPage,
|
|
23
|
+
onFetchPreviousPage
|
|
24
|
+
}: GenieChatMessageListProps): react_jsx_runtime0.JSX.Element;
|
|
19
25
|
//#endregion
|
|
20
26
|
export { GenieChatMessageList };
|
|
21
27
|
//# sourceMappingURL=genie-chat-message-list.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"
|
|
1
|
+
{"version":3,"file":"genie-chat-message-list.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"mappings":";;;;UAQiB,yBAAA;;EAEf,QAAA,EAAU,gBAAA;EAFK;EAIf,MAAA,EAAQ,eAAA;;EAER,SAAA;EAJA;EAMA,eAAA;EAJA;EAMA,mBAAA;AAAA;;iBAkIc,oBAAA,CAAA;EACd,QAAA;EACA,MAAA;EACA,SAAA;EACA,eAAA;EACA;AAAA,GACC,yBAAA,GAAyB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -3,7 +3,7 @@ import { ScrollArea } from "../ui/scroll-area.js";
|
|
|
3
3
|
import { Skeleton } from "../ui/skeleton.js";
|
|
4
4
|
import { Spinner } from "../ui/spinner.js";
|
|
5
5
|
import { GenieChatMessage } from "./genie-chat-message.js";
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
|
|
9
9
|
//#region src/react/genie/genie-chat-message-list.tsx
|
|
@@ -16,27 +16,102 @@ const STATUS_LABELS = {
|
|
|
16
16
|
function formatStatus(status) {
|
|
17
17
|
return STATUS_LABELS[status] ?? status.replace(/_/g, " ").toLowerCase();
|
|
18
18
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
if (last?.role === "assistant" && last.id === "") return /* @__PURE__ */ jsxs("div", {
|
|
22
|
-
className: "flex items-center gap-2 text-sm text-muted-foreground px-11",
|
|
23
|
-
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", { children: formatStatus(last.status) })]
|
|
24
|
-
});
|
|
25
|
-
return null;
|
|
19
|
+
function getViewport(scrollRef) {
|
|
20
|
+
return scrollRef.current?.querySelector("[data-slot=\"scroll-area-viewport\"]");
|
|
26
21
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Manages scroll position: scrolls to bottom on append/initial load,
|
|
24
|
+
* preserves position when older messages are prepended.
|
|
25
|
+
*/
|
|
26
|
+
function useScrollManagement(scrollRef, messages, status) {
|
|
27
|
+
const prevFirstMessageIdRef = useRef(null);
|
|
28
|
+
const prevScrollHeightRef = useRef(0);
|
|
29
|
+
const prevMessageCountRef = useRef(0);
|
|
30
30
|
useEffect(() => {
|
|
31
|
-
const viewport = scrollRef
|
|
32
|
-
if (viewport)
|
|
31
|
+
const viewport = getViewport(scrollRef);
|
|
32
|
+
if (!viewport) return;
|
|
33
|
+
const observer = new ResizeObserver(() => {
|
|
34
|
+
prevScrollHeightRef.current = viewport.scrollHeight;
|
|
35
|
+
});
|
|
36
|
+
observer.observe(viewport);
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, [scrollRef]);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const viewport = getViewport(scrollRef);
|
|
41
|
+
if (!viewport) return;
|
|
42
|
+
const count = messages.length;
|
|
43
|
+
const countChanged = count !== prevMessageCountRef.current;
|
|
44
|
+
prevMessageCountRef.current = count;
|
|
45
|
+
if (!countChanged) {
|
|
46
|
+
prevScrollHeightRef.current = viewport.scrollHeight;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const firstMessageId = messages[0]?.id ?? null;
|
|
50
|
+
if (prevFirstMessageIdRef.current !== null && firstMessageId !== prevFirstMessageIdRef.current && prevScrollHeightRef.current > 0) {
|
|
51
|
+
const delta = viewport.scrollHeight - prevScrollHeightRef.current;
|
|
52
|
+
viewport.scrollTop += delta;
|
|
53
|
+
} else viewport.scrollTop = viewport.scrollHeight;
|
|
54
|
+
prevFirstMessageIdRef.current = firstMessageId;
|
|
55
|
+
prevScrollHeightRef.current = viewport.scrollHeight;
|
|
33
56
|
}, [messages.length, status]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Observes a sentinel element at the top of the scroll area and triggers
|
|
60
|
+
* `onFetchPreviousPage` when the user scrolls to the top (only if content overflows).
|
|
61
|
+
* Returns a ref to attach to the sentinel element.
|
|
62
|
+
*/
|
|
63
|
+
function useLoadOlderOnScroll(scrollRef, shouldObserve, onFetchPreviousPage) {
|
|
64
|
+
const sentinelRef = useRef(null);
|
|
65
|
+
const onFetchPreviousPageRef = useRef(onFetchPreviousPage);
|
|
66
|
+
onFetchPreviousPageRef.current = onFetchPreviousPage;
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const sentinel = sentinelRef.current;
|
|
69
|
+
const viewport = getViewport(scrollRef);
|
|
70
|
+
if (!sentinel || !viewport || !shouldObserve) return;
|
|
71
|
+
let armed = false;
|
|
72
|
+
const frameId = requestAnimationFrame(() => {
|
|
73
|
+
armed = true;
|
|
74
|
+
});
|
|
75
|
+
const observer = new IntersectionObserver((entries) => {
|
|
76
|
+
if (!armed) return;
|
|
77
|
+
const isScrollable = viewport.scrollHeight > viewport.clientHeight;
|
|
78
|
+
if (entries[0]?.isIntersecting && isScrollable) onFetchPreviousPageRef.current?.();
|
|
79
|
+
}, {
|
|
80
|
+
root: viewport,
|
|
81
|
+
threshold: 0
|
|
82
|
+
});
|
|
83
|
+
observer.observe(sentinel);
|
|
84
|
+
return () => {
|
|
85
|
+
cancelAnimationFrame(frameId);
|
|
86
|
+
observer.disconnect();
|
|
87
|
+
};
|
|
88
|
+
}, [scrollRef, shouldObserve]);
|
|
89
|
+
return sentinelRef;
|
|
90
|
+
}
|
|
91
|
+
/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */
|
|
92
|
+
function GenieChatMessageList({ messages, status, className, hasPreviousPage = false, onFetchPreviousPage }) {
|
|
93
|
+
const scrollRef = useRef(null);
|
|
94
|
+
const sentinelRef = useLoadOlderOnScroll(scrollRef, hasPreviousPage && status !== "loading-older", onFetchPreviousPage);
|
|
95
|
+
useScrollManagement(scrollRef, messages, status);
|
|
96
|
+
const lastMessage = messages[messages.length - 1];
|
|
97
|
+
const showStreamingIndicator = status === "streaming" && lastMessage?.role === "assistant" && lastMessage.id === "";
|
|
34
98
|
return /* @__PURE__ */ jsx(ScrollArea, {
|
|
35
99
|
ref: scrollRef,
|
|
36
100
|
className: cn("flex-1 min-h-0 p-4", className),
|
|
37
101
|
children: /* @__PURE__ */ jsxs("div", {
|
|
38
102
|
className: "flex flex-col gap-4",
|
|
39
103
|
children: [
|
|
104
|
+
hasPreviousPage && /* @__PURE__ */ jsx("div", {
|
|
105
|
+
ref: sentinelRef,
|
|
106
|
+
className: "h-px"
|
|
107
|
+
}),
|
|
108
|
+
status === "loading-older" && /* @__PURE__ */ jsxs("div", {
|
|
109
|
+
className: "flex items-center justify-center gap-2 py-2",
|
|
110
|
+
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", {
|
|
111
|
+
className: "text-sm text-muted-foreground",
|
|
112
|
+
children: "Loading older messages..."
|
|
113
|
+
})]
|
|
114
|
+
}),
|
|
40
115
|
status === "loading-history" && messages.length === 0 && /* @__PURE__ */ jsxs("div", {
|
|
41
116
|
className: "flex flex-col gap-4",
|
|
42
117
|
children: [
|
|
@@ -45,11 +120,11 @@ function GenieChatMessageList({ messages, status, className }) {
|
|
|
45
120
|
/* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-2/3 self-end" })
|
|
46
121
|
]
|
|
47
122
|
}),
|
|
48
|
-
messages.map((msg) => {
|
|
49
|
-
|
|
50
|
-
|
|
123
|
+
messages.filter((msg) => msg.role !== "assistant" || msg.id !== "" || msg.content).map((msg) => /* @__PURE__ */ jsx(GenieChatMessage, { message: msg }, msg.id)),
|
|
124
|
+
showStreamingIndicator && /* @__PURE__ */ jsxs("div", {
|
|
125
|
+
className: "flex items-center gap-2 text-sm text-muted-foreground px-11",
|
|
126
|
+
children: [/* @__PURE__ */ jsx(Spinner, { className: "h-3 w-3" }), /* @__PURE__ */ jsx("span", { children: formatStatus(lastMessage.status) })]
|
|
51
127
|
}),
|
|
52
|
-
status === "streaming" && messages.length > 0 && /* @__PURE__ */ jsx(StreamingIndicator, { messages }),
|
|
53
128
|
messages.length === 0 && status === "idle" && /* @__PURE__ */ jsx("div", {
|
|
54
129
|
className: "flex items-center justify-center h-full text-muted-foreground text-sm py-12",
|
|
55
130
|
children: "Start a conversation by typing a question below."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message-list.js","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { Spinner } from \"../ui/spinner\";\nimport { GenieChatMessage } from \"./genie-chat-message\";\nimport type { GenieChatStatus, GenieMessageItem } from \"./types\";\n\nexport interface GenieChatMessageListProps {\n /** Array of messages to display */\n messages: GenieMessageItem[];\n /** Current chat status (controls loading indicators and skeleton placeholders) */\n status: GenieChatStatus;\n /** Additional CSS class for the scroll area */\n className?: string;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n ASKING_AI: \"Asking AI...\",\n EXECUTING_QUERY: \"Executing query...\",\n FILTERING_RESULTS: \"Filtering results...\",\n COMPLETED: \"Done\",\n};\n\nfunction formatStatus(status: string): string {\n return STATUS_LABELS[status] ?? status.replace(/_/g, \" \").toLowerCase();\n}\n\nfunction StreamingIndicator({ messages }: { messages: GenieMessageItem[] }) {\n const last = messages[messages.length - 1];\n if (last?.role === \"assistant\" && last.id === \"\") {\n return (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(last.status)}</span>\n </div>\n );\n }\n return null;\n}\n\n/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */\nexport function GenieChatMessageList({\n messages,\n status,\n className,\n}: GenieChatMessageListProps) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Scroll only the ScrollArea viewport, not the page\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentional triggers for auto-scroll\n useEffect(() => {\n const viewport = scrollRef.current?.querySelector<HTMLElement>(\n '[data-slot=\"scroll-area-viewport\"]',\n );\n if (viewport) {\n viewport.scrollTop = viewport.scrollHeight;\n }\n }, [messages.length, status]);\n\n return (\n <ScrollArea ref={scrollRef} className={cn(\"flex-1 min-h-0 p-4\", className)}>\n <div className=\"flex flex-col gap-4\">\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages.map((msg) => {\n if (msg.role === \"assistant\" && msg.id === \"\" && !msg.content) {\n return null;\n }\n return <GenieChatMessage key={msg.id} message={msg} />;\n })}\n\n {status === \"streaming\" && messages.length > 0 && (\n <StreamingIndicator messages={messages} />\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":";;;;;;;;;AAiBA,MAAM,gBAAwC;CAC5C,WAAW;CACX,iBAAiB;CACjB,mBAAmB;CACnB,WAAW;CACZ;AAED,SAAS,aAAa,QAAwB;AAC5C,QAAO,cAAc,WAAW,OAAO,QAAQ,MAAM,IAAI,CAAC,aAAa;;AAGzE,SAAS,mBAAmB,EAAE,YAA8C;CAC1E,MAAM,OAAO,SAAS,SAAS,SAAS;AACxC,KAAI,MAAM,SAAS,eAAe,KAAK,OAAO,GAC5C,QACE,qBAAC,OAAD;EAAK,WAAU;YAAf,CACE,oBAAC,SAAD,EAAS,WAAU,WAAY,GAC/B,oBAAC,QAAD,YAAO,aAAa,KAAK,OAAO,EAAQ,EACpC;;AAGV,QAAO;;;AAIT,SAAgB,qBAAqB,EACnC,UACA,QACA,aAC4B;CAC5B,MAAM,YAAY,OAAuB,KAAK;AAI9C,iBAAgB;EACd,MAAM,WAAW,UAAU,SAAS,cAClC,uCACD;AACD,MAAI,SACF,UAAS,YAAY,SAAS;IAE/B,CAAC,SAAS,QAAQ,OAAO,CAAC;AAE7B,QACE,oBAAC,YAAD;EAAY,KAAK;EAAW,WAAW,GAAG,sBAAsB,UAAU;YACxE,qBAAC,OAAD;GAAK,WAAU;aAAf;IACG,WAAW,qBAAqB,SAAS,WAAW,KACnD,qBAAC,OAAD;KAAK,WAAU;eAAf;MACE,oBAAC,UAAD,EAAU,WAAU,cAAe;MACnC,oBAAC,UAAD,EAAU,WAAU,yBAA0B;MAC9C,oBAAC,UAAD,EAAU,WAAU,uBAAwB;MACxC;;IAGP,SAAS,KAAK,QAAQ;AACrB,SAAI,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,CAAC,IAAI,QACpD,QAAO;AAET,YAAO,oBAAC,kBAAD,EAA+B,SAAS,KAAO,EAAxB,IAAI,GAAoB;MACtD;IAED,WAAW,eAAe,SAAS,SAAS,KAC3C,oBAAC,oBAAD,EAA8B,UAAY;IAG3C,SAAS,WAAW,KAAK,WAAW,UACnC,oBAAC,OAAD;KAAK,WAAU;eAA8E;KAEvF;IAEJ;;EACK"}
|
|
1
|
+
{"version":3,"file":"genie-chat-message-list.js","names":[],"sources":["../../../src/react/genie/genie-chat-message-list.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { Spinner } from \"../ui/spinner\";\nimport { GenieChatMessage } from \"./genie-chat-message\";\nimport type { GenieChatStatus, GenieMessageItem } from \"./types\";\n\nexport interface GenieChatMessageListProps {\n /** Array of messages to display */\n messages: GenieMessageItem[];\n /** Current chat status (controls loading indicators and skeleton placeholders) */\n status: GenieChatStatus;\n /** Additional CSS class for the scroll area */\n className?: string;\n /** Whether a previous page of older messages exists */\n hasPreviousPage?: boolean;\n /** Callback to fetch the previous page of messages */\n onFetchPreviousPage?: () => void;\n}\n\nconst STATUS_LABELS: Record<string, string> = {\n ASKING_AI: \"Asking AI...\",\n EXECUTING_QUERY: \"Executing query...\",\n FILTERING_RESULTS: \"Filtering results...\",\n COMPLETED: \"Done\",\n};\n\nfunction formatStatus(status: string): string {\n return STATUS_LABELS[status] ?? status.replace(/_/g, \" \").toLowerCase();\n}\n\nfunction getViewport(scrollRef: React.RefObject<HTMLDivElement | null>) {\n return scrollRef.current?.querySelector<HTMLElement>(\n '[data-slot=\"scroll-area-viewport\"]',\n );\n}\n\n/**\n * Manages scroll position: scrolls to bottom on append/initial load,\n * preserves position when older messages are prepended.\n */\nfunction useScrollManagement(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n messages: GenieMessageItem[],\n status: GenieChatStatus,\n) {\n const prevFirstMessageIdRef = useRef<string | null>(null);\n const prevScrollHeightRef = useRef(0);\n const prevMessageCountRef = useRef(0);\n\n // Keep prevScrollHeightRef fresh when async content (images, embeds)\n // changes the viewport height between renders.\n useEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const observer = new ResizeObserver(() => {\n prevScrollHeightRef.current = viewport.scrollHeight;\n });\n observer.observe(viewport);\n return () => observer.disconnect();\n }, [scrollRef]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: react to message count AND status so prevScrollHeightRef stays accurate when the loading indicator appears/disappears\n useLayoutEffect(() => {\n const viewport = getViewport(scrollRef);\n if (!viewport) return;\n\n const count = messages.length;\n const countChanged = count !== prevMessageCountRef.current;\n prevMessageCountRef.current = count;\n\n // Nothing to do if message count didn't change (e.g. status-only transition)\n if (!countChanged) {\n prevScrollHeightRef.current = viewport.scrollHeight;\n return;\n }\n\n const firstMessageId = messages[0]?.id ?? null;\n const wasPrepend =\n prevFirstMessageIdRef.current !== null &&\n firstMessageId !== prevFirstMessageIdRef.current;\n\n if (wasPrepend && prevScrollHeightRef.current > 0) {\n // Older messages prepended — preserve scroll position\n const delta = viewport.scrollHeight - prevScrollHeightRef.current;\n viewport.scrollTop += delta;\n } else {\n // Messages appended or initial load — scroll to bottom\n viewport.scrollTop = viewport.scrollHeight;\n }\n\n prevFirstMessageIdRef.current = firstMessageId;\n prevScrollHeightRef.current = viewport.scrollHeight;\n }, [messages.length, status]);\n}\n\n/**\n * Observes a sentinel element at the top of the scroll area and triggers\n * `onFetchPreviousPage` when the user scrolls to the top (only if content overflows).\n * Returns a ref to attach to the sentinel element.\n */\nfunction useLoadOlderOnScroll(\n scrollRef: React.RefObject<HTMLDivElement | null>,\n shouldObserve: boolean,\n onFetchPreviousPage?: () => void,\n) {\n const sentinelRef = useRef<HTMLDivElement>(null);\n const onFetchPreviousPageRef = useRef(onFetchPreviousPage);\n onFetchPreviousPageRef.current = onFetchPreviousPage;\n\n useEffect(() => {\n const sentinel = sentinelRef.current;\n const viewport = getViewport(scrollRef);\n if (!sentinel || !viewport || !shouldObserve) return;\n\n // The observer fires synchronously on observe() if the sentinel is\n // already visible. We arm it on the next frame so that synchronous\n // initial fire is ignored, but a real intersection (user genuinely\n // at the top on a short conversation) triggers on subsequent frames.\n let armed = false;\n const frameId = requestAnimationFrame(() => {\n armed = true;\n });\n\n const observer = new IntersectionObserver(\n (entries) => {\n if (!armed) return;\n const isScrollable = viewport.scrollHeight > viewport.clientHeight;\n if (entries[0]?.isIntersecting && isScrollable) {\n onFetchPreviousPageRef.current?.();\n }\n },\n { root: viewport, threshold: 0 },\n );\n\n observer.observe(sentinel);\n return () => {\n cancelAnimationFrame(frameId);\n observer.disconnect();\n };\n }, [scrollRef, shouldObserve]);\n\n return sentinelRef;\n}\n\n/** Scrollable message list that renders Genie chat messages with auto-scroll, skeleton loaders, and a streaming indicator. */\nexport function GenieChatMessageList({\n messages,\n status,\n className,\n hasPreviousPage = false,\n onFetchPreviousPage,\n}: GenieChatMessageListProps) {\n const scrollRef = useRef<HTMLDivElement>(null);\n\n const sentinelRef = useLoadOlderOnScroll(\n scrollRef,\n hasPreviousPage && status !== \"loading-older\",\n onFetchPreviousPage,\n );\n useScrollManagement(scrollRef, messages, status);\n\n const lastMessage = messages[messages.length - 1];\n const showStreamingIndicator =\n status === \"streaming\" &&\n lastMessage?.role === \"assistant\" &&\n lastMessage.id === \"\";\n\n return (\n <ScrollArea ref={scrollRef} className={cn(\"flex-1 min-h-0 p-4\", className)}>\n <div className=\"flex flex-col gap-4\">\n {hasPreviousPage && <div ref={sentinelRef} className=\"h-px\" />}\n\n {status === \"loading-older\" && (\n <div className=\"flex items-center justify-center gap-2 py-2\">\n <Spinner className=\"h-3 w-3\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading older messages...\n </span>\n </div>\n )}\n\n {status === \"loading-history\" && messages.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-12 w-3/4\" />\n <Skeleton className=\"h-20 w-4/5 self-start\" />\n <Skeleton className=\"h-12 w-2/3 self-end\" />\n </div>\n )}\n\n {messages\n .filter(\n (msg) => msg.role !== \"assistant\" || msg.id !== \"\" || msg.content,\n )\n .map((msg) => (\n <GenieChatMessage key={msg.id} message={msg} />\n ))}\n\n {showStreamingIndicator && (\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground px-11\">\n <Spinner className=\"h-3 w-3\" />\n <span>{formatStatus(lastMessage.status)}</span>\n </div>\n )}\n\n {messages.length === 0 && status === \"idle\" && (\n <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm py-12\">\n Start a conversation by typing a question below.\n </div>\n )}\n </div>\n </ScrollArea>\n );\n}\n"],"mappings":";;;;;;;;;AAqBA,MAAM,gBAAwC;CAC5C,WAAW;CACX,iBAAiB;CACjB,mBAAmB;CACnB,WAAW;CACZ;AAED,SAAS,aAAa,QAAwB;AAC5C,QAAO,cAAc,WAAW,OAAO,QAAQ,MAAM,IAAI,CAAC,aAAa;;AAGzE,SAAS,YAAY,WAAmD;AACtE,QAAO,UAAU,SAAS,cACxB,uCACD;;;;;;AAOH,SAAS,oBACP,WACA,UACA,QACA;CACA,MAAM,wBAAwB,OAAsB,KAAK;CACzD,MAAM,sBAAsB,OAAO,EAAE;CACrC,MAAM,sBAAsB,OAAO,EAAE;AAIrC,iBAAgB;EACd,MAAM,WAAW,YAAY,UAAU;AACvC,MAAI,CAAC,SAAU;EAEf,MAAM,WAAW,IAAI,qBAAqB;AACxC,uBAAoB,UAAU,SAAS;IACvC;AACF,WAAS,QAAQ,SAAS;AAC1B,eAAa,SAAS,YAAY;IACjC,CAAC,UAAU,CAAC;AAGf,uBAAsB;EACpB,MAAM,WAAW,YAAY,UAAU;AACvC,MAAI,CAAC,SAAU;EAEf,MAAM,QAAQ,SAAS;EACvB,MAAM,eAAe,UAAU,oBAAoB;AACnD,sBAAoB,UAAU;AAG9B,MAAI,CAAC,cAAc;AACjB,uBAAoB,UAAU,SAAS;AACvC;;EAGF,MAAM,iBAAiB,SAAS,IAAI,MAAM;AAK1C,MAHE,sBAAsB,YAAY,QAClC,mBAAmB,sBAAsB,WAEzB,oBAAoB,UAAU,GAAG;GAEjD,MAAM,QAAQ,SAAS,eAAe,oBAAoB;AAC1D,YAAS,aAAa;QAGtB,UAAS,YAAY,SAAS;AAGhC,wBAAsB,UAAU;AAChC,sBAAoB,UAAU,SAAS;IACtC,CAAC,SAAS,QAAQ,OAAO,CAAC;;;;;;;AAQ/B,SAAS,qBACP,WACA,eACA,qBACA;CACA,MAAM,cAAc,OAAuB,KAAK;CAChD,MAAM,yBAAyB,OAAO,oBAAoB;AAC1D,wBAAuB,UAAU;AAEjC,iBAAgB;EACd,MAAM,WAAW,YAAY;EAC7B,MAAM,WAAW,YAAY,UAAU;AACvC,MAAI,CAAC,YAAY,CAAC,YAAY,CAAC,cAAe;EAM9C,IAAI,QAAQ;EACZ,MAAM,UAAU,4BAA4B;AAC1C,WAAQ;IACR;EAEF,MAAM,WAAW,IAAI,sBAClB,YAAY;AACX,OAAI,CAAC,MAAO;GACZ,MAAM,eAAe,SAAS,eAAe,SAAS;AACtD,OAAI,QAAQ,IAAI,kBAAkB,aAChC,wBAAuB,WAAW;KAGtC;GAAE,MAAM;GAAU,WAAW;GAAG,CACjC;AAED,WAAS,QAAQ,SAAS;AAC1B,eAAa;AACX,wBAAqB,QAAQ;AAC7B,YAAS,YAAY;;IAEtB,CAAC,WAAW,cAAc,CAAC;AAE9B,QAAO;;;AAIT,SAAgB,qBAAqB,EACnC,UACA,QACA,WACA,kBAAkB,OAClB,uBAC4B;CAC5B,MAAM,YAAY,OAAuB,KAAK;CAE9C,MAAM,cAAc,qBAClB,WACA,mBAAmB,WAAW,iBAC9B,oBACD;AACD,qBAAoB,WAAW,UAAU,OAAO;CAEhD,MAAM,cAAc,SAAS,SAAS,SAAS;CAC/C,MAAM,yBACJ,WAAW,eACX,aAAa,SAAS,eACtB,YAAY,OAAO;AAErB,QACE,oBAAC;EAAW,KAAK;EAAW,WAAW,GAAG,sBAAsB,UAAU;YACxE,qBAAC;GAAI,WAAU;;IACZ,mBAAmB,oBAAC;KAAI,KAAK;KAAa,WAAU;MAAS;IAE7D,WAAW,mBACV,qBAAC;KAAI,WAAU;gBACb,oBAAC,WAAQ,WAAU,YAAY,EAC/B,oBAAC;MAAK,WAAU;gBAAgC;OAEzC;MACH;IAGP,WAAW,qBAAqB,SAAS,WAAW,KACnD,qBAAC;KAAI,WAAU;;MACb,oBAAC,YAAS,WAAU,eAAe;MACnC,oBAAC,YAAS,WAAU,0BAA0B;MAC9C,oBAAC,YAAS,WAAU,wBAAwB;;MACxC;IAGP,SACE,QACE,QAAQ,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,IAAI,QAC3D,CACA,KAAK,QACJ,oBAAC,oBAA8B,SAAS,OAAjB,IAAI,GAAoB,CAC/C;IAEH,0BACC,qBAAC;KAAI,WAAU;gBACb,oBAAC,WAAQ,WAAU,YAAY,EAC/B,oBAAC,oBAAM,aAAa,YAAY,OAAO,GAAQ;MAC3C;IAGP,SAAS,WAAW,KAAK,WAAW,UACnC,oBAAC;KAAI,WAAU;eAA8E;MAEvF;;IAEJ;GACK"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GenieMessageItem } from "./types.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/react/genie/genie-chat-message.d.ts
|
|
5
5
|
interface GenieChatMessageProps {
|
|
@@ -12,7 +12,7 @@ interface GenieChatMessageProps {
|
|
|
12
12
|
declare function GenieChatMessage({
|
|
13
13
|
message,
|
|
14
14
|
className
|
|
15
|
-
}: GenieChatMessageProps):
|
|
15
|
+
}: GenieChatMessageProps): react_jsx_runtime0.JSX.Element;
|
|
16
16
|
//#endregion
|
|
17
17
|
export { GenieChatMessage };
|
|
18
18
|
//# sourceMappingURL=genie-chat-message.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"
|
|
1
|
+
{"version":3,"file":"genie-chat-message.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"mappings":";;;;UAyBiB,qBAAA;;EAEf,OAAA,EAAS,gBAAA;EAFM;EAIf,SAAA;AAAA;;iBAQc,gBAAA,CAAA;EACd,OAAA;EACA;AAAA,GACC,qBAAA,GAAqB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import { marked } from \"marked\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar, AvatarFallback } from \"../ui/avatar\";\nimport { Card } from \"../ui/card\";\nimport type { GenieAttachmentResponse, GenieMessageItem } from \"./types\";\n\n/**\n * Using `marked` instead of `react-markdown` because `react-markdown` depends on\n * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.\n * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm\",\n \"[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0\",\n \"[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto\",\n \"[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded\",\n \"[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1\",\n \"[&_table]:border-collapse [&_th]:border [&_td]:border\",\n \"[&_th]:border-border [&_td]:border-border\",\n \"[&_a]:underline\",\n);\n\nexport interface GenieChatMessageProps {\n /** The message object to render */\n message: GenieMessageItem;\n /** Additional CSS class */\n className?: string;\n}\n\nfunction isQueryAttachment(att: GenieAttachmentResponse): boolean {\n return !!(att.query?.title || att.query?.query);\n}\n\n/** Renders a single Genie message bubble with optional expandable SQL query attachments. */\nexport function GenieChatMessage({\n message,\n className,\n}: GenieChatMessageProps) {\n const isUser = message.role === \"user\";\n const queryAttachments = message.attachments.filter(isQueryAttachment);\n const html = useMemo(\n () => (message.content ? (marked.parse(message.content) as string) : \"\"),\n [message.content],\n );\n\n return (\n <div\n className={cn(\n \"flex gap-3\",\n isUser ? \"flex-row-reverse\" : \"flex-row\",\n className,\n )}\n >\n <Avatar className=\"h-8 w-8 shrink-0 mt-1\">\n <AvatarFallback\n className={cn(\n \"text-xs font-medium\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {isUser ? \"You\" : \"AI\"}\n </AvatarFallback>\n </Avatar>\n\n <div\n className={cn(\n \"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\",\n isUser ? \"items-end\" : \"items-start\",\n )}\n >\n <Card\n className={cn(\n \"px-4 py-3 max-w-full overflow-hidden\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {html && (\n <div\n className={markdownStyles}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {message.error && (\n <p className=\"text-sm text-destructive mt-1\">{message.error}</p>\n )}\n </Card>\n\n {queryAttachments.length > 0 && (\n <div className=\"flex flex-col gap-2 w-full min-w-0\">\n {queryAttachments.map((att) => (\n <Card\n key={att.attachmentId ?? \"query\"}\n className=\"px-4 py-3 text-xs overflow-hidden shadow-none\"\n >\n <details>\n <summary className=\"cursor-pointer select-none font-medium\">\n {att.query?.title ?? \"SQL Query\"}\n </summary>\n <div className=\"mt-2 flex flex-col gap-1\">\n {att.query?.description && (\n <span className=\"text-muted-foreground\">\n {att.query.description}\n </span>\n )}\n {att.query?.query && (\n <pre className=\"mt-1 p-2 rounded bg-background text-[11px] whitespace-pre-wrap break-all\">\n {att.query.query}\n </pre>\n )}\n </div>\n </details>\n </Card>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,OAAO,WAAW;CAAE,QAAQ;CAAM,KAAK;CAAM,CAAC;AAE9C,MAAM,iBAAiB,GACrB,WACA,kDACA,gGACA,6EACA,qEACA,yDACA,6CACA,kBACD;AASD,SAAS,kBAAkB,KAAuC;AAChE,QAAO,CAAC,EAAE,IAAI,OAAO,SAAS,IAAI,OAAO;;;AAI3C,SAAgB,iBAAiB,EAC/B,SACA,aACwB;CACxB,MAAM,SAAS,QAAQ,SAAS;CAChC,MAAM,mBAAmB,QAAQ,YAAY,OAAO,kBAAkB;CACtE,MAAM,OAAO,cACJ,QAAQ,UAAW,OAAO,MAAM,QAAQ,QAAQ,GAAc,IACrE,CAAC,QAAQ,QAAQ,CAClB;AAED,QACE,qBAAC
|
|
1
|
+
{"version":3,"file":"genie-chat-message.js","names":[],"sources":["../../../src/react/genie/genie-chat-message.tsx"],"sourcesContent":["import { marked } from \"marked\";\nimport { useMemo } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar, AvatarFallback } from \"../ui/avatar\";\nimport { Card } from \"../ui/card\";\nimport type { GenieAttachmentResponse, GenieMessageItem } from \"./types\";\n\n/**\n * Using `marked` instead of `react-markdown` because `react-markdown` depends on\n * `micromark-util-symbol` which has broken ESM exports with `rolldown-vite`.\n * Content comes from our own Genie API so `dangerouslySetInnerHTML` is safe.\n */\nmarked.setOptions({ breaks: true, gfm: true });\n\nconst markdownStyles = cn(\n \"text-sm\",\n \"[&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0\",\n \"[&_pre]:bg-background/50 [&_pre]:p-2 [&_pre]:rounded [&_pre]:text-xs [&_pre]:overflow-x-auto\",\n \"[&_code]:text-xs [&_code]:bg-background/50 [&_code]:px-1 [&_code]:rounded\",\n \"[&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1\",\n \"[&_table]:border-collapse [&_th]:border [&_td]:border\",\n \"[&_th]:border-border [&_td]:border-border\",\n \"[&_a]:underline\",\n);\n\nexport interface GenieChatMessageProps {\n /** The message object to render */\n message: GenieMessageItem;\n /** Additional CSS class */\n className?: string;\n}\n\nfunction isQueryAttachment(att: GenieAttachmentResponse): boolean {\n return !!(att.query?.title || att.query?.query);\n}\n\n/** Renders a single Genie message bubble with optional expandable SQL query attachments. */\nexport function GenieChatMessage({\n message,\n className,\n}: GenieChatMessageProps) {\n const isUser = message.role === \"user\";\n const queryAttachments = message.attachments.filter(isQueryAttachment);\n const html = useMemo(\n () => (message.content ? (marked.parse(message.content) as string) : \"\"),\n [message.content],\n );\n\n return (\n <div\n className={cn(\n \"flex gap-3\",\n isUser ? \"flex-row-reverse\" : \"flex-row\",\n className,\n )}\n >\n <Avatar className=\"h-8 w-8 shrink-0 mt-1\">\n <AvatarFallback\n className={cn(\n \"text-xs font-medium\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {isUser ? \"You\" : \"AI\"}\n </AvatarFallback>\n </Avatar>\n\n <div\n className={cn(\n \"flex flex-col gap-2 max-w-[80%] min-w-0 overflow-hidden\",\n isUser ? \"items-end\" : \"items-start\",\n )}\n >\n <Card\n className={cn(\n \"px-4 py-3 max-w-full overflow-hidden\",\n isUser ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n )}\n >\n {html && (\n <div\n className={markdownStyles}\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {message.error && (\n <p className=\"text-sm text-destructive mt-1\">{message.error}</p>\n )}\n </Card>\n\n {queryAttachments.length > 0 && (\n <div className=\"flex flex-col gap-2 w-full min-w-0\">\n {queryAttachments.map((att) => (\n <Card\n key={att.attachmentId ?? \"query\"}\n className=\"px-4 py-3 text-xs overflow-hidden shadow-none\"\n >\n <details>\n <summary className=\"cursor-pointer select-none font-medium\">\n {att.query?.title ?? \"SQL Query\"}\n </summary>\n <div className=\"mt-2 flex flex-col gap-1\">\n {att.query?.description && (\n <span className=\"text-muted-foreground\">\n {att.query.description}\n </span>\n )}\n {att.query?.query && (\n <pre className=\"mt-1 p-2 rounded bg-background text-[11px] whitespace-pre-wrap break-all\">\n {att.query.query}\n </pre>\n )}\n </div>\n </details>\n </Card>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,OAAO,WAAW;CAAE,QAAQ;CAAM,KAAK;CAAM,CAAC;AAE9C,MAAM,iBAAiB,GACrB,WACA,kDACA,gGACA,6EACA,qEACA,yDACA,6CACA,kBACD;AASD,SAAS,kBAAkB,KAAuC;AAChE,QAAO,CAAC,EAAE,IAAI,OAAO,SAAS,IAAI,OAAO;;;AAI3C,SAAgB,iBAAiB,EAC/B,SACA,aACwB;CACxB,MAAM,SAAS,QAAQ,SAAS;CAChC,MAAM,mBAAmB,QAAQ,YAAY,OAAO,kBAAkB;CACtE,MAAM,OAAO,cACJ,QAAQ,UAAW,OAAO,MAAM,QAAQ,QAAQ,GAAc,IACrE,CAAC,QAAQ,QAAQ,CAClB;AAED,QACE,qBAAC;EACC,WAAW,GACT,cACA,SAAS,qBAAqB,YAC9B,UACD;aAED,oBAAC;GAAO,WAAU;aAChB,oBAAC;IACC,WAAW,GACT,uBACA,SAAS,uCAAuC,WACjD;cAEA,SAAS,QAAQ;KACH;IACV,EAET,qBAAC;GACC,WAAW,GACT,2DACA,SAAS,cAAc,cACxB;cAED,qBAAC;IACC,WAAW,GACT,wCACA,SAAS,uCAAuC,WACjD;eAEA,QACC,oBAAC;KACC,WAAW;KACX,yBAAyB,EAAE,QAAQ,MAAM;MACzC,EAGH,QAAQ,SACP,oBAAC;KAAE,WAAU;eAAiC,QAAQ;MAAU;KAE7D,EAEN,iBAAiB,SAAS,KACzB,oBAAC;IAAI,WAAU;cACZ,iBAAiB,KAAK,QACrB,oBAAC;KAEC,WAAU;eAEV,qBAAC,wBACC,oBAAC;MAAQ,WAAU;gBAChB,IAAI,OAAO,SAAS;OACb,EACV,qBAAC;MAAI,WAAU;iBACZ,IAAI,OAAO,eACV,oBAAC;OAAK,WAAU;iBACb,IAAI,MAAM;QACN,EAER,IAAI,OAAO,SACV,oBAAC;OAAI,WAAU;iBACZ,IAAI,MAAM;QACP;OAEJ,IACE;OAnBL,IAAI,gBAAgB,QAoBpB,CACP;KACE;IAEJ;GACF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GenieChatProps } from "./types.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/react/genie/genie-chat.d.ts
|
|
5
5
|
/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */
|
|
@@ -8,7 +8,7 @@ declare function GenieChat({
|
|
|
8
8
|
basePath,
|
|
9
9
|
placeholder,
|
|
10
10
|
className
|
|
11
|
-
}: GenieChatProps):
|
|
11
|
+
}: GenieChatProps): react_jsx_runtime0.JSX.Element;
|
|
12
12
|
//#endregion
|
|
13
13
|
export { GenieChat };
|
|
14
14
|
//# sourceMappingURL=genie-chat.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat.tsx"],"
|
|
1
|
+
{"version":3,"file":"genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/genie-chat.tsx"],"mappings":";;;;;iBAQgB,SAAA,CAAA;EACd,KAAA;EACA,QAAA;EACA,WAAA;EACA;AAAA,GACC,cAAA,GAAc,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -8,7 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
//#region src/react/genie/genie-chat.tsx
|
|
9
9
|
/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */
|
|
10
10
|
function GenieChat({ alias, basePath, placeholder, className }) {
|
|
11
|
-
const { messages, status, error, sendMessage, reset } = useGenieChat({
|
|
11
|
+
const { messages, status, error, sendMessage, reset, hasPreviousPage, fetchPreviousPage } = useGenieChat({
|
|
12
12
|
alias,
|
|
13
13
|
basePath
|
|
14
14
|
});
|
|
@@ -27,7 +27,9 @@ function GenieChat({ alias, basePath, placeholder, className }) {
|
|
|
27
27
|
}),
|
|
28
28
|
/* @__PURE__ */ jsx(GenieChatMessageList, {
|
|
29
29
|
messages,
|
|
30
|
-
status
|
|
30
|
+
status,
|
|
31
|
+
hasPreviousPage,
|
|
32
|
+
onFetchPreviousPage: fetchPreviousPage
|
|
31
33
|
}),
|
|
32
34
|
error && /* @__PURE__ */ jsx("div", {
|
|
33
35
|
className: "shrink-0 px-4 py-2 text-sm text-destructive bg-destructive/10 border-t",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"genie-chat.js","names":[],"sources":["../../../src/react/genie/genie-chat.tsx"],"sourcesContent":["import { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { GenieChatInput } from \"./genie-chat-input\";\nimport { GenieChatMessageList } from \"./genie-chat-message-list\";\nimport type { GenieChatProps } from \"./types\";\nimport { useGenieChat } from \"./use-genie-chat\";\n\n/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */\nexport function GenieChat({\n alias,\n basePath,\n placeholder,\n className,\n}: GenieChatProps) {\n const {
|
|
1
|
+
{"version":3,"file":"genie-chat.js","names":[],"sources":["../../../src/react/genie/genie-chat.tsx"],"sourcesContent":["import { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { GenieChatInput } from \"./genie-chat-input\";\nimport { GenieChatMessageList } from \"./genie-chat-message-list\";\nimport type { GenieChatProps } from \"./types\";\nimport { useGenieChat } from \"./use-genie-chat\";\n\n/** Full-featured chat interface for a single Databricks AI/BI Genie space. Handles message streaming, conversation history, and auto-reconnection via SSE. */\nexport function GenieChat({\n alias,\n basePath,\n placeholder,\n className,\n}: GenieChatProps) {\n const {\n messages,\n status,\n error,\n sendMessage,\n reset,\n hasPreviousPage,\n fetchPreviousPage,\n } = useGenieChat({\n alias,\n basePath,\n });\n\n return (\n <div className={cn(\"flex flex-col h-full overflow-hidden\", className)}>\n {messages.length > 0 && (\n <div className=\"shrink-0 flex justify-end px-4 pt-3 pb-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={reset}\n className=\"text-xs text-muted-foreground\"\n >\n New conversation\n </Button>\n </div>\n )}\n\n <GenieChatMessageList\n messages={messages}\n status={status}\n hasPreviousPage={hasPreviousPage}\n onFetchPreviousPage={fetchPreviousPage}\n />\n\n {error && (\n <div className=\"shrink-0 px-4 py-2 text-sm text-destructive bg-destructive/10 border-t\">\n {error}\n </div>\n )}\n\n <GenieChatInput\n onSend={sendMessage}\n disabled={status === \"streaming\" || status === \"loading-history\"}\n placeholder={placeholder}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,UAAU,EACxB,OACA,UACA,aACA,aACiB;CACjB,MAAM,EACJ,UACA,QACA,OACA,aACA,OACA,iBACA,sBACE,aAAa;EACf;EACA;EACD,CAAC;AAEF,QACE,qBAAC;EAAI,WAAW,GAAG,wCAAwC,UAAU;;GAClE,SAAS,SAAS,KACjB,oBAAC;IAAI,WAAU;cACb,oBAAC;KACC,SAAQ;KACR,MAAK;KACL,SAAS;KACT,WAAU;eACX;MAEQ;KACL;GAGR,oBAAC;IACW;IACF;IACS;IACjB,qBAAqB;KACrB;GAED,SACC,oBAAC;IAAI,WAAU;cACZ;KACG;GAGR,oBAAC;IACC,QAAQ;IACR,UAAU,WAAW,eAAe,WAAW;IAClC;KACb;;GACE"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GenieAttachmentResponse, GenieMessageResponse, GenieStreamEvent } from "../../shared/src/genie.js";
|
|
2
|
+
import { GenieChatProps, GenieChatStatus, GenieMessageItem, UseGenieChatOptions, UseGenieChatReturn } from "./types.js";
|
|
3
|
+
import { GenieChat } from "./genie-chat.js";
|
|
4
|
+
import { GenieChatInput } from "./genie-chat-input.js";
|
|
5
|
+
import { GenieChatMessage } from "./genie-chat-message.js";
|
|
6
|
+
import { GenieChatMessageList } from "./genie-chat-message-list.js";
|
|
7
|
+
import { useGenieChat } from "./use-genie-chat.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { GenieAttachmentResponse, GenieMessageResponse, GenieStreamEvent } from "../../shared/src/genie.js";
|
|
2
|
+
import "../../shared/src/index.js";
|
|
2
3
|
|
|
3
4
|
//#region src/react/genie/types.d.ts
|
|
4
|
-
type GenieChatStatus = "idle" | "loading-history" | "streaming" | "error";
|
|
5
|
+
type GenieChatStatus = "idle" | "loading-history" | "loading-older" | "streaming" | "error";
|
|
5
6
|
interface GenieMessageItem {
|
|
6
7
|
id: string;
|
|
7
8
|
role: "user" | "assistant";
|
|
@@ -28,6 +29,12 @@ interface UseGenieChatReturn {
|
|
|
28
29
|
error: string | null;
|
|
29
30
|
sendMessage: (content: string) => void;
|
|
30
31
|
reset: () => void;
|
|
32
|
+
/** Whether a previous page of older messages exists */
|
|
33
|
+
hasPreviousPage: boolean;
|
|
34
|
+
/** Whether a previous page is currently being fetched */
|
|
35
|
+
isFetchingPreviousPage: boolean;
|
|
36
|
+
/** Fetch the previous page of older messages */
|
|
37
|
+
fetchPreviousPage: () => void;
|
|
31
38
|
}
|
|
32
39
|
interface GenieChatProps {
|
|
33
40
|
/** Genie space alias (must match a key registered with the genie plugin on the server) */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/genie/types.ts"],"
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/react/genie/types.ts"],"mappings":";;;;KAQY,eAAA;AAAA,UAOK,gBAAA;EACf,EAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,WAAA,EAAa,uBAAA;EACb,YAAA,EAAc,GAAA;EACd,KAAA;AAAA;AAAA,UAGe,mBAAA;EATf;EAWA,KAAA;EATA;EAWA,QAAA;EATA;EAWA,YAAA;EAVA;EAYA,YAAA;AAAA;AAAA,UAGe,kBAAA;EACf,QAAA,EAAU,gBAAA;EACV,MAAA,EAAQ,eAAA;EACR,cAAA;EACA,KAAA;EACA,WAAA,GAAc,OAAA;EACd,KAAA;EAbA;EAeA,eAAA;EAXA;EAaA,sBAAA;EAbY;EAeZ,iBAAA;AAAA;AAAA,UAGe,cAAA;EAbQ;EAevB,KAAA;EAhBU;EAkBV,QAAA;EAjBQ;EAmBR,WAAA;EAjBA;EAmBA,SAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"
|
|
1
|
+
{"version":3,"file":"use-genie-chat.d.ts","names":[],"sources":["../../../src/react/genie/use-genie-chat.ts"],"mappings":";;;;;AAqJA;;;;;;;iBAAgB,YAAA,CAAa,OAAA,EAAS,mBAAA,GAAsB,kBAAA"}
|