tempest-react-sdk 0.38.3 → 0.39.1
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/README.md +1 -1
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/access/access-control-context.cjs.map +1 -1
- package/dist/access/use-can.cjs.map +1 -1
- package/dist/app/AppProviders.cjs.map +1 -1
- package/dist/app/AppProviders.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-microphone.cjs.map +1 -1
- package/dist/auth/create-auth-store.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/br/BrazilMap.cjs +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js +2 -2
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js +2 -2
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/MapLegend.cjs.map +1 -1
- package/dist/br/MapLegend.js.map +1 -1
- package/dist/br/MapMarkers.cjs.map +1 -1
- package/dist/br/MapMarkers.js.map +1 -1
- package/dist/br/MunicipalitySearch.cjs.map +1 -1
- package/dist/br/MunicipalitySearch.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/use-map-hover.cjs.map +1 -1
- package/dist/br/use-map-zoom.cjs.map +1 -1
- package/dist/capture/media-recorder.cjs +1 -1
- package/dist/capture/media-recorder.js +2 -2
- package/dist/capture/use-barcode-scanner.cjs +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js +2 -2
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-torch.cjs.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/charts/AreaChart.cjs.map +1 -1
- package/dist/charts/AreaChart.js.map +1 -1
- package/dist/charts/BarChart.cjs.map +1 -1
- package/dist/charts/BarChart.js.map +1 -1
- package/dist/charts/LineChart.cjs.map +1 -1
- package/dist/charts/LineChart.js.map +1 -1
- package/dist/charts/PieChart.cjs.map +1 -1
- package/dist/charts/PieChart.js.map +1 -1
- package/dist/charts/RadarChart.cjs.map +1 -1
- package/dist/charts/RadarChart.js.map +1 -1
- package/dist/charts/use-chart-colors.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Accordion/Accordion.cjs.map +1 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/Alert/Alert.cjs.map +1 -1
- package/dist/components/Alert/Alert.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppShell/AppShell.cjs.map +1 -1
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/Avatar/Avatar.cjs.map +1 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.cjs.map +1 -1
- package/dist/components/AvatarGroup/AvatarGroup.js.map +1 -1
- package/dist/components/Badge/Badge.cjs.map +1 -1
- package/dist/components/Badge/Badge.js.map +1 -1
- package/dist/components/Banner/Banner.cjs.map +1 -1
- package/dist/components/Banner/Banner.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js +38 -41
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.cjs.map +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/Chat/ChatComposer.cjs.map +1 -1
- package/dist/components/Chat/ChatComposer.js.map +1 -1
- package/dist/components/Chat/chat-groups.cjs +1 -1
- package/dist/components/Chat/chat-groups.cjs.map +1 -1
- package/dist/components/Chat/chat-groups.js +2 -2
- package/dist/components/Chat/chat-groups.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.cjs.map +1 -1
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Collapsible/Collapsible.cjs.map +1 -1
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataList/DataList.cjs.map +1 -1
- package/dist/components/DataList/DataList.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +2 -2
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.cjs.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -1
- package/dist/components/Divider/Divider.cjs.map +1 -1
- package/dist/components/Divider/Divider.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.cjs.map +1 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.cjs.map +1 -1
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.cjs.map +1 -1
- package/dist/components/FloatingActionButton/FloatingActionButton.js.map +1 -1
- package/dist/components/Form/Form.cjs.map +1 -1
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/HoverCard/HoverCard.cjs.map +1 -1
- package/dist/components/HoverCard/HoverCard.js.map +1 -1
- package/dist/components/Image/Image.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/Input/Input.cjs.map +1 -1
- package/dist/components/Input/Input.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Label/Label.cjs.map +1 -1
- package/dist/components/Label/Label.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js +2 -2
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Masonry/Masonry.cjs.map +1 -1
- package/dist/components/Masonry/Masonry.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NProgress/NProgress.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.cjs.map +1 -1
- package/dist/components/Navbar/Navbar.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/NotificationCenter/use-notification-inbox.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.cjs.map +1 -1
- package/dist/components/OfflineIndicator/OfflineIndicator.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/QRCode.cjs.map +1 -1
- package/dist/components/QRCode/QRCode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.cjs.map +1 -1
- package/dist/components/RelativeTime/RelativeTime.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.cjs.map +1 -1
- package/dist/components/SearchBar/SearchBar.js.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/components/Select/Select.cjs.map +1 -1
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/Spinner/Spinner.cjs.map +1 -1
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/Stat/Stat.cjs.map +1 -1
- package/dist/components/Stat/Stat.js.map +1 -1
- package/dist/components/Stepper/Stepper.cjs.map +1 -1
- package/dist/components/Stepper/Stepper.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Switch/Switch.cjs.map +1 -1
- package/dist/components/Switch/Switch.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tabs/Tabs.cjs.map +1 -1
- package/dist/components/Tabs/Tabs.js.map +1 -1
- package/dist/components/Tag/Tag.cjs.map +1 -1
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Textarea/Textarea.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Timeline/Timeline.cjs.map +1 -1
- package/dist/components/Timeline/Timeline.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Toggle/Toggle.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js +1 -4
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.cjs.map +1 -1
- package/dist/components/UpdatePrompt/UpdatePrompt.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/VisuallyHidden/VisuallyHidden.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/data/data-provider-context.cjs.map +1 -1
- package/dist/data/use-resource.cjs.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/error-boundary/use-error-handler.cjs.map +1 -1
- package/dist/feature-flags/FeatureFlagsProvider.cjs.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/use-viacep.cjs.map +1 -1
- package/dist/forms/use-zod-form.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/leaflet-map.cjs.map +1 -1
- package/dist/geo/leaflet-map.js.map +1 -1
- package/dist/geo/use-position-tracker.cjs.map +1 -1
- package/dist/hooks/use-announce.cjs.map +1 -1
- package/dist/hooks/use-async.cjs.map +1 -1
- package/dist/hooks/use-before-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-breakpoint.cjs.map +1 -1
- package/dist/hooks/use-click-outside.cjs.map +1 -1
- package/dist/hooks/use-client-filter.cjs.map +1 -1
- package/dist/hooks/use-clipboard.cjs.map +1 -1
- package/dist/hooks/use-counter.cjs.map +1 -1
- package/dist/hooks/use-debounce.cjs.map +1 -1
- package/dist/hooks/use-deep-memo.cjs.map +1 -1
- package/dist/hooks/use-disclosure.cjs.map +1 -1
- package/dist/hooks/use-document-visibility.cjs.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-geolocation.cjs.map +1 -1
- package/dist/hooks/use-hover.cjs.map +1 -1
- package/dist/hooks/use-idle.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.cjs +1 -1
- package/dist/hooks/use-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.js +1 -1
- package/dist/hooks/use-install-prompt.js.map +1 -1
- package/dist/hooks/use-intersection-observer.cjs.map +1 -1
- package/dist/hooks/use-interval.cjs.map +1 -1
- package/dist/hooks/use-is-first-render.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs +1 -1
- package/dist/hooks/use-keyboard-shortcut.cjs.map +1 -1
- package/dist/hooks/use-keyboard-shortcut.js +1 -1
- package/dist/hooks/use-keyboard-shortcut.js.map +1 -1
- package/dist/hooks/use-list-state.cjs.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
- package/dist/hooks/use-long-press.cjs.map +1 -1
- package/dist/hooks/use-map.cjs.map +1 -1
- package/dist/hooks/use-media-devices.cjs.map +1 -1
- package/dist/hooks/use-media-permission.cjs.map +1 -1
- package/dist/hooks/use-media-query.cjs.map +1 -1
- package/dist/hooks/use-object-url.cjs.map +1 -1
- package/dist/hooks/use-online.cjs.map +1 -1
- package/dist/hooks/use-pagination.cjs.map +1 -1
- package/dist/hooks/use-previous.cjs.map +1 -1
- package/dist/hooks/use-queue.cjs.map +1 -1
- package/dist/hooks/use-resize-observer.cjs.map +1 -1
- package/dist/hooks/use-scroll-overflow.cjs.map +1 -1
- package/dist/hooks/use-service-worker-update.cjs.map +1 -1
- package/dist/hooks/use-set.cjs.map +1 -1
- package/dist/hooks/use-sortable.cjs +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js +0 -2
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/hooks/use-stable-callback.cjs.map +1 -1
- package/dist/hooks/use-storage-estimate.cjs.map +1 -1
- package/dist/hooks/use-throttle.cjs.map +1 -1
- package/dist/hooks/use-timeout.cjs.map +1 -1
- package/dist/hooks/use-toggle.cjs.map +1 -1
- package/dist/hooks/use-window-size.cjs.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +7 -5
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/use-poll.cjs.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/icons/Icon.cjs.map +1 -1
- package/dist/icons/IconProvider.cjs.map +1 -1
- package/dist/icons/icon-context.cjs.map +1 -1
- package/dist/icons/use-icon.cjs.map +1 -1
- package/dist/imaging/use-image-processing.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/offline/use-offline-sync.cjs.map +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/query/QueryProvider.cjs.map +1 -1
- package/dist/query/constants.cjs +1 -1
- package/dist/query/constants.cjs.map +1 -1
- package/dist/query/constants.js +10 -10
- package/dist/query/constants.js.map +1 -1
- package/dist/query/create-query-keys.cjs +1 -1
- package/dist/query/create-query-keys.cjs.map +1 -1
- package/dist/query/create-query-keys.js +1 -1
- package/dist/query/create-query-keys.js.map +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/use-cursor-query.cjs.map +1 -1
- package/dist/query/use-offline-mutation.cjs.map +1 -1
- package/dist/query/use-paginated-query.cjs.map +1 -1
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +7 -2
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/sse/use-event-stream.cjs.map +1 -1
- package/dist/store/create-store.cjs.map +1 -1
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
- package/dist/telemetry/TelemetryProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/color.cjs +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/utils/cn.cjs +1 -1
- package/dist/utils/cn.cjs.map +1 -1
- package/dist/utils/cn.js +1 -1
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/objects.cjs +1 -1
- package/dist/utils/objects.cjs.map +1 -1
- package/dist/utils/objects.js +1 -1
- package/dist/utils/objects.js.map +1 -1
- package/dist/utils/xlsx.cjs +1 -1
- package/dist/utils/xlsx.cjs.map +1 -1
- package/dist/utils/xlsx.js +3 -3
- package/dist/utils/xlsx.js.map +1 -1
- package/dist/vision/core/canvas.cjs +1 -1
- package/dist/vision/core/canvas.cjs.map +1 -1
- package/dist/vision/core/canvas.js +4 -4
- package/dist/vision/core/canvas.js.map +1 -1
- package/dist/vision/core/exceptions.cjs +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js +2 -2
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/metadata.cjs +2 -2
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js +11 -8
- package/dist/vision/core/metadata.js.map +1 -1
- package/dist/vision/core/session.cjs +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js +8 -5
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/fusion.cjs +2 -0
- package/dist/vision/fusion.cjs.map +1 -0
- package/dist/vision/fusion.js +42 -0
- package/dist/vision/fusion.js.map +1 -0
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +23 -20
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/io/image.cjs +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js +4 -2
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/labels.cjs +1 -1
- package/dist/vision/labels.cjs.map +1 -1
- package/dist/vision/labels.js +7 -4
- package/dist/vision/labels.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js +3 -10
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js +11 -15
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/preprocess/image.cjs +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js +4 -2
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/preprocess/pipeline.cjs +2 -0
- package/dist/vision/preprocess/pipeline.cjs.map +1 -0
- package/dist/vision/preprocess/pipeline.js +61 -0
- package/dist/vision/preprocess/pipeline.js.map +1 -0
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +23 -2
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/base.cjs +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js +11 -2
- package/dist/vision/tasks/base.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +2 -2
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detectClassify.cjs +2 -0
- package/dist/vision/tasks/detectClassify.cjs.map +1 -0
- package/dist/vision/tasks/detectClassify.js +222 -0
- package/dist/vision/tasks/detectClassify.js.map +1 -0
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +54 -29
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +53 -28
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/types.cjs.map +1 -1
- package/dist/vision/types.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-live-luminance.cjs.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +1517 -962
- package/dist/vision.js +25 -22
- package/dist/vite/create-vite-config.cjs +1 -1
- package/dist/vite/create-vite-config.cjs.map +1 -1
- package/dist/vite/create-vite-config.js +3 -3
- package/dist/vite/create-vite-config.js.map +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +5 -3
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +2 -2
- package/template/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RadarChart.js","names":[],"sources":["../../src/charts/RadarChart.tsx"],"sourcesContent":["import {\n Legend,\n PolarAngleAxis,\n PolarGrid,\n PolarRadiusAxis,\n RadarChart as RRadarChart,\n Radar,\n ResponsiveContainer,\n Tooltip,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * RadarChart — themed wrapper over recharts `RadarChart`.\n *\n * Plots one radar polygon per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). `index` drives the angle\n * axis. When `width` is provided the chart renders at that fixed size without a\n * ResponsiveContainer; otherwise it fills its parent.\n */\nexport function RadarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RRadarChart data={data} width={width} height={height}>\n <PolarGrid />\n <PolarAngleAxis dataKey={index} />\n <PolarRadiusAxis />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Radar\n key={category}\n dataKey={category}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RRadarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,EAAW,EACvB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,gBAAa,IACb,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IACF,kBAAC,GAAD;EAAmB;EAAa;EAAe;
|
|
1
|
+
{"version":3,"file":"RadarChart.js","names":[],"sources":["../../src/charts/RadarChart.tsx"],"sourcesContent":["import {\n Legend,\n PolarAngleAxis,\n PolarGrid,\n PolarRadiusAxis,\n RadarChart as RRadarChart,\n Radar,\n ResponsiveContainer,\n Tooltip,\n} from \"recharts\";\nimport { cn } from \"@/utils/cn\";\nimport { useChartColors } from \"./use-chart-colors\";\nimport { toTooltipFormatter } from \"./types\";\nimport type { CartesianChartProps } from \"./types\";\n\n/**\n * RadarChart — themed wrapper over recharts `RadarChart`.\n *\n * Plots one radar polygon per entry in `categories`, colored from `colors`\n * (cycling the `--tempest-chart-*` theme tokens by default). `index` drives the angle\n * axis. When `width` is provided the chart renders at that fixed size without a\n * ResponsiveContainer; otherwise it fills its parent.\n */\nexport function RadarChart({\n data,\n index,\n categories,\n colors,\n height = 300,\n width,\n showLegend = true,\n showTooltip = true,\n valueFormatter,\n className,\n}: CartesianChartProps) {\n const palette = useChartColors(colors);\n const chart = (\n <RRadarChart data={data} width={width} height={height}>\n <PolarGrid />\n <PolarAngleAxis dataKey={index} />\n <PolarRadiusAxis />\n {showTooltip ? <Tooltip formatter={toTooltipFormatter(valueFormatter)} /> : null}\n {showLegend ? <Legend /> : null}\n {categories.map((category, i) => {\n const color = palette[i % palette.length];\n return (\n <Radar\n key={category}\n dataKey={category}\n stroke={color}\n fill={color}\n fillOpacity={0.3}\n />\n );\n })}\n </RRadarChart>\n );\n\n return (\n <div className={cn(className)}>\n {width !== undefined ? (\n chart\n ) : (\n <ResponsiveContainer width=\"100%\" height={height}>\n {chart}\n </ResponsiveContainer>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,EAAW,EACvB,SACA,UACA,eACA,WACA,YAAS,KACT,UACA,gBAAa,IACb,iBAAc,IACd,mBACA,gBACoB;CACpB,IAAM,IAAU,EAAe,CAAM,GAC/B,IACF,kBAAC,GAAD;EAAmB;EAAa;EAAe;EAA/C,UAAA;GACI,kBAAC,GAAD,CAAY,CAAA;GACZ,kBAAC,GAAD,EAAgB,SAAS,EAAQ,CAAA;GACjC,kBAAC,GAAD,CAAkB,CAAA;GACjB,IAAc,kBAAC,GAAD,EAAS,WAAW,EAAmB,CAAc,EAAI,CAAA,IAAI;GAC3E,IAAa,kBAAC,GAAD,CAAS,CAAA,IAAI;GAC1B,EAAW,KAAK,GAAU,MAAM;IAC7B,IAAM,IAAQ,EAAQ,IAAI,EAAQ;IAClC,OACI,kBAAC,GAAD;KAEI,SAAS;KACT,QAAQ;KACR,MAAM;KACN,aAAa;IAChB,GALQ,CAKR;GAET,CAAC;EACQ;;CAGjB,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,CAAS;EACvB,UAAA,MAAU,KAAA,IAGP,kBAAC,GAAD;GAAqB,OAAM;GAAe;GACrC,UAAA;EACgB,CAAA,IAJrB;CAMH,CAAA;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chart-colors.cjs","names":[],"sources":["../../src/charts/use-chart-colors.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\n\nimport { resolveChartColors } from \"./palette\";\n\n/**\n * Theme-aware series colors for a chart.\n *\n * Resolves the `--tempest-chart-*` tokens on mount and again whenever the theme\n * attribute flips, so switching to dark re-colors the series instead of leaving\n * light-theme colors on a dark canvas. Passing `explicit` short-circuits the\n * whole thing — an explicit `colors` prop always wins and no observer is set up.\n *\n * @param explicit - Colors passed by the caller. When present, returned as-is.\n * @param element - Element to resolve tokens against. Default `<html>`.\n * @returns Colors in cycle order.\n *\n * @example\n * ```tsx\n * function Sales({ data }: { data: ChartDatum[] }) {\n * const colors = useChartColors();\n * return <BarChart data={data} colors={colors} />;\n * }\n * ```\n */\nexport function useChartColors(explicit?: string[], element?: Element | null): string[] {\n const [resolved, setResolved] = useState<string[]>(() =>\n explicit ? explicit : resolveChartColors(element),\n );\n\n useEffect(() => {\n if (explicit) return;\n if (typeof window === \"undefined\" || typeof MutationObserver === \"undefined\") return;\n\n const target = element ?? document.documentElement;\n if (!target) return;\n\n const sync = (): void => setResolved(resolveChartColors(target));\n sync();\n\n const roots = new Set<Element>([target]);\n const documentRoot = target.ownerDocument?.documentElement;\n if (documentRoot) roots.add(documentRoot);\n\n const observer = new MutationObserver(sync);\n for (const root of roots) {\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\"data-tempest-theme\", \"class\", \"style\"],\n });\n }\n\n return () => observer.disconnect();\n }, [explicit, element]);\n\n return explicit ?? resolved;\n}\n"],"mappings":"wDAwBA,SAAgB,EAAe,EAAqB,EAAoC,CACpF,GAAM,CAAC,EAAU,IAAA,
|
|
1
|
+
{"version":3,"file":"use-chart-colors.cjs","names":[],"sources":["../../src/charts/use-chart-colors.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\n\nimport { resolveChartColors } from \"./palette\";\n\n/**\n * Theme-aware series colors for a chart.\n *\n * Resolves the `--tempest-chart-*` tokens on mount and again whenever the theme\n * attribute flips, so switching to dark re-colors the series instead of leaving\n * light-theme colors on a dark canvas. Passing `explicit` short-circuits the\n * whole thing — an explicit `colors` prop always wins and no observer is set up.\n *\n * @param explicit - Colors passed by the caller. When present, returned as-is.\n * @param element - Element to resolve tokens against. Default `<html>`.\n * @returns Colors in cycle order.\n *\n * @example\n * ```tsx\n * function Sales({ data }: { data: ChartDatum[] }) {\n * const colors = useChartColors();\n * return <BarChart data={data} colors={colors} />;\n * }\n * ```\n */\nexport function useChartColors(explicit?: string[], element?: Element | null): string[] {\n const [resolved, setResolved] = useState<string[]>(() =>\n explicit ? explicit : resolveChartColors(element),\n );\n\n useEffect(() => {\n if (explicit) return;\n if (typeof window === \"undefined\" || typeof MutationObserver === \"undefined\") return;\n\n const target = element ?? document.documentElement;\n if (!target) return;\n\n const sync = (): void => setResolved(resolveChartColors(target));\n sync();\n\n const roots = new Set<Element>([target]);\n const documentRoot = target.ownerDocument?.documentElement;\n if (documentRoot) roots.add(documentRoot);\n\n const observer = new MutationObserver(sync);\n for (const root of roots) {\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\"data-tempest-theme\", \"class\", \"style\"],\n });\n }\n\n return () => observer.disconnect();\n }, [explicit, element]);\n\n return explicit ?? resolved;\n}\n"],"mappings":"wDAwBA,SAAgB,EAAe,EAAqB,EAAoC,CACpF,GAAM,CAAC,EAAU,IAAA,EAAe,EAAA,SAAA,KAC5B,GAAsB,EAAA,mBAAmB,CAAO,CACpD,EA2BA,OAzBA,EAAA,EAAA,UAAA,KAAgB,CAEZ,GADI,GACA,OAAO,OAAW,KAAe,OAAO,iBAAqB,IAAa,OAE9E,IAAM,EAAS,GAAW,SAAS,gBACnC,GAAI,CAAC,EAAQ,OAEb,IAAM,MAAmB,EAAY,EAAA,mBAAmB,CAAM,CAAC,EAC/D,EAAK,EAEL,IAAM,EAAQ,IAAI,IAAa,CAAC,CAAM,CAAC,EACjC,EAAe,EAAO,eAAe,gBACvC,GAAc,EAAM,IAAI,CAAY,EAExC,IAAM,EAAW,IAAI,iBAAiB,CAAI,EAC1C,IAAK,IAAM,KAAQ,EACf,EAAS,QAAQ,EAAM,CACnB,WAAY,GACZ,gBAAiB,CAAC,qBAAsB,QAAS,OAAO,CAC5D,CAAC,EAGL,UAAa,EAAS,WAAW,CACrC,EAAG,CAAC,EAAU,CAAO,CAAC,EAEf,GAAY,CACvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChat.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the widest surface in\n * the SDK on purpose: it is a whole chat page, and its 25 props are four groups —\n * the transcript (messages, votes, pending, showSystem), the turn actions an app has\n * to own (onSend, onStop, onRegenerate, onEditSubmit, onFeedback, onRetry,\n * onSendError), render slots (renderAvatar, renderContent, header, emptyState,\n * suggestions, composerActions, composerFooter) and composer settings (composerRef,\n * composerDisabled, maxRows, placeholder, locale, defaultReasoningOpen,\n * showLineNumbers). The parts already are separate components — AIChatTurn and\n * AIChatComposer are exported — and this is the assembly whose entire job is to\n * accept the union of their configuration.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport {\n aiChatStrings,\n isGenerating,\n lastAssistantId,\n tailSignature,\n visibleTurns,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The transcript, **oldest first**. Never reordered by the component. */\n messages: readonly AIChatMessage[];\n /** Renders the composer when given. Receives the trimmed prompt. */\n onSend?: (text: string) => void | Promise<void>;\n /** Abort the turn in flight. Shows the stop button while generating. */\n onStop?: () => void;\n /** Ask again for the newest assistant turn. */\n onRegenerate?: (message: AIChatMessage) => void;\n /** Re-submit an edited user turn. The app decides what to drop after it. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /** Rating on an assistant turn. */\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Retry a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n /**\n * The request is out and nothing has arrived yet.\n *\n * Distinct from a turn with `streaming: true`: apps that only push a message\n * once the first token lands need somewhere to say \"we asked\", and without it the\n * screen is frozen for however long the model takes to start.\n */\n pending?: boolean;\n /** Prompts offered on an empty transcript. Clicking one sends it. */\n suggestions?: readonly string[];\n /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render a body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /** Ratings to show as pressed, by message id. Omit to keep them local. */\n votes?: Readonly<Record<string, AIChatVote>>;\n /** Rendered above the transcript, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no turns and no suggestions. */\n emptyState?: ReactNode;\n /** Show `\"system\"` turns. Default `false`. */\n showSystem?: boolean;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /**\n * Reach the composer imperatively — `focus()`, `getValue()`, `setValue()`.\n *\n * What makes dictation (or a slash-command menu, or \"edit and resend\") possible\n * without this component knowing anything about them: pair it with\n * `composerActions` and the button you put in the composer can write into the\n * field. Speech recognition is **not** wired in here on purpose — it would make\n * every consumer of `AIChat` pay for an API that streams audio to a third party.\n */\n composerRef?: Ref<AIChatComposerHandle>;\n /** Under the composer field — token count, model name, a disclaimer. */\n composerFooter?: ReactNode;\n /** Disable the composer — no credits, conversation archived, offline. */\n composerDisabled?: boolean;\n /** Largest height the composer grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n * either way.\n */\n onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n * messages={turns}\n * pending={pending}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.current?.abort()}\n * onRegenerate={(turn) => reask(turn)}\n * onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n * suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n messages,\n onSend,\n onStop,\n onRegenerate,\n onEditSubmit,\n onFeedback,\n onRetry,\n pending = false,\n suggestions = [],\n renderAvatar,\n renderContent,\n votes,\n header,\n emptyState,\n showSystem = false,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n locale = \"pt-BR\",\n placeholder,\n composerActions,\n composerRef,\n composerFooter,\n composerDisabled,\n maxRows,\n onSendError,\n className,\n ...rest\n}: AIChatProps) {\n const strings = aiChatStrings(locale);\n const turns = visibleTurns({ messages, showSystem });\n const generating = pending || isGenerating(messages);\n const newestAssistant = lastAssistantId(messages);\n const signature = tailSignature(messages);\n\n const composer = useRef<AIChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n const wasGenerating = useRef(false);\n const [atBottom, setAtBottom] = useState(true);\n const announce = useAnnounce();\n\n /**\n * Remember whether the reader is at the bottom, before the next tokens land.\n *\n * A transcript that always scrolls to the newest text yanks somebody out of the\n * answer they were re-reading — and with a streaming answer that would happen\n * dozens of times per second. So the jump only happens when they were already at\n * the bottom. The slack covers a partially visible last line.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n stuckToBottom.current = next;\n setAtBottom(next);\n };\n\n const jumpToLatest = (): void => {\n const node = thread.current;\n if (!node) return;\n stuckToBottom.current = true;\n setAtBottom(true);\n node.scrollTop = node.scrollHeight;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [signature, pending]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest turn; from then on `trackPosition` decides.\n }, []);\n\n /**\n * Announce the start and the end of a generation, and nothing in between.\n *\n * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n * streaming text makes a screen reader read the answer again on every token,\n * which is unusable. The two moments that matter are announced here instead —\n * through the shared `useAnnounce` region rather than a private one, so a page\n * holding a chat plus a table plus toasts still has exactly one live region per\n * politeness — and the finished answer is read from the log at the reader's own\n * pace.\n */\n useEffect(() => {\n if (generating) {\n announce(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n announce(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done, announce]);\n\n const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n const detached = !atBottom && turns.length > 0;\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div className={styles.threadWrapper}>\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {turns.length === 0 && !showSuggestions\n ? (emptyState ?? (\n <EmptyState title={strings.empty} description={strings.emptyHint} />\n ))\n : turns.map((message) => (\n <AIChatTurn\n key={message.id}\n message={message}\n locale={locale}\n canRegenerate={\n message.id === newestAssistant && message.streaming !== true\n }\n onRegenerate={onRegenerate}\n onFeedback={onFeedback}\n onEditSubmit={onEditSubmit}\n onEditError={onSendError}\n onRetry={onRetry}\n renderAvatar={renderAvatar}\n renderContent={renderContent}\n vote={votes?.[message.id]}\n defaultReasoningOpen={defaultReasoningOpen}\n showLineNumbers={showLineNumbers}\n />\n ))}\n\n {showSuggestions && (\n <div className={styles.suggestions}>\n <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n {suggestions.map((suggestion) => (\n <button\n key={suggestion}\n type=\"button\"\n className={styles.suggestion}\n onClick={() => void onSend?.(suggestion)}\n >\n {suggestion}\n </button>\n ))}\n </div>\n )}\n\n {pending && !isGenerating(messages) && (\n <p className={styles.thinking}>\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n {strings.thinking}\n </p>\n )}\n </div>\n\n {detached && (\n <button\n type=\"button\"\n className={styles.jump}\n onClick={jumpToLatest}\n aria-label={strings.jumpToLatest}\n title={strings.jumpToLatest}\n >\n <ArrowDown size={16} aria-hidden />\n </button>\n )}\n </div>\n\n {onSend && (\n <AIChatComposer\n ref={composerRef ?? composer}\n onSend={onSend}\n onStop={onStop}\n generating={generating}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n footer={composerFooter}\n disabled={composerDisabled}\n maxRows={maxRows}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIChat.cjs","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the widest surface in\n * the SDK on purpose: it is a whole chat page, and its 25 props are four groups —\n * the transcript (messages, votes, pending, showSystem), the turn actions an app has\n * to own (onSend, onStop, onRegenerate, onEditSubmit, onFeedback, onRetry,\n * onSendError), render slots (renderAvatar, renderContent, header, emptyState,\n * suggestions, composerActions, composerFooter) and composer settings (composerRef,\n * composerDisabled, maxRows, placeholder, locale, defaultReasoningOpen,\n * showLineNumbers). The parts already are separate components — AIChatTurn and\n * AIChatComposer are exported — and this is the assembly whose entire job is to\n * accept the union of their configuration.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport {\n aiChatStrings,\n isGenerating,\n lastAssistantId,\n tailSignature,\n visibleTurns,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The transcript, **oldest first**. Never reordered by the component. */\n messages: readonly AIChatMessage[];\n /** Renders the composer when given. Receives the trimmed prompt. */\n onSend?: (text: string) => void | Promise<void>;\n /** Abort the turn in flight. Shows the stop button while generating. */\n onStop?: () => void;\n /** Ask again for the newest assistant turn. */\n onRegenerate?: (message: AIChatMessage) => void;\n /** Re-submit an edited user turn. The app decides what to drop after it. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /** Rating on an assistant turn. */\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Retry a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n /**\n * The request is out and nothing has arrived yet.\n *\n * Distinct from a turn with `streaming: true`: apps that only push a message\n * once the first token lands need somewhere to say \"we asked\", and without it the\n * screen is frozen for however long the model takes to start.\n */\n pending?: boolean;\n /** Prompts offered on an empty transcript. Clicking one sends it. */\n suggestions?: readonly string[];\n /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render a body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /** Ratings to show as pressed, by message id. Omit to keep them local. */\n votes?: Readonly<Record<string, AIChatVote>>;\n /** Rendered above the transcript, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no turns and no suggestions. */\n emptyState?: ReactNode;\n /** Show `\"system\"` turns. Default `false`. */\n showSystem?: boolean;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /**\n * Reach the composer imperatively — `focus()`, `getValue()`, `setValue()`.\n *\n * What makes dictation (or a slash-command menu, or \"edit and resend\") possible\n * without this component knowing anything about them: pair it with\n * `composerActions` and the button you put in the composer can write into the\n * field. Speech recognition is **not** wired in here on purpose — it would make\n * every consumer of `AIChat` pay for an API that streams audio to a third party.\n */\n composerRef?: Ref<AIChatComposerHandle>;\n /** Under the composer field — token count, model name, a disclaimer. */\n composerFooter?: ReactNode;\n /** Disable the composer — no credits, conversation archived, offline. */\n composerDisabled?: boolean;\n /** Largest height the composer grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n * either way.\n */\n onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n * messages={turns}\n * pending={pending}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.current?.abort()}\n * onRegenerate={(turn) => reask(turn)}\n * onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n * suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n messages,\n onSend,\n onStop,\n onRegenerate,\n onEditSubmit,\n onFeedback,\n onRetry,\n pending = false,\n suggestions = [],\n renderAvatar,\n renderContent,\n votes,\n header,\n emptyState,\n showSystem = false,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n locale = \"pt-BR\",\n placeholder,\n composerActions,\n composerRef,\n composerFooter,\n composerDisabled,\n maxRows,\n onSendError,\n className,\n ...rest\n}: AIChatProps) {\n const strings = aiChatStrings(locale);\n const turns = visibleTurns({ messages, showSystem });\n const generating = pending || isGenerating(messages);\n const newestAssistant = lastAssistantId(messages);\n const signature = tailSignature(messages);\n\n const composer = useRef<AIChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n const wasGenerating = useRef(false);\n const [atBottom, setAtBottom] = useState(true);\n const announce = useAnnounce();\n\n /**\n * Remember whether the reader is at the bottom, before the next tokens land.\n *\n * A transcript that always scrolls to the newest text yanks somebody out of the\n * answer they were re-reading — and with a streaming answer that would happen\n * dozens of times per second. So the jump only happens when they were already at\n * the bottom. The slack covers a partially visible last line.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n stuckToBottom.current = next;\n setAtBottom(next);\n };\n\n const jumpToLatest = (): void => {\n const node = thread.current;\n if (!node) return;\n stuckToBottom.current = true;\n setAtBottom(true);\n node.scrollTop = node.scrollHeight;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [signature, pending]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest turn; from then on `trackPosition` decides.\n }, []);\n\n /**\n * Announce the start and the end of a generation, and nothing in between.\n *\n * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n * streaming text makes a screen reader read the answer again on every token,\n * which is unusable. The two moments that matter are announced here instead —\n * through the shared `useAnnounce` region rather than a private one, so a page\n * holding a chat plus a table plus toasts still has exactly one live region per\n * politeness — and the finished answer is read from the log at the reader's own\n * pace.\n */\n useEffect(() => {\n if (generating) {\n announce(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n announce(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done, announce]);\n\n const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n const detached = !atBottom && turns.length > 0;\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div className={styles.threadWrapper}>\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {turns.length === 0 && !showSuggestions\n ? (emptyState ?? (\n <EmptyState title={strings.empty} description={strings.emptyHint} />\n ))\n : turns.map((message) => (\n <AIChatTurn\n key={message.id}\n message={message}\n locale={locale}\n canRegenerate={\n message.id === newestAssistant && message.streaming !== true\n }\n onRegenerate={onRegenerate}\n onFeedback={onFeedback}\n onEditSubmit={onEditSubmit}\n onEditError={onSendError}\n onRetry={onRetry}\n renderAvatar={renderAvatar}\n renderContent={renderContent}\n vote={votes?.[message.id]}\n defaultReasoningOpen={defaultReasoningOpen}\n showLineNumbers={showLineNumbers}\n />\n ))}\n\n {showSuggestions && (\n <div className={styles.suggestions}>\n <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n {suggestions.map((suggestion) => (\n <button\n key={suggestion}\n type=\"button\"\n className={styles.suggestion}\n onClick={() => void onSend?.(suggestion)}\n >\n {suggestion}\n </button>\n ))}\n </div>\n )}\n\n {pending && !isGenerating(messages) && (\n <p className={styles.thinking}>\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n {strings.thinking}\n </p>\n )}\n </div>\n\n {detached && (\n <button\n type=\"button\"\n className={styles.jump}\n onClick={jumpToLatest}\n aria-label={strings.jumpToLatest}\n title={strings.jumpToLatest}\n >\n <ArrowDown size={16} aria-hidden />\n </button>\n )}\n </div>\n\n {onSend && (\n <AIChatComposer\n ref={composerRef ?? composer}\n onSend={onSend}\n onStop={onStop}\n generating={generating}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n footer={composerFooter}\n disabled={composerDisabled}\n maxRows={maxRows}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChat.js","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the widest surface in\n * the SDK on purpose: it is a whole chat page, and its 25 props are four groups —\n * the transcript (messages, votes, pending, showSystem), the turn actions an app has\n * to own (onSend, onStop, onRegenerate, onEditSubmit, onFeedback, onRetry,\n * onSendError), render slots (renderAvatar, renderContent, header, emptyState,\n * suggestions, composerActions, composerFooter) and composer settings (composerRef,\n * composerDisabled, maxRows, placeholder, locale, defaultReasoningOpen,\n * showLineNumbers). The parts already are separate components — AIChatTurn and\n * AIChatComposer are exported — and this is the assembly whose entire job is to\n * accept the union of their configuration.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport {\n aiChatStrings,\n isGenerating,\n lastAssistantId,\n tailSignature,\n visibleTurns,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The transcript, **oldest first**. Never reordered by the component. */\n messages: readonly AIChatMessage[];\n /** Renders the composer when given. Receives the trimmed prompt. */\n onSend?: (text: string) => void | Promise<void>;\n /** Abort the turn in flight. Shows the stop button while generating. */\n onStop?: () => void;\n /** Ask again for the newest assistant turn. */\n onRegenerate?: (message: AIChatMessage) => void;\n /** Re-submit an edited user turn. The app decides what to drop after it. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /** Rating on an assistant turn. */\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Retry a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n /**\n * The request is out and nothing has arrived yet.\n *\n * Distinct from a turn with `streaming: true`: apps that only push a message\n * once the first token lands need somewhere to say \"we asked\", and without it the\n * screen is frozen for however long the model takes to start.\n */\n pending?: boolean;\n /** Prompts offered on an empty transcript. Clicking one sends it. */\n suggestions?: readonly string[];\n /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render a body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /** Ratings to show as pressed, by message id. Omit to keep them local. */\n votes?: Readonly<Record<string, AIChatVote>>;\n /** Rendered above the transcript, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no turns and no suggestions. */\n emptyState?: ReactNode;\n /** Show `\"system\"` turns. Default `false`. */\n showSystem?: boolean;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /**\n * Reach the composer imperatively — `focus()`, `getValue()`, `setValue()`.\n *\n * What makes dictation (or a slash-command menu, or \"edit and resend\") possible\n * without this component knowing anything about them: pair it with\n * `composerActions` and the button you put in the composer can write into the\n * field. Speech recognition is **not** wired in here on purpose — it would make\n * every consumer of `AIChat` pay for an API that streams audio to a third party.\n */\n composerRef?: Ref<AIChatComposerHandle>;\n /** Under the composer field — token count, model name, a disclaimer. */\n composerFooter?: ReactNode;\n /** Disable the composer — no credits, conversation archived, offline. */\n composerDisabled?: boolean;\n /** Largest height the composer grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n * either way.\n */\n onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n * messages={turns}\n * pending={pending}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.current?.abort()}\n * onRegenerate={(turn) => reask(turn)}\n * onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n * suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n messages,\n onSend,\n onStop,\n onRegenerate,\n onEditSubmit,\n onFeedback,\n onRetry,\n pending = false,\n suggestions = [],\n renderAvatar,\n renderContent,\n votes,\n header,\n emptyState,\n showSystem = false,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n locale = \"pt-BR\",\n placeholder,\n composerActions,\n composerRef,\n composerFooter,\n composerDisabled,\n maxRows,\n onSendError,\n className,\n ...rest\n}: AIChatProps) {\n const strings = aiChatStrings(locale);\n const turns = visibleTurns({ messages, showSystem });\n const generating = pending || isGenerating(messages);\n const newestAssistant = lastAssistantId(messages);\n const signature = tailSignature(messages);\n\n const composer = useRef<AIChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n const wasGenerating = useRef(false);\n const [atBottom, setAtBottom] = useState(true);\n const announce = useAnnounce();\n\n /**\n * Remember whether the reader is at the bottom, before the next tokens land.\n *\n * A transcript that always scrolls to the newest text yanks somebody out of the\n * answer they were re-reading — and with a streaming answer that would happen\n * dozens of times per second. So the jump only happens when they were already at\n * the bottom. The slack covers a partially visible last line.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n stuckToBottom.current = next;\n setAtBottom(next);\n };\n\n const jumpToLatest = (): void => {\n const node = thread.current;\n if (!node) return;\n stuckToBottom.current = true;\n setAtBottom(true);\n node.scrollTop = node.scrollHeight;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [signature, pending]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest turn; from then on `trackPosition` decides.\n }, []);\n\n /**\n * Announce the start and the end of a generation, and nothing in between.\n *\n * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n * streaming text makes a screen reader read the answer again on every token,\n * which is unusable. The two moments that matter are announced here instead —\n * through the shared `useAnnounce` region rather than a private one, so a page\n * holding a chat plus a table plus toasts still has exactly one live region per\n * politeness — and the finished answer is read from the log at the reader's own\n * pace.\n */\n useEffect(() => {\n if (generating) {\n announce(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n announce(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done, announce]);\n\n const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n const detached = !atBottom && turns.length > 0;\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div className={styles.threadWrapper}>\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {turns.length === 0 && !showSuggestions\n ? (emptyState ?? (\n <EmptyState title={strings.empty} description={strings.emptyHint} />\n ))\n : turns.map((message) => (\n <AIChatTurn\n key={message.id}\n message={message}\n locale={locale}\n canRegenerate={\n message.id === newestAssistant && message.streaming !== true\n }\n onRegenerate={onRegenerate}\n onFeedback={onFeedback}\n onEditSubmit={onEditSubmit}\n onEditError={onSendError}\n onRetry={onRetry}\n renderAvatar={renderAvatar}\n renderContent={renderContent}\n vote={votes?.[message.id]}\n defaultReasoningOpen={defaultReasoningOpen}\n showLineNumbers={showLineNumbers}\n />\n ))}\n\n {showSuggestions && (\n <div className={styles.suggestions}>\n <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n {suggestions.map((suggestion) => (\n <button\n key={suggestion}\n type=\"button\"\n className={styles.suggestion}\n onClick={() => void onSend?.(suggestion)}\n >\n {suggestion}\n </button>\n ))}\n </div>\n )}\n\n {pending && !isGenerating(messages) && (\n <p className={styles.thinking}>\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n {strings.thinking}\n </p>\n )}\n </div>\n\n {detached && (\n <button\n type=\"button\"\n className={styles.jump}\n onClick={jumpToLatest}\n aria-label={strings.jumpToLatest}\n title={strings.jumpToLatest}\n >\n <ArrowDown size={16} aria-hidden />\n </button>\n )}\n </div>\n\n {onSend && (\n <AIChatComposer\n ref={composerRef ?? composer}\n onSend={onSend}\n onStop={onStop}\n generating={generating}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n footer={composerFooter}\n disabled={composerDisabled}\n maxRows={maxRows}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIChat.js","names":[],"sources":["../../../src/components/AIChat/AIChat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the widest surface in\n * the SDK on purpose: it is a whole chat page, and its 25 props are four groups —\n * the transcript (messages, votes, pending, showSystem), the turn actions an app has\n * to own (onSend, onStop, onRegenerate, onEditSubmit, onFeedback, onRetry,\n * onSendError), render slots (renderAvatar, renderContent, header, emptyState,\n * suggestions, composerActions, composerFooter) and composer settings (composerRef,\n * composerDisabled, maxRows, placeholder, locale, defaultReasoningOpen,\n * showLineNumbers). The parts already are separate components — AIChatTurn and\n * AIChatComposer are exported — and this is the assembly whose entire job is to\n * accept the union of their configuration.\n */\nimport { ArrowDown } from \"lucide-react\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n type Ref,\n} from \"react\";\n\nimport { useAnnounce } from \"@/hooks/use-announce\";\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport {\n aiChatStrings,\n isGenerating,\n lastAssistantId,\n tailSignature,\n visibleTurns,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport { AIChatComposer, type AIChatComposerHandle } from \"./AIChatComposer\";\nimport { AIChatTurn } from \"./AIChatTurn\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface AIChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The transcript, **oldest first**. Never reordered by the component. */\n messages: readonly AIChatMessage[];\n /** Renders the composer when given. Receives the trimmed prompt. */\n onSend?: (text: string) => void | Promise<void>;\n /** Abort the turn in flight. Shows the stop button while generating. */\n onStop?: () => void;\n /** Ask again for the newest assistant turn. */\n onRegenerate?: (message: AIChatMessage) => void;\n /** Re-submit an edited user turn. The app decides what to drop after it. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /** Rating on an assistant turn. */\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Retry a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n /**\n * The request is out and nothing has arrived yet.\n *\n * Distinct from a turn with `streaming: true`: apps that only push a message\n * once the first token lands need somewhere to say \"we asked\", and without it the\n * screen is frozen for however long the model takes to start.\n */\n pending?: boolean;\n /** Prompts offered on an empty transcript. Clicking one sends it. */\n suggestions?: readonly string[];\n /** Avatar for a turn — an `<Avatar>`, an `<Icon>`, a logo. */\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render a body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /** Ratings to show as pressed, by message id. Omit to keep them local. */\n votes?: Readonly<Record<string, AIChatVote>>;\n /** Rendered above the transcript, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no turns and no suggestions. */\n emptyState?: ReactNode;\n /** Show `\"system\"` turns. Default `false`. */\n showSystem?: boolean;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /**\n * Reach the composer imperatively — `focus()`, `getValue()`, `setValue()`.\n *\n * What makes dictation (or a slash-command menu, or \"edit and resend\") possible\n * without this component knowing anything about them: pair it with\n * `composerActions` and the button you put in the composer can write into the\n * field. Speech recognition is **not** wired in here on purpose — it would make\n * every consumer of `AIChat` pay for an API that streams audio to a third party.\n */\n composerRef?: Ref<AIChatComposerHandle>;\n /** Under the composer field — token count, model name, a disclaimer. */\n composerFooter?: ReactNode;\n /** Disable the composer — no credits, conversation archived, offline. */\n composerDisabled?: boolean;\n /** Largest height the composer grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` **or** `onEditSubmit` rejects. The draft stays in the field\n * either way.\n */\n onSendError?: (error: unknown) => void;\n}\n\n/** How close to the bottom still counts as \"reading the newest turn\", in pixels. */\nconst BOTTOM_SLACK = 48;\n\n/**\n * A conversation with a model: role-based turns, Markdown answers, a reasoning\n * block, a streaming caret, per-turn actions and a composer that turns into a stop\n * button while a turn is generating.\n *\n * This is the shape ChatGPT, Claude and DeepSeek converged on, and it is a different\n * component from {@link Chat}, not a variant of it. A human thread is addressed by\n * author and cares about delivery state; a model transcript is addressed by role,\n * has no delivery state at all, and needs three things a human thread never does —\n * partial output, reasoning separate from the answer, and re-asking.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and emits\n * intent (`onSend`, `onStop`, `onRegenerate`, `onEditSubmit`, `onFeedback`). The\n * transport stays with the app, because \"how do I stream from my backend\" has a\n * different answer per provider — the SDK's `createEventStream` covers SSE, `fetch`\n * with a `ReadableStream` covers the rest, and either way the app owns the\n * `AbortController` it hands to `onStop`.\n *\n * @example\n * <AIChat\n * messages={turns}\n * pending={pending}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.current?.abort()}\n * onRegenerate={(turn) => reask(turn)}\n * onFeedback={(turn, vote) => track(\"answer_rated\", { id: turn.id, vote })}\n * suggestions={[\"Resuma o último relatório\", \"Quais pedidos atrasaram?\"]}\n * />\n */\nexport function AIChat({\n messages,\n onSend,\n onStop,\n onRegenerate,\n onEditSubmit,\n onFeedback,\n onRetry,\n pending = false,\n suggestions = [],\n renderAvatar,\n renderContent,\n votes,\n header,\n emptyState,\n showSystem = false,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n locale = \"pt-BR\",\n placeholder,\n composerActions,\n composerRef,\n composerFooter,\n composerDisabled,\n maxRows,\n onSendError,\n className,\n ...rest\n}: AIChatProps) {\n const strings = aiChatStrings(locale);\n const turns = visibleTurns({ messages, showSystem });\n const generating = pending || isGenerating(messages);\n const newestAssistant = lastAssistantId(messages);\n const signature = tailSignature(messages);\n\n const composer = useRef<AIChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n const wasGenerating = useRef(false);\n const [atBottom, setAtBottom] = useState(true);\n const announce = useAnnounce();\n\n /**\n * Remember whether the reader is at the bottom, before the next tokens land.\n *\n * A transcript that always scrolls to the newest text yanks somebody out of the\n * answer they were re-reading — and with a streaming answer that would happen\n * dozens of times per second. So the jump only happens when they were already at\n * the bottom. The slack covers a partially visible last line.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const next = node.scrollHeight - node.scrollTop - node.clientHeight < BOTTOM_SLACK;\n stuckToBottom.current = next;\n setAtBottom(next);\n };\n\n const jumpToLatest = (): void => {\n const node = thread.current;\n if (!node) return;\n stuckToBottom.current = true;\n setAtBottom(true);\n node.scrollTop = node.scrollHeight;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [signature, pending]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest turn; from then on `trackPosition` decides.\n }, []);\n\n /**\n * Announce the start and the end of a generation, and nothing in between.\n *\n * The transcript is a `role=\"log\"` **without** `aria-live`: a live region over\n * streaming text makes a screen reader read the answer again on every token,\n * which is unusable. The two moments that matter are announced here instead —\n * through the shared `useAnnounce` region rather than a private one, so a page\n * holding a chat plus a table plus toasts still has exactly one live region per\n * politeness — and the finished answer is read from the log at the reader's own\n * pace.\n */\n useEffect(() => {\n if (generating) {\n announce(strings.generating);\n wasGenerating.current = true;\n return;\n }\n if (wasGenerating.current) {\n announce(strings.done);\n wasGenerating.current = false;\n }\n }, [generating, strings.generating, strings.done, announce]);\n\n const showSuggestions = turns.length === 0 && suggestions.length > 0 && onSend !== undefined;\n const detached = !atBottom && turns.length > 0;\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div className={styles.threadWrapper}>\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {turns.length === 0 && !showSuggestions\n ? (emptyState ?? (\n <EmptyState title={strings.empty} description={strings.emptyHint} />\n ))\n : turns.map((message) => (\n <AIChatTurn\n key={message.id}\n message={message}\n locale={locale}\n canRegenerate={\n message.id === newestAssistant && message.streaming !== true\n }\n onRegenerate={onRegenerate}\n onFeedback={onFeedback}\n onEditSubmit={onEditSubmit}\n onEditError={onSendError}\n onRetry={onRetry}\n renderAvatar={renderAvatar}\n renderContent={renderContent}\n vote={votes?.[message.id]}\n defaultReasoningOpen={defaultReasoningOpen}\n showLineNumbers={showLineNumbers}\n />\n ))}\n\n {showSuggestions && (\n <div className={styles.suggestions}>\n <p className={styles.suggestionsTitle}>{strings.emptyHint}</p>\n {suggestions.map((suggestion) => (\n <button\n key={suggestion}\n type=\"button\"\n className={styles.suggestion}\n onClick={() => void onSend?.(suggestion)}\n >\n {suggestion}\n </button>\n ))}\n </div>\n )}\n\n {pending && !isGenerating(messages) && (\n <p className={styles.thinking}>\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n {strings.thinking}\n </p>\n )}\n </div>\n\n {detached && (\n <button\n type=\"button\"\n className={styles.jump}\n onClick={jumpToLatest}\n aria-label={strings.jumpToLatest}\n title={strings.jumpToLatest}\n >\n <ArrowDown size={16} aria-hidden />\n </button>\n )}\n </div>\n\n {onSend && (\n <AIChatComposer\n ref={composerRef ?? composer}\n onSend={onSend}\n onStop={onStop}\n generating={generating}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n footer={composerFooter}\n disabled={composerDisabled}\n maxRows={maxRows}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAkHA,IAAM,IAAe;AA+BrB,SAAgB,EAAO,EACnB,aACA,WACA,WACA,kBACA,kBACA,gBACA,YACA,aAAU,IACV,iBAAc,CAAC,GACf,iBACA,kBACA,UACA,WACA,eACA,gBAAa,IACb,0BAAuB,IACvB,qBAAkB,IAClB,YAAS,SACT,gBACA,oBACA,gBACA,mBACA,sBACA,YACA,gBACA,cACA,GAAG,KACS;CACZ,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAQ,EAAa;EAAE;EAAU;CAAW,CAAC,GAC7C,IAAa,KAAW,EAAa,CAAQ,GAC7C,IAAkB,EAAgB,CAAQ,GAC1C,IAAY,EAAc,CAAQ,GAElC,IAAW,EAAoC,IAAI,GACnD,IAAS,EAA8B,IAAI,GAC3C,IAAgB,EAAO,EAAI,GAC3B,IAAgB,EAAO,EAAK,GAC5B,CAAC,IAAU,KAAe,EAAS,EAAI,GACvC,IAAW,EAAY,GAUvB,WAA4B;EAC9B,IAAM,IAAO,EAAO;EACpB,IAAI,CAAC,GAAM;EACX,IAAM,IAAO,EAAK,eAAe,EAAK,YAAY,EAAK,eAAe;EAEtE,AADA,EAAc,UAAU,GACxB,EAAY,CAAI;CACpB,GAEM,WAA2B;EAC7B,IAAM,IAAO,EAAO;EACf,MACL,EAAc,UAAU,IACxB,EAAY,EAAI,GAChB,EAAK,YAAY,EAAK;CAC1B;CA0BA,AAxBA,QAAsB;EAClB,IAAM,IAAO,EAAO;EAChB,CAAC,KAAQ,CAAC,EAAc,YAC5B,EAAK,YAAY,EAAK;CAC1B,GAAG,CAAC,GAAW,CAAO,CAAC,GAEvB,QAAgB;EACZ,IAAM,IAAO,EAAO;EACf,MACL,EAAK,YAAY,EAAK;CAE1B,GAAG,CAAC,CAAC,GAaL,QAAgB;EACZ,IAAI,GAAY;GAEZ,AADA,EAAS,EAAQ,UAAU,GAC3B,EAAc,UAAU;GACxB;EACJ;EACA,AAEI,EAAc,aADd,EAAS,EAAQ,IAAI,GACG;CAEhC,GAAG;EAAC;EAAY,EAAQ;EAAY,EAAQ;EAAM;CAAQ,CAAC;CAE3D,IAAM,IAAkB,EAAM,WAAW,KAAK,EAAY,SAAS,KAAK,MAAW,KAAA,GAC7E,KAAW,CAAC,MAAY,EAAM,SAAS;CAE7C,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,CAAS;EAAG,GAAI;EAAjD,UAAA;GACK,KAAU,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAAS,UAAA;GAAe,CAAA;GAE7D,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACI,kBAAC,OAAD;KACI,KAAK;KACL,WAAW,EAAO;KAClB,UAAU;KACV,MAAK;KACL,UAAU;KACV,cAAY,EAAQ;KANxB,UAAA;MAQK,EAAM,WAAW,KAAK,CAAC,IACjB,KACG,kBAAC,GAAD;OAAY,OAAO,EAAQ;OAAO,aAAa,EAAQ;MAAY,CAAA,IAEvE,EAAM,KAAK,MACP,kBAAC,GAAD;OAEa;OACD;OACR,eACI,EAAQ,OAAO,KAAmB,EAAQ,cAAc;OAE9C;OACF;OACE;OACd,aAAa;OACJ;OACK;OACC;OACf,MAAM,IAAQ,EAAQ;OACA;OACL;MACpB,GAhBQ,EAAQ,EAgBhB,CACJ;MAEN,KACG,kBAAC,OAAD;OAAK,WAAW,EAAO;OAAvB,UAAA,CACI,kBAAC,KAAD;QAAG,WAAW,EAAO;QAAmB,UAAA,EAAQ;OAAa,CAAA,GAC5D,EAAY,KAAK,MACd,kBAAC,UAAD;QAEI,MAAK;QACL,WAAW,EAAO;QAClB,eAAe,KAAK,IAAS,CAAU;QAEtC,UAAA;OACG,GANC,CAMD,CACX,CACA;;MAGR,KAAW,CAAC,EAAa,CAAQ,KAC9B,kBAAC,KAAD;OAAG,WAAW,EAAO;OAArB,UAAA,CACI,kBAAC,QAAD;QAAM,WAAW,EAAO;QAAM,eAAY;QAA1C,UAAA;SACI,kBAAC,QAAD,CAAO,CAAA;SACP,kBAAC,QAAD,CAAO,CAAA;SACP,kBAAC,QAAD,CAAO,CAAA;QACL;OACL,CAAA,GAAA,EAAQ,QACV;;KAEN;IAEJ,CAAA,GAAA,MACG,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,SAAS;KACT,cAAY,EAAQ;KACpB,OAAO,EAAQ;KAEf,UAAA,kBAAC,GAAD;MAAW,MAAM;MAAI,eAAA;KAAa,CAAA;IAC9B,CAAA,CAEX;;GAEJ,KACG,kBAAC,GAAD;IACI,KAAK,KAAe;IACZ;IACA;IACI;IACJ;IACK;IACb,SAAS;IACT,QAAQ;IACR,UAAU;IACD;IACT,SAAS;GACZ,CAAA;EAEJ;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatComposer.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — a textarea that grows, submits on\n * Enter, offers a stop button while a turn is generating and clears only when onSend\n * resolves. The eight props are that contract (onSend, onStop, generating, onError)\n * plus its slots (actions, footer) and shape (maxRows, locale); the body is the\n * autosize measurement and the key handling, which both need the same ref and cannot\n * be lifted out of it.\n */\nimport {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus, read or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n /**\n * The current draft.\n *\n * The counterpart `setValue` needs to be usable for anything **additive**. The\n * field is uncontrolled, so without this the only way to append to a draft — a\n * dictated phrase, a picked slash-command, a pasted citation — is to shadow the\n * whole value in app state through `onChange` and hope the two never drift.\n */\n getValue: () => string;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n getValue: () => value,\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIChatComposer.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — a textarea that grows, submits on\n * Enter, offers a stop button while a turn is generating and clears only when onSend\n * resolves. The eight props are that contract (onSend, onStop, generating, onError)\n * plus its slots (actions, footer) and shape (maxRows, locale); the body is the\n * autosize measurement and the key handling, which both need the same ref and cannot\n * be lifted out of it.\n */\nimport {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus, read or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n /**\n * The current draft.\n *\n * The counterpart `setValue` needs to be usable for anything **additive**. The\n * field is uncontrolled, so without this the only way to append to a draft — a\n * dictated phrase, a picked slash-command, a pasted citation — is to shadow the\n * whole value in app state through `onChange` and hope the two never drift.\n */\n getValue: () => string;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n getValue: () => value,\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatComposer.js","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — a textarea that grows, submits on\n * Enter, offers a stop button while a turn is generating and clears only when onSend\n * resolves. The eight props are that contract (onSend, onStop, generating, onError)\n * plus its slots (actions, footer) and shape (maxRows, locale); the body is the\n * autosize measurement and the key handling, which both need the same ref and cannot\n * be lifted out of it.\n */\nimport {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus, read or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n /**\n * The current draft.\n *\n * The counterpart `setValue` needs to be usable for anything **additive**. The\n * field is uncontrolled, so without this the only way to append to a draft — a\n * dictated phrase, a picked slash-command, a pasted citation — is to shadow the\n * whole value in app state through `onChange` and hope the two never drift.\n */\n getValue: () => string;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n getValue: () => value,\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":";;;;;;AAiGA,IAAa,IAAiB,EAC1B,SACI,EACI,WACA,WACA,gBAAa,IACb,YAAS,SACT,YACA,WACA,aAAU,GACV,YACA,cACA,aACA,gBACA,cACA,aACA,GAAG,KAEP,GACF;CACE,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAW,EAAmC,IAAI,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAK;CAkBtC,AAhBA,EAAoB,UAAY;EAC5B,aAAa,EAAS,SAAS,MAAM;EACrC,WAAW,MAAiB;GAExB,AADA,EAAS,CAAI,GACb,EAAS,SAAS,MAAM;EAC5B;EACA,gBAAgB;CACpB,EAAE,GASF,QAAsB;EAClB,IAAM,IAAO,EAAS;EACtB,IAAI,CAAC,GAAM;EACX,EAAK,MAAM,SAAS;EAEpB,IAAM,KADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,KAAK,MAClD;EAEzB,AADA,EAAK,MAAM,SAAS,GAAG,KAAK,IAAI,EAAK,cAAc,CAAG,EAAE,KACxD,EAAK,MAAM,YAAY,EAAK,eAAe,IAAM,SAAS;CAC9D,GAAG,CAAC,GAAO,CAAO,CAAC;CAEnB,IAAM,IAAS,YAA2B;EACtC,IAAM,IAAO,EAAM,KAAK;EACpB,OAAC,KAAQ,KAAQ,KAAY,IACjC;KAAQ,EAAI;GACZ,IAAI;IAEA,AADA,MAAM,EAAO,CAAI,GACjB,EAAS,EAAE;GACf,SAAS,GAAO;IACZ,IAAU,CAAK;GACnB,UAAU;IACN,EAAQ,EAAK;GACjB;EARY;CAShB,GAEM,KAAgB,MAA2B;EAE7C,AADA,EAAM,eAAe,GACrB,EAAY;CAChB,GAUM,KAAiB,MAAoD;EACvE,QAAY,CAAK,GACb,GAAM,kBACV;OAAI,EAAM,QAAQ,YAAY,KAAc,GAAQ;IAEhD,AADA,EAAM,eAAe,GACrB,EAAO;IACP;GACJ;GACI,EAAM,QAAQ,WAAW,EAAM,YAC/B,EAAM,YAAY,eAAe,EAAM,YAAY,QACvD,EAAM,eAAe,GACrB,EAAY;EAJZ;CAKJ,GAEM,IAAW,KAAc,MAAW,KAAA;CAE1C,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,UAAU;YAA3D,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACI,kBAAC,YAAD;IACI,GAAI;IACJ,KAAK;IACL,WAAW,EAAO;IAClB,MAAM;IACC;IACG;IACV,aAAa,KAAe,EAAQ;IACpC,WAAW,MAAU;KAEjB,AADA,EAAS,EAAM,OAAO,KAAK,GAC3B,IAAW,CAAK;IACpB;IACA,WAAW;GACd,CAAA,GACD,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACK,GACA,IACG,kBAAC,UAAD;KAAQ,MAAK;KAAS,WAAW,EAAO;KAAM,SAAS;eAAvD,CACI,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAW,EAAO;KAAY,CAAA,GACtD,EAAQ,IACL;SAER,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,UAAU,KAAY,KAAQ,KAAc,EAAM,KAAK,MAAM;eAE5D,EAAQ;IACL,CAAA,CAEX;KACJ;MACJ,KAAU,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAiB;EAAY,CAAA,CAC7D;;AAEd,CACJ"}
|
|
1
|
+
{"version":3,"file":"AIChatComposer.js","names":[],"sources":["../../../src/components/AIChat/AIChatComposer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — a textarea that grows, submits on\n * Enter, offers a stop button while a turn is generating and clears only when onSend\n * resolves. The eight props are that contract (onSend, onStop, generating, onError)\n * plus its slots (actions, footer) and shape (maxRows, locale); the body is the\n * autosize measurement and the key handling, which both need the same ref and cannot\n * be lifted out of it.\n */\nimport {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { aiChatStrings } from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface AIChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed prompt. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /**\n * Abort the turn in flight.\n *\n * When given together with `generating`, the send button becomes a stop button\n * and `Escape` aborts too.\n */\n onStop?: () => void;\n /** A turn is being generated. Replaces send with stop and refuses to send. */\n generating?: boolean;\n /** Locale for the placeholder and the button labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Left of the send button — an attach control, a model picker, a tool toggle. */\n actions?: ReactNode;\n /** Under the field — a token count, the model name, a disclaimer. */\n footer?: ReactNode;\n /** Largest height the field grows to, in lines. Default 8. */\n maxRows?: number;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a DOM event handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on. The visible\n * signal is the prompt still sitting in the field; wire this to a toast to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus, read or refill the field. */\nexport interface AIChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a prompt back in the field. */\n setValue: (text: string) => void;\n /**\n * The current draft.\n *\n * The counterpart `setValue` needs to be usable for anything **additive**. The\n * field is uncontrolled, so without this the only way to append to a draft — a\n * dictated phrase, a picked slash-command, a pasted citation — is to shadow the\n * whole value in app state through `onChange` and hope the two never drift.\n */\n getValue: () => string;\n}\n\n/**\n * The prompt field of a conversation with a model: a textarea that grows with its\n * content, sends on `Enter`, keeps `Shift+Enter` for a newline, and turns into a\n * stop button while a turn is streaming.\n *\n * Uncontrolled on purpose. A draft changes on every keystroke, and lifting that into\n * app state re-renders the whole transcript per character — with a streaming answer\n * above, that is the one place where \"controlled by default\" costs something\n * visible. Apps that need the draft (a persisted composer, a slash-command menu)\n * read it from `onChange` or drive it through the ref.\n *\n * @example\n * <AIChatComposer\n * generating={generating}\n * onSend={(text) => ask(text)}\n * onStop={() => controller.abort()}\n * footer={<small>Claude Opus 5 · pode errar</small>}\n * />\n */\nexport const AIChatComposer = forwardRef<AIChatComposerHandle, AIChatComposerProps>(\n function AIChatComposer(\n {\n onSend,\n onStop,\n generating = false,\n locale = \"pt-BR\",\n actions,\n footer,\n maxRows = 8,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n ) {\n const strings = aiChatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n getValue: () => value,\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the current\n * height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled || generating) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line, `Escape` aborts a turn in\n * flight.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key === \"Escape\" && generating && onStop) {\n event.preventDefault();\n onStop();\n return;\n }\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n const showStop = generating && onStop !== undefined;\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <div className={styles.composerBox}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n {showStop ? (\n <button type=\"button\" className={styles.stop} onClick={onStop}>\n <span aria-hidden=\"true\" className={styles.stopGlyph} />\n {strings.stop}\n </button>\n ) : (\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || generating || value.trim() === \"\"}\n >\n {strings.send}\n </button>\n )}\n </div>\n </div>\n {footer && <div className={styles.composerFooter}>{footer}</div>}\n </form>\n );\n },\n);\n"],"mappings":";;;;;;AAiGA,IAAa,IAAiB,EAC1B,SACI,EACI,WACA,WACA,gBAAa,IACb,YAAS,SACT,YACA,WACA,aAAU,GACV,YACA,cACA,aACA,gBACA,cACA,aACA,GAAG,KAEP,GACF;CACE,IAAM,IAAU,EAAc,CAAM,GAC9B,IAAW,EAAmC,IAAI,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAK;CAkBtC,AAhBA,EAAoB,UAAY;EAC5B,aAAa,EAAS,SAAS,MAAM;EACrC,WAAW,MAAiB;GAExB,AADA,EAAS,CAAI,GACb,EAAS,SAAS,MAAM;EAC5B;EACA,gBAAgB;CACpB,EAAE,GASF,QAAsB;EAClB,IAAM,IAAO,EAAS;EACtB,IAAI,CAAC,GAAM;EACX,EAAK,MAAM,SAAS;EAEpB,IAAM,KADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,KAAK,MAClD;EAEzB,AADA,EAAK,MAAM,SAAS,GAAG,KAAK,IAAI,EAAK,cAAc,CAAG,EAAE,KACxD,EAAK,MAAM,YAAY,EAAK,eAAe,IAAM,SAAS;CAC9D,GAAG,CAAC,GAAO,CAAO,CAAC;CAEnB,IAAM,IAAS,YAA2B;EACtC,IAAM,IAAO,EAAM,KAAK;EACpB,OAAC,KAAQ,KAAQ,KAAY,IACjC;KAAQ,EAAI;GACZ,IAAI;IAEA,AADA,MAAM,EAAO,CAAI,GACjB,EAAS,EAAE;GACf,SAAS,GAAO;IACZ,IAAU,CAAK;GACnB,UAAU;IACN,EAAQ,EAAK;GACjB;EARY;CAShB,GAEM,KAAgB,MAA2B;EAE7C,AADA,EAAM,eAAe,GACrB,EAAY;CAChB,GAUM,KAAiB,MAAoD;EACvE,QAAY,CAAK,GACb,GAAM,kBACV;OAAI,EAAM,QAAQ,YAAY,KAAc,GAAQ;IAEhD,AADA,EAAM,eAAe,GACrB,EAAO;IACP;GACJ;GACI,EAAM,QAAQ,WAAW,EAAM,YAC/B,EAAM,YAAY,eAAe,EAAM,YAAY,QACvD,EAAM,eAAe,GACrB,EAAY;EAJZ;CAKJ,GAEM,IAAW,KAAc,MAAW,KAAA;CAE1C,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,UAAU;EAA3D,UAAA,CACI,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAvB,UAAA,CACI,kBAAC,YAAD;IACI,GAAI;IACJ,KAAK;IACL,WAAW,EAAO;IAClB,MAAM;IACC;IACG;IACV,aAAa,KAAe,EAAQ;IACpC,WAAW,MAAU;KAEjB,AADA,EAAS,EAAM,OAAO,KAAK,GAC3B,IAAW,CAAK;IACpB;IACA,WAAW;GACd,CAAA,GACD,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACK,GACA,IACG,kBAAC,UAAD;KAAQ,MAAK;KAAS,WAAW,EAAO;KAAM,SAAS;KAAvD,UAAA,CACI,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAW,EAAO;KAAY,CAAA,GACtD,EAAQ,IACL;IAER,CAAA,IAAA,kBAAC,UAAD;KACI,MAAK;KACL,WAAW,EAAO;KAClB,UAAU,KAAY,KAAQ,KAAc,EAAM,KAAK,MAAM;KAE5D,UAAA,EAAQ;IACL,CAAA,CAEX;GACJ,CAAA,CAAA;EACJ,CAAA,GAAA,KAAU,kBAAC,OAAD;GAAK,WAAW,EAAO;GAAiB,UAAA;EAAY,CAAA,CAC7D;;AAEd,CACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatTurn.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"2YAkFA,SAAgB,EAAW,CACvB,UACA,SAAS,QACT,gBAAgB,GAChB,eACA,aACA,eACA,cACA,UACA,eACA,gBACA,OACA,uBAAuB,GACvB,kBAAkB,IACF,CAChB,IAAM,EAAU,EAAA,cAAc,CAAM,EAC9B,CAAE,OAAM,UAAS,YAAW,YAAW,QAAO,QAAO,YAAW,eAAgB,EAChF,CAAC,EAAW,IAAA,EAAA,EAAA,SAAA,CAA4C,IAAI,EAC5D,CAAC,EAAS,IAAA,EAAA,EAAA,SAAA,CAAuB,EAAK,EACtC,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAqB,CAAO,EACpC,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAAsB,EAAK,EAEpC,EAAc,GAAQ,EACtB,EAAS,IAAS,OAUlB,GAAA,EAAA,EAAA,QAAA,MAEE,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CACI,OAAQ,EACS,kBACjB,UAAW,EAAA,QAAO,YACrB,CAAA,EAEL,CAAC,EAAS,CAAe,CAC7B,EASM,EACE,EAAsB,EAAc,CAAO,EAC3C,GAAe,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,eAAQ,CAAa,CAAA,EAC3D,IAAY,GAAW,KACpB,EAGL,EAAa,SAA2B,CAC1C,IAAM,EAAO,EAAM,KAAK,EACpB,MAAC,GAAQ,GAAU,CAAC,GACxB,GAAU,EAAI,EACd,GAAI,CACA,MAAM,EAAa,EAAS,CAAI,EAChC,EAAW,EAAK,CACpB,OAAS,EAAO,CACZ,IAAc,CAAK,CACvB,QAAU,CACN,EAAU,EAAK,CACnB,CARc,CASlB,EAEM,EAAQ,GAA2B,CACjC,IAAgB,IACpB,EAAa,CAAI,EACjB,IAAa,EAAS,CAAI,EAC9B,EAEM,EAAc,CAAC,GAAa,CAAC,EAC7B,EACF,IAAe,IAAA,IAAc,GAAiB,IAAiB,IAAA,GAEnE,OACI,EAAA,EAAA,KAAA,CAAC,UAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,KACP,GAAU,EAAA,QAAO,SACjB,IAAS,UAAY,EAAA,QAAO,WAC5B,GAAa,EAAA,QAAO,aACxB,EACA,YAAW,GAAa,IAAA,GACxB,YAAW,WARf,EAUI,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAW,EAAA,QAAO,oBAA1B,CACK,IACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAQ,cAAY,gBACvC,EAAa,CAAO,CACnB,CAAA,GAEV,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAU,EAAA,QAAO,kBAAkB,WAC/D,EAAA,UAAU,EAAM,CAAO,CACtB,CAAA,EACL,GAAS,CAAC,IAAU,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,CAAY,CAAA,EAChE,IAAc,IAAA,KACX,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAM,SAAU,IAAI,KAAK,CAAS,CAAC,CAAC,YAAY,WACnE,EAAA,SAAS,EAAW,CAAM,CACzB,CAAA,CAEN,IAEP,GAAe,EAAY,OAAS,IACjC,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,qBACjB,EAAY,IAAK,IACd,EAAA,EAAA,IAAA,CAAC,KAAD,CAAwB,UAAW,EAAA,QAAO,oBACrC,EAAW,KACR,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,MAClB,IAAK,EAAW,IAChB,IAAK,EAAW,IACnB,CAAA,GAED,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,wBAAiB,EAAW,IAAW,CAAA,GAC/D,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,wBACnB,EAAW,OAAS,IAAA,GAEf,EAAW,SADX,EAAA,YAAY,EAAW,IAAI,CAE/B,CAAA,CACR,CAAA,CAAA,CAEN,EAjBK,EAAW,EAiBhB,CACP,CACD,CAAA,EAGP,IACG,EAAA,EAAA,IAAA,CAAC,EAAA,YAAD,CACI,UAAW,EAAA,QAAO,UAClB,YAAa,GAAyB,IAAc,IAAQ,IAAY,GACxE,SACI,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CACI,UAAW,EAAA,QAAO,iBAClB,KAAM,GACN,cAAA,EACH,CAAA,EACA,EAAQ,SACX,CAAA,CAAA,YAGN,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,uBAAgB,CAAe,CAAA,CAC7C,CAAA,EAGhB,GACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,gBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,eAAD,CAAA,UACI,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,QAAS,GAAG,EAAQ,GAAG,gBAAS,EAAQ,OAAe,CAAA,CAClD,CAAA,GAChB,EAAA,EAAA,IAAA,CAAC,WAAD,CACI,GAAI,GAAG,EAAQ,GAAG,OAClB,UAAW,EAAA,QAAO,UAClB,MAAO,EACP,KAAM,KAAK,IAAI,EAAM,MAAM;CAAI,CAAC,CAAC,OAAS,EAAG,EAAE,EAC/C,SAAW,GAAU,EAAS,EAAM,OAAO,KAAK,CACnD,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,qBAAvB,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,YAAe,CACX,EAAS,CAAO,EAChB,EAAW,EAAK,CACpB,WAEC,EAAQ,MACL,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,SAAU,GAAU,EAAM,KAAK,IAAM,GACrC,YAAe,KAAK,EAAW,WAE9B,EAAQ,IACL,CAAA,CACP,GACJ,KAEL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAa,EAAA,QAAO,aAAa,WAAjE,CACK,EACA,IAAc,IAAQ,IAAY,KAC/B,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAM,cAAY,gBAA1C,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,GACP,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,GACP,EAAA,EAAA,IAAA,CAAC,OAAD,CAAO,CAAA,CACL,GAET,IAGR,IACG,EAAA,EAAA,KAAA,CAAC,IAAD,CAAG,UAAW,EAAA,QAAO,MAAO,KAAK,iBAAjC,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,CAAY,CAAA,EAClB,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,MAClB,YAAe,EAAQ,CAAO,WAE7B,EAAQ,KACL,CAAA,CAEb,IAGN,IACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAS,aAAY,EAAQ,YAAa,KAAK,iBAAtE,EACI,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACI,UAAW,EAAA,QAAO,OAClB,MAAO,EACP,aAAY,EAAQ,KACpB,MAAO,EAAQ,eAEf,EAAA,EAAA,IAAA,CAAC,EAAA,KAAD,CAAM,KAAM,GAAI,cAAA,EAAa,CAAA,CACrB,CAAA,EAEX,GAAU,IACP,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,aAAY,EAAQ,KACpB,MAAO,EAAQ,KACf,YAAe,CACX,EAAS,CAAO,EAChB,EAAW,EAAI,CACnB,YAEA,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAQ,KAAM,GAAI,cAAA,EAAa,CAAA,CAC3B,CAAA,EAGX,CAAC,GAAU,IACR,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,GAAiB,IACd,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,OAClB,aAAY,EAAQ,WACpB,MAAO,EAAQ,WACf,YAAe,EAAa,CAAO,YAEnC,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAW,KAAM,GAAI,cAAA,EAAa,CAAA,CAC9B,CAAA,EAEX,IACG,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GACP,EAAA,QAAO,OACP,IAAgB,MAAQ,EAAA,QAAO,YACnC,EACA,aAAY,EAAQ,KACpB,MAAO,EAAQ,KACf,eAAc,IAAgB,KAC9B,YAAe,EAAK,IAAI,YAExB,EAAA,EAAA,IAAA,CAAC,EAAA,SAAD,CAAU,KAAM,GAAI,cAAA,EAAa,CAAA,CAC7B,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GACP,EAAA,QAAO,OACP,IAAgB,QAAU,EAAA,QAAO,YACrC,EACA,aAAY,EAAQ,IACpB,MAAO,EAAQ,IACf,eAAc,IAAgB,OAC9B,YAAe,EAAK,MAAM,YAE1B,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CAAY,KAAM,GAAI,cAAA,EAAa,CAAA,CAC/B,CAAA,CACV,CAAA,CAAA,CAER,CAAA,CAAA,CAEL,GAEJ,GAEjB"}
|
|
1
|
+
{"version":3,"file":"AIChatTurn.cjs","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AIChatTurn.js","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AIChatTurn.js","names":[],"sources":["../../../src/components/AIChat/AIChatTurn.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — one turn renders six\n * things that each have their own state: Markdown body, collapsible reasoning,\n * attachments, streaming caret, inline edit form and the action row. The 13 props\n * are the callbacks those actions need plus the render slots, and the body is long\n * because the edit form and the action row share the turn's message and its pending\n * state.\n */\nimport { ChevronRight, Copy, Pencil, RefreshCw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { useMemo, useState, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\nimport { formatBytes } from \"@/utils/numbers\";\n\nimport { Collapsible } from \"../Collapsible\";\nimport { CopyButton } from \"../CopyButton\";\nimport { Markdown } from \"../Markdown\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n aiChatStrings,\n roleLabel,\n turnTime,\n type AIChatMessage,\n type AIChatVote,\n} from \"./ai-chat-turns\";\nimport styles from \"./AIChat.module.css\";\n\nexport interface AIChatTurnProps {\n /** The turn to render. */\n message: AIChatMessage;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /**\n * Offer the regenerate control.\n *\n * Only the newest assistant turn should get it — re-asking an older one throws\n * away every turn after it, which is a different operation and needs its own\n * confirmation.\n */\n canRegenerate?: boolean;\n onRegenerate?: (message: AIChatMessage) => void;\n onFeedback?: (message: AIChatMessage, vote: AIChatVote) => void;\n /** Enables the edit control on a user turn. Receives the edited prompt. */\n onEditSubmit?: (message: AIChatMessage, text: string) => void | Promise<void>;\n /**\n * Called when `onEditSubmit` rejects. The editor stays open with the draft either\n * way.\n *\n * Without it the rejection is swallowed after the draft is preserved: re-throwing\n * out of a click handler surfaces as an unhandled promise rejection, which is\n * console noise for the developer and nothing the user can act on.\n */\n onEditError?: (error: unknown) => void;\n /** Enables the retry control on a turn that carries an `error`. */\n onRetry?: (message: AIChatMessage) => void;\n renderAvatar?: (message: AIChatMessage) => ReactNode;\n /** Render the body yourself — a tool-call card, a chart, a citation list. */\n renderContent?: (message: AIChatMessage) => ReactNode;\n /**\n * Rating to show as pressed.\n *\n * Pass it to keep votes in app state (persisted across a reload); leave it out\n * and the pressed state is kept locally, which is enough for a fire-and-forget\n * `onFeedback`.\n */\n vote?: AIChatVote;\n /** Reasoning blocks start expanded. Default `false`. */\n defaultReasoningOpen?: boolean;\n /** Show line numbers in fenced code. Default `false`. */\n showLineNumbers?: boolean;\n}\n\n/**\n * One turn of a conversation with a model.\n *\n * Exported for apps that build their own transcript layout (a split view, a diff of\n * two answers) but still want the SDK's turn: Markdown body, reasoning block,\n * attachments, streaming caret, error state and the action row.\n *\n * @example\n * <AIChatTurn message={turn} canRegenerate onRegenerate={(m) => reask(m)} />\n */\nexport function AIChatTurn({\n message,\n locale = \"pt-BR\",\n canRegenerate = false,\n onRegenerate,\n onFeedback,\n onEditSubmit,\n onEditError,\n onRetry,\n renderAvatar,\n renderContent,\n vote,\n defaultReasoningOpen = false,\n showLineNumbers = false,\n}: AIChatTurnProps) {\n const strings = aiChatStrings(locale);\n const { role, content, reasoning, streaming, error, model, createdAt, attachments } = message;\n const [localVote, setLocalVote] = useState<AIChatVote | null>(null);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(content);\n const [saving, setSaving] = useState(false);\n\n const currentVote = vote ?? localVote;\n const isUser = role === \"user\";\n\n /**\n * Memoised so a streaming answer re-parses **only the turn that is growing**.\n *\n * `Markdown` parses in its own render, and React skips re-rendering a child whose\n * element is referentially identical to the previous one. Holding the element\n * across renders is therefore what keeps a fifty-turn transcript from re-parsing\n * every finished answer on every token of the newest one.\n */\n const markdown = useMemo(\n () => (\n <Markdown\n source={content}\n showLineNumbers={showLineNumbers}\n className={styles.markdownRoot}\n />\n ),\n [content, showLineNumbers],\n );\n\n /**\n * Body of the turn.\n *\n * A user turn is plain text with newlines preserved, an assistant turn is\n * Markdown. A model emits Markdown by contract; a person typing `2 * 3 * 4` did\n * not mean to open an emphasis span.\n */\n const body = ((): ReactNode => {\n if (renderContent) return renderContent(message);\n if (isUser) return <div className={styles.plain}>{content}</div>;\n if (content === \"\") return null;\n return markdown;\n })();\n\n const submitEdit = async (): Promise<void> => {\n const text = draft.trim();\n if (!text || saving || !onEditSubmit) return;\n setSaving(true);\n try {\n await onEditSubmit(message, text);\n setEditing(false);\n } catch (error) {\n onEditError?.(error);\n } finally {\n setSaving(false);\n }\n };\n\n const rate = (next: AIChatVote): void => {\n if (currentVote === next) return;\n setLocalVote(next);\n onFeedback?.(message, next);\n };\n\n const showActions = !streaming && !editing;\n const hasAssistantActions =\n onFeedback !== undefined || (canRegenerate && onRegenerate !== undefined);\n\n return (\n <article\n className={cn(\n styles.turn,\n isUser && styles.userTurn,\n role === \"system\" && styles.systemTurn,\n streaming && styles.streamingTurn,\n )}\n aria-busy={streaming || undefined}\n data-role={role}\n >\n <header className={styles.turnHeader}>\n {renderAvatar && (\n <span className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(message)}\n </span>\n )}\n <span className={cn(styles.role, isUser && styles.visuallyHiddenRole)}>\n {roleLabel(role, strings)}\n </span>\n {model && !isUser && <span className={styles.model}>{model}</span>}\n {createdAt !== undefined && (\n <time className={styles.time} dateTime={new Date(createdAt).toISOString()}>\n {turnTime(createdAt, locale)}\n </time>\n )}\n </header>\n\n {attachments && attachments.length > 0 && (\n <ul className={styles.attachments}>\n {attachments.map((attachment) => (\n <li key={attachment.id} className={styles.attachment}>\n {attachment.url ? (\n <img\n className={styles.thumb}\n src={attachment.url}\n alt={attachment.name}\n />\n ) : (\n <>\n <span className={styles.attachmentName}>{attachment.name}</span>\n <span className={styles.attachmentMeta}>\n {attachment.size !== undefined\n ? formatBytes(attachment.size)\n : attachment.mimeType}\n </span>\n </>\n )}\n </li>\n ))}\n </ul>\n )}\n\n {reasoning && (\n <Collapsible\n className={styles.reasoning}\n defaultOpen={defaultReasoningOpen || (streaming === true && content === \"\")}\n trigger={\n <>\n <ChevronRight\n className={styles.reasoningChevron}\n size={14}\n aria-hidden\n />\n {strings.reasoning}\n </>\n }\n >\n <div className={styles.reasoningBody}>{reasoning}</div>\n </Collapsible>\n )}\n\n {editing ? (\n <div className={styles.editor}>\n <VisuallyHidden>\n <label htmlFor={`${message.id}-edit`}>{strings.editing}</label>\n </VisuallyHidden>\n <textarea\n id={`${message.id}-edit`}\n className={styles.editField}\n value={draft}\n rows={Math.min(draft.split(\"\\n\").length + 1, 10)}\n onChange={(event) => setDraft(event.target.value)}\n />\n <div className={styles.editActions}>\n <button\n type=\"button\"\n className={styles.ghost}\n onClick={() => {\n setDraft(content);\n setEditing(false);\n }}\n >\n {strings.cancel}\n </button>\n <button\n type=\"button\"\n className={styles.primary}\n disabled={saving || draft.trim() === \"\"}\n onClick={() => void submitEdit()}\n >\n {strings.save}\n </button>\n </div>\n </div>\n ) : (\n <div className={cn(styles.body, streaming && styles.streamingBody)}>\n {body}\n {streaming === true && content === \"\" && (\n <span className={styles.dots} aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n )}\n </div>\n )}\n\n {error && (\n <p className={styles.error} role=\"alert\">\n <span>{error}</span>\n {onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </p>\n )}\n\n {showActions && (\n <div className={styles.actions} aria-label={strings.turnActions} role=\"group\">\n <CopyButton\n className={styles.action}\n value={content}\n aria-label={strings.copy}\n title={strings.copy}\n >\n <Copy size={14} aria-hidden />\n </CopyButton>\n\n {isUser && onEditSubmit && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.edit}\n title={strings.edit}\n onClick={() => {\n setDraft(content);\n setEditing(true);\n }}\n >\n <Pencil size={14} aria-hidden />\n </button>\n )}\n\n {!isUser && hasAssistantActions && (\n <>\n {canRegenerate && onRegenerate && (\n <button\n type=\"button\"\n className={styles.action}\n aria-label={strings.regenerate}\n title={strings.regenerate}\n onClick={() => onRegenerate(message)}\n >\n <RefreshCw size={14} aria-hidden />\n </button>\n )}\n {onFeedback && (\n <>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"up\" && styles.actionActive,\n )}\n aria-label={strings.good}\n title={strings.good}\n aria-pressed={currentVote === \"up\"}\n onClick={() => rate(\"up\")}\n >\n <ThumbsUp size={14} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(\n styles.action,\n currentVote === \"down\" && styles.actionActive,\n )}\n aria-label={strings.bad}\n title={strings.bad}\n aria-pressed={currentVote === \"down\"}\n onClick={() => rate(\"down\")}\n >\n <ThumbsDown size={14} aria-hidden />\n </button>\n </>\n )}\n </>\n )}\n </div>\n )}\n </article>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Accordion.cjs","names":[],"sources":["../../../src/components/Accordion/Accordion.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Accordion.module.css\";\n\nexport interface AccordionItem {\n /** Stable identifier. */\n id: string;\n title: ReactNode;\n children: ReactNode;\n disabled?: boolean;\n}\n\nexport interface AccordionProps {\n items: AccordionItem[];\n /** When `true` multiple items can be open simultaneously. Default `false`. */\n multiple?: boolean;\n /** Controlled open ids. */\n value?: string[];\n /** Uncontrolled default open ids. */\n defaultValue?: string[];\n onChange?: (openIds: string[]) => void;\n className?: string;\n}\n\n/**\n * Accessible accordion. Each item collapses/expands its content. Single-mode by\n * default — pass `multiple` to allow more than one item open at a time. Can be\n * controlled via `value` + `onChange`, or uncontrolled via `defaultValue`.\n */\nexport function Accordion({\n items,\n multiple = false,\n value,\n defaultValue = [],\n onChange,\n className,\n}: AccordionProps) {\n const reactId = useId();\n const isControlled = value !== undefined;\n const [internalOpen, setInternalOpen] = useState<string[]>(defaultValue);\n const openIds = isControlled ? value : internalOpen;\n\n const toggle = useCallback(\n (id: string): void => {\n const isOpen = openIds.includes(id);\n const next = multiple\n ? isOpen\n ? openIds.filter((x) => x !== id)\n : [...openIds, id]\n : isOpen\n ? []\n : [id];\n if (!isControlled) setInternalOpen(next);\n onChange?.(next);\n },\n [openIds, multiple, isControlled, onChange],\n );\n\n return (\n <div className={cn(styles.accordion, className)}>\n {items.map((item) => {\n const open = openIds.includes(item.id);\n const panelId = `${reactId}-${item.id}-panel`;\n const triggerId = `${reactId}-${item.id}-trigger`;\n return (\n <div key={item.id} className={cn(styles.item, open && styles.open)}>\n <h3 className={styles.header}>\n <button\n type=\"button\"\n id={triggerId}\n className={styles.trigger}\n aria-expanded={open}\n aria-controls={panelId}\n disabled={item.disabled}\n onClick={() => toggle(item.id)}\n >\n <span>{item.title}</span>\n <ChevronIcon className={styles.chevron} />\n </button>\n </h3>\n {open && (\n <div\n id={panelId}\n role=\"region\"\n aria-labelledby={triggerId}\n className={styles.panel}\n >\n {item.children}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden\n >\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"gIA8BA,SAAgB,EAAU,CACtB,QACA,WAAW,GACX,QACA,eAAe,CAAC,EAChB,WACA,aACe,CACf,IAAM,GAAA,
|
|
1
|
+
{"version":3,"file":"Accordion.cjs","names":[],"sources":["../../../src/components/Accordion/Accordion.tsx"],"sourcesContent":["import { useCallback, useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Accordion.module.css\";\n\nexport interface AccordionItem {\n /** Stable identifier. */\n id: string;\n title: ReactNode;\n children: ReactNode;\n disabled?: boolean;\n}\n\nexport interface AccordionProps {\n items: AccordionItem[];\n /** When `true` multiple items can be open simultaneously. Default `false`. */\n multiple?: boolean;\n /** Controlled open ids. */\n value?: string[];\n /** Uncontrolled default open ids. */\n defaultValue?: string[];\n onChange?: (openIds: string[]) => void;\n className?: string;\n}\n\n/**\n * Accessible accordion. Each item collapses/expands its content. Single-mode by\n * default — pass `multiple` to allow more than one item open at a time. Can be\n * controlled via `value` + `onChange`, or uncontrolled via `defaultValue`.\n */\nexport function Accordion({\n items,\n multiple = false,\n value,\n defaultValue = [],\n onChange,\n className,\n}: AccordionProps) {\n const reactId = useId();\n const isControlled = value !== undefined;\n const [internalOpen, setInternalOpen] = useState<string[]>(defaultValue);\n const openIds = isControlled ? value : internalOpen;\n\n const toggle = useCallback(\n (id: string): void => {\n const isOpen = openIds.includes(id);\n const next = multiple\n ? isOpen\n ? openIds.filter((x) => x !== id)\n : [...openIds, id]\n : isOpen\n ? []\n : [id];\n if (!isControlled) setInternalOpen(next);\n onChange?.(next);\n },\n [openIds, multiple, isControlled, onChange],\n );\n\n return (\n <div className={cn(styles.accordion, className)}>\n {items.map((item) => {\n const open = openIds.includes(item.id);\n const panelId = `${reactId}-${item.id}-panel`;\n const triggerId = `${reactId}-${item.id}-trigger`;\n return (\n <div key={item.id} className={cn(styles.item, open && styles.open)}>\n <h3 className={styles.header}>\n <button\n type=\"button\"\n id={triggerId}\n className={styles.trigger}\n aria-expanded={open}\n aria-controls={panelId}\n disabled={item.disabled}\n onClick={() => toggle(item.id)}\n >\n <span>{item.title}</span>\n <ChevronIcon className={styles.chevron} />\n </button>\n </h3>\n {open && (\n <div\n id={panelId}\n role=\"region\"\n aria-labelledby={triggerId}\n className={styles.panel}\n >\n {item.children}\n </div>\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction ChevronIcon({ className }: { className?: string }) {\n return (\n <svg\n className={className}\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden\n >\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"gIA8BA,SAAgB,EAAU,CACtB,QACA,WAAW,GACX,QACA,eAAe,CAAC,EAChB,WACA,aACe,CACf,IAAM,GAAA,EAAU,EAAA,MAAA,CAAM,EAChB,EAAe,IAAU,IAAA,GACzB,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAmB,CAAY,EACjE,EAAU,EAAe,EAAQ,EAEjC,GAAA,EAAS,EAAA,YAAA,CACV,GAAqB,CAClB,IAAM,EAAS,EAAQ,SAAS,CAAE,EAC5B,EAAO,EACP,EACI,EAAQ,OAAQ,GAAM,IAAM,CAAE,EAC9B,CAAC,GAAG,EAAS,CAAE,EACnB,EACE,CAAC,EACD,CAAC,CAAE,EACN,GAAc,EAAgB,CAAI,EACvC,IAAW,CAAI,CACnB,EACA,CAAC,EAAS,EAAU,EAAc,CAAQ,CAC9C,EAEA,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,UAAW,CAAS,EACzC,SAAA,EAAM,IAAK,GAAS,CACjB,IAAM,EAAO,EAAQ,SAAS,EAAK,EAAE,EAC/B,EAAU,GAAG,EAAQ,GAAG,EAAK,GAAG,QAChC,EAAY,GAAG,EAAQ,GAAG,EAAK,GAAG,UACxC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAmB,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAQ,EAAA,QAAO,IAAI,EAAjE,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,OAClB,UAAA,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,KAAK,SACL,GAAI,EACJ,UAAW,EAAA,QAAO,QAClB,gBAAe,EACf,gBAAe,EACf,SAAU,EAAK,SACf,YAAe,EAAO,EAAK,EAAE,EAPjC,SAAA,EASI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAK,KAAY,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,UAAW,EAAA,QAAO,OAAU,CAAA,CACrC,GACR,CAAA,EACH,IACG,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,GAAI,EACJ,KAAK,SACL,kBAAiB,EACjB,UAAW,EAAA,QAAO,MAEjB,SAAA,EAAK,QACL,CAAA,CAER,CAzBK,EAAA,EAAK,EAyBV,CAEb,CAAC,CACA,CAAA,CAEb,CAEA,SAAS,EAAY,CAAE,aAAqC,CACxD,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CACe,YACX,MAAM,KACN,OAAO,KACP,QAAQ,YACR,KAAK,OACL,cAAA,GAEA,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,eACF,OAAO,eACP,YAAY,IACZ,cAAc,QACd,eAAe,OAClB,CAAA,CACA,CAAA,CAEb"}
|