@djangocfg/ui-tools 2.1.404 → 2.1.408
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 +9 -11
- package/dist/file-icon/index.cjs +449 -61
- package/dist/file-icon/index.cjs.map +1 -1
- package/dist/file-icon/index.d.cts +56 -18
- package/dist/file-icon/index.d.ts +56 -18
- package/dist/file-icon/index.mjs +448 -62
- package/dist/file-icon/index.mjs.map +1 -1
- package/dist/tree/index.cjs +49 -22
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +9 -3
- package/dist/tree/index.d.ts +9 -3
- package/dist/tree/index.mjs +49 -22
- package/dist/tree/index.mjs.map +1 -1
- package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
- package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
- package/package.json +13 -16
- package/src/components/FloatingToolbar/index.tsx +37 -3
- package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
- package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
- package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
- package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
- package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
- package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
- package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
- package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
- package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
- package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
- package/src/lib/page-snapshot/capture/budget.ts +95 -0
- package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
- package/src/lib/page-snapshot/capture/classify.ts +111 -0
- package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
- package/src/lib/page-snapshot/capture/fold.ts +96 -0
- package/src/lib/page-snapshot/capture/scope.ts +169 -0
- package/src/lib/page-snapshot/capture/walk.ts +250 -0
- package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
- package/src/lib/page-snapshot/cst/directives.ts +47 -0
- package/src/lib/page-snapshot/cst/payload.ts +50 -0
- package/src/lib/page-snapshot/cst/serialize.ts +84 -0
- package/src/lib/page-snapshot/cst/types.ts +115 -0
- package/src/lib/page-snapshot/engine.ts +176 -0
- package/src/lib/page-snapshot/index.ts +93 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
- package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
- package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
- package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
- package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
- package/src/lib/page-snapshot/react/provider.tsx +162 -0
- package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
- package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
- package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
- package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
- package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
- package/src/lib/page-snapshot/redaction/audit.ts +58 -0
- package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
- package/src/lib/page-snapshot/redaction/index.ts +75 -0
- package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
- package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
- package/src/lib/page-snapshot/refs/registry.ts +46 -0
- package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
- package/src/lib/page-snapshot/staleness/hash.ts +20 -0
- package/src/lib/page-snapshot/tokens.ts +15 -0
- package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
- package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
- package/src/tools/AudioPlayer/lazy.tsx +13 -27
- package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
- package/src/tools/Chat/README.md +267 -39
- package/src/tools/Chat/composer/Composer.tsx +471 -0
- package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
- package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
- package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
- package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
- package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
- package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
- package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
- package/src/tools/Chat/composer/index.ts +45 -0
- package/src/tools/Chat/composer/size-context.tsx +26 -0
- package/src/tools/Chat/composer/types.ts +143 -0
- package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
- package/src/tools/Chat/context/ChatProvider.tsx +54 -3
- package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
- package/src/tools/Chat/core/index.ts +23 -1
- package/src/tools/Chat/core/markdown.ts +1 -1
- package/src/tools/Chat/core/metadata.ts +47 -0
- package/src/tools/Chat/core/payload-dispatch.ts +1 -1
- package/src/tools/Chat/core/transport/http.ts +71 -32
- package/src/tools/Chat/core/transport/sse.ts +18 -10
- package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
- package/src/tools/Chat/highlight/README.md +103 -0
- package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
- package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
- package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
- package/src/tools/Chat/highlight/index.ts +21 -0
- package/src/tools/Chat/highlight/resolveRef.ts +42 -0
- package/src/tools/Chat/highlight/types.ts +49 -0
- package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
- package/src/tools/Chat/hooks/index.ts +0 -5
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
- package/src/tools/Chat/hooks/useChat.ts +47 -14
- package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
- package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
- package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
- package/src/tools/Chat/index.ts +25 -219
- package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
- package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
- package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
- package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
- package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
- package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
- package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
- package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
- package/src/tools/Chat/launcher/header/index.ts +26 -0
- package/src/tools/Chat/launcher/index.ts +3 -10
- package/src/tools/Chat/lazy.tsx +38 -284
- package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
- package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
- package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
- package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
- package/src/tools/Chat/messages/blocks/index.ts +12 -0
- package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
- package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
- package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
- package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
- package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
- package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
- package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
- package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
- package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
- package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
- package/src/tools/Chat/{components → messages}/index.ts +11 -5
- package/src/tools/Chat/public.ts +212 -0
- package/src/tools/Chat/shell/ChatRoot.tsx +326 -0
- package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
- package/src/tools/Chat/shell/index.ts +15 -0
- package/src/tools/Chat/types/block.ts +120 -0
- package/src/tools/Chat/types/config.ts +0 -5
- package/src/tools/Chat/types/index.ts +17 -0
- package/src/tools/Chat/types/message.ts +3 -0
- package/src/tools/Chat/utils/index.ts +4 -0
- package/src/tools/CodeEditor/README.md +4 -6
- package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
- package/src/tools/CodeEditor/components/Editor.tsx +96 -44
- package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
- package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
- package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
- package/src/tools/CodeEditor/lazy.tsx +6 -0
- package/src/tools/CodeEditor/lib/index.ts +1 -1
- package/src/tools/CodeEditor/lib/themes.ts +3 -39
- package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
- package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
- package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
- package/src/tools/CronScheduler/index.tsx +1 -1
- package/src/tools/CronScheduler/types/index.ts +8 -3
- package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
- package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
- package/src/tools/FileIcon/FileIcon.tsx +24 -39
- package/src/tools/FileIcon/get-file-icon.ts +73 -0
- package/src/tools/FileIcon/icons/icon-data.ts +399 -0
- package/src/tools/FileIcon/index.ts +4 -0
- package/src/tools/FileIcon/loader.ts +17 -35
- package/src/tools/FileIcon/specialFolders.ts +18 -0
- package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
- package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
- package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
- package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
- package/src/tools/ImageViewer/components/ImageViewer.tsx +106 -26
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
- package/src/tools/ImageViewer/utils/constants.ts +3 -0
- package/src/tools/ImageViewer/utils/index.ts +1 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
- package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
- package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
- package/src/tools/LottiePlayer/index.tsx +14 -4
- package/src/tools/LottiePlayer/lazy.tsx +11 -3
- package/src/tools/LottiePlayer/types.ts +31 -1
- package/src/tools/LottiePlayer/useLottie.ts +32 -9
- package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
- package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
- package/src/tools/Map/components/MapCluster.tsx +28 -21
- package/src/tools/Map/components/MapContainer.tsx +11 -4
- package/src/tools/Map/components/MapLegend.tsx +46 -15
- package/src/tools/Map/components/MapMarker.tsx +31 -2
- package/src/tools/Map/hooks/useMapEvents.ts +64 -105
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
- package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
- package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
- package/src/tools/MarkdownEditor/lazy.tsx +32 -7
- package/src/tools/MarkdownEditor/styles.css +13 -0
- package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
- package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
- package/src/tools/MarkdownMessage/components.tsx +22 -9
- package/src/tools/MarkdownMessage/types.ts +24 -1
- package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
- package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
- package/src/tools/Mermaid/index.tsx +6 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
- package/src/tools/OpenapiViewer/constants.ts +3 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
- package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
- package/src/tools/PrettyCode/lazy.tsx +1 -1
- package/src/tools/SpeechRecognition/README.md +1 -1
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
- package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
- package/src/tools/SpeechRecognition/components/index.ts +2 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
- package/src/tools/SpeechRecognition/core/language.ts +23 -6
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
- package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
- package/src/tools/Tree/README.md +4 -8
- package/src/tools/Tree/TreeRoot.tsx +22 -10
- package/src/tools/Tree/components/TreeContent.tsx +24 -4
- package/src/tools/Tree/components/TreeLabel.tsx +8 -2
- package/src/tools/Tree/components/TreeRow.tsx +16 -6
- package/src/tools/Tree/data/flatten.ts +10 -4
- package/src/tools/Tree/types.ts +4 -0
- package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
- package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
- package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
- package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
- package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
- package/src/tools/Uploader/utils/formatters.ts +8 -3
- package/src/tools/VideoPlayer/README.md +87 -230
- package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
- package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +39 -0
- package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
- package/src/tools/VideoPlayer/canvas/index.ts +12 -0
- package/src/tools/VideoPlayer/canvas/jsx-augmentation.ts +47 -0
- package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +40 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +78 -0
- package/src/tools/VideoPlayer/index.ts +51 -65
- package/src/tools/VideoPlayer/lazy.tsx +11 -54
- package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
- package/src/tools/VideoPlayer/parts/index.ts +15 -0
- package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
- package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
- package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
- package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
- package/src/tools/VideoPlayer/styles/video-player.css +141 -0
- package/src/tools/VideoPlayer/types.ts +82 -0
- package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
- package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
- package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
- package/src/tools/index.ts +37 -29
- package/src/tools/Chat/components/AudioToggle.tsx +0 -78
- package/src/tools/Chat/components/ChatRoot.tsx +0 -305
- package/src/tools/Chat/components/Composer.tsx +0 -216
- package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
- package/src/tools/Chat/types.ts +0 -9
- package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
- package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
- package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
- package/src/tools/Tour/README.md +0 -373
- package/src/tools/Tour/components/Tour.tsx +0 -12
- package/src/tools/Tour/components/TourContent.tsx +0 -171
- package/src/tools/Tour/components/TourNavigation.tsx +0 -77
- package/src/tools/Tour/components/TourProgress.tsx +0 -88
- package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
- package/src/tools/Tour/components/index.ts +0 -5
- package/src/tools/Tour/context/TourContext.ts +0 -19
- package/src/tools/Tour/context/TourProvider.tsx +0 -292
- package/src/tools/Tour/context/index.ts +0 -2
- package/src/tools/Tour/hooks/index.ts +0 -3
- package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
- package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
- package/src/tools/Tour/hooks/useTour.ts +0 -42
- package/src/tools/Tour/index.ts +0 -38
- package/src/tools/Tour/types/index.ts +0 -224
- package/src/tools/Tour/utils/dom.ts +0 -98
- package/src/tools/Tour/utils/index.ts +0 -3
- package/src/tools/Tour/utils/logger.ts +0 -3
- package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
- package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
- package/src/tools/VideoPlayer/components/index.ts +0 -14
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
- package/src/tools/VideoPlayer/context/index.ts +0 -8
- package/src/tools/VideoPlayer/hooks/index.ts +0 -12
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
- package/src/tools/VideoPlayer/providers/index.ts +0 -8
- package/src/tools/VideoPlayer/types/index.ts +0 -38
- package/src/tools/VideoPlayer/types/player.ts +0 -116
- package/src/tools/VideoPlayer/types/provider.ts +0 -93
- package/src/tools/VideoPlayer/types/sources.ts +0 -97
- package/src/tools/VideoPlayer/utils/debug.ts +0 -14
- package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
- package/src/tools/VideoPlayer/utils/index.ts +0 -12
- package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
- /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
- /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
- /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
- /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
- /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
|
@@ -11,9 +11,15 @@ describe('toBCP47', () => {
|
|
|
11
11
|
expect(toBCP47('no')).toBe('nb-NO');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it('
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
it('resolves catalogue ISO codes to their canonical dialect', () => {
|
|
15
|
+
// Not in the built-in table, but present in the Web Speech
|
|
16
|
+
// catalogue — must resolve to a real region, never `uk-UK`/`cs-CS`.
|
|
17
|
+
expect(toBCP47('uk')).toBe('uk-UA');
|
|
18
|
+
expect(toBCP47('cs')).toBe('cs-CZ');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('falls back to <code>-<UPPER(code)> for fully unknown ISO codes', () => {
|
|
22
|
+
expect(toBCP47('xx')).toBe('xx-XX');
|
|
17
23
|
});
|
|
18
24
|
|
|
19
25
|
it('passes BCP-47 input through unchanged', () => {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
+
|
|
7
|
+
export interface RecordingPulseProps {
|
|
8
|
+
/** When false the overlay is fully hidden (no DOM cost, no animation). */
|
|
9
|
+
active: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Live RMS mic level 0..1 (from `useMicLevel` / `useSpeechRecognition`).
|
|
12
|
+
* Drives the pulse amplitude so the ring "breathes" with the voice.
|
|
13
|
+
* Omit for a steady ambient pulse.
|
|
14
|
+
*/
|
|
15
|
+
level?: number;
|
|
16
|
+
/** Tailwind colour token for the ring. @default 'bg-destructive' */
|
|
17
|
+
tone?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pulsing circle overlay that signals an active recording — Gemini-style
|
|
23
|
+
* `circle-overlay`. Sits absolutely over a round mic button (`inset-0`),
|
|
24
|
+
* so the parent must be `relative`.
|
|
25
|
+
*
|
|
26
|
+
* Two layers: a steady `animate-ping` halo for the "still recording"
|
|
27
|
+
* baseline, plus a level-driven ring that scales with mic amplitude so
|
|
28
|
+
* loud speech visibly bumps the pulse. Pointer-events are off — the
|
|
29
|
+
* button underneath stays fully clickable.
|
|
30
|
+
*/
|
|
31
|
+
export function RecordingPulse({
|
|
32
|
+
active,
|
|
33
|
+
level = 0,
|
|
34
|
+
tone = 'bg-destructive',
|
|
35
|
+
className,
|
|
36
|
+
}: RecordingPulseProps): React.ReactElement | null {
|
|
37
|
+
if (!active) return null;
|
|
38
|
+
|
|
39
|
+
// Map RMS 0..1 → a calm 1.0–1.45 scale. Speech rarely exceeds ~0.5
|
|
40
|
+
// RMS, so we amplify the low end for a lively-but-not-jittery ring.
|
|
41
|
+
const amp = Math.min(1, Math.max(0, level));
|
|
42
|
+
const scale = 1 + Math.min(0.45, amp * 0.9);
|
|
43
|
+
const glowOpacity = 0.25 + Math.min(0.35, amp * 0.7);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<span aria-hidden className={cn('pointer-events-none absolute inset-0', className)}>
|
|
47
|
+
{/* Steady halo — guarantees visible feedback even in silence. */}
|
|
48
|
+
<span className={cn('absolute inset-0 rounded-full opacity-30 animate-ping', tone)} />
|
|
49
|
+
{/* Amplitude ring — scales smoothly with the live mic level. */}
|
|
50
|
+
<span
|
|
51
|
+
className={cn(
|
|
52
|
+
'absolute inset-0 rounded-full transition-transform duration-100 ease-out',
|
|
53
|
+
tone,
|
|
54
|
+
)}
|
|
55
|
+
style={{ transform: `scale(${scale})`, opacity: glowOpacity }}
|
|
56
|
+
/>
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -14,3 +14,5 @@ export { ErrorBanner } from './ErrorBanner';
|
|
|
14
14
|
export type { ErrorBannerProps } from './ErrorBanner';
|
|
15
15
|
export { PushToTalkHint } from './PushToTalkHint';
|
|
16
16
|
export type { PushToTalkHintProps } from './PushToTalkHint';
|
|
17
|
+
export { RecordingPulse } from './RecordingPulse';
|
|
18
|
+
export type { RecordingPulseProps } from './RecordingPulse';
|
|
@@ -37,8 +37,8 @@ export interface ExternalEngineHandle {
|
|
|
37
37
|
*/
|
|
38
38
|
emitPartial(text: string): void;
|
|
39
39
|
/**
|
|
40
|
-
* Push the final transcript. Closes the current segment
|
|
41
|
-
*
|
|
40
|
+
* Push the final transcript. Closes the current segment. With the
|
|
41
|
+
* default `closeOnFinal`, also closes the whole session.
|
|
42
42
|
*/
|
|
43
43
|
emitFinal(text: string, confidence?: number): void;
|
|
44
44
|
/** Surface a backend error. Engine transitions to `closed`. */
|
|
@@ -85,6 +85,17 @@ export interface ExternalEngineOptions {
|
|
|
85
85
|
* native side to confirm the capture session opened.
|
|
86
86
|
*/
|
|
87
87
|
autoMarkListening?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* If `true` (default), the engine closes the whole session right
|
|
90
|
+
* after the first `emitFinal` — the common single-shot case (one
|
|
91
|
+
* recording → one transcript, e.g. cmdop Wails push-to-talk).
|
|
92
|
+
*
|
|
93
|
+
* Set `false` for rolling backends that emit many partial/final
|
|
94
|
+
* pairs within a single capture session (Deepgram-style streaming).
|
|
95
|
+
* The session then stays `listening` after each `emitFinal` and only
|
|
96
|
+
* closes on an explicit `stop()` / `abort()` or `emitError`.
|
|
97
|
+
*/
|
|
98
|
+
closeOnFinal?: boolean;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
/**
|
|
@@ -124,6 +135,7 @@ export function createExternalEngine(
|
|
|
124
135
|
opts: ExternalEngineOptions,
|
|
125
136
|
): RecognitionEngine {
|
|
126
137
|
const bus = createEngineBus();
|
|
138
|
+
const closeOnFinal = opts.closeOnFinal ?? true;
|
|
127
139
|
let currentSegmentId: string | null = null;
|
|
128
140
|
let unsubscribe: Unsub | null = null;
|
|
129
141
|
let running = false;
|
|
@@ -145,11 +157,16 @@ export function createExternalEngine(
|
|
|
145
157
|
if (!running) return;
|
|
146
158
|
const id = currentSegmentId ?? newSegmentId();
|
|
147
159
|
bus.emit('final', text, id, confidence);
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
// Single-shot engines (default) go idle right after their final —
|
|
161
|
+
// close the session so consumers' `onStop` fires without a
|
|
162
|
+
// separate `stop()` call. Rolling engines keep the session open
|
|
163
|
+
// and just reset the segment so the next partial starts fresh.
|
|
164
|
+
if (closeOnFinal) {
|
|
165
|
+
bus.emit('state', 'closed');
|
|
166
|
+
teardown();
|
|
167
|
+
} else {
|
|
168
|
+
currentSegmentId = null;
|
|
169
|
+
}
|
|
153
170
|
},
|
|
154
171
|
emitError(err: RecognitionError): void {
|
|
155
172
|
bus.emit('error', err);
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* STT services expect.
|
|
6
6
|
*
|
|
7
7
|
* We keep a small built-in table for the locales we ship translations
|
|
8
|
-
* for;
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* for; codes outside it are looked up in the full Web Speech catalogue
|
|
9
|
+
* (so `uk` → `uk-UA`, not the invalid `uk-UK`), and only fall through
|
|
10
|
+
* to `<code>-<UPPER(code)>` as a last resort. The mapping is also
|
|
11
|
+
* re-exported so consumers can extend it.
|
|
11
12
|
*/
|
|
12
13
|
|
|
14
|
+
import { WEB_SPEECH_LANGUAGES } from './languages-catalog';
|
|
15
|
+
|
|
13
16
|
const ISO_TO_BCP47: Record<string, string> = {
|
|
14
17
|
en: 'en-US',
|
|
15
18
|
ru: 'ru-RU',
|
|
@@ -32,11 +35,23 @@ const ISO_TO_BCP47: Record<string, string> = {
|
|
|
32
35
|
|
|
33
36
|
export const DEFAULT_ISO_TO_BCP47 = ISO_TO_BCP47;
|
|
34
37
|
|
|
38
|
+
/** ISO-639 primary subtag → default catalogue dialect (e.g. `uk` → `uk-UA`). */
|
|
39
|
+
const CATALOG_ISO_TO_TAG: Record<string, string> = (() => {
|
|
40
|
+
const map: Record<string, string> = {};
|
|
41
|
+
for (const lang of WEB_SPEECH_LANGUAGES) {
|
|
42
|
+
const first = lang.dialects[0];
|
|
43
|
+
if (first) map[lang.iso.toLowerCase()] = first.code;
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
})();
|
|
47
|
+
|
|
35
48
|
/**
|
|
36
49
|
* Normalise any of:
|
|
37
50
|
* - BCP-47 ("en-US", "ru-RU") — passed through.
|
|
38
|
-
* - ISO 639-1 ("en", "ru") — mapped via the table
|
|
39
|
-
*
|
|
51
|
+
* - ISO 639-1 ("en", "ru") — mapped via the built-in table, then
|
|
52
|
+
* the full Web Speech catalogue, then
|
|
53
|
+
* `<code>-<UPPER(code)>` as a last
|
|
54
|
+
* resort.
|
|
40
55
|
* - `null`/`undefined`/empty — returns `undefined`.
|
|
41
56
|
*/
|
|
42
57
|
export function toBCP47(
|
|
@@ -48,7 +63,9 @@ export function toBCP47(
|
|
|
48
63
|
if (!trimmed) return undefined;
|
|
49
64
|
if (trimmed.includes('-')) return trimmed; // already BCP-47
|
|
50
65
|
const lower = trimmed.toLowerCase();
|
|
51
|
-
return
|
|
66
|
+
return (
|
|
67
|
+
table[lower] ?? CATALOG_ISO_TO_TAG[lower] ?? `${lower}-${lower.toUpperCase()}`
|
|
68
|
+
);
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
/**
|
|
@@ -13,17 +13,27 @@ export interface UsePushToTalkOptions {
|
|
|
13
13
|
|
|
14
14
|
const MOD_KEYS = new Set(['shift', 'ctrl', 'alt', 'meta', 'mod']);
|
|
15
15
|
|
|
16
|
+
/** `mod` resolves to ⌘ on Apple platforms, Ctrl elsewhere. */
|
|
17
|
+
function isApplePlatform(): boolean {
|
|
18
|
+
if (typeof navigator === 'undefined') return false;
|
|
19
|
+
const p =
|
|
20
|
+
(navigator as { userAgentData?: { platform?: string } }).userAgentData
|
|
21
|
+
?.platform ?? navigator.platform;
|
|
22
|
+
return /mac|iphone|ipad|ipod/i.test(p ?? '');
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
function parseChord(chord: string): { mods: Set<string>; main: string | null } {
|
|
17
26
|
const parts = chord
|
|
18
27
|
.toLowerCase()
|
|
19
28
|
.split('+')
|
|
20
29
|
.map((s) => s.trim());
|
|
30
|
+
const modAlias = isApplePlatform() ? 'meta' : 'ctrl';
|
|
21
31
|
const mods = new Set<string>();
|
|
22
32
|
let main: string | null = null;
|
|
23
33
|
for (const part of parts) {
|
|
24
34
|
if (MOD_KEYS.has(part)) {
|
|
25
|
-
mods.add(part === 'mod' ?
|
|
26
|
-
} else {
|
|
35
|
+
mods.add(part === 'mod' ? modAlias : part);
|
|
36
|
+
} else if (part) {
|
|
27
37
|
main = part;
|
|
28
38
|
}
|
|
29
39
|
}
|
|
@@ -34,12 +44,33 @@ function matches(e: KeyboardEvent, mods: Set<string>, main: string | null): bool
|
|
|
34
44
|
if (mods.has('shift') !== e.shiftKey) return false;
|
|
35
45
|
if (mods.has('ctrl') !== e.ctrlKey) return false;
|
|
36
46
|
if (mods.has('alt') !== e.altKey) return false;
|
|
37
|
-
|
|
38
|
-
if (mods.has('meta') !== (e.metaKey || (!e.metaKey && false))) return false;
|
|
47
|
+
if (mods.has('meta') !== e.metaKey) return false;
|
|
39
48
|
if (main && e.key.toLowerCase() !== main) return false;
|
|
40
49
|
return true;
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Key-up matcher — modifier state is unreliable on `keyup` (releasing
|
|
54
|
+
* the modifier itself drops the flag before the main key's keyup), so
|
|
55
|
+
* we only check the main key here. Pure-modifier chords match when the
|
|
56
|
+
* released key is one of the chord's modifiers.
|
|
57
|
+
*/
|
|
58
|
+
function matchesRelease(
|
|
59
|
+
e: KeyboardEvent,
|
|
60
|
+
mods: Set<string>,
|
|
61
|
+
main: string | null,
|
|
62
|
+
): boolean {
|
|
63
|
+
const key = e.key.toLowerCase();
|
|
64
|
+
if (main) return key === main;
|
|
65
|
+
// Pure-modifier chord: stop when any chord modifier is released.
|
|
66
|
+
return (
|
|
67
|
+
(mods.has('shift') && key === 'shift') ||
|
|
68
|
+
(mods.has('ctrl') && key === 'control') ||
|
|
69
|
+
(mods.has('alt') && key === 'alt') ||
|
|
70
|
+
(mods.has('meta') && key === 'meta')
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
/**
|
|
44
75
|
* Hold-to-talk wiring. Press → `start()`, release → `stop()`. Ignores
|
|
45
76
|
* repeats and skips keydown inside `<input>` / `<textarea>` unless a
|
|
@@ -70,7 +101,7 @@ export function usePushToTalk(
|
|
|
70
101
|
void recognition.start();
|
|
71
102
|
};
|
|
72
103
|
const onUp = (e: KeyboardEvent): void => {
|
|
73
|
-
if (!
|
|
104
|
+
if (!matchesRelease(e, mods, main)) return;
|
|
74
105
|
if (recognition.status !== 'listening' && recognition.status !== 'starting') return;
|
|
75
106
|
void recognition.stop();
|
|
76
107
|
};
|
|
@@ -98,12 +98,22 @@ export function useSpeechRecognition(
|
|
|
98
98
|
];
|
|
99
99
|
return () => {
|
|
100
100
|
offs.forEach((off) => off());
|
|
101
|
+
// Release the mic / socket if the consumer unmounts (or swaps
|
|
102
|
+
// the engine) mid-session. Without this the MediaRecorder and
|
|
103
|
+
// getUserMedia stream keep running headless after unmount.
|
|
104
|
+
engine.abort();
|
|
101
105
|
};
|
|
102
106
|
}, [engine]);
|
|
103
107
|
|
|
104
|
-
// AutoStop driven by silence + maxMs caps.
|
|
108
|
+
// AutoStop driven by silence + maxMs caps. `level` updates ~60fps,
|
|
109
|
+
// so it must NOT live in the effect deps — otherwise the silence
|
|
110
|
+
// interval is torn down and recreated every animation frame, which
|
|
111
|
+
// both wastes CPU and resets the silence timer before it can fire.
|
|
112
|
+
// The interval reads the freshest level through a ref instead.
|
|
105
113
|
const silenceTimer = useRef<number | null>(null);
|
|
106
114
|
const maxTimer = useRef<number | null>(null);
|
|
115
|
+
const levelRef = useRef(level);
|
|
116
|
+
levelRef.current = level;
|
|
107
117
|
useEffect(() => {
|
|
108
118
|
if (state.status !== 'listening') return undefined;
|
|
109
119
|
const { silenceMs, maxMs, silenceThreshold = 0.02 } = config.autoStop ?? {};
|
|
@@ -113,9 +123,10 @@ export function useSpeechRecognition(
|
|
|
113
123
|
void engine.stop();
|
|
114
124
|
}, maxMs);
|
|
115
125
|
}
|
|
126
|
+
let checkInterval: number | null = null;
|
|
116
127
|
if (silenceMs) {
|
|
117
|
-
|
|
118
|
-
if (
|
|
128
|
+
checkInterval = window.setInterval(() => {
|
|
129
|
+
if (levelRef.current < silenceThreshold) {
|
|
119
130
|
if (silenceTimer.current == null) {
|
|
120
131
|
silenceTimer.current = window.setTimeout(() => {
|
|
121
132
|
log.engine.debug('autoStop silence detected');
|
|
@@ -127,19 +138,15 @@ export function useSpeechRecognition(
|
|
|
127
138
|
silenceTimer.current = null;
|
|
128
139
|
}
|
|
129
140
|
}, 200);
|
|
130
|
-
return () => {
|
|
131
|
-
clearInterval(checkInterval);
|
|
132
|
-
if (silenceTimer.current != null) clearTimeout(silenceTimer.current);
|
|
133
|
-
silenceTimer.current = null;
|
|
134
|
-
if (maxTimer.current != null) clearTimeout(maxTimer.current);
|
|
135
|
-
maxTimer.current = null;
|
|
136
|
-
};
|
|
137
141
|
}
|
|
138
142
|
return () => {
|
|
143
|
+
if (checkInterval != null) clearInterval(checkInterval);
|
|
144
|
+
if (silenceTimer.current != null) clearTimeout(silenceTimer.current);
|
|
145
|
+
silenceTimer.current = null;
|
|
139
146
|
if (maxTimer.current != null) clearTimeout(maxTimer.current);
|
|
140
147
|
maxTimer.current = null;
|
|
141
148
|
};
|
|
142
|
-
}, [state.status, config.autoStop,
|
|
149
|
+
}, [state.status, config.autoStop, engine]);
|
|
143
150
|
|
|
144
151
|
const start = useCallback(async () => {
|
|
145
152
|
if (state.status === 'listening' || state.status === 'starting') return;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type * as React from 'react';
|
|
4
4
|
import { useCallback, useEffect, useRef } from 'react';
|
|
5
|
-
import { Loader2, Mic } from 'lucide-react';
|
|
5
|
+
import { AlertCircle, Loader2, Mic } from 'lucide-react';
|
|
6
6
|
|
|
7
7
|
import { useCountdownFromSeconds, useNotificationSounds } from '@djangocfg/ui-core/hooks';
|
|
8
8
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
@@ -13,8 +13,12 @@ import { useVoiceSupport } from '../hooks/useVoiceSupport';
|
|
|
13
13
|
import { getSpeechLogger } from '../core/logger';
|
|
14
14
|
import { normaliseFinal } from '../core/transcript';
|
|
15
15
|
import { DEFAULT_VOICE_SOUNDS, type VoiceSoundEvent } from '../core/audio/defaults';
|
|
16
|
+
import { RecordingPulse } from '../components/RecordingPulse';
|
|
16
17
|
import type { RecognitionEngine } from '../types';
|
|
17
18
|
|
|
19
|
+
/** High-level visual state the composer mic button reflects. */
|
|
20
|
+
export type VoiceSlotState = 'idle' | 'listening' | 'processing' | 'error';
|
|
21
|
+
|
|
18
22
|
const log = getSpeechLogger();
|
|
19
23
|
|
|
20
24
|
export interface VoiceComposerSlotProps {
|
|
@@ -51,18 +55,26 @@ export interface VoiceComposerSlotProps {
|
|
|
51
55
|
* — users can silence everything via their own UI.
|
|
52
56
|
*/
|
|
53
57
|
sounds?: boolean | { start?: string; stop?: string };
|
|
58
|
+
/** Notified whenever the high-level visual state changes. */
|
|
59
|
+
onStateChange?: (state: VoiceSlotState) => void;
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Per-size geometry. Mirrors the chat composer action-bar buttons
|
|
64
|
+
* (`composer-kit/ComposerButton` `BUTTON_SIZE`) so the slot drops into
|
|
65
|
+
* `composerSlots.inlineEnd` round and correctly sized with no host CSS.
|
|
66
|
+
*/
|
|
56
67
|
const SIZE_CLS: Record<NonNullable<VoiceComposerSlotProps['size']>, string> = {
|
|
57
|
-
sm: 'h-
|
|
58
|
-
md: 'h-9 w-9 [&_svg]:
|
|
59
|
-
lg: 'h-
|
|
68
|
+
sm: 'h-7 w-7 [&_svg]:size-4',
|
|
69
|
+
md: 'h-9 w-9 [&_svg]:size-[1.125rem]',
|
|
70
|
+
lg: 'h-11 w-11 [&_svg]:size-5',
|
|
60
71
|
};
|
|
61
72
|
|
|
62
73
|
const STORAGE_KEY = 'djangocfg-stt:voice-sounds';
|
|
63
74
|
|
|
64
75
|
/**
|
|
65
|
-
* Drop-in slot for
|
|
76
|
+
* Drop-in microphone slot for `<Composer>` — pass it as a raw node via
|
|
77
|
+
* `composerSlots.inlineEnd` (inline layout) or `composerSlots.blockStart`.
|
|
66
78
|
*
|
|
67
79
|
* Renders a microphone button — but only when the browser + device
|
|
68
80
|
* combination can actually do speech recognition. Firefox, in-app
|
|
@@ -89,6 +101,7 @@ export function VoiceComposerSlot({
|
|
|
89
101
|
className,
|
|
90
102
|
onFinish,
|
|
91
103
|
sounds = true,
|
|
104
|
+
onStateChange,
|
|
92
105
|
}: VoiceComposerSlotProps): React.ReactElement | null {
|
|
93
106
|
const support = useVoiceSupport(engine);
|
|
94
107
|
|
|
@@ -113,7 +126,7 @@ export function VoiceComposerSlot({
|
|
|
113
126
|
|
|
114
127
|
// Resolve value/onChange: prop wins; otherwise pull from the
|
|
115
128
|
// registered composer handle. The slot can therefore be dropped into
|
|
116
|
-
// `
|
|
129
|
+
// `composerBlockStart` of `ChatRoot` with zero props.
|
|
117
130
|
const resolvedGetValue = useCallback((): string => {
|
|
118
131
|
if (value !== undefined) return value;
|
|
119
132
|
return composerHandleRef.current?.getValue?.() ?? '';
|
|
@@ -289,51 +302,106 @@ export function VoiceComposerSlot({
|
|
|
289
302
|
void rec.start();
|
|
290
303
|
}, [rec, resolvedGetValue]);
|
|
291
304
|
|
|
305
|
+
// Derive the high-level visual state from the engine status. The
|
|
306
|
+
// engine owns the truth — `processing` is the post-speech tail while
|
|
307
|
+
// the final result settles (`stopping`), `error` is a recoverable
|
|
308
|
+
// failure that resets to `idle` on the next press.
|
|
309
|
+
const stopping = rec.status === 'stopping';
|
|
310
|
+
const slotState: VoiceSlotState = listening
|
|
311
|
+
? 'listening'
|
|
312
|
+
: stopping
|
|
313
|
+
? 'processing'
|
|
314
|
+
: rec.status === 'error'
|
|
315
|
+
? 'error'
|
|
316
|
+
: 'idle';
|
|
317
|
+
|
|
318
|
+
// Notify the host on every transition (after render, ref-guarded so
|
|
319
|
+
// identical states never fire twice).
|
|
320
|
+
const lastStateRef = useRef<VoiceSlotState | null>(null);
|
|
321
|
+
const onStateChangeRef = useRef(onStateChange);
|
|
322
|
+
onStateChangeRef.current = onStateChange;
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (lastStateRef.current === slotState) return;
|
|
325
|
+
lastStateRef.current = slotState;
|
|
326
|
+
onStateChangeRef.current?.(slotState);
|
|
327
|
+
}, [slotState]);
|
|
328
|
+
|
|
292
329
|
if (!support.supported) return null;
|
|
293
330
|
if (hideOnMobile && support.isMobile) return null;
|
|
294
331
|
|
|
295
|
-
|
|
332
|
+
// Tooltip: countdown + hotkey hint while listening, error copy on
|
|
333
|
+
// failure, plain prompt otherwise.
|
|
334
|
+
const tooltip =
|
|
335
|
+
slotState === 'listening'
|
|
336
|
+
? `Listening — ${countdown.label || `${maxSeconds}s left`} · Enter to finish · Esc to cancel`
|
|
337
|
+
: slotState === 'processing'
|
|
338
|
+
? 'Transcribing…'
|
|
339
|
+
: slotState === 'error'
|
|
340
|
+
? rec.error?.message || 'Dictation failed — tap to retry'
|
|
341
|
+
: 'Dictate message';
|
|
296
342
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
343
|
+
const ariaLabel =
|
|
344
|
+
slotState === 'listening'
|
|
345
|
+
? 'Stop dictation'
|
|
346
|
+
: slotState === 'error'
|
|
347
|
+
? 'Dictation failed — retry'
|
|
348
|
+
: 'Dictate message';
|
|
303
349
|
|
|
304
350
|
return (
|
|
305
351
|
<span className="inline-flex items-center gap-1.5 !h-auto">
|
|
306
|
-
{listening && countdown.label ? (
|
|
352
|
+
{slotState === 'listening' && countdown.label ? (
|
|
307
353
|
<span
|
|
308
354
|
aria-hidden
|
|
309
|
-
className="rounded-full bg-destructive/10 px-1.5 py-0.5 font-mono text-[10px] leading-none text-destructive tabular-nums"
|
|
355
|
+
className="rounded-full bg-destructive/10 px-1.5 py-0.5 font-mono text-[10px] leading-none text-destructive tabular-nums animate-in fade-in duration-200"
|
|
310
356
|
>
|
|
311
357
|
{countdown.label}
|
|
312
358
|
</span>
|
|
313
359
|
) : null}
|
|
360
|
+
{slotState === 'error' ? (
|
|
361
|
+
<span
|
|
362
|
+
aria-hidden
|
|
363
|
+
className="rounded-full bg-destructive/10 px-1.5 py-0.5 text-[10px] leading-none text-destructive animate-in fade-in duration-200"
|
|
364
|
+
>
|
|
365
|
+
Failed
|
|
366
|
+
</span>
|
|
367
|
+
) : null}
|
|
314
368
|
<button
|
|
315
369
|
type="button"
|
|
316
370
|
onClick={toggle}
|
|
317
|
-
aria-pressed={listening}
|
|
318
|
-
aria-label={
|
|
371
|
+
aria-pressed={slotState === 'listening'}
|
|
372
|
+
aria-label={ariaLabel}
|
|
319
373
|
title={tooltip}
|
|
374
|
+
data-state={slotState}
|
|
320
375
|
className={cn(
|
|
321
|
-
'relative inline-flex items-center justify-center rounded-full transition-
|
|
376
|
+
'relative inline-flex items-center justify-center rounded-full transition-all duration-200',
|
|
322
377
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
323
378
|
SIZE_CLS[size],
|
|
324
|
-
listening
|
|
325
|
-
|
|
326
|
-
|
|
379
|
+
slotState === 'listening' &&
|
|
380
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
381
|
+
slotState === 'processing' &&
|
|
382
|
+
'bg-primary/10 text-primary hover:bg-primary/15',
|
|
383
|
+
slotState === 'error' &&
|
|
384
|
+
'bg-destructive/10 text-destructive hover:bg-destructive/15',
|
|
385
|
+
slotState === 'idle' &&
|
|
386
|
+
'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
327
387
|
className,
|
|
328
388
|
)}
|
|
329
389
|
>
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
390
|
+
{/* Recording feedback — pulsing circle overlay driven by the
|
|
391
|
+
live mic level. Hidden in every non-listening state. */}
|
|
392
|
+
<RecordingPulse active={slotState === 'listening'} level={rec.level} />
|
|
393
|
+
{slotState === 'processing' ? (
|
|
394
|
+
<Loader2 className="animate-spin" />
|
|
395
|
+
) : slotState === 'error' ? (
|
|
396
|
+
<AlertCircle />
|
|
397
|
+
) : (
|
|
398
|
+
<Mic
|
|
399
|
+
className={cn(
|
|
400
|
+
'transition-transform duration-200',
|
|
401
|
+
slotState === 'listening' && 'scale-110',
|
|
402
|
+
)}
|
|
334
403
|
/>
|
|
335
404
|
)}
|
|
336
|
-
{stopping ? <Loader2 className="animate-spin" /> : <Mic />}
|
|
337
405
|
</button>
|
|
338
406
|
</span>
|
|
339
407
|
);
|
|
@@ -3,4 +3,4 @@ export type { DictationFieldProps } from './DictationField';
|
|
|
3
3
|
export { VoiceMessageRecorder } from './VoiceMessageRecorder';
|
|
4
4
|
export type { VoiceMessageRecorderProps } from './VoiceMessageRecorder';
|
|
5
5
|
export { VoiceComposerSlot } from './VoiceComposerSlot';
|
|
6
|
-
export type { VoiceComposerSlotProps } from './VoiceComposerSlot';
|
|
6
|
+
export type { VoiceComposerSlotProps, VoiceSlotState } from './VoiceComposerSlot';
|
package/src/tools/Tree/README.md
CHANGED
|
@@ -275,11 +275,7 @@ The active mode is also exposed on each row as
|
|
|
275
275
|
## VSCode-style file icons
|
|
276
276
|
|
|
277
277
|
Tree is generic over `T` — it has no opinion on whether nodes are files. For a
|
|
278
|
-
ready-made VSCode-style icon set,
|
|
279
|
-
|
|
280
|
-
```bash
|
|
281
|
-
pnpm add material-file-icons
|
|
282
|
-
```
|
|
278
|
+
ready-made VSCode-style icon set, use the `file-icon` companion subpath:
|
|
283
279
|
|
|
284
280
|
```tsx
|
|
285
281
|
import { TreeRoot } from '@djangocfg/ui-tools/tree';
|
|
@@ -292,9 +288,9 @@ import { createFileIconSlot } from '@djangocfg/ui-tools/file-icon';
|
|
|
292
288
|
/>
|
|
293
289
|
```
|
|
294
290
|
|
|
295
|
-
`material-file-icons`
|
|
296
|
-
|
|
297
|
-
|
|
291
|
+
The icon SVGs are vendored statically from `material-file-icons` (MIT) — no
|
|
292
|
+
runtime dependency, no install step, and resolution is synchronous in any
|
|
293
|
+
bundler.
|
|
298
294
|
|
|
299
295
|
Folders use a small built-in mapping (`src` → `FolderCode`,
|
|
300
296
|
`node_modules` → `Package`, `.git` → `FolderGit2`, `dist`/`build`/`.next`
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useRef } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
4
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
5
|
|
|
6
6
|
import { TreeProvider, useTreeContext } from './context/TreeContext';
|
|
7
|
-
import { TreeContent } from './components/TreeContent';
|
|
7
|
+
import { TreeContent, treeRowDomId } from './components/TreeContent';
|
|
8
8
|
import { TreeSearchInput } from './components/TreeSearchInput';
|
|
9
9
|
import { appearanceToStyle } from './data/appearance';
|
|
10
10
|
import { useTreeKeyboard } from './hooks/useTreeKeyboard';
|
|
@@ -125,15 +125,22 @@ function TreeRootShell<T>({
|
|
|
125
125
|
[keyboardRef],
|
|
126
126
|
);
|
|
127
127
|
|
|
128
|
-
//
|
|
128
|
+
// Keep the focused row scrolled into view whenever focus moves (keyboard
|
|
129
|
+
// nav, type-ahead, programmatic). Centralised so every focus source gets
|
|
130
|
+
// consistent scrolling — previously only type-ahead scrolled.
|
|
131
|
+
const focusedId = ctx.focused;
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!focusedId) return;
|
|
134
|
+
const el = containerRef.current?.querySelector<HTMLElement>(
|
|
135
|
+
`[data-tree-row][data-id="${CSS.escape(focusedId)}"]`,
|
|
136
|
+
);
|
|
137
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
138
|
+
}, [focusedId]);
|
|
139
|
+
|
|
140
|
+
// Type-ahead jump — focus update; scrolling handled by the effect above.
|
|
129
141
|
const onTypeAheadMatch = useCallback(
|
|
130
142
|
(id: string) => {
|
|
131
143
|
ctx.setFocus(id);
|
|
132
|
-
// Scroll the row into view if it has rendered.
|
|
133
|
-
const el = containerRef.current?.querySelector<HTMLElement>(
|
|
134
|
-
`[data-tree-row][data-id="${CSS.escape(id)}"]`,
|
|
135
|
-
);
|
|
136
|
-
el?.scrollIntoView({ block: 'nearest' });
|
|
137
144
|
},
|
|
138
145
|
[ctx],
|
|
139
146
|
);
|
|
@@ -150,8 +157,13 @@ function TreeRootShell<T>({
|
|
|
150
157
|
<div
|
|
151
158
|
ref={setContainerRef}
|
|
152
159
|
tabIndex={0}
|
|
160
|
+
role="tree"
|
|
161
|
+
aria-label={ctx.labels.ariaLabel}
|
|
162
|
+
aria-multiselectable={ctx.selectionMode === 'multiple' || undefined}
|
|
163
|
+
aria-activedescendant={focusedId ? treeRowDomId(focusedId) : undefined}
|
|
153
164
|
className={cn(
|
|
154
|
-
'group/tree flex h-full w-full flex-col gap-2 outline-none',
|
|
165
|
+
'group/tree flex h-full w-full flex-col gap-2 rounded-sm outline-none',
|
|
166
|
+
'focus-visible:ring-1 focus-visible:ring-ring/50',
|
|
155
167
|
className,
|
|
156
168
|
)}
|
|
157
169
|
style={{ ...appearanceToStyle(ctx.appearance), ...style }}
|
|
@@ -159,7 +171,7 @@ function TreeRootShell<T>({
|
|
|
159
171
|
>
|
|
160
172
|
{enableSearch ? <TreeSearchInput className="mx-2 mt-2" /> : null}
|
|
161
173
|
<div className="min-h-0 flex-1 overflow-auto px-1">
|
|
162
|
-
<TreeContent<T
|
|
174
|
+
<TreeContent<T> role="group">{renderRow}</TreeContent>
|
|
163
175
|
</div>
|
|
164
176
|
</div>
|
|
165
177
|
);
|