@vertesia/ui 1.5.0-dev.20260717.131047Z → 1.5.0-dev.20260722.120446Z
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/lib/core/components/SidePanel.js +1 -1
- package/lib/core/components/SidePanel.js.map +1 -1
- package/lib/core/components/shadcn/button.d.ts +10 -3
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +41 -9
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/modal/dialog.js +1 -1
- package/lib/core/components/shadcn/modal/dialog.js.map +1 -1
- package/lib/core/components/shadcn/theme/ThemeSwitcher.js +1 -1
- package/lib/core/components/shadcn/theme/ThemeSwitcher.js.map +1 -1
- package/lib/features/agent/chat/AgentChatPlaybackControls.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentChatPlaybackControls.js +2 -2
- package/lib/features/agent/chat/AgentChatPlaybackControls.js.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.js +2 -2
- package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
- package/lib/features/agent/chat/ArtifactsTab.d.ts.map +1 -1
- package/lib/features/agent/chat/ArtifactsTab.js +5 -2
- package/lib/features/agent/chat/ArtifactsTab.js.map +1 -1
- package/lib/features/agent/chat/ImageLightbox.d.ts.map +1 -1
- package/lib/features/agent/chat/ImageLightbox.js +1 -1
- package/lib/features/agent/chat/ImageLightbox.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +5 -5
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/Header.js +4 -2
- package/lib/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +2 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js +2 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageItem.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts +6 -0
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js +15 -1
- package/lib/features/agent/chat/ModernAgentOutput/requestInputMessages.js.map +1 -1
- package/lib/features/environment/SelectEnvironment.d.ts +28 -0
- package/lib/features/environment/SelectEnvironment.d.ts.map +1 -0
- package/lib/features/environment/SelectEnvironment.js +97 -0
- package/lib/features/environment/SelectEnvironment.js.map +1 -0
- package/lib/features/environment/index.d.ts +2 -0
- package/lib/features/environment/index.d.ts.map +1 -0
- package/lib/features/environment/index.js +2 -0
- package/lib/features/environment/index.js.map +1 -0
- package/lib/features/index.d.ts +2 -0
- package/lib/features/index.d.ts.map +1 -1
- package/lib/features/index.js +2 -0
- package/lib/features/index.js.map +1 -1
- package/lib/features/magic-pdf/AnnotatedImageSlider.js +12 -12
- package/lib/features/magic-pdf/AnnotatedImageSlider.js.map +1 -1
- package/lib/features/magic-pdf/DownloadPopover.d.ts.map +1 -1
- package/lib/features/magic-pdf/DownloadPopover.js +3 -17
- package/lib/features/magic-pdf/DownloadPopover.js.map +1 -1
- package/lib/features/magic-pdf/ExtractedContentView.js +2 -6
- package/lib/features/magic-pdf/ExtractedContentView.js.map +1 -1
- package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.d.ts +25 -0
- package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.d.ts.map +1 -0
- package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.js +42 -0
- package/lib/features/magic-pdf/GroundedExtractionAssistantPanel.js.map +1 -0
- package/lib/features/magic-pdf/GroundedExtractionView.d.ts +27 -0
- package/lib/features/magic-pdf/GroundedExtractionView.d.ts.map +1 -0
- package/lib/features/magic-pdf/GroundedExtractionView.js +377 -0
- package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -0
- package/lib/features/magic-pdf/MagicPdfProvider.d.ts +0 -2
- package/lib/features/magic-pdf/MagicPdfProvider.d.ts.map +1 -1
- package/lib/features/magic-pdf/MagicPdfProvider.js +15 -42
- package/lib/features/magic-pdf/MagicPdfProvider.js.map +1 -1
- package/lib/features/magic-pdf/MagicPdfView.d.ts.map +1 -1
- package/lib/features/magic-pdf/MagicPdfView.js +2 -19
- package/lib/features/magic-pdf/MagicPdfView.js.map +1 -1
- package/lib/features/magic-pdf/index.d.ts +1 -0
- package/lib/features/magic-pdf/index.d.ts.map +1 -1
- package/lib/features/magic-pdf/index.js +1 -0
- package/lib/features/magic-pdf/index.js.map +1 -1
- package/lib/features/magic-pdf/types.d.ts +1 -1
- package/lib/features/magic-pdf/types.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +2 -2
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/RemoteMcpConnectionButton.d.ts.map +1 -1
- package/lib/features/oauth/RemoteMcpConnectionButton.js +1 -1
- package/lib/features/oauth/RemoteMcpConnectionButton.js.map +1 -1
- package/lib/features/pdf-viewer/PdfPageSlider.js +2 -2
- package/lib/features/pdf-viewer/PdfPageSlider.js.map +1 -1
- package/lib/features/pdf-viewer/SimplePdfViewer.d.ts.map +1 -1
- package/lib/features/pdf-viewer/SimplePdfViewer.js +3 -3
- package/lib/features/pdf-viewer/SimplePdfViewer.js.map +1 -1
- package/lib/features/permissions/UserPermissionsProvider.js +1 -1
- package/lib/features/permissions/UserPermissionsProvider.js.map +1 -1
- package/lib/features/store/objects/DocumentQuickFilter.d.ts.map +1 -1
- package/lib/features/store/objects/DocumentQuickFilter.js +5 -5
- package/lib/features/store/objects/DocumentQuickFilter.js.map +1 -1
- package/lib/features/store/objects/DocumentSearchResults.d.ts.map +1 -1
- package/lib/features/store/objects/DocumentSearchResults.js +2 -2
- package/lib/features/store/objects/DocumentSearchResults.js.map +1 -1
- package/lib/features/store/objects/components/ContentDispositionButton.d.ts.map +1 -1
- package/lib/features/store/objects/components/ContentDispositionButton.js +1 -1
- package/lib/features/store/objects/components/ContentDispositionButton.js.map +1 -1
- package/lib/features/store/objects/components/ContentOverview.d.ts.map +1 -1
- package/lib/features/store/objects/components/ContentOverview.js +78 -21
- package/lib/features/store/objects/components/ContentOverview.js.map +1 -1
- package/lib/features/store/objects/components/DocumentIcon.js +1 -1
- package/lib/features/store/objects/components/VectorSearchWidget.js +1 -1
- package/lib/features/store/objects/components/VectorSearchWidget.js.map +1 -1
- package/lib/features/store/objects/components/useContentPanelHooks.d.ts +1 -2
- package/lib/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
- package/lib/features/store/objects/components/useContentPanelHooks.js +4 -8
- package/lib/features/store/objects/components/useContentPanelHooks.js.map +1 -1
- package/lib/features/store/objects/layout/renderers.d.ts.map +1 -1
- package/lib/features/store/objects/layout/renderers.js +2 -2
- package/lib/features/store/objects/layout/renderers.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.js +4 -4
- package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.d.ts +15 -0
- package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -0
- package/lib/features/store/types/IntakePolicyEditor.js +455 -0
- package/lib/features/store/types/IntakePolicyEditor.js.map +1 -0
- package/lib/features/store/types/IntakePolicyForm.d.ts +11 -0
- package/lib/features/store/types/IntakePolicyForm.d.ts.map +1 -0
- package/lib/features/store/types/IntakePolicyForm.js +91 -0
- package/lib/features/store/types/IntakePolicyForm.js.map +1 -0
- package/lib/features/store/types/SelectContentTypeModal.js +1 -1
- package/lib/features/store/types/SelectContentTypeModal.js.map +1 -1
- package/lib/features/store/types/index.d.ts +2 -0
- package/lib/features/store/types/index.d.ts.map +1 -1
- package/lib/features/store/types/index.js +2 -0
- package/lib/features/store/types/index.js.map +1 -1
- package/lib/features/store/types/intake-policy-editor.logic.d.ts +7 -0
- package/lib/features/store/types/intake-policy-editor.logic.d.ts.map +1 -0
- package/lib/features/store/types/intake-policy-editor.logic.js +60 -0
- package/lib/features/store/types/intake-policy-editor.logic.js.map +1 -0
- package/lib/features/store/types/typeCatalogCache.d.ts +5 -0
- package/lib/features/store/types/typeCatalogCache.d.ts.map +1 -0
- package/lib/features/store/types/typeCatalogCache.js +40 -0
- package/lib/features/store/types/typeCatalogCache.js.map +1 -0
- package/lib/features/user/UserInfo.js +7 -3
- package/lib/features/user/UserInfo.js.map +1 -1
- package/lib/features/views/ViewExperience.d.ts +20 -0
- package/lib/features/views/ViewExperience.d.ts.map +1 -0
- package/lib/features/views/ViewExperience.js +188 -0
- package/lib/features/views/ViewExperience.js.map +1 -0
- package/lib/features/views/ViewNavigation.d.ts +3 -0
- package/lib/features/views/ViewNavigation.d.ts.map +1 -0
- package/lib/features/views/ViewNavigation.js +36 -0
- package/lib/features/views/ViewNavigation.js.map +1 -0
- package/lib/features/views/ViewResults.d.ts +9 -0
- package/lib/features/views/ViewResults.d.ts.map +1 -0
- package/lib/features/views/ViewResults.js +229 -0
- package/lib/features/views/ViewResults.js.map +1 -0
- package/lib/features/views/ViewSearch.d.ts +3 -0
- package/lib/features/views/ViewSearch.d.ts.map +1 -0
- package/lib/features/views/ViewSearch.js +74 -0
- package/lib/features/views/ViewSearch.js.map +1 -0
- package/lib/features/views/index.d.ts +7 -0
- package/lib/features/views/index.d.ts.map +1 -0
- package/lib/features/views/index.js +7 -0
- package/lib/features/views/index.js.map +1 -0
- package/lib/features/views/types.d.ts +39 -0
- package/lib/features/views/types.d.ts.map +1 -0
- package/lib/features/views/types.js +2 -0
- package/lib/features/views/types.js.map +1 -0
- package/lib/features/views/viewState.d.ts +14 -0
- package/lib/features/views/viewState.d.ts.map +1 -0
- package/lib/features/views/viewState.js +117 -0
- package/lib/features/views/viewState.js.map +1 -0
- package/lib/i18n/locales/ar.json +16 -0
- package/lib/i18n/locales/de.json +12 -0
- package/lib/i18n/locales/en.json +245 -33
- package/lib/i18n/locales/es.json +13 -0
- package/lib/i18n/locales/fr.json +13 -0
- package/lib/i18n/locales/it.json +13 -0
- package/lib/i18n/locales/ja.json +11 -0
- package/lib/i18n/locales/ko.json +11 -0
- package/lib/i18n/locales/pt.json +13 -0
- package/lib/i18n/locales/ru.json +14 -0
- package/lib/i18n/locales/tr.json +12 -0
- package/lib/i18n/locales/zh-TW.json +11 -0
- package/lib/i18n/locales/zh.json +11 -0
- package/lib/router/HistoryNavigator.d.ts.map +1 -1
- package/lib/router/HistoryNavigator.js +23 -13
- package/lib/router/HistoryNavigator.js.map +1 -1
- package/lib/router/Nav.d.ts.map +1 -1
- package/lib/router/Nav.js +15 -6
- package/lib/router/Nav.js.map +1 -1
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +5 -3
- package/lib/session/UserSession.js.map +1 -1
- package/lib/vertesia-ui-core.js +4 -4
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +34 -27
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.js +15 -15
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +1 -1
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +7 -7
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/lib/widgets/form/Form.js +1 -1
- package/lib/widgets/form/Form.js.map +1 -1
- package/lib/widgets/json-view/JSONSwitcher.js +1 -1
- package/lib/widgets/json-view/JSONSwitcher.js.map +1 -1
- package/lib/widgets/json-view/JSONView.js +6 -6
- package/lib/widgets/json-view/JSONView.js.map +1 -1
- package/lib/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -1
- package/lib/widgets/monacoEditor/MonacoEditor.js +29 -0
- package/lib/widgets/monacoEditor/MonacoEditor.js.map +1 -1
- package/lib/widgets/schema-editor/ManagedSchema.d.ts +8 -0
- package/lib/widgets/schema-editor/ManagedSchema.d.ts.map +1 -1
- package/lib/widgets/schema-editor/ManagedSchema.js +16 -0
- package/lib/widgets/schema-editor/ManagedSchema.js.map +1 -1
- package/lib/widgets/schema-editor/editor/EditableSchemaProperty.d.ts +2 -0
- package/lib/widgets/schema-editor/editor/EditableSchemaProperty.d.ts.map +1 -1
- package/lib/widgets/schema-editor/editor/EditableSchemaProperty.js +1 -0
- package/lib/widgets/schema-editor/editor/EditableSchemaProperty.js.map +1 -1
- package/lib/widgets/schema-editor/editor/PropertyViewer.d.ts.map +1 -1
- package/lib/widgets/schema-editor/editor/PropertyViewer.js +4 -1
- package/lib/widgets/schema-editor/editor/PropertyViewer.js.map +1 -1
- package/lib/widgets/schema-editor/editor/SchemaEditor.d.ts.map +1 -1
- package/lib/widgets/schema-editor/editor/SchemaEditor.js +22 -11
- package/lib/widgets/schema-editor/editor/SchemaEditor.js.map +1 -1
- package/package.json +32 -32
- package/src/__tests__/a11y.test.tsx +73 -0
- package/src/core/components/SidePanel.tsx +1 -1
- package/src/core/components/shadcn/button.tsx +67 -12
- package/src/core/components/shadcn/modal/dialog.tsx +1 -1
- package/src/core/components/shadcn/theme/ThemeSwitcher.tsx +1 -1
- package/src/features/agent/chat/AgentChatPlaybackControls.tsx +0 -5
- package/src/features/agent/chat/AgentRequestInputOverlay.test.tsx +31 -0
- package/src/features/agent/chat/AgentRequestInputOverlay.tsx +6 -2
- package/src/features/agent/chat/ArtifactsTab.tsx +19 -9
- package/src/features/agent/chat/ImageLightbox.tsx +0 -1
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +48 -0
- package/src/features/agent/chat/ModernAgentConversation.tsx +0 -2
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.test.tsx +6 -1
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +21 -15
- package/src/features/agent/chat/ModernAgentOutput/Header.tsx +3 -3
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +0 -2
- package/src/features/agent/chat/ModernAgentOutput/MessageItem.tsx +4 -1
- package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.test.ts +80 -0
- package/src/features/agent/chat/ModernAgentOutput/requestInputMessages.ts +21 -1
- package/src/features/environment/SelectEnvironment.tsx +186 -0
- package/src/features/environment/index.ts +1 -0
- package/src/features/index.ts +2 -0
- package/src/features/magic-pdf/AnnotatedImageSlider.tsx +62 -66
- package/src/features/magic-pdf/DownloadPopover.tsx +5 -48
- package/src/features/magic-pdf/ExtractedContentView.tsx +2 -14
- package/src/features/magic-pdf/GroundedExtractionAssistantPanel.tsx +86 -0
- package/src/features/magic-pdf/GroundedExtractionView.test.tsx +65 -0
- package/src/features/magic-pdf/GroundedExtractionView.tsx +1055 -0
- package/src/features/magic-pdf/MagicPdfProvider.tsx +17 -47
- package/src/features/magic-pdf/MagicPdfView.tsx +3 -51
- package/src/features/magic-pdf/index.ts +1 -0
- package/src/features/magic-pdf/types.ts +1 -1
- package/src/features/oauth/McpConnectionsButton.tsx +0 -2
- package/src/features/oauth/McpConnectionsDialog.tsx +0 -1
- package/src/features/oauth/RemoteMcpConnectionButton.tsx +0 -1
- package/src/features/pdf-viewer/PdfPageSlider.tsx +5 -5
- package/src/features/pdf-viewer/SimplePdfViewer.tsx +11 -12
- package/src/features/permissions/UserPermissionsProvider.tsx +1 -1
- package/src/features/store/objects/DocumentQuickFilter.tsx +13 -15
- package/src/features/store/objects/DocumentSearchResults.tsx +3 -6
- package/src/features/store/objects/components/ContentDispositionButton.tsx +5 -1
- package/src/features/store/objects/components/ContentOverview.tsx +144 -44
- package/src/features/store/objects/components/DocumentIcon.tsx +1 -1
- package/src/features/store/objects/components/VectorSearchWidget.tsx +2 -2
- package/src/features/store/objects/components/useContentPanelHooks.ts +3 -15
- package/src/features/store/objects/layout/renderers.tsx +1 -2
- package/src/features/store/objects/selection/SelectionActions.tsx +1 -1
- package/src/features/store/types/ContentObjectTypesTable.tsx +12 -14
- package/src/features/store/types/IntakePolicyEditor.tsx +679 -0
- package/src/features/store/types/IntakePolicyForm.tsx +838 -0
- package/src/features/store/types/SelectContentTypeModal.tsx +2 -2
- package/src/features/store/types/index.ts +2 -0
- package/src/features/store/types/intake-policy-editor.logic.test.ts +109 -0
- package/src/features/store/types/intake-policy-editor.logic.ts +87 -0
- package/src/features/store/types/typeCatalogCache.ts +45 -0
- package/src/features/user/UserInfo.tsx +7 -7
- package/src/features/views/ViewExperience.test.tsx +273 -0
- package/src/features/views/ViewExperience.tsx +408 -0
- package/src/features/views/ViewNavigation.test.tsx +86 -0
- package/src/features/views/ViewNavigation.tsx +187 -0
- package/src/features/views/ViewResults.test.ts +82 -0
- package/src/features/views/ViewResults.tsx +609 -0
- package/src/features/views/ViewSearch.test.tsx +147 -0
- package/src/features/views/ViewSearch.tsx +205 -0
- package/src/features/views/index.ts +6 -0
- package/src/features/views/types.ts +57 -0
- package/src/features/views/viewState.test.ts +111 -0
- package/src/features/views/viewState.ts +132 -0
- package/src/i18n/locales/ar.json +16 -0
- package/src/i18n/locales/de.json +12 -0
- package/src/i18n/locales/en.json +965 -753
- package/src/i18n/locales/es.json +13 -0
- package/src/i18n/locales/fr.json +13 -0
- package/src/i18n/locales/it.json +13 -0
- package/src/i18n/locales/ja.json +11 -0
- package/src/i18n/locales/ko.json +11 -0
- package/src/i18n/locales/pt.json +13 -0
- package/src/i18n/locales/ru.json +14 -0
- package/src/i18n/locales/tr.json +12 -0
- package/src/i18n/locales/zh-TW.json +11 -0
- package/src/i18n/locales/zh.json +11 -0
- package/src/router/HistoryNavigator.test.ts +48 -0
- package/src/router/HistoryNavigator.ts +24 -13
- package/src/router/Nav.tsx +18 -8
- package/src/session/UserSession.test.ts +39 -0
- package/src/session/UserSession.ts +5 -3
- package/src/widgets/form/Form.tsx +1 -1
- package/src/widgets/json-view/JSONSwitcher.tsx +2 -2
- package/src/widgets/json-view/JSONView.tsx +7 -7
- package/src/widgets/monacoEditor/MonacoEditor.tsx +29 -0
- package/src/widgets/schema-editor/ManagedSchema.ts +22 -0
- package/src/widgets/schema-editor/editor/EditableSchemaProperty.tsx +3 -0
- package/src/widgets/schema-editor/editor/PropertyViewer.tsx +14 -3
- package/src/widgets/schema-editor/editor/SchemaEditor.tsx +80 -29
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Button,
|
|
4
|
+
cn,
|
|
5
|
+
ErrorBox,
|
|
6
|
+
errorMessage,
|
|
7
|
+
ResizableHandle,
|
|
8
|
+
ResizablePanel,
|
|
9
|
+
ResizablePanelGroup,
|
|
10
|
+
Spinner,
|
|
11
|
+
useFetch,
|
|
12
|
+
VTooltip,
|
|
13
|
+
} from '@vertesia/ui/core';
|
|
14
|
+
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
15
|
+
import { useUserSession } from '@vertesia/ui/session';
|
|
16
|
+
import {
|
|
17
|
+
CheckCircle2,
|
|
18
|
+
ChevronLeft,
|
|
19
|
+
ChevronRight,
|
|
20
|
+
Eye,
|
|
21
|
+
FileDown,
|
|
22
|
+
FileJson2,
|
|
23
|
+
FileText,
|
|
24
|
+
Grid3X3,
|
|
25
|
+
Image as ImageIcon,
|
|
26
|
+
Sparkles,
|
|
27
|
+
X,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
30
|
+
import { GroundedExtractionAssistantPanel } from './GroundedExtractionAssistantPanel';
|
|
31
|
+
|
|
32
|
+
const ADVANCED_PROCESSING_PREFIX = 'magic-pdf';
|
|
33
|
+
|
|
34
|
+
interface GroundedBox {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
w: number;
|
|
38
|
+
h: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Citation paths sometimes carry a leading "data." or "$." (the extraction
|
|
43
|
+
* schema nests the object under "data", and some models echo it). The server
|
|
44
|
+
* strips it for value resolution; the UI must match on the same normalized form
|
|
45
|
+
* so per-field scores, checkmarks and click-to-locate work for both variants.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeCitationPath(path: string): string {
|
|
48
|
+
return path.replace(/^\$\.?/, '').replace(/^data\./, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface GroundedCitation {
|
|
52
|
+
path: string;
|
|
53
|
+
page: number;
|
|
54
|
+
block_ids: number[];
|
|
55
|
+
cells?: { start: string; end: string };
|
|
56
|
+
verified: boolean;
|
|
57
|
+
source_text?: string;
|
|
58
|
+
value?: string;
|
|
59
|
+
boxes: GroundedBox[];
|
|
60
|
+
misaligned?: boolean;
|
|
61
|
+
snapped?: boolean;
|
|
62
|
+
reviewed?: boolean;
|
|
63
|
+
confidence?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GroundedExtractionFile {
|
|
67
|
+
version: number;
|
|
68
|
+
object_id: string;
|
|
69
|
+
run_id: string;
|
|
70
|
+
model?: string;
|
|
71
|
+
generated_at: string;
|
|
72
|
+
pages: Record<string, { width: number; height: number; rotation?: number }>;
|
|
73
|
+
data: Record<string, unknown>;
|
|
74
|
+
citations: GroundedCitation[];
|
|
75
|
+
/** Mean provenance confidence in [0,1]; 1.0 = verified against a digital text layer */
|
|
76
|
+
confidence?: number;
|
|
77
|
+
/** Content hardness: 0 = clean digital text, 1 = unreadable */
|
|
78
|
+
hardness?: { score: number; escalated?: boolean };
|
|
79
|
+
conflicts?: { path: string; kept: unknown; dropped: unknown }[];
|
|
80
|
+
/** Doc-level trust verdict: whether the content can be used without a human check */
|
|
81
|
+
verdict?: 'good_to_go' | 'needs_review';
|
|
82
|
+
/** One-sentence rationale for `verdict` */
|
|
83
|
+
verdict_reason?: string;
|
|
84
|
+
/** Post-extraction review result, when a review ran */
|
|
85
|
+
review?: {
|
|
86
|
+
assessment: 'complete' | 'issues_found';
|
|
87
|
+
summary?: string;
|
|
88
|
+
corrections_applied?: number;
|
|
89
|
+
verdict?: 'good_to_go' | 'needs_review';
|
|
90
|
+
verdict_reason?: string;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface GroundedExtractionViewProps {
|
|
95
|
+
objectId: string;
|
|
96
|
+
onClose?: () => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type PageImageMode = 'original' | 'grid';
|
|
100
|
+
|
|
101
|
+
/** True when a grounded extraction result exists for the object */
|
|
102
|
+
export function useGroundedExtractionAvailable(objectId: string): boolean {
|
|
103
|
+
const { client } = useUserSession();
|
|
104
|
+
const [available, setAvailable] = useState(false);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
setAvailable(false);
|
|
108
|
+
client.files
|
|
109
|
+
.getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/grounded-extraction.json`)
|
|
110
|
+
.then((r) => fetch(r.url, { method: 'HEAD' }))
|
|
111
|
+
.then((res) => {
|
|
112
|
+
if (!cancelled) setAvailable(res.ok);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => {
|
|
115
|
+
// no grounded extraction for this object
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
cancelled = true;
|
|
119
|
+
};
|
|
120
|
+
}, [client, objectId]);
|
|
121
|
+
return available;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface GroundedSummary {
|
|
125
|
+
confidence?: number;
|
|
126
|
+
verified: number;
|
|
127
|
+
total: number;
|
|
128
|
+
verdict?: 'good_to_go' | 'needs_review';
|
|
129
|
+
verdict_reason?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Lightweight summary of the object's grounded extraction, or null when none exists */
|
|
133
|
+
export function useGroundedSummary(objectId: string): GroundedSummary | null {
|
|
134
|
+
const { client } = useUserSession();
|
|
135
|
+
const [summary, setSummary] = useState<GroundedSummary | null>(null);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
let cancelled = false;
|
|
138
|
+
setSummary(null);
|
|
139
|
+
client.files
|
|
140
|
+
.getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/grounded-extraction.json`)
|
|
141
|
+
.then((r) => fetch(r.url))
|
|
142
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
143
|
+
.then((file: GroundedExtractionFile | null) => {
|
|
144
|
+
if (cancelled || !file?.citations) return;
|
|
145
|
+
setSummary({
|
|
146
|
+
confidence: file.confidence,
|
|
147
|
+
verified: file.citations.filter((c) => c.verified).length,
|
|
148
|
+
total: file.citations.length,
|
|
149
|
+
verdict: file.verdict,
|
|
150
|
+
verdict_reason: file.verdict_reason,
|
|
151
|
+
});
|
|
152
|
+
})
|
|
153
|
+
.catch(() => {
|
|
154
|
+
// no grounded extraction for this object
|
|
155
|
+
});
|
|
156
|
+
return () => {
|
|
157
|
+
cancelled = true;
|
|
158
|
+
};
|
|
159
|
+
}, [client, objectId]);
|
|
160
|
+
return summary;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Embeddable grounded-extraction review panel (fills its container): extracted
|
|
165
|
+
* properties on one side, source pages with citation boxes on the other.
|
|
166
|
+
* Clicking a property highlights where in the document it was read.
|
|
167
|
+
*/
|
|
168
|
+
export function GroundedExtractionPanel({ objectId, onClose }: GroundedExtractionViewProps) {
|
|
169
|
+
const { t } = useUITranslation();
|
|
170
|
+
const { client } = useUserSession();
|
|
171
|
+
|
|
172
|
+
const {
|
|
173
|
+
data: extraction,
|
|
174
|
+
error,
|
|
175
|
+
isLoading,
|
|
176
|
+
refetch,
|
|
177
|
+
} = useFetch<GroundedExtractionFile>(async () => {
|
|
178
|
+
const response = await client.files.getDownloadUrl(
|
|
179
|
+
`${ADVANCED_PROCESSING_PREFIX}/${objectId}/grounded-extraction.json`,
|
|
180
|
+
);
|
|
181
|
+
const result = await fetch(response.url);
|
|
182
|
+
if (!result.ok) {
|
|
183
|
+
throw new Error(t('grounded.notFound'));
|
|
184
|
+
}
|
|
185
|
+
return result.json();
|
|
186
|
+
}, [objectId]);
|
|
187
|
+
|
|
188
|
+
if (error) {
|
|
189
|
+
return (
|
|
190
|
+
<div className="flex h-full items-center justify-center">
|
|
191
|
+
<div className="flex flex-col items-center gap-4 p-8 max-w-md">
|
|
192
|
+
<ErrorBox title={t('grounded.failedToLoad')}>{errorMessage(error)}</ErrorBox>
|
|
193
|
+
{onClose && (
|
|
194
|
+
<Button variant="outline" onClick={onClose}>
|
|
195
|
+
{t('pdf.close')}
|
|
196
|
+
</Button>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (isLoading || !extraction) {
|
|
204
|
+
return (
|
|
205
|
+
<div className="flex h-full items-center justify-center">
|
|
206
|
+
<Spinner size="lg" />
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div className="relative h-full">
|
|
213
|
+
<GroundedExtractionViewImpl
|
|
214
|
+
extraction={extraction}
|
|
215
|
+
objectId={objectId}
|
|
216
|
+
onClose={onClose}
|
|
217
|
+
onExtractionChanged={refetch}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Full-screen route wrapper around GroundedExtractionPanel.
|
|
225
|
+
*/
|
|
226
|
+
export function GroundedExtractionView({ objectId, onClose }: GroundedExtractionViewProps) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="fixed inset-0 bg-background z-50">
|
|
229
|
+
<GroundedExtractionPanel objectId={objectId} onClose={onClose} />
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function GroundedExtractionViewImpl({
|
|
235
|
+
extraction,
|
|
236
|
+
objectId,
|
|
237
|
+
onClose,
|
|
238
|
+
onExtractionChanged,
|
|
239
|
+
}: {
|
|
240
|
+
extraction: GroundedExtractionFile;
|
|
241
|
+
objectId: string;
|
|
242
|
+
onClose?: () => void;
|
|
243
|
+
onExtractionChanged?: () => void;
|
|
244
|
+
}) {
|
|
245
|
+
const { t } = useUITranslation();
|
|
246
|
+
const { client } = useUserSession();
|
|
247
|
+
const pageNumbers = useMemo(
|
|
248
|
+
() =>
|
|
249
|
+
Object.keys(extraction.pages)
|
|
250
|
+
.map((p) => parseInt(p, 10))
|
|
251
|
+
.sort((a, b) => a - b),
|
|
252
|
+
[extraction.pages],
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const downloadArtifact = (path: string) => {
|
|
256
|
+
client.files
|
|
257
|
+
.getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/${path}`)
|
|
258
|
+
.then((r) => window.open(r.url, '_blank'))
|
|
259
|
+
.catch(() => {
|
|
260
|
+
// artifact absent for this document
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
const [page, setPage] = useState(pageNumbers[0] ?? 1);
|
|
264
|
+
const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined);
|
|
265
|
+
const [showBreakdown, setShowBreakdown] = useState(false);
|
|
266
|
+
const [showVerdict, setShowVerdict] = useState(false);
|
|
267
|
+
const [pageImageMode, setPageImageMode] = useState<PageImageMode>('original');
|
|
268
|
+
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
269
|
+
|
|
270
|
+
const breakdown = useMemo(() => {
|
|
271
|
+
const groups = { digital: 0, ocr: 0, reviewerConfirmed: 0, snapped: 0, imageRead: 0 };
|
|
272
|
+
const unverified: GroundedCitation[] = [];
|
|
273
|
+
for (const c of extraction.citations) {
|
|
274
|
+
if (c.verified) {
|
|
275
|
+
// Programmatic verification: the value matched the extracted text
|
|
276
|
+
// (digital text layer verbatim, or the OCR text).
|
|
277
|
+
if (c.snapped) groups.snapped++;
|
|
278
|
+
else if ((c.confidence ?? 0) >= 1) groups.digital++;
|
|
279
|
+
else groups.ocr++;
|
|
280
|
+
} else if (c.reviewed) {
|
|
281
|
+
// Model verification: the reviewer read the page image and confirmed
|
|
282
|
+
// the value. This is a strong signal — and the ONLY one available for
|
|
283
|
+
// scanned/handwritten content, where there is no text layer to match.
|
|
284
|
+
groups.reviewerConfirmed++;
|
|
285
|
+
} else {
|
|
286
|
+
// Read from the image but neither text-matched nor reviewer-confirmed.
|
|
287
|
+
groups.imageRead++;
|
|
288
|
+
unverified.push(c);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Two kinds of verification, both trustworthy:
|
|
292
|
+
// - digitally verified: matched the document's text (digital layer or OCR)
|
|
293
|
+
// - model verified: the reviewer confirmed it against the page image
|
|
294
|
+
const digitallyVerified = groups.digital + groups.ocr + groups.snapped;
|
|
295
|
+
const modelVerified = groups.reviewerConfirmed;
|
|
296
|
+
return {
|
|
297
|
+
groups,
|
|
298
|
+
unverified,
|
|
299
|
+
digitallyVerified,
|
|
300
|
+
modelVerified,
|
|
301
|
+
totalVerified: digitallyVerified + modelVerified,
|
|
302
|
+
};
|
|
303
|
+
}, [extraction.citations]);
|
|
304
|
+
|
|
305
|
+
const citationsByPath = useMemo(() => {
|
|
306
|
+
const map = new Map<string, GroundedCitation[]>();
|
|
307
|
+
for (const citation of extraction.citations) {
|
|
308
|
+
const key = normalizeCitationPath(citation.path);
|
|
309
|
+
const list = map.get(key) ?? [];
|
|
310
|
+
list.push(citation);
|
|
311
|
+
map.set(key, list);
|
|
312
|
+
}
|
|
313
|
+
return map;
|
|
314
|
+
}, [extraction.citations]);
|
|
315
|
+
|
|
316
|
+
const totalCitations = extraction.citations.length;
|
|
317
|
+
|
|
318
|
+
const selectPath = (path: string) => {
|
|
319
|
+
setSelectedPath(path);
|
|
320
|
+
const citation = citationsByPath.get(normalizeCitationPath(path))?.[0];
|
|
321
|
+
if (citation) {
|
|
322
|
+
setPage(citation.page);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const pageIndex = pageNumbers.indexOf(page);
|
|
327
|
+
const selectedCitation = selectedPath
|
|
328
|
+
? citationsByPath.get(normalizeCitationPath(selectedPath))?.find((citation) => citation.page === page)
|
|
329
|
+
: undefined;
|
|
330
|
+
const selectedCellRange = selectedCitation?.cells
|
|
331
|
+
? selectedCitation.cells.start === selectedCitation.cells.end
|
|
332
|
+
? selectedCitation.cells.start
|
|
333
|
+
: `${selectedCitation.cells.start}:${selectedCitation.cells.end}`
|
|
334
|
+
: undefined;
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<ResizablePanelGroup direction="horizontal" className="absolute inset-0">
|
|
338
|
+
<ResizablePanel defaultSize={55} minSize={25} className="flex flex-col bg-muted">
|
|
339
|
+
{/* Page navigation */}
|
|
340
|
+
<div className="relative flex h-9 shrink-0 items-center justify-center bg-sidebar px-2 border-b border-sidebar-border">
|
|
341
|
+
{selectedCellRange && (
|
|
342
|
+
<Badge
|
|
343
|
+
variant="secondary"
|
|
344
|
+
className="absolute start-2 max-w-32 truncate font-mono text-[11px]"
|
|
345
|
+
title={selectedCellRange}
|
|
346
|
+
>
|
|
347
|
+
{selectedCellRange}
|
|
348
|
+
</Badge>
|
|
349
|
+
)}
|
|
350
|
+
<div className="flex items-center gap-2">
|
|
351
|
+
<Button
|
|
352
|
+
variant="ghost"
|
|
353
|
+
size="xs"
|
|
354
|
+
aria-label={t('grounded.previousPage')}
|
|
355
|
+
disabled={pageIndex <= 0}
|
|
356
|
+
onClick={() => setPage(pageNumbers[pageIndex - 1])}
|
|
357
|
+
>
|
|
358
|
+
<ChevronLeft className="size-4" />
|
|
359
|
+
</Button>
|
|
360
|
+
<span className="text-xs text-muted-foreground">
|
|
361
|
+
{t('pdf.pageOf', { pageNumber: page, totalPages: pageNumbers.length })}
|
|
362
|
+
</span>
|
|
363
|
+
<Button
|
|
364
|
+
variant="ghost"
|
|
365
|
+
size="xs"
|
|
366
|
+
aria-label={t('grounded.nextPage')}
|
|
367
|
+
disabled={pageIndex >= pageNumbers.length - 1}
|
|
368
|
+
onClick={() => setPage(pageNumbers[pageIndex + 1])}
|
|
369
|
+
>
|
|
370
|
+
<ChevronRight className="size-4" />
|
|
371
|
+
</Button>
|
|
372
|
+
</div>
|
|
373
|
+
<fieldset className="absolute end-2 flex items-center gap-0.5 rounded border-0 bg-muted p-0.5">
|
|
374
|
+
<legend className="sr-only">{t('grounded.checkerboard')}</legend>
|
|
375
|
+
<VTooltip description={t('pdf.originalImages')} placement="bottom" size="xs" asChild>
|
|
376
|
+
<Button
|
|
377
|
+
variant={pageImageMode === 'original' ? 'outline' : 'ghost'}
|
|
378
|
+
size="xs"
|
|
379
|
+
aria-label={t('pdf.originalImages')}
|
|
380
|
+
aria-pressed={pageImageMode === 'original'}
|
|
381
|
+
className="h-6 gap-1 px-1.5"
|
|
382
|
+
onClick={() => setPageImageMode('original')}
|
|
383
|
+
>
|
|
384
|
+
<ImageIcon className="size-3.5" />
|
|
385
|
+
<span className="hidden sm:inline">{t('pdf.page')}</span>
|
|
386
|
+
</Button>
|
|
387
|
+
</VTooltip>
|
|
388
|
+
<VTooltip description={t('grounded.checkerboard')} placement="bottom" size="xs" asChild>
|
|
389
|
+
<Button
|
|
390
|
+
variant={pageImageMode === 'grid' ? 'outline' : 'ghost'}
|
|
391
|
+
size="xs"
|
|
392
|
+
aria-label={t('grounded.checkerboard')}
|
|
393
|
+
aria-pressed={pageImageMode === 'grid'}
|
|
394
|
+
className="h-6 gap-1 px-1.5"
|
|
395
|
+
onClick={() => setPageImageMode('grid')}
|
|
396
|
+
>
|
|
397
|
+
<Grid3X3 className="size-3.5" />
|
|
398
|
+
<span className="hidden sm:inline">{t('grounded.checkerboard')}</span>
|
|
399
|
+
</Button>
|
|
400
|
+
</VTooltip>
|
|
401
|
+
</fieldset>
|
|
402
|
+
</div>
|
|
403
|
+
<div className="flex-1 overflow-auto p-2">
|
|
404
|
+
<PageWithOverlay
|
|
405
|
+
objectId={objectId}
|
|
406
|
+
page={page}
|
|
407
|
+
dims={extraction.pages[String(page)]}
|
|
408
|
+
citations={extraction.citations.filter((c) => c.page === page)}
|
|
409
|
+
selectedPath={selectedPath}
|
|
410
|
+
onSelectPath={setSelectedPath}
|
|
411
|
+
imageMode={pageImageMode}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
</ResizablePanel>
|
|
415
|
+
<ResizableHandle className="w-[4px] bg-border cursor-ew-resize" />
|
|
416
|
+
<ResizablePanel defaultSize={45} minSize={20} className="flex flex-col">
|
|
417
|
+
{/* Header */}
|
|
418
|
+
<div className="flex h-9 items-center justify-between shrink-0 bg-sidebar px-2 border-b border-sidebar-border">
|
|
419
|
+
<span className="text-sm font-medium">{t('grounded.title')}</span>
|
|
420
|
+
<div className="flex items-center gap-2">
|
|
421
|
+
<VTooltip description={t('grounded.assistantTooltip')} placement="bottom" size="xs" asChild>
|
|
422
|
+
<Button
|
|
423
|
+
variant={assistantOpen ? 'primary' : 'ghost'}
|
|
424
|
+
size="xs"
|
|
425
|
+
aria-label={t('grounded.assistant')}
|
|
426
|
+
onClick={() => setAssistantOpen((v) => !v)}
|
|
427
|
+
>
|
|
428
|
+
<Sparkles className="size-4" />
|
|
429
|
+
</Button>
|
|
430
|
+
</VTooltip>
|
|
431
|
+
{extraction.verdict && (
|
|
432
|
+
<button
|
|
433
|
+
type="button"
|
|
434
|
+
className="cursor-pointer"
|
|
435
|
+
onClick={() => setShowVerdict((v) => !v)}
|
|
436
|
+
aria-expanded={showVerdict}
|
|
437
|
+
aria-label={t('grounded.verdictDetails')}
|
|
438
|
+
>
|
|
439
|
+
<Badge variant={extraction.verdict === 'good_to_go' ? 'success' : 'attention'}>
|
|
440
|
+
{extraction.verdict === 'good_to_go'
|
|
441
|
+
? t('grounded.verdictGoodToGo')
|
|
442
|
+
: t('grounded.verdictNeedsReview')}
|
|
443
|
+
</Badge>
|
|
444
|
+
</button>
|
|
445
|
+
)}
|
|
446
|
+
{typeof extraction.hardness?.score === 'number' && (
|
|
447
|
+
<Badge
|
|
448
|
+
variant={
|
|
449
|
+
extraction.hardness.score >= 0.5
|
|
450
|
+
? 'attention'
|
|
451
|
+
: extraction.hardness.score >= 0.2
|
|
452
|
+
? 'info'
|
|
453
|
+
: 'secondary'
|
|
454
|
+
}
|
|
455
|
+
title={t('grounded.hardnessHint')}
|
|
456
|
+
>
|
|
457
|
+
{t('grounded.hardness', {
|
|
458
|
+
percent: Math.round(extraction.hardness.score * 100),
|
|
459
|
+
})}
|
|
460
|
+
{extraction.hardness.escalated ? ' ↑' : ''}
|
|
461
|
+
</Badge>
|
|
462
|
+
)}
|
|
463
|
+
{extraction.citations.length > 0 &&
|
|
464
|
+
(() => {
|
|
465
|
+
// Real verification: fraction of values actually confirmed —
|
|
466
|
+
// digitally (verbatim/OCR match) OR by the reviewer against the
|
|
467
|
+
// image. NOT mean confidence, which scores image-read values high
|
|
468
|
+
// even when nothing was verified (e.g. 98% "confidence" on 2/64).
|
|
469
|
+
const verifiedPct = Math.round(
|
|
470
|
+
(100 * breakdown.totalVerified) / extraction.citations.length,
|
|
471
|
+
);
|
|
472
|
+
return (
|
|
473
|
+
<Badge
|
|
474
|
+
variant={
|
|
475
|
+
verifiedPct >= 95
|
|
476
|
+
? 'success'
|
|
477
|
+
: verifiedPct >= 50
|
|
478
|
+
? 'attention'
|
|
479
|
+
: 'destructive'
|
|
480
|
+
}
|
|
481
|
+
title={t('grounded.confidenceHint')}
|
|
482
|
+
>
|
|
483
|
+
{t('grounded.confidence', { percent: verifiedPct })}
|
|
484
|
+
</Badge>
|
|
485
|
+
);
|
|
486
|
+
})()}
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
className="flex cursor-pointer items-center gap-1"
|
|
490
|
+
onClick={() => setShowBreakdown((v) => !v)}
|
|
491
|
+
aria-expanded={showBreakdown}
|
|
492
|
+
aria-label={t('grounded.breakdownTitle')}
|
|
493
|
+
>
|
|
494
|
+
{breakdown.digitallyVerified > 0 && (
|
|
495
|
+
<Badge variant="success" title={t('grounded.digitalVerifiedHint')}>
|
|
496
|
+
{t('grounded.digitalVerifiedOf', {
|
|
497
|
+
count: breakdown.digitallyVerified,
|
|
498
|
+
total: totalCitations,
|
|
499
|
+
})}
|
|
500
|
+
</Badge>
|
|
501
|
+
)}
|
|
502
|
+
{breakdown.modelVerified > 0 && (
|
|
503
|
+
<Badge variant="success" title={t('grounded.modelVerifiedHint')}>
|
|
504
|
+
{t('grounded.modelVerifiedOf', {
|
|
505
|
+
count: breakdown.modelVerified,
|
|
506
|
+
total: totalCitations,
|
|
507
|
+
})}
|
|
508
|
+
</Badge>
|
|
509
|
+
)}
|
|
510
|
+
{breakdown.groups.imageRead > 0 && (
|
|
511
|
+
<Badge variant="attention" title={t('grounded.unverifiedHint')}>
|
|
512
|
+
{t('grounded.unverifiedCount', { count: breakdown.groups.imageRead })}
|
|
513
|
+
</Badge>
|
|
514
|
+
)}
|
|
515
|
+
</button>
|
|
516
|
+
<Button
|
|
517
|
+
variant="ghost"
|
|
518
|
+
size="xs"
|
|
519
|
+
aria-label={t('grounded.downloadCitations')}
|
|
520
|
+
title={t('grounded.downloadCitations')}
|
|
521
|
+
onClick={() => downloadArtifact('grounded-extraction.json')}
|
|
522
|
+
>
|
|
523
|
+
<FileJson2 className="size-4" />
|
|
524
|
+
</Button>
|
|
525
|
+
<Button
|
|
526
|
+
variant="ghost"
|
|
527
|
+
size="xs"
|
|
528
|
+
aria-label={t('grounded.downloadBlocks')}
|
|
529
|
+
title={t('grounded.downloadBlocks')}
|
|
530
|
+
onClick={() => downloadArtifact(`pages/page-${page}.json`)}
|
|
531
|
+
>
|
|
532
|
+
<FileText className="size-4" />
|
|
533
|
+
</Button>
|
|
534
|
+
<Button
|
|
535
|
+
variant="ghost"
|
|
536
|
+
size="xs"
|
|
537
|
+
aria-label={t('grounded.downloadAnnotated')}
|
|
538
|
+
title={t('grounded.downloadAnnotated')}
|
|
539
|
+
onClick={() => downloadArtifact('grounded-annotated.pdf')}
|
|
540
|
+
>
|
|
541
|
+
<FileDown className="size-4" />
|
|
542
|
+
</Button>
|
|
543
|
+
{!!onClose && (
|
|
544
|
+
<Button variant="ghost" size="xs" onClick={onClose} aria-label={t('pdf.close')}>
|
|
545
|
+
<X className="size-4" />
|
|
546
|
+
</Button>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
{showVerdict && extraction.verdict && (
|
|
551
|
+
<div className="shrink-0 border-b border-border bg-background px-3 py-2 text-sm">
|
|
552
|
+
<div className="flex items-center gap-2">
|
|
553
|
+
<span
|
|
554
|
+
className={cn(
|
|
555
|
+
'font-medium',
|
|
556
|
+
extraction.verdict === 'good_to_go' ? 'text-success' : 'text-attention',
|
|
557
|
+
)}
|
|
558
|
+
>
|
|
559
|
+
{extraction.verdict === 'good_to_go'
|
|
560
|
+
? t('grounded.verdictGoodToGo')
|
|
561
|
+
: t('grounded.verdictNeedsReview')}
|
|
562
|
+
</span>
|
|
563
|
+
<span className="text-xs text-muted-foreground">
|
|
564
|
+
{extraction.review
|
|
565
|
+
? t('grounded.verdictByReviewer')
|
|
566
|
+
: t('grounded.verdictByConfidence')}
|
|
567
|
+
</span>
|
|
568
|
+
</div>
|
|
569
|
+
{(extraction.verdict_reason ?? extraction.review?.verdict_reason) && (
|
|
570
|
+
<p className="mt-1 text-xs">
|
|
571
|
+
{extraction.verdict_reason ?? extraction.review?.verdict_reason}
|
|
572
|
+
</p>
|
|
573
|
+
)}
|
|
574
|
+
{extraction.review?.summary && (
|
|
575
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
576
|
+
<span className="font-medium text-foreground">{t('grounded.reviewSummary')}: </span>
|
|
577
|
+
{extraction.review.summary}
|
|
578
|
+
</p>
|
|
579
|
+
)}
|
|
580
|
+
{typeof extraction.review?.corrections_applied === 'number' &&
|
|
581
|
+
extraction.review.corrections_applied > 0 && (
|
|
582
|
+
<p className="mt-1 text-xs text-info">
|
|
583
|
+
{t('grounded.reviewCorrections', {
|
|
584
|
+
count: extraction.review.corrections_applied,
|
|
585
|
+
})}
|
|
586
|
+
</p>
|
|
587
|
+
)}
|
|
588
|
+
</div>
|
|
589
|
+
)}
|
|
590
|
+
{showBreakdown && (
|
|
591
|
+
<div className="shrink-0 border-b border-border bg-background px-3 py-2 text-sm max-h-72 overflow-auto">
|
|
592
|
+
<div className="font-medium mb-1">{t('grounded.breakdownTitle')}</div>
|
|
593
|
+
<div className="grid grid-cols-2 gap-x-4 text-xs">
|
|
594
|
+
<span className="col-span-2 mt-1 font-medium text-success">
|
|
595
|
+
{t('grounded.digitalVerifiedOf', {
|
|
596
|
+
count: breakdown.digitallyVerified,
|
|
597
|
+
total: totalCitations,
|
|
598
|
+
})}
|
|
599
|
+
</span>
|
|
600
|
+
<span className="ps-2 text-muted-foreground">{t('grounded.breakdownDigital')}</span>
|
|
601
|
+
<span className="text-end text-success">{breakdown.groups.digital}</span>
|
|
602
|
+
<span className="ps-2 text-muted-foreground">{t('grounded.breakdownOcr')}</span>
|
|
603
|
+
<span className="text-end text-success">{breakdown.groups.ocr}</span>
|
|
604
|
+
<span className="ps-2 text-muted-foreground">{t('grounded.breakdownSnapped')}</span>
|
|
605
|
+
<span className="text-end text-success">{breakdown.groups.snapped}</span>
|
|
606
|
+
<span className="col-span-2 mt-2 font-medium text-success">
|
|
607
|
+
{t('grounded.modelVerifiedOf', {
|
|
608
|
+
count: breakdown.modelVerified,
|
|
609
|
+
total: totalCitations,
|
|
610
|
+
})}
|
|
611
|
+
</span>
|
|
612
|
+
<span className="ps-2 text-muted-foreground">{t('grounded.breakdownReviewer')}</span>
|
|
613
|
+
<span className="text-end text-success">{breakdown.groups.reviewerConfirmed}</span>
|
|
614
|
+
{breakdown.groups.imageRead > 0 && (
|
|
615
|
+
<>
|
|
616
|
+
<span className="col-span-2 mt-2 font-medium text-attention">
|
|
617
|
+
{t('grounded.unverifiedCount', { count: breakdown.groups.imageRead })}
|
|
618
|
+
</span>
|
|
619
|
+
<span className="ps-2 text-muted-foreground">
|
|
620
|
+
{t('grounded.breakdownImageRead')}
|
|
621
|
+
</span>
|
|
622
|
+
<span className="text-end text-attention">{breakdown.groups.imageRead}</span>
|
|
623
|
+
</>
|
|
624
|
+
)}
|
|
625
|
+
</div>
|
|
626
|
+
<p className="mt-2 text-xs text-muted-foreground">{t('grounded.breakdownVerificationHelp')}</p>
|
|
627
|
+
{breakdown.unverified.length > 0 && (
|
|
628
|
+
<div className="mt-2">
|
|
629
|
+
<div className="text-xs font-medium text-muted-foreground mb-1">
|
|
630
|
+
{t('grounded.breakdownUnverifiedList')}
|
|
631
|
+
</div>
|
|
632
|
+
<ul className="space-y-0.5">
|
|
633
|
+
{breakdown.unverified.map((c) => (
|
|
634
|
+
<li key={c.path}>
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
className="w-full text-start text-xs rounded px-1 py-0.5 hover:bg-muted cursor-pointer"
|
|
638
|
+
onClick={() => {
|
|
639
|
+
selectPath(c.path);
|
|
640
|
+
setShowBreakdown(false);
|
|
641
|
+
}}
|
|
642
|
+
>
|
|
643
|
+
<span className="text-muted-foreground">{c.path}</span>{' '}
|
|
644
|
+
<span>{String(c.value ?? '')}</span>
|
|
645
|
+
</button>
|
|
646
|
+
</li>
|
|
647
|
+
))}
|
|
648
|
+
</ul>
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
{/* Extracted data */}
|
|
654
|
+
<div className="flex-1 overflow-auto p-3">
|
|
655
|
+
<DataNode
|
|
656
|
+
value={extraction.data}
|
|
657
|
+
path=""
|
|
658
|
+
citationsByPath={citationsByPath}
|
|
659
|
+
selectedPath={selectedPath}
|
|
660
|
+
onSelect={selectPath}
|
|
661
|
+
/>
|
|
662
|
+
{extraction.conflicts && extraction.conflicts.length > 0 && (
|
|
663
|
+
<div className="mt-4 text-xs text-attention">
|
|
664
|
+
{t('grounded.conflicts', { count: extraction.conflicts.length })}
|
|
665
|
+
</div>
|
|
666
|
+
)}
|
|
667
|
+
</div>
|
|
668
|
+
</ResizablePanel>
|
|
669
|
+
{assistantOpen && (
|
|
670
|
+
<>
|
|
671
|
+
<ResizableHandle className="w-[4px] bg-border cursor-ew-resize" />
|
|
672
|
+
<ResizablePanel defaultSize={30} minSize={20} className="flex flex-col border-s border-border">
|
|
673
|
+
<GroundedExtractionAssistantPanel
|
|
674
|
+
objectId={objectId}
|
|
675
|
+
onExtractionChanged={onExtractionChanged}
|
|
676
|
+
selectedField={selectedPath}
|
|
677
|
+
onClose={() => setAssistantOpen(false)}
|
|
678
|
+
/>
|
|
679
|
+
</ResizablePanel>
|
|
680
|
+
</>
|
|
681
|
+
)}
|
|
682
|
+
</ResizablePanelGroup>
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function PageWithOverlay({
|
|
687
|
+
objectId,
|
|
688
|
+
page,
|
|
689
|
+
dims,
|
|
690
|
+
citations,
|
|
691
|
+
selectedPath,
|
|
692
|
+
onSelectPath,
|
|
693
|
+
imageMode,
|
|
694
|
+
}: {
|
|
695
|
+
objectId: string;
|
|
696
|
+
page: number;
|
|
697
|
+
dims?: { width: number; height: number; rotation?: number };
|
|
698
|
+
citations: GroundedCitation[];
|
|
699
|
+
selectedPath?: string;
|
|
700
|
+
onSelectPath: (path: string) => void;
|
|
701
|
+
imageMode: PageImageMode;
|
|
702
|
+
}) {
|
|
703
|
+
const { t } = useUITranslation();
|
|
704
|
+
const { client } = useUserSession();
|
|
705
|
+
const [imageUrl, setImageUrl] = useState<string | undefined>(undefined);
|
|
706
|
+
|
|
707
|
+
useEffect(() => {
|
|
708
|
+
let cancelled = false;
|
|
709
|
+
setImageUrl(undefined);
|
|
710
|
+
client.files
|
|
711
|
+
.getDownloadUrl(`${ADVANCED_PROCESSING_PREFIX}/${objectId}/pages/page-${page}.${imageMode}.jpg`)
|
|
712
|
+
.then((r) => {
|
|
713
|
+
if (!cancelled) setImageUrl(r.url);
|
|
714
|
+
})
|
|
715
|
+
.catch((err: unknown) => console.warn('Failed to load page image', err));
|
|
716
|
+
return () => {
|
|
717
|
+
cancelled = true;
|
|
718
|
+
};
|
|
719
|
+
}, [client, imageMode, objectId, page]);
|
|
720
|
+
|
|
721
|
+
if (!imageUrl || !dims) {
|
|
722
|
+
return (
|
|
723
|
+
<div className="flex items-center justify-center h-64">
|
|
724
|
+
<Spinner />
|
|
725
|
+
</div>
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Citation boxes live in the page's (possibly OCR-rotated) coordinate space,
|
|
730
|
+
// which `dims` describes. They overlay directly on a container that is that
|
|
731
|
+
// same space — so when the page is rotated we rotate the image to fill that
|
|
732
|
+
// space rather than transforming every box.
|
|
733
|
+
const boxOverlays = citations.flatMap((citation) =>
|
|
734
|
+
citation.boxes.map((box, i) => {
|
|
735
|
+
const isSelected =
|
|
736
|
+
selectedPath !== undefined &&
|
|
737
|
+
normalizeCitationPath(selectedPath) === normalizeCitationPath(citation.path);
|
|
738
|
+
const cellRange = citation.cells
|
|
739
|
+
? citation.cells.start === citation.cells.end
|
|
740
|
+
? citation.cells.start
|
|
741
|
+
: `${citation.cells.start}:${citation.cells.end}`
|
|
742
|
+
: undefined;
|
|
743
|
+
return (
|
|
744
|
+
<button
|
|
745
|
+
key={`${citation.path}-${i}`}
|
|
746
|
+
type="button"
|
|
747
|
+
aria-label={citation.path}
|
|
748
|
+
title={`${citation.path}${cellRange ? `\n${cellRange}` : ''}${citation.source_text ? `\n${citation.source_text}` : ''}`}
|
|
749
|
+
onClick={() => onSelectPath(normalizeCitationPath(citation.path))}
|
|
750
|
+
className={cn(
|
|
751
|
+
'absolute cursor-pointer border rounded-[1px] transition-colors',
|
|
752
|
+
citation.misaligned && 'border-dashed',
|
|
753
|
+
isSelected
|
|
754
|
+
? 'border-2 border-destructive bg-destructive/20 z-10'
|
|
755
|
+
: selectedPath
|
|
756
|
+
? 'border-info/30 hover:border-info'
|
|
757
|
+
: citation.verified
|
|
758
|
+
? 'border-success/70 hover:bg-success/20'
|
|
759
|
+
: citation.reviewed
|
|
760
|
+
? 'border-info/70 hover:bg-info/20'
|
|
761
|
+
: 'border-attention/80 hover:bg-attention/20',
|
|
762
|
+
)}
|
|
763
|
+
style={{
|
|
764
|
+
// inline position: the app's button base styles override the
|
|
765
|
+
// `absolute` utility class with position: relative
|
|
766
|
+
position: 'absolute',
|
|
767
|
+
left: `${(box.x / dims.width) * 100}%`,
|
|
768
|
+
top: `${(box.y / dims.height) * 100}%`,
|
|
769
|
+
width: `${(box.w / dims.width) * 100}%`,
|
|
770
|
+
height: `${(box.h / dims.height) * 100}%`,
|
|
771
|
+
}}
|
|
772
|
+
/>
|
|
773
|
+
);
|
|
774
|
+
}),
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// The stored page image is in the source orientation; the OCR pass may have
|
|
778
|
+
// rotated the page to upright to read it, so `rotation` (CW degrees) is how
|
|
779
|
+
// much the image must turn to match the citation coordinates. A 90/270 turn
|
|
780
|
+
// also transposes the image box, so it is sized to the container's transpose.
|
|
781
|
+
const rot = (((dims.rotation ?? 0) % 360) + 360) % 360;
|
|
782
|
+
if (rot !== 0) {
|
|
783
|
+
const transpose = rot === 90 || rot === 270;
|
|
784
|
+
return (
|
|
785
|
+
<div className="relative w-full" style={{ aspectRatio: `${dims.width} / ${dims.height}` }}>
|
|
786
|
+
<img
|
|
787
|
+
src={imageUrl}
|
|
788
|
+
alt={t('grounded.pageAlt', { page })}
|
|
789
|
+
className="absolute select-none"
|
|
790
|
+
style={{
|
|
791
|
+
left: '50%',
|
|
792
|
+
top: '50%',
|
|
793
|
+
width: `${(transpose ? dims.height / dims.width : 1) * 100}%`,
|
|
794
|
+
height: `${(transpose ? dims.width / dims.height : 1) * 100}%`,
|
|
795
|
+
maxWidth: 'none',
|
|
796
|
+
transform: `translate(-50%, -50%) rotate(${rot}deg)`,
|
|
797
|
+
transformOrigin: 'center center',
|
|
798
|
+
}}
|
|
799
|
+
/>
|
|
800
|
+
{boxOverlays}
|
|
801
|
+
</div>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return (
|
|
806
|
+
<div className="relative w-full">
|
|
807
|
+
<img src={imageUrl} alt={t('grounded.pageAlt', { page })} className="w-full select-none" />
|
|
808
|
+
{boxOverlays}
|
|
809
|
+
</div>
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function DataNode({
|
|
814
|
+
value,
|
|
815
|
+
path,
|
|
816
|
+
citationsByPath,
|
|
817
|
+
selectedPath,
|
|
818
|
+
onSelect,
|
|
819
|
+
}: {
|
|
820
|
+
value: unknown;
|
|
821
|
+
path: string;
|
|
822
|
+
citationsByPath: Map<string, GroundedCitation[]>;
|
|
823
|
+
selectedPath?: string;
|
|
824
|
+
onSelect: (path: string) => void;
|
|
825
|
+
}) {
|
|
826
|
+
if (Array.isArray(value)) {
|
|
827
|
+
if (value.length > 0 && value.every((item) => item !== null && typeof item === 'object')) {
|
|
828
|
+
return (
|
|
829
|
+
<ArrayTable
|
|
830
|
+
items={value as Record<string, unknown>[]}
|
|
831
|
+
path={path}
|
|
832
|
+
citationsByPath={citationsByPath}
|
|
833
|
+
selectedPath={selectedPath}
|
|
834
|
+
onSelect={onSelect}
|
|
835
|
+
/>
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return (
|
|
839
|
+
<div className="space-y-1">
|
|
840
|
+
{value.map((item, i) => (
|
|
841
|
+
<DataNode
|
|
842
|
+
key={`${path}[${i}]`}
|
|
843
|
+
value={item}
|
|
844
|
+
path={`${path}[${i}]`}
|
|
845
|
+
citationsByPath={citationsByPath}
|
|
846
|
+
selectedPath={selectedPath}
|
|
847
|
+
onSelect={onSelect}
|
|
848
|
+
/>
|
|
849
|
+
))}
|
|
850
|
+
</div>
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (value !== null && typeof value === 'object') {
|
|
854
|
+
return (
|
|
855
|
+
<div className={path ? 'ps-3 border-s border-border space-y-0.5' : 'space-y-0.5'}>
|
|
856
|
+
{Object.entries(value as Record<string, unknown>).map(([key, child]) => {
|
|
857
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
858
|
+
const isLeaf = child === null || typeof child !== 'object';
|
|
859
|
+
if (isLeaf) {
|
|
860
|
+
return (
|
|
861
|
+
<LeafRow
|
|
862
|
+
key={childPath}
|
|
863
|
+
label={key}
|
|
864
|
+
value={child}
|
|
865
|
+
path={childPath}
|
|
866
|
+
citationsByPath={citationsByPath}
|
|
867
|
+
selectedPath={selectedPath}
|
|
868
|
+
onSelect={onSelect}
|
|
869
|
+
/>
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
// Arrays of objects render as a table with its own labeled header
|
|
873
|
+
const rendersOwnLabel =
|
|
874
|
+
Array.isArray(child) &&
|
|
875
|
+
child.length > 0 &&
|
|
876
|
+
child.every((item) => item !== null && typeof item === 'object');
|
|
877
|
+
return (
|
|
878
|
+
<div key={childPath} className="pt-1">
|
|
879
|
+
{!rendersOwnLabel && (
|
|
880
|
+
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
881
|
+
{key}
|
|
882
|
+
</div>
|
|
883
|
+
)}
|
|
884
|
+
<DataNode
|
|
885
|
+
value={child}
|
|
886
|
+
path={childPath}
|
|
887
|
+
citationsByPath={citationsByPath}
|
|
888
|
+
selectedPath={selectedPath}
|
|
889
|
+
onSelect={onSelect}
|
|
890
|
+
/>
|
|
891
|
+
</div>
|
|
892
|
+
);
|
|
893
|
+
})}
|
|
894
|
+
</div>
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function ArrayTable({
|
|
901
|
+
items,
|
|
902
|
+
path,
|
|
903
|
+
citationsByPath,
|
|
904
|
+
selectedPath,
|
|
905
|
+
onSelect,
|
|
906
|
+
}: {
|
|
907
|
+
items: Record<string, unknown>[];
|
|
908
|
+
path: string;
|
|
909
|
+
citationsByPath: Map<string, GroundedCitation[]>;
|
|
910
|
+
selectedPath?: string;
|
|
911
|
+
onSelect: (path: string) => void;
|
|
912
|
+
}) {
|
|
913
|
+
const { t } = useUITranslation();
|
|
914
|
+
const columns = useMemo(() => {
|
|
915
|
+
const keys = new Set<string>();
|
|
916
|
+
for (const item of items) {
|
|
917
|
+
for (const key of Object.keys(item)) keys.add(key);
|
|
918
|
+
}
|
|
919
|
+
return Array.from(keys);
|
|
920
|
+
}, [items]);
|
|
921
|
+
|
|
922
|
+
return (
|
|
923
|
+
<div className="pt-1">
|
|
924
|
+
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
925
|
+
{path.split('.').pop()} ({items.length})
|
|
926
|
+
</div>
|
|
927
|
+
<div className="overflow-x-auto">
|
|
928
|
+
<table className="w-full text-xs">
|
|
929
|
+
<thead>
|
|
930
|
+
<tr className="text-start text-muted-foreground">
|
|
931
|
+
{columns.map((col) => (
|
|
932
|
+
<th key={col} scope="col" className="py-1 pe-2 font-medium">
|
|
933
|
+
{col}
|
|
934
|
+
</th>
|
|
935
|
+
))}
|
|
936
|
+
</tr>
|
|
937
|
+
</thead>
|
|
938
|
+
<tbody>
|
|
939
|
+
{items.map((item, i) => (
|
|
940
|
+
<tr key={`${path}[${i}]`} className="border-t border-border/50">
|
|
941
|
+
{columns.map((col) => {
|
|
942
|
+
const cellPath = `${path}[${i}].${col}`;
|
|
943
|
+
const citation = citationsByPath.get(cellPath)?.[0];
|
|
944
|
+
const isSelected = selectedPath === cellPath;
|
|
945
|
+
return (
|
|
946
|
+
<td key={col} className="py-0.5 pe-2">
|
|
947
|
+
<button
|
|
948
|
+
type="button"
|
|
949
|
+
onClick={() => onSelect(cellPath)}
|
|
950
|
+
className={cn(
|
|
951
|
+
'text-start rounded px-1 w-full',
|
|
952
|
+
isSelected
|
|
953
|
+
? 'bg-destructive/15 ring-1 ring-destructive'
|
|
954
|
+
: citation
|
|
955
|
+
? 'hover:bg-muted cursor-pointer'
|
|
956
|
+
: 'text-muted-foreground cursor-default',
|
|
957
|
+
)}
|
|
958
|
+
>
|
|
959
|
+
{formatValue(item[col])}
|
|
960
|
+
{citation && typeof citation.confidence === 'number' && (
|
|
961
|
+
<span
|
|
962
|
+
className={cn(
|
|
963
|
+
'ms-1 text-[10px]',
|
|
964
|
+
scoreColor(citation.confidence),
|
|
965
|
+
)}
|
|
966
|
+
title={t('grounded.confidenceHint')}
|
|
967
|
+
>
|
|
968
|
+
{formatScore(citation.confidence)}
|
|
969
|
+
</span>
|
|
970
|
+
)}
|
|
971
|
+
</button>
|
|
972
|
+
</td>
|
|
973
|
+
);
|
|
974
|
+
})}
|
|
975
|
+
</tr>
|
|
976
|
+
))}
|
|
977
|
+
</tbody>
|
|
978
|
+
</table>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function LeafRow({
|
|
985
|
+
label,
|
|
986
|
+
value,
|
|
987
|
+
path,
|
|
988
|
+
citationsByPath,
|
|
989
|
+
selectedPath,
|
|
990
|
+
onSelect,
|
|
991
|
+
}: {
|
|
992
|
+
label: string;
|
|
993
|
+
value: unknown;
|
|
994
|
+
path: string;
|
|
995
|
+
citationsByPath: Map<string, GroundedCitation[]>;
|
|
996
|
+
selectedPath?: string;
|
|
997
|
+
onSelect: (path: string) => void;
|
|
998
|
+
}) {
|
|
999
|
+
const { t } = useUITranslation();
|
|
1000
|
+
const citation = citationsByPath.get(path)?.[0];
|
|
1001
|
+
const isSelected = selectedPath === path;
|
|
1002
|
+
|
|
1003
|
+
return (
|
|
1004
|
+
<button
|
|
1005
|
+
type="button"
|
|
1006
|
+
onClick={() => citation && onSelect(path)}
|
|
1007
|
+
className={cn(
|
|
1008
|
+
'flex items-center gap-2 w-full text-start text-sm rounded px-1.5 py-0.5',
|
|
1009
|
+
isSelected ? 'bg-destructive/15 ring-1 ring-destructive' : citation ? 'hover:bg-muted' : '',
|
|
1010
|
+
!citation && 'cursor-default',
|
|
1011
|
+
)}
|
|
1012
|
+
>
|
|
1013
|
+
<span className="text-muted-foreground min-w-28 shrink-0">{label}</span>
|
|
1014
|
+
<span className="flex-1 wrap-break-word">{formatValue(value)}</span>
|
|
1015
|
+
{citation && typeof citation.confidence === 'number' && (
|
|
1016
|
+
<span
|
|
1017
|
+
className={cn('text-xs shrink-0', scoreColor(citation.confidence))}
|
|
1018
|
+
title={t('grounded.confidenceHint')}
|
|
1019
|
+
>
|
|
1020
|
+
{formatScore(citation.confidence)}
|
|
1021
|
+
</span>
|
|
1022
|
+
)}
|
|
1023
|
+
{citation &&
|
|
1024
|
+
(citation.verified ? (
|
|
1025
|
+
<CheckCircle2 aria-label={t('grounded.verified')} className="size-3.5 shrink-0 text-success" />
|
|
1026
|
+
) : citation.reviewed ? (
|
|
1027
|
+
<Sparkles aria-label={t('grounded.aiVerified')} className="size-3.5 shrink-0 text-info" />
|
|
1028
|
+
) : (
|
|
1029
|
+
<Eye aria-label={t('grounded.readFromImage')} className="size-3.5 shrink-0 text-attention" />
|
|
1030
|
+
))}
|
|
1031
|
+
</button>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Tier scores (1.0, 0.99, 0.5...) are policy values and display as integers;
|
|
1037
|
+
* OCR-measured scores carry real decimals from tesseract and keep two.
|
|
1038
|
+
*/
|
|
1039
|
+
function formatScore(confidence: number): string {
|
|
1040
|
+
const pct = confidence * 100;
|
|
1041
|
+
return Number.isInteger(pct) ? `${pct}%` : `${(Math.floor(pct * 100) / 100).toFixed(2)}%`;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function scoreColor(confidence: number): string {
|
|
1045
|
+
if (confidence >= 0.95) return 'text-success';
|
|
1046
|
+
if (confidence >= 0.7) return 'text-attention';
|
|
1047
|
+
return 'text-destructive';
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function formatValue(value: unknown): string {
|
|
1051
|
+
if (value === null || value === undefined || value === '') return '—';
|
|
1052
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
1053
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
1054
|
+
return String(value);
|
|
1055
|
+
}
|