@puredesktop/puredesktop-ui-bridge 1.0.0-beta.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/agents/appAgentConnectors.test.ts +37 -0
- package/src/agents/appAgentConnectors.ts +138 -0
- package/src/agents/runtime/agentLiveReducer.test.ts +72 -72
- package/src/agents/runtime/agentLiveReducer.ts +139 -139
- package/src/agents/runtime/agentRunEventTypes.ts +115 -115
- package/src/agents/runtime/contextWindowUsage.test.ts +38 -38
- package/src/agents/runtime/contextWindowUsage.ts +25 -25
- package/src/agents/runtime/index.ts +24 -24
- package/src/agents/runtime/mapAgentMessages.ts +79 -54
- package/src/agents/runtime/mapAgentSnapshot.test.ts +98 -0
- package/src/agents/runtime/mapAgentSnapshot.ts +124 -71
- package/src/agents/runtime/mergeUsageTurn.ts +23 -23
- package/src/agents/runtime/pendingToolCalls.test.ts +85 -85
- package/src/agents/runtime/pendingToolCalls.ts +14 -14
- package/src/agents/runtime/toolRequiresApproval.ts +20 -20
- package/src/agents/runtime/unresolvedToolCalls.ts +42 -42
- package/src/bridge/agentModelTiers.ts +50 -50
- package/src/bridge/agentToolHandlers.ts +23 -23
- package/src/bridge/agentToolHelpers.test.ts +27 -27
- package/src/bridge/agentToolHelpers.ts +20 -20
- package/src/bridge/agentTypes.ts +377 -294
- package/src/bridge/agents.d.mts +144 -113
- package/src/bridge/agents.mjs +287 -233
- package/src/bridge/agents.ts +77 -62
- package/src/bridge/appSettings.d.mts +9 -9
- package/src/bridge/appSettings.mjs +18 -18
- package/src/bridge/appSettings.ts +4 -4
- package/src/bridge/applyThemeFromHost.d.mts +7 -7
- package/src/bridge/applyThemeFromHost.mjs +33 -33
- package/src/bridge/assets.d.mts +66 -66
- package/src/bridge/assets.mjs +121 -121
- package/src/bridge/calendarDraftIntent.ts +55 -0
- package/src/bridge/calendarInviteIntent.ts +64 -0
- package/src/bridge/catalog.d.mts +5 -5
- package/src/bridge/catalog.mjs +24 -24
- package/src/bridge/catalog.ts +1 -1
- package/src/bridge/client.d.mts +19 -19
- package/src/bridge/client.mjs +102 -102
- package/src/bridge/collectionAssetAdapter.ts +63 -63
- package/src/bridge/collectionAssets.d.mts +1 -1
- package/src/bridge/collectionAssets.mjs +2 -2
- package/src/bridge/collectionAssets.ts +1 -1
- package/src/bridge/collectionDocumentHtml.test.ts +71 -71
- package/src/bridge/collectionDocumentHtml.ts +118 -118
- package/src/bridge/collectionImagePaste.test.ts +32 -32
- package/src/bridge/collectionImagePaste.ts +130 -130
- package/src/bridge/context.mjs +12 -12
- package/src/bridge/credentials.mjs +22 -22
- package/src/bridge/dialog.d.mts +5 -5
- package/src/bridge/dialog.mjs +13 -13
- package/src/bridge/events.d.mts +14 -14
- package/src/bridge/events.mjs +12 -12
- package/src/bridge/filePreviewKind.test.ts +43 -43
- package/src/bridge/filePreviewKind.ts +76 -76
- package/src/bridge/fs.d.mts +41 -41
- package/src/bridge/fs.mjs +82 -82
- package/src/bridge/fs.test.ts +16 -16
- package/src/bridge/fs.ts +10 -10
- package/src/bridge/googleAuth.ts +159 -0
- package/src/bridge/googleProviderConfig.ts +73 -0
- package/src/bridge/index.ts +12 -12
- package/src/bridge/inlinePreview.d.mts +35 -35
- package/src/bridge/inlinePreview.mjs +99 -99
- package/src/bridge/inlinePreview.test.ts +56 -56
- package/src/bridge/inlinePreview.ts +13 -13
- package/src/bridge/methods.d.mts +125 -115
- package/src/bridge/methods.mjs +143 -132
- package/src/bridge/os.d.mts +11 -11
- package/src/bridge/os.mjs +26 -26
- package/src/bridge/plugins.d.mts +9 -9
- package/src/bridge/plugins.mjs +31 -31
- package/src/bridge/plugins.ts +5 -5
- package/src/bridge/preferences.d.mts +7 -7
- package/src/bridge/preferences.mjs +17 -17
- package/src/bridge/preferences.ts +13 -13
- package/src/bridge/printDocument.test.ts +47 -47
- package/src/bridge/printDocument.ts +75 -75
- package/src/bridge/pureRender/adapters.test.ts +144 -144
- package/src/bridge/pureRender/adapters.ts +175 -175
- package/src/bridge/pureRender/assets.test.ts +44 -44
- package/src/bridge/pureRender/base.css +76 -76
- package/src/bridge/pureRender/compiler.test.ts +279 -279
- package/src/bridge/pureRender/compiler.ts +660 -660
- package/src/bridge/pureRender/document.test.ts +283 -283
- package/src/bridge/pureRender/document.ts +35 -35
- package/src/bridge/pureRender/extraction.test.ts +432 -432
- package/src/bridge/pureRender/extraction.ts +1023 -1023
- package/src/bridge/pureRender/flow.css +3 -3
- package/src/bridge/pureRender/index.ts +66 -66
- package/src/bridge/pureRender/normalize.test.ts +442 -442
- package/src/bridge/pureRender/normalize.ts +682 -682
- package/src/bridge/pureRender/presets.ts +365 -365
- package/src/bridge/pureRender/profiles/book.css +19 -19
- package/src/bridge/pureRender/profiles/manuscript.css +11 -11
- package/src/bridge/pureRender/profiles/writer.css +10 -10
- package/src/bridge/pureRender/reset.css +5 -5
- package/src/bridge/pureRender/samples.ts +94 -94
- package/src/bridge/pureRender/styles.ts +164 -164
- package/src/bridge/pureRender/theme.test.ts +53 -53
- package/src/bridge/pureRender/theme.ts +258 -258
- package/src/bridge/pureRender/tokens/default.css +36 -36
- package/src/bridge/pureRender/tokens/default.json +32 -32
- package/src/bridge/pureRender/types.ts +264 -264
- package/src/bridge/pureRender/visualLayoutInspection.test.ts +57 -57
- package/src/bridge/pureRender/visualLayoutInspection.ts +161 -161
- package/src/bridge/react/useDebouncedEffect.ts +21 -21
- package/src/bridge/react/useIpc.tsx +75 -75
- package/src/bridge/react/usePlatformAgentRunLiveState.tsx +78 -78
- package/src/bridge/react/usePlatformAgentSessionDrive.test.tsx +134 -0
- package/src/bridge/react/usePlatformAgentSessionDrive.tsx +749 -729
- package/src/bridge/react/usePlatformAgentSessions.tsx +60 -60
- package/src/bridge/react/usePlatformAgentTools.tsx +82 -82
- package/src/bridge/react/usePlatformAppManifest.tsx +32 -32
- package/src/bridge/react/usePlatformBridge.tsx +56 -56
- package/src/bridge/react/usePlatformDeepLink.tsx +57 -57
- package/src/bridge/react/usePlatformFileBinaryDataUrl.tsx +54 -54
- package/src/bridge/react/usePlatformFilePreview.tsx +92 -92
- package/src/bridge/react/usePlatformPreferences.tsx +77 -77
- package/src/bridge/react/usePlatformThemeDocumentAttrs.ts +35 -35
- package/src/bridge/react/usePlatformThemeFromBridge.tsx +16 -16
- package/src/bridge/react/usePlatformViewportResource.test.tsx +106 -106
- package/src/bridge/react/usePlatformViewportResource.tsx +72 -72
- package/src/bridge/react/useScheduledTask.tsx +59 -59
- package/src/bridge/render.d.mts +31 -31
- package/src/bridge/render.mjs +62 -62
- package/src/bridge/render.ts +8 -8
- package/src/bridge/screenAnnotation.test.ts +103 -103
- package/src/bridge/screenAnnotation.ts +269 -269
- package/src/bridge/theme.d.mts +3 -3
- package/src/bridge/theme.mjs +9 -9
- package/src/bridge/types.ts +447 -439
- package/src/bridge/vision.d.mts +22 -0
- package/src/bridge/vision.mjs +20 -0
- package/src/bridge/watch.d.mts +9 -9
- package/src/bridge/watch.mjs +34 -34
- package/src/bridge/watch.ts +1 -1
- package/src/bridge/window.d.mts +44 -44
- package/src/bridge/window.mjs +33 -33
- package/src/chrome/index.ts +1 -1
- package/src/commitments/commitments.test.ts +201 -0
- package/src/commitments/commitments.ts +358 -0
- package/src/components/accessibility/AccessibilityReviewDialog.test.tsx +104 -104
- package/src/components/accessibility/AccessibilityReviewDialog.tsx +350 -350
- package/src/components/accessibility/accessibilityReview.test.ts +101 -101
- package/src/components/accessibility/accessibilityReview.ts +639 -639
- package/src/components/accessibility/index.ts +16 -16
- package/src/components/agent-feedback/ScreenAnnotationAgentDialog.tsx +509 -509
- package/src/components/agent-feedback/index.ts +5 -5
- package/src/components/agents/AgentComposer.tsx +135 -34
- package/src/components/agents/AgentContextUsageBar.tsx +127 -127
- package/src/components/agents/AgentDrawerPanel.test.tsx +361 -0
- package/src/components/agents/AgentDrawerPanel.tsx +122 -82
- package/src/components/agents/AgentFollowUpList.tsx +56 -0
- package/src/components/agents/AgentMarkdown.tsx +12 -12
- package/src/components/agents/AgentMessageBubble.tsx +171 -82
- package/src/components/agents/AgentMessageList.tsx +50 -47
- package/src/components/agents/AgentModelMenu.tsx +59 -55
- package/src/components/agents/AgentQuestionPromptList.tsx +284 -0
- package/src/components/agents/AgentThinkingBlock.tsx +66 -66
- package/src/components/agents/AgentToolApprovalActions.tsx +65 -65
- package/src/components/agents/AgentToolCallCard.tsx +13 -8
- package/src/components/agents/AgentToolPendingList.tsx +39 -39
- package/src/components/agents/AgentToolPendingReview.tsx +118 -118
- package/src/components/agents/agentPanelStyles.ts +150 -79
- package/src/components/agents/agentToolDisplay.ts +87 -0
- package/src/components/agents/agentTypes.ts +139 -96
- package/src/components/agents/formatToolCallArguments.test.ts +16 -16
- package/src/components/agents/formatToolCallArguments.ts +9 -9
- package/src/components/agents/index.ts +40 -40
- package/src/components/assets/AssetLibraryDialog.tsx +5 -5
- package/src/components/assets/asset-library/AssetLibraryDialog.tsx +43 -43
- package/src/components/assets/asset-library/AssetLibraryWorkspace.tsx +54 -54
- package/src/components/assets/asset-library/detail/AssetLibraryDetailActions.tsx +68 -68
- package/src/components/assets/asset-library/detail/AssetLibraryDetailHeader.tsx +56 -56
- package/src/components/assets/asset-library/detail/AssetLibraryDetailPane.tsx +90 -90
- package/src/components/assets/asset-library/detail/AssetLibraryDetailPreview.tsx +67 -67
- package/src/components/assets/asset-library/detail/AssetLibraryInlinePreview.tsx +87 -87
- package/src/components/assets/asset-library/detail/AssetLibraryInlineSourceField.tsx +29 -29
- package/src/components/assets/asset-library/detail/AssetLibraryMetadataFields.tsx +34 -34
- package/src/components/assets/asset-library/detail/AssetLibraryUsagePanel.tsx +46 -46
- package/src/components/assets/asset-library/detail/AssetLibraryVersionsPanel.tsx +145 -145
- package/src/components/assets/asset-library/helpers/assetKind.ts +74 -74
- package/src/components/assets/asset-library/helpers.ts +41 -41
- package/src/components/assets/asset-library/hooks/useAssetDetailDraft.ts +81 -81
- package/src/components/assets/asset-library/hooks/useAssetLibraryDialog.ts +233 -233
- package/src/components/assets/asset-library/hooks/useAssetLibraryMutations.ts +266 -266
- package/src/components/assets/asset-library/hooks/useAssetLibrarySelectionSync.ts +24 -24
- package/src/components/assets/asset-library/hooks/useAssetLibraryVersions.ts +145 -145
- package/src/components/assets/asset-library/hooks/useInlineAssetPreview.ts +44 -44
- package/src/components/assets/asset-library/index.ts +19 -19
- package/src/components/assets/asset-library/layout/assetLibraryLayout.ts +201 -201
- package/src/components/assets/asset-library/shared/fieldStyles.ts +62 -62
- package/src/components/assets/asset-library/sidebar/AssetLibraryFilterPills.tsx +89 -89
- package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +91 -91
- package/src/components/assets/asset-library/sidebar/AssetLibraryListRow.tsx +99 -99
- package/src/components/assets/asset-library/sidebar/AssetLibraryListStatus.tsx +24 -24
- package/src/components/assets/asset-library/sidebar/AssetLibrarySidebar.tsx +48 -48
- package/src/components/assets/asset-library/sidebar/AssetLibrarySidebarActions.tsx +38 -38
- package/src/components/assets/asset-library/sidebar/AssetLibrarySidebarToolbar.tsx +78 -78
- package/src/components/assets/asset-library/sidebar/AssetListThumbnail.tsx +86 -86
- package/src/components/assets/asset-library/types.ts +128 -128
- package/src/components/assets/asset-library/utils/assetLibraryUtils.ts +181 -181
- package/src/components/assets/asset-library/utils/pathBasename.ts +5 -5
- package/src/components/assets/asset-library/utils/sanitizeUploadFileName.ts +7 -7
- package/src/components/assets/asset-library/utils/uploadCollectionAsset.ts +52 -52
- package/src/components/chrome/OverlayActionCapsule.test.tsx +82 -82
- package/src/components/chrome/OverlayActionCapsule.tsx +90 -90
- package/src/components/chrome/PanelTabStrip.tsx +113 -113
- package/src/components/chrome/PlatformIcon.tsx +27 -27
- package/src/components/chrome/PureAppWordmark.tsx +94 -89
- package/src/components/chrome/ShellWindowControls.tsx +58 -51
- package/src/components/chrome/ThemeModeButton.tsx +16 -16
- package/src/components/chrome/Titlebar.tsx +79 -79
- package/src/components/chrome/WorkspaceTabStrip.tsx +321 -300
- package/src/components/chrome/index.ts +31 -31
- package/src/components/chrome/workspaceTabTypes.ts +29 -29
- package/src/components/common/buttons/Button.tsx +241 -241
- package/src/components/common/buttons/ChoiceChip.tsx +73 -73
- package/src/components/common/buttons/CleanButton.tsx +34 -34
- package/src/components/common/buttons/IconButton.tsx +89 -89
- package/src/components/common/buttons/QuietAction.tsx +29 -29
- package/src/components/common/buttons/SegmentedControl.tsx +67 -67
- package/src/components/common/buttons/TextActionButton.tsx +55 -55
- package/src/components/common/buttons/TextLink.tsx +38 -38
- package/src/components/common/canvas/CanvasElementFrame.test.tsx +79 -79
- package/src/components/common/canvas/CanvasElementFrame.tsx +182 -182
- package/src/components/common/canvas/canvasGeometry.test.ts +66 -66
- package/src/components/common/canvas/canvasGeometry.ts +133 -133
- package/src/components/common/canvas/index.ts +27 -27
- package/src/components/common/canvas/useCanvasDragResize.test.tsx +152 -152
- package/src/components/common/canvas/useCanvasDragResize.ts +161 -161
- package/src/components/common/cards/OpenReviewCard.tsx +209 -0
- package/src/components/common/cards/index.ts +5 -0
- package/src/components/common/chat/ChatAssistantMessage.tsx +81 -81
- package/src/components/common/chat/ChatBox.test.tsx +61 -0
- package/src/components/common/chat/ChatBox.tsx +193 -189
- package/src/components/common/chat/ChatMarkdown.tsx +109 -109
- package/src/components/common/chat/ChatModelMenu.tsx +290 -258
- package/src/components/common/chat/ChatStatusRow.tsx +24 -24
- package/src/components/common/chat/ChatThinkingIndicator.tsx +54 -54
- package/src/components/common/chat/ChatThread.tsx +59 -59
- package/src/components/common/chat/ChatToolRow.tsx +147 -141
- package/src/components/common/chat/ChatUserBubble.tsx +45 -45
- package/src/components/common/chat/chatUtils.ts +7 -7
- package/src/components/common/chat/index.ts +24 -24
- package/src/components/common/connections/ConnectionSetup.tsx +534 -529
- package/src/components/common/connections/index.ts +26 -26
- package/src/components/common/containers/AppFrame.test.tsx +87 -41
- package/src/components/common/containers/AppFrame.tsx +66 -65
- package/src/components/common/containers/AppHeader.tsx +60 -60
- package/src/components/common/containers/AutoFillGrid.tsx +43 -37
- package/src/components/common/containers/Collapsible.tsx +43 -43
- package/src/components/common/containers/CollapsibleGrid.tsx +185 -174
- package/src/components/common/containers/MotionScope.tsx +32 -32
- package/src/components/common/containers/PageHeader.tsx +35 -35
- package/src/components/common/containers/PanelToolbarHeader.tsx +114 -113
- package/src/components/common/containers/QuietDisclosure.tsx +91 -91
- package/src/components/common/containers/QuietSurface.tsx +40 -41
- package/src/components/common/containers/ScrollPanel.tsx +33 -33
- package/src/components/common/containers/StatusBar.tsx +42 -42
- package/src/components/common/containers/SurfacePanel.tsx +66 -66
- package/src/components/common/containers/ToolbarStrip.tsx +80 -80
- package/src/components/common/containers/flex.ts +117 -117
- package/src/components/common/containers/useOpacityPresence.ts +80 -0
- package/src/components/common/containers/useOpenTransition.ts +33 -33
- package/src/components/common/context/ContextSelector.tsx +277 -269
- package/src/components/common/context/index.ts +7 -7
- package/src/components/common/desk/ActionReader.tsx +270 -270
- package/src/components/common/desk/ActivityLedger.tsx +143 -143
- package/src/components/common/desk/CreationDesk.tsx +139 -139
- package/src/components/common/desk/DailyBrief.tsx +160 -160
- package/src/components/common/desk/DeskCard.tsx +157 -157
- package/src/components/common/desk/index.ts +30 -30
- package/src/components/common/dropdown/ContextMenu.tsx +87 -87
- package/src/components/common/dropdown/MenuButtonDropdown.tsx +121 -121
- package/src/components/common/dropdown/MenuDropdown.tsx +63 -63
- package/src/components/common/dropdown/MenuDropdownEntry.tsx +51 -51
- package/src/components/common/dropdown/MenuDropdownItem.tsx +82 -82
- package/src/components/common/dropdown/MenuDropdownSubmenu.tsx +145 -145
- package/src/components/common/dropdown/MenuPanel.tsx +75 -69
- package/src/components/common/dropdown/MenuPortal.tsx +164 -162
- package/src/components/common/dropdown/MenuPortalLayer.tsx +6 -6
- package/src/components/common/dropdown/SelectMenu.tsx +246 -243
- package/src/components/common/dropdown/SelectMenuControl.tsx +23 -23
- package/src/components/common/dropdown/SelectMenuOptions.tsx +60 -38
- package/src/components/common/dropdown/menuPortalConstants.ts +11 -11
- package/src/components/common/dropdown/menuTypes.ts +71 -71
- package/src/components/common/dropdown/menuViewportPosition.test.ts +139 -139
- package/src/components/common/dropdown/menuViewportPosition.ts +122 -122
- package/src/components/common/dropdown/selectMenuLayout.test.ts +42 -42
- package/src/components/common/dropdown/selectMenuLayout.ts +123 -123
- package/src/components/common/dropdown/useMenuAnchorPosition.ts +109 -109
- package/src/components/common/dropdown/useSelectMenuAnchor.ts +30 -30
- package/src/components/common/evidence/StructuredEvidenceView.test.tsx +77 -77
- package/src/components/common/evidence/StructuredEvidenceView.tsx +312 -312
- package/src/components/common/evidence/index.ts +13 -13
- package/src/components/common/feedback/Badge.tsx +35 -33
- package/src/components/common/feedback/CapabilityPills.tsx +36 -36
- package/src/components/common/feedback/EmptyState.tsx +166 -132
- package/src/components/common/feedback/InfoHint.tsx +67 -67
- package/src/components/common/feedback/InlineBanner.tsx +82 -82
- package/src/components/common/feedback/PlatformChip.tsx +104 -104
- package/src/components/common/feedback/QuietPill.tsx +75 -75
- package/src/components/common/feedback/WorkProgressPanel.tsx +156 -156
- package/src/components/common/feedback/index.ts +33 -33
- package/src/components/common/files/FileTable.tsx +225 -0
- package/src/components/common/inputs/BareInput.tsx +31 -31
- package/src/components/common/inputs/BareTextarea.tsx +10 -10
- package/src/components/common/inputs/Checkbox.tsx +13 -13
- package/src/components/common/inputs/EditableSecretField.tsx +292 -0
- package/src/components/common/inputs/FieldControlShell.tsx +49 -49
- package/src/components/common/inputs/FormField.tsx +40 -40
- package/src/components/common/inputs/HybridSearchField.tsx +295 -280
- package/src/components/common/inputs/InlineComposer.tsx +197 -197
- package/src/components/common/inputs/PaletteCirclePicker.tsx +26 -26
- package/src/components/common/inputs/PaletteDropdown.tsx +418 -413
- package/src/components/common/inputs/Radio.tsx +19 -19
- package/src/components/common/inputs/SearchField.tsx +128 -128
- package/src/components/common/inputs/SelectField.tsx +170 -169
- package/src/components/common/inputs/SurfacePalettePicker.tsx +54 -54
- package/src/components/common/inputs/TextAreaField.tsx +22 -22
- package/src/components/common/inputs/TextField.tsx +51 -51
- package/src/components/common/inputs/ToggleSwitch.tsx +110 -110
- package/src/components/common/inputs/WorkspacePicker.tsx +349 -349
- package/src/components/common/inputs/fieldStyles.ts +141 -137
- package/src/components/common/layout/PanelGroup.tsx +485 -479
- package/src/components/common/layout/index.ts +31 -31
- package/src/components/common/layout/panelLayoutConstants.ts +7 -7
- package/src/components/common/layout/panelLayoutPersistence.ts +35 -35
- package/src/components/common/layout/panelLayoutState.test.ts +38 -38
- package/src/components/common/layout/panelLayoutState.ts +174 -174
- package/src/components/common/layout/panelLayoutTypes.ts +19 -19
- package/src/components/common/layout/tabs/Tabs.tsx +136 -136
- package/src/components/common/lists/listRowStyles.ts +32 -32
- package/src/components/common/lists/quiet-ledger/QuietActivityList.tsx +64 -64
- package/src/components/common/lists/quiet-ledger/QuietLedgerRow.tsx +119 -119
- package/src/components/common/lists/quiet-ledger/index.ts +9 -9
- package/src/components/common/lists/reorderable/DragHandle.tsx +65 -65
- package/src/components/common/lists/reorderable/ReorderableList.tsx +280 -280
- package/src/components/common/lists/reorderable/ReorderableListRow.tsx +102 -102
- package/src/components/common/lists/reorderable/ReorderableSections.tsx +344 -344
- package/src/components/common/lists/reorderable/ReorderableSectionsDemo.tsx +94 -94
- package/src/components/common/lists/reorderable/index.ts +15 -15
- package/src/components/common/lists/reorderable/reorderableUtils.test.ts +28 -28
- package/src/components/common/lists/reorderable/reorderableUtils.ts +23 -23
- package/src/components/common/lists/reorderable/types.ts +40 -40
- package/src/components/common/lists/reorderable/useReorderableDragState.ts +49 -49
- package/src/components/common/media/ImageReferencePicker.tsx +242 -242
- package/src/components/common/menubar/MenuBar.tsx +78 -78
- package/src/components/common/menubar/MenuBarTrigger.tsx +70 -70
- package/src/components/common/narrative/NarrativeMention.tsx +74 -74
- package/src/components/common/navigation/NavigationCardRow.tsx +98 -0
- package/src/components/common/navigation/PathBreadcrumbs.tsx +78 -78
- package/src/components/common/navigation/UnderlineTabs.tsx +83 -83
- package/src/components/common/navigation/index.ts +15 -0
- package/src/components/common/overlays/InstallAppPromptDialog.tsx +59 -59
- package/src/components/common/overlays/Modal.tsx +292 -285
- package/src/components/common/overlays/ModalChromeHeader.tsx +105 -105
- package/src/components/common/overlays/ObjectActionStrip.tsx +80 -80
- package/src/components/common/people/AutoUpdateReviewList.tsx +182 -182
- package/src/components/common/people/EntityList.tsx +122 -122
- package/src/components/common/people/EntityPortrait.tsx +64 -64
- package/src/components/common/people/LinkedDossierList.tsx +99 -99
- package/src/components/common/people/OpportunityList.tsx +94 -94
- package/src/components/common/people/PeopleSourceList.tsx +145 -145
- package/src/components/common/people/RelationshipDossier.tsx +168 -168
- package/src/components/common/people/RelationshipMap.tsx +362 -362
- package/src/components/common/people/index.ts +40 -40
- package/src/components/common/preview/FileAudioPreview.tsx +89 -89
- package/src/components/common/preview/FileImagePreview.tsx +88 -88
- package/src/components/common/preview/FilePdfPreview.tsx +87 -87
- package/src/components/common/preview/FileTextPreview.tsx +96 -96
- package/src/components/common/preview/FileVideoPreview.tsx +87 -87
- package/src/components/common/preview/PlatformFilePreview.tsx +166 -166
- package/src/components/common/preview/previewLayout.ts +27 -27
- package/src/components/common/render/Each.tsx +42 -42
- package/src/components/common/research/EvidenceDossier.tsx +904 -894
- package/src/components/common/research/index.ts +7 -7
- package/src/components/common/tiles/CatalogCard.tsx +116 -116
- package/src/components/common/tiles/Tile.tsx +45 -45
- package/src/components/common/tiles/TileGrid.tsx +19 -13
- package/src/components/common/typography/Heading.tsx +27 -27
- package/src/components/common/typography/Kicker.tsx +36 -30
- package/src/components/common/typography/Text.tsx +80 -80
- package/src/components/credentials/CredentialsPanel.tsx +236 -169
- package/src/components/credentials/credentialProviderIcon.tsx +79 -0
- package/src/components/credentials/credentialsTypes.ts +14 -14
- package/src/components/credentials/lobeIcons.d.ts +4 -0
- package/src/components/data/DataChart.tsx +913 -913
- package/src/components/data/dataTable.ts +99 -99
- package/src/components/editor/EditorCommentPromptDialog.tsx +124 -124
- package/src/components/editor/EditorLinkPromptDialog.tsx +65 -65
- package/src/components/print-design/PrintDesignPanel.test.tsx +1446 -1446
- package/src/components/print-design/PrintDesignPanel.tsx +3045 -3043
- package/src/components/print-design/index.ts +9 -9
- package/src/components/proofing/PagedProofDialog.tsx +453 -453
- package/src/components/proofing/index.ts +4 -4
- package/src/editor/DocumentEditor.tsx +28 -28
- package/src/editor/EditorViewModeToggle.tsx +38 -35
- package/src/editor/index.ts +98 -97
- package/src/editor/toolbarOverlays.ts +8 -8
- package/src/index.ts +231 -209
- package/src/theme/ThemeProvider.tsx +115 -113
- package/src/theme/appAccents.ts +115 -87
- package/src/theme/buildThemeBridgePayload.ts +23 -23
- package/src/theme/composePlatformTheme.test.ts +47 -30
- package/src/theme/composePlatformTheme.ts +23 -23
- package/src/theme/platformFont.d.mts +7 -7
- package/src/theme/platformFont.mjs +20 -20
- package/src/theme/platformFont.ts +6 -6
- package/src/theme/reportsShellChromeSelectors.ts +136 -136
- package/src/theme/styled.d.ts +5 -5
- package/src/theme/surfacePalettes.ts +179 -178
- package/src/theme/surfaceRoleSelectors.ts +113 -113
- package/src/theme/themeVars.ts +13 -13
- package/src/theme/themes/dark.ts +59 -59
- package/src/theme/themes/light.ts +60 -60
- package/src/theme/themes/palette.ts +44 -44
- package/src/theme/themes/shared.ts +68 -68
- package/src/theme/tokens.ts +114 -114
- package/src/theme/utils/GlobalStyles.ts +88 -88
- package/src/theme/utils/appFrameDocumentStyles.d.mts +5 -5
- package/src/theme/utils/appFrameDocumentStyles.mjs +89 -89
- package/src/theme/utils/appFrameDocumentStyles.ts +5 -5
- package/src/theme/utils/applyTheme.test.ts +19 -0
- package/src/theme/utils/applyTheme.ts +59 -50
- package/src/theme/utils/cssVariables.ts +49 -49
- package/src/theme/utils/useInjectThemeIntoIframe.ts +40 -40
- package/src/utils/extensionIconMap.ts +123 -123
- package/src/utils/generalUtils.ts +157 -157
- package/src/writing/DraftAssistantDialog.tsx +279 -279
- package/src/writing/WritingStyleManager.tsx +416 -416
- package/src/writing/condenseWritingStyle.ts +103 -103
- package/src/writing/draftTextWithAgent.ts +134 -134
- package/src/writing/index.ts +39 -39
- package/src/writing/styleStore.ts +32 -32
- package/src/writing/writingStyle.test.ts +122 -122
- package/src/writing/writingStyle.ts +217 -217
|
@@ -1,1023 +1,1023 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PrintStylePaletteObservation,
|
|
3
|
-
PrintStyleProfile,
|
|
4
|
-
PrintStyleRule,
|
|
5
|
-
PrintStyleTypeObservation,
|
|
6
|
-
} from './types.js'
|
|
7
|
-
|
|
8
|
-
export const PRINT_TOKEN_EXTRACTION_PROMPT = `PRINT-TOKEN EXTRACTION PROMPT - v1.0
|
|
9
|
-
=========================================================================
|
|
10
|
-
Sent to a vision model alongside one or more reference images (a journal
|
|
11
|
-
page, a book spread, a typeset PDF, a cover). Produces a STYLE PROFILE that
|
|
12
|
-
the compiler turns into a print theme.json for the Pure Render system.
|
|
13
|
-
|
|
14
|
-
Pair this prompt with: print-style-profile.schema.json (the output shape).
|
|
15
|
-
=========================================================================
|
|
16
|
-
|
|
17
|
-
ROLE
|
|
18
|
-
You are a print design-system extractor. Given one or more reference images of
|
|
19
|
-
TYPESET PAGES, reverse-engineer a reusable visual system that could set NEW text
|
|
20
|
-
in the same family. You are not describing the image; you are inferring the rules
|
|
21
|
-
a typesetter followed.
|
|
22
|
-
|
|
23
|
-
DIVISION OF LABOR (critical)
|
|
24
|
-
- You judge classification, relationships, mood, and rules - what you are good at.
|
|
25
|
-
- You do NOT report exact colors or measurements as truth. A separate pixel pass
|
|
26
|
-
refines every numeric value. Give approx values as HINTS only.
|
|
27
|
-
- You NEVER name or identify a typeface. Report ATTRIBUTES that let a substitute be
|
|
28
|
-
chosen from a licensed library.
|
|
29
|
-
- Print-specific: estimate things in RELATIVE terms (ratios, proportions of the
|
|
30
|
-
page, x-heights relative to cap height) rather than absolute points, because you
|
|
31
|
-
cannot measure the trim size from pixels.
|
|
32
|
-
|
|
33
|
-
PER IMAGE, record OBSERVATIONS (what is literally present):
|
|
34
|
-
|
|
35
|
-
1. palette - print is restrained. By ROLE, not swatch:
|
|
36
|
-
- surface (paper/background), ink (body text), accent (rules/links/numbers - note
|
|
37
|
-
how sparingly used), and any secondary (captions, folios).
|
|
38
|
-
- For each: role, approx_hex (HINT), where_seen, coverage (dominant|frequent|sparse).
|
|
39
|
-
|
|
40
|
-
2. type - by ATTRIBUTE not identity, per ROLE you can see (display/headings, body,
|
|
41
|
-
caption/folio, mono/code):
|
|
42
|
-
- class (serif|sans|slab|script), weight, width, contrast/stress (thick-thin),
|
|
43
|
-
x_height (low|medium|high), case_usage, and the SERIF DETAIL if serif
|
|
44
|
-
(bracketed|slab|hairline|wedge) - this most determines the substitute.
|
|
45
|
-
- pairing: heading/body relationship, and size_contrast (dramatic|moderate|subtle)
|
|
46
|
-
- implies the scale ratio.
|
|
47
|
-
- type_scale: relative or estimated scale for title, chapter title, section
|
|
48
|
-
heading, body, caption, and footnote. Keep absolute point sizes low-confidence
|
|
49
|
-
unless the source provides a known trim/scale.
|
|
50
|
-
|
|
51
|
-
3. text setting - the heart of print. Estimate RELATIVELY:
|
|
52
|
-
- alignment (justified|flush-left), hyphenation (present|absent),
|
|
53
|
-
- measure: approx characters per line (count a representative full line),
|
|
54
|
-
- leading_feel (tight|normal|airy) - ratio of line gap to text size,
|
|
55
|
-
- paragraphing (indented runs | space-between | both),
|
|
56
|
-
- density (airy|balanced|dense).
|
|
57
|
-
|
|
58
|
-
4. page model - what you can infer about the page:
|
|
59
|
-
- columns (1|2|n),
|
|
60
|
-
- margin_generosity (tight|moderate|generous) and which margins are larger
|
|
61
|
-
(inner/outer/top/bottom asymmetry -> suggests recto/verso canon),
|
|
62
|
-
- running heads (present|absent, position), folio (position),
|
|
63
|
-
- figure/table treatment (captions above|below, rule style, numbering visible).
|
|
64
|
-
|
|
65
|
-
5. page furniture and ornaments - manuscript/book references often depend on
|
|
66
|
-
details outside body text. Capture these as structured observations:
|
|
67
|
-
- running_heads: presence, position, transform, rule/no rule, font_role.
|
|
68
|
-
- folio: position, style, suppress_on first/opener pages if visible.
|
|
69
|
-
- opener: chapter/opening spacing, recto_start, ornament, drop_cap.
|
|
70
|
-
- ornaments: none|rule|divider|fleuron|chapter-mark, weight, placement.
|
|
71
|
-
- table_treatment: rule_weight, header_treatment, row_rules, density.
|
|
72
|
-
- caption_treatment: position, label_style, alignment.
|
|
73
|
-
|
|
74
|
-
6. finish & mood - register (scholarly|literary|editorial|technical|...),
|
|
75
|
-
temperature, era. Subjective; only you can supply it.
|
|
76
|
-
|
|
77
|
-
THEN INFER THE RULES (the most important output)
|
|
78
|
-
List the constraints the setting obeys - what must stay true for a NEW page to
|
|
79
|
-
belong to this family.
|
|
80
|
-
|
|
81
|
-
REQUIREMENTS
|
|
82
|
-
- Cite evidence for every field (where in the image you saw it).
|
|
83
|
-
- Give confidence 0..1 per field. When unsure, LOWER confidence - do NOT guess to
|
|
84
|
-
fill a slot. A low-confidence field is dropped for a safe default; a confident
|
|
85
|
-
wrong guess corrupts the system.
|
|
86
|
-
- Output OBSERVATIONS and RULES only. Do NOT output final tokens or a theme - the
|
|
87
|
-
compiler derives those. Leave "derived": null.
|
|
88
|
-
- Flag bad exemplars: if a source is a poor reference, set exemplar_quality.usable=false
|
|
89
|
-
with a reason, so it is not faithfully reproduced.
|
|
90
|
-
- Multiple images: observations PER source id. Do NOT merge or average; the compiler
|
|
91
|
-
reconciles.
|
|
92
|
-
|
|
93
|
-
OUTPUT
|
|
94
|
-
Strict JSON conforming to print-style-profile.schema.json. No prose, no markdown,
|
|
95
|
-
no commentary outside the JSON.`
|
|
96
|
-
|
|
97
|
-
export const PRINT_STYLE_PROFILE_SCHEMA = {
|
|
98
|
-
type: 'object',
|
|
99
|
-
required: [
|
|
100
|
-
'schema_version',
|
|
101
|
-
'sources',
|
|
102
|
-
'exemplar_quality',
|
|
103
|
-
'observations',
|
|
104
|
-
'rules',
|
|
105
|
-
'derived',
|
|
106
|
-
],
|
|
107
|
-
properties: {
|
|
108
|
-
schema_version: { const: '1.0' },
|
|
109
|
-
sources: {
|
|
110
|
-
type: 'array',
|
|
111
|
-
items: {
|
|
112
|
-
type: 'object',
|
|
113
|
-
required: ['id'],
|
|
114
|
-
properties: { id: { type: 'string' }, note: { type: 'string' } },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
exemplar_quality: {
|
|
118
|
-
type: 'object',
|
|
119
|
-
required: ['usable', 'reason', 'confidence'],
|
|
120
|
-
properties: {
|
|
121
|
-
usable: { type: 'boolean' },
|
|
122
|
-
reason: { type: 'string' },
|
|
123
|
-
confidence: { type: 'number', minimum: 0, maximum: 1 },
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
observations: {
|
|
127
|
-
type: 'object',
|
|
128
|
-
properties: {
|
|
129
|
-
palette: {
|
|
130
|
-
type: 'array',
|
|
131
|
-
items: {
|
|
132
|
-
type: 'object',
|
|
133
|
-
required: ['role', 'confidence'],
|
|
134
|
-
properties: {
|
|
135
|
-
role: { enum: ['surface', 'ink', 'accent', 'secondary'] },
|
|
136
|
-
approx_hex: {
|
|
137
|
-
type: 'string',
|
|
138
|
-
description: 'HINT only; refined by pixel pass',
|
|
139
|
-
},
|
|
140
|
-
coverage: { enum: ['dominant', 'frequent', 'sparse'] },
|
|
141
|
-
where_seen: { type: 'string' },
|
|
142
|
-
confidence: { type: 'number' },
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
type: {
|
|
147
|
-
type: 'array',
|
|
148
|
-
items: {
|
|
149
|
-
type: 'object',
|
|
150
|
-
required: ['role', 'class', 'confidence'],
|
|
151
|
-
properties: {
|
|
152
|
-
role: { enum: ['display', 'body', 'caption', 'mono'] },
|
|
153
|
-
class: { enum: ['serif', 'sans', 'slab', 'script'] },
|
|
154
|
-
mood: {
|
|
155
|
-
enum: [
|
|
156
|
-
'old-style',
|
|
157
|
-
'transitional',
|
|
158
|
-
'slab',
|
|
159
|
-
'humanist-sans',
|
|
160
|
-
'grotesque-sans',
|
|
161
|
-
'geometric-sans',
|
|
162
|
-
'mono',
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
serif_detail: {
|
|
166
|
-
enum: ['bracketed', 'slab', 'hairline', 'wedge', 'none'],
|
|
167
|
-
},
|
|
168
|
-
weight: { type: 'string' },
|
|
169
|
-
width: { type: 'string' },
|
|
170
|
-
stress: { enum: ['high-contrast', 'moderate', 'low-contrast'] },
|
|
171
|
-
x_height: { enum: ['low', 'medium', 'high'] },
|
|
172
|
-
case_usage: { type: 'string' },
|
|
173
|
-
where_seen: { type: 'string' },
|
|
174
|
-
confidence: { type: 'number' },
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
type_pairing: {
|
|
179
|
-
type: 'object',
|
|
180
|
-
properties: {
|
|
181
|
-
size_contrast: { enum: ['dramatic', 'moderate', 'subtle'] },
|
|
182
|
-
note: { type: 'string' },
|
|
183
|
-
confidence: { type: 'number' },
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
type_scale: {
|
|
187
|
-
type: 'object',
|
|
188
|
-
properties: {
|
|
189
|
-
body_pt: { type: 'number' },
|
|
190
|
-
title_scale: { type: 'number' },
|
|
191
|
-
chapter_title_scale: { type: 'number' },
|
|
192
|
-
section_scale: { type: 'number' },
|
|
193
|
-
caption_scale: { type: 'number' },
|
|
194
|
-
footnote_scale: { type: 'number' },
|
|
195
|
-
confidence: { type: 'number' },
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
text_setting: {
|
|
199
|
-
type: 'object',
|
|
200
|
-
properties: {
|
|
201
|
-
alignment: { enum: ['justified', 'flush-left'] },
|
|
202
|
-
hyphenation: { enum: ['present', 'absent'] },
|
|
203
|
-
measure_cpl: { type: 'number' },
|
|
204
|
-
leading_feel: { enum: ['tight', 'normal', 'airy'] },
|
|
205
|
-
paragraphing: { enum: ['indented', 'spaced', 'both'] },
|
|
206
|
-
density: { enum: ['airy', 'balanced', 'dense'] },
|
|
207
|
-
confidence: { type: 'number' },
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
page_model: {
|
|
211
|
-
type: 'object',
|
|
212
|
-
properties: {
|
|
213
|
-
columns: { type: 'integer' },
|
|
214
|
-
margin_generosity: { enum: ['tight', 'moderate', 'generous'] },
|
|
215
|
-
margin_asymmetry: { type: 'string' },
|
|
216
|
-
running_heads: { enum: ['present', 'absent'] },
|
|
217
|
-
folio_position: { type: 'string' },
|
|
218
|
-
caption_position: { enum: ['above', 'below'] },
|
|
219
|
-
confidence: { type: 'number' },
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
running_heads: {
|
|
223
|
-
type: 'object',
|
|
224
|
-
properties: {
|
|
225
|
-
presence: { enum: ['present', 'absent'] },
|
|
226
|
-
position: { type: 'string' },
|
|
227
|
-
transform: {
|
|
228
|
-
enum: ['uppercase', 'small-caps', 'title-case', 'none'],
|
|
229
|
-
},
|
|
230
|
-
rule: { enum: ['none', 'hairline', 'solid'] },
|
|
231
|
-
font_role: {
|
|
232
|
-
enum: ['display', 'body', 'caption', 'sans'],
|
|
233
|
-
},
|
|
234
|
-
evidence: { type: 'string' },
|
|
235
|
-
confidence: { type: 'number' },
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
folio: {
|
|
239
|
-
type: 'object',
|
|
240
|
-
properties: {
|
|
241
|
-
position: { type: 'string' },
|
|
242
|
-
style: { enum: ['plain', 'small-caps', 'bracketed'] },
|
|
243
|
-
suppress_on: {
|
|
244
|
-
type: 'array',
|
|
245
|
-
items: { type: 'string' },
|
|
246
|
-
},
|
|
247
|
-
evidence: { type: 'string' },
|
|
248
|
-
confidence: { type: 'number' },
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
opener: {
|
|
252
|
-
type: 'object',
|
|
253
|
-
properties: {
|
|
254
|
-
spacing: { enum: ['compact', 'balanced', 'generous'] },
|
|
255
|
-
recto_start: { type: 'boolean' },
|
|
256
|
-
ornament: {
|
|
257
|
-
enum: ['none', 'rule', 'divider', 'fleuron', 'chapter-mark'],
|
|
258
|
-
},
|
|
259
|
-
drop_cap: { type: 'boolean' },
|
|
260
|
-
evidence: { type: 'string' },
|
|
261
|
-
confidence: { type: 'number' },
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
ornaments: {
|
|
265
|
-
type: 'object',
|
|
266
|
-
properties: {
|
|
267
|
-
kind: {
|
|
268
|
-
enum: ['none', 'rule', 'divider', 'fleuron', 'chapter-mark'],
|
|
269
|
-
},
|
|
270
|
-
weight: { enum: ['hairline', 'light', 'medium'] },
|
|
271
|
-
placement: { type: 'string' },
|
|
272
|
-
evidence: { type: 'string' },
|
|
273
|
-
confidence: { type: 'number' },
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
table_treatment: {
|
|
277
|
-
type: 'object',
|
|
278
|
-
properties: {
|
|
279
|
-
rule_weight: { enum: ['hairline', 'light', 'medium'] },
|
|
280
|
-
header_treatment: { type: 'string' },
|
|
281
|
-
row_rules: { enum: ['none', 'hairline', 'light'] },
|
|
282
|
-
density: { enum: ['airy', 'balanced', 'dense'] },
|
|
283
|
-
evidence: { type: 'string' },
|
|
284
|
-
confidence: { type: 'number' },
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
caption_treatment: {
|
|
288
|
-
type: 'object',
|
|
289
|
-
properties: {
|
|
290
|
-
position: { enum: ['above', 'below'] },
|
|
291
|
-
label_style: { enum: ['bold', 'small-caps', 'plain'] },
|
|
292
|
-
alignment: { enum: ['left', 'center', 'justified'] },
|
|
293
|
-
evidence: { type: 'string' },
|
|
294
|
-
confidence: { type: 'number' },
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
finish: {
|
|
298
|
-
type: 'object',
|
|
299
|
-
properties: {
|
|
300
|
-
register: { type: 'string' },
|
|
301
|
-
temperature: { type: 'string' },
|
|
302
|
-
era: { type: 'string' },
|
|
303
|
-
confidence: { type: 'number' },
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
rules: {
|
|
309
|
-
type: 'array',
|
|
310
|
-
items: {
|
|
311
|
-
type: 'object',
|
|
312
|
-
required: ['rule', 'confidence'],
|
|
313
|
-
properties: {
|
|
314
|
-
rule: { type: 'string' },
|
|
315
|
-
evidence: { type: 'string' },
|
|
316
|
-
confidence: { type: 'number' },
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
derived: {
|
|
321
|
-
type: ['object', 'null'],
|
|
322
|
-
description: 'compiler-only; model leaves null',
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
} as const
|
|
326
|
-
|
|
327
|
-
export interface PrintStyleProfilePromptInput {
|
|
328
|
-
mode: 'image' | 'description'
|
|
329
|
-
concerns?: string
|
|
330
|
-
imageName?: string
|
|
331
|
-
imageMimeType?: string
|
|
332
|
-
imagePixelSummary?: string
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function buildPrintStyleProfilePrompt(
|
|
336
|
-
input: PrintStyleProfilePromptInput,
|
|
337
|
-
): string {
|
|
338
|
-
const reference =
|
|
339
|
-
input.mode === 'image'
|
|
340
|
-
? [
|
|
341
|
-
`Reference image: ${input.imageName ?? 'uploaded image'}`,
|
|
342
|
-
input.imageMimeType ? `MIME type: ${input.imageMimeType}` : '',
|
|
343
|
-
input.imagePixelSummary
|
|
344
|
-
? `Pixel pass summary: ${input.imagePixelSummary}`
|
|
345
|
-
: 'Use the attached image as the visual source.',
|
|
346
|
-
]
|
|
347
|
-
.filter(Boolean)
|
|
348
|
-
.join('\n')
|
|
349
|
-
: [
|
|
350
|
-
'Text-only design brief. This is valid style evidence.',
|
|
351
|
-
'Do not reject the exemplar merely because no image is attached.',
|
|
352
|
-
'Infer the print style profile from the user concerns and keep visual-only fields low-confidence.',
|
|
353
|
-
].join('\n')
|
|
354
|
-
|
|
355
|
-
return [
|
|
356
|
-
PRINT_TOKEN_EXTRACTION_PROMPT,
|
|
357
|
-
'',
|
|
358
|
-
'JSON SCHEMA',
|
|
359
|
-
JSON.stringify(PRINT_STYLE_PROFILE_SCHEMA, null, 2),
|
|
360
|
-
'',
|
|
361
|
-
'USER DESIGN INPUT',
|
|
362
|
-
reference,
|
|
363
|
-
`Concerns/preferences: ${input.concerns?.trim() || 'none supplied'}`,
|
|
364
|
-
'',
|
|
365
|
-
'Return only one JSON object matching the schema.',
|
|
366
|
-
].join('\n')
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function parsePrintStyleProfileResponse(
|
|
370
|
-
text: string,
|
|
371
|
-
): PrintStyleProfile {
|
|
372
|
-
const parsed = JSON.parse(extractJsonObject(text)) as unknown
|
|
373
|
-
if (!isPrintStyleProfile(parsed)) {
|
|
374
|
-
throw new Error(
|
|
375
|
-
'Model response did not match the print style profile schema',
|
|
376
|
-
)
|
|
377
|
-
}
|
|
378
|
-
return parsed
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function reconcilePrintStyleProfiles(
|
|
382
|
-
profiles: readonly PrintStyleProfile[],
|
|
383
|
-
): PrintStyleProfile {
|
|
384
|
-
const usable = profiles.filter(profile => profile.exemplar_quality.usable)
|
|
385
|
-
const inputs = usable.length > 0 ? usable : profiles
|
|
386
|
-
if (inputs.length === 0) {
|
|
387
|
-
throw new Error('No print style profiles to reconcile')
|
|
388
|
-
}
|
|
389
|
-
if (inputs.length === 1) return inputs[0]!
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
schema_version: '1.0',
|
|
393
|
-
sources: inputs.flatMap(profile => profile.sources ?? []),
|
|
394
|
-
exemplar_quality: {
|
|
395
|
-
usable: usable.length > 0,
|
|
396
|
-
reason:
|
|
397
|
-
usable.length === profiles.length
|
|
398
|
-
? 'reconciled multiple usable print references'
|
|
399
|
-
: 'reconciled usable print references; discarded unusable exemplars',
|
|
400
|
-
confidence: average(
|
|
401
|
-
inputs.map(profile => profile.exemplar_quality.confidence),
|
|
402
|
-
),
|
|
403
|
-
},
|
|
404
|
-
observations: {
|
|
405
|
-
palette: bestByRole(
|
|
406
|
-
inputs.flatMap(profile => profile.observations.palette ?? []),
|
|
407
|
-
),
|
|
408
|
-
type: bestByRole(
|
|
409
|
-
inputs.flatMap(profile => profile.observations.type ?? []),
|
|
410
|
-
),
|
|
411
|
-
type_pairing: bestByConfidence(
|
|
412
|
-
inputs.map(profile => profile.observations.type_pairing),
|
|
413
|
-
),
|
|
414
|
-
type_scale: bestByConfidence(
|
|
415
|
-
inputs.map(profile => profile.observations.type_scale),
|
|
416
|
-
),
|
|
417
|
-
text_setting: bestByConfidence(
|
|
418
|
-
inputs.map(profile => profile.observations.text_setting),
|
|
419
|
-
),
|
|
420
|
-
page_model: bestByConfidence(
|
|
421
|
-
inputs.map(profile => profile.observations.page_model),
|
|
422
|
-
),
|
|
423
|
-
running_heads: bestByConfidence(
|
|
424
|
-
inputs.map(profile => profile.observations.running_heads),
|
|
425
|
-
),
|
|
426
|
-
folio: bestByConfidence(
|
|
427
|
-
inputs.map(profile => profile.observations.folio),
|
|
428
|
-
),
|
|
429
|
-
opener: bestByConfidence(
|
|
430
|
-
inputs.map(profile => profile.observations.opener),
|
|
431
|
-
),
|
|
432
|
-
ornaments: bestByConfidence(
|
|
433
|
-
inputs.map(profile => profile.observations.ornaments),
|
|
434
|
-
),
|
|
435
|
-
table_treatment: bestByConfidence(
|
|
436
|
-
inputs.map(profile => profile.observations.table_treatment),
|
|
437
|
-
),
|
|
438
|
-
caption_treatment: bestByConfidence(
|
|
439
|
-
inputs.map(profile => profile.observations.caption_treatment),
|
|
440
|
-
),
|
|
441
|
-
finish: bestByConfidence(
|
|
442
|
-
inputs.map(profile => profile.observations.finish),
|
|
443
|
-
),
|
|
444
|
-
},
|
|
445
|
-
rules: [
|
|
446
|
-
...reconcileRules(inputs.flatMap(profile => profile.rules ?? [])),
|
|
447
|
-
...detectReconciliationConflicts(inputs),
|
|
448
|
-
],
|
|
449
|
-
derived: null,
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export function isPrintStyleProfile(
|
|
454
|
-
value: unknown,
|
|
455
|
-
): value is PrintStyleProfile {
|
|
456
|
-
if (!value || typeof value !== 'object') return false
|
|
457
|
-
const candidate = value as Partial<PrintStyleProfile>
|
|
458
|
-
return (
|
|
459
|
-
typeof candidate.schema_version === 'string' &&
|
|
460
|
-
Boolean(candidate.observations) &&
|
|
461
|
-
typeof candidate.observations === 'object' &&
|
|
462
|
-
Boolean(candidate.exemplar_quality) &&
|
|
463
|
-
typeof candidate.exemplar_quality === 'object' &&
|
|
464
|
-
typeof candidate.exemplar_quality.usable === 'boolean' &&
|
|
465
|
-
typeof candidate.exemplar_quality.reason === 'string' &&
|
|
466
|
-
typeof candidate.exemplar_quality.confidence === 'number' &&
|
|
467
|
-
Array.isArray(candidate.rules) &&
|
|
468
|
-
candidate.derived === null
|
|
469
|
-
)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export interface PrintReferenceImageSample {
|
|
473
|
-
paper: string
|
|
474
|
-
ink: string
|
|
475
|
-
accent: string
|
|
476
|
-
accentConfidence: number
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export async function samplePrintReferenceImageColors(
|
|
480
|
-
image: File,
|
|
481
|
-
): Promise<PrintReferenceImageSample> {
|
|
482
|
-
const bitmap = await createImageBitmap(image)
|
|
483
|
-
try {
|
|
484
|
-
const canvas = document.createElement('canvas')
|
|
485
|
-
canvas.width = 80
|
|
486
|
-
canvas.height = 80
|
|
487
|
-
const context = canvas.getContext('2d')
|
|
488
|
-
if (!context) return defaultPrintReferenceImageSample()
|
|
489
|
-
|
|
490
|
-
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
|
|
491
|
-
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data
|
|
492
|
-
const buckets = new Map<
|
|
493
|
-
string,
|
|
494
|
-
{ rgb: [number, number, number]; count: number }
|
|
495
|
-
>()
|
|
496
|
-
for (let index = 0; index < pixels.length; index += 16) {
|
|
497
|
-
const alpha = pixels[index + 3]
|
|
498
|
-
if (alpha < 128) continue
|
|
499
|
-
const rgb: [number, number, number] = [
|
|
500
|
-
quantizeColor(pixels[index]),
|
|
501
|
-
quantizeColor(pixels[index + 1]),
|
|
502
|
-
quantizeColor(pixels[index + 2]),
|
|
503
|
-
]
|
|
504
|
-
const key = rgb.join(',')
|
|
505
|
-
const bucket = buckets.get(key)
|
|
506
|
-
if (bucket) bucket.count += 1
|
|
507
|
-
else buckets.set(key, { rgb, count: 1 })
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const ranked = Array.from(buckets.values()).sort(
|
|
511
|
-
(a, b) => b.count - a.count,
|
|
512
|
-
)
|
|
513
|
-
const light = ranked.find(bucket => colorLuminance(bucket.rgb) > 0.72)
|
|
514
|
-
const dark =
|
|
515
|
-
ranked
|
|
516
|
-
.filter(bucket => colorLuminance(bucket.rgb) < 0.36)
|
|
517
|
-
.sort((a, b) => b.count - a.count)[0] ?? ranked[ranked.length - 1]
|
|
518
|
-
const saturated = ranked
|
|
519
|
-
.filter(bucket => colorSaturation(bucket.rgb) > 0.22)
|
|
520
|
-
.sort(
|
|
521
|
-
(a, b) =>
|
|
522
|
-
colorSaturation(b.rgb) * b.count - colorSaturation(a.rgb) * a.count,
|
|
523
|
-
)[0]
|
|
524
|
-
|
|
525
|
-
return {
|
|
526
|
-
paper: rgbToHex(light?.rgb ?? [255, 255, 255]),
|
|
527
|
-
ink: rgbToHex(dark?.rgb ?? [26, 26, 26]),
|
|
528
|
-
accent: rgbToHex(saturated?.rgb ?? [70, 88, 75]),
|
|
529
|
-
accentConfidence: saturated ? 0.72 : 0.45,
|
|
530
|
-
}
|
|
531
|
-
} finally {
|
|
532
|
-
bitmap.close()
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
export function buildBookPrintStyleProfileFromReference({
|
|
537
|
-
source,
|
|
538
|
-
prompt,
|
|
539
|
-
sample,
|
|
540
|
-
}: {
|
|
541
|
-
source: string
|
|
542
|
-
prompt: string
|
|
543
|
-
sample?: PrintReferenceImageSample
|
|
544
|
-
}): PrintStyleProfile {
|
|
545
|
-
const text = prompt.toLowerCase()
|
|
546
|
-
const sans = text.includes('sans')
|
|
547
|
-
const dramatic = text.includes('dramatic') || text.includes('literary')
|
|
548
|
-
const airy = text.includes('airy') || text.includes('generous')
|
|
549
|
-
return {
|
|
550
|
-
schema_version: '1.0',
|
|
551
|
-
sources: [{ id: source, note: 'book print design reference' }],
|
|
552
|
-
exemplar_quality: {
|
|
553
|
-
usable: true,
|
|
554
|
-
reason: sample
|
|
555
|
-
? 'local pixel extraction from uploaded book reference image'
|
|
556
|
-
: 'book design concern inferred from description',
|
|
557
|
-
confidence: sample ? 0.78 : 0.68,
|
|
558
|
-
},
|
|
559
|
-
observations: {
|
|
560
|
-
palette: [
|
|
561
|
-
{
|
|
562
|
-
role: 'surface',
|
|
563
|
-
approx_hex:
|
|
564
|
-
sample?.paper ?? (text.includes('cream') ? '#FBFAF4' : '#FFFFFF'),
|
|
565
|
-
coverage: 'dominant',
|
|
566
|
-
where_seen: sample ? 'largest light color cluster' : 'description',
|
|
567
|
-
confidence: sample ? 0.82 : 0.64,
|
|
568
|
-
},
|
|
569
|
-
{
|
|
570
|
-
role: 'ink',
|
|
571
|
-
approx_hex: sample?.ink ?? '#1A1A1A',
|
|
572
|
-
coverage: 'frequent',
|
|
573
|
-
where_seen: sample ? 'dark text-like pixels' : 'description',
|
|
574
|
-
confidence: sample ? 0.76 : 0.64,
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
role: 'accent',
|
|
578
|
-
approx_hex:
|
|
579
|
-
sample?.accent ?? (text.includes('red') ? '#7A2634' : '#46584B'),
|
|
580
|
-
coverage: 'sparse',
|
|
581
|
-
where_seen: sample
|
|
582
|
-
? 'most saturated non-neutral cluster'
|
|
583
|
-
: 'description',
|
|
584
|
-
confidence: sample?.accentConfidence ?? 0.62,
|
|
585
|
-
},
|
|
586
|
-
],
|
|
587
|
-
type: [
|
|
588
|
-
{
|
|
589
|
-
role: 'display',
|
|
590
|
-
class: sans ? 'sans' : 'serif',
|
|
591
|
-
serif_detail: dramatic ? 'hairline' : 'bracketed',
|
|
592
|
-
confidence: 0.72,
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
role: 'body',
|
|
596
|
-
class: sans ? 'sans' : 'serif',
|
|
597
|
-
serif_detail: 'bracketed',
|
|
598
|
-
confidence: 0.72,
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
role: 'caption',
|
|
602
|
-
class: 'sans',
|
|
603
|
-
confidence: 0.7,
|
|
604
|
-
},
|
|
605
|
-
],
|
|
606
|
-
type_pairing: {
|
|
607
|
-
size_contrast: dramatic ? 'dramatic' : 'moderate',
|
|
608
|
-
confidence: 0.72,
|
|
609
|
-
},
|
|
610
|
-
type_scale: {
|
|
611
|
-
body_pt: dramatic ? 10.8 : 10.6,
|
|
612
|
-
title_scale: dramatic ? 2.6 : 2.2,
|
|
613
|
-
chapter_title_scale: dramatic ? 2.2 : 1.9,
|
|
614
|
-
section_scale: dramatic ? 1.5 : 1.35,
|
|
615
|
-
caption_scale: 0.82,
|
|
616
|
-
footnote_scale: 0.78,
|
|
617
|
-
confidence: sample ? 0.68 : 0.62,
|
|
618
|
-
},
|
|
619
|
-
text_setting: {
|
|
620
|
-
alignment: text.includes('flush') ? 'flush-left' : 'justified',
|
|
621
|
-
hyphenation: text.includes('no hyphen') ? 'absent' : 'present',
|
|
622
|
-
measure_cpl: text.includes('narrow') ? 54 : 58,
|
|
623
|
-
leading_feel: airy ? 'airy' : 'normal',
|
|
624
|
-
confidence: 0.7,
|
|
625
|
-
},
|
|
626
|
-
page_model: {
|
|
627
|
-
columns: 1,
|
|
628
|
-
margin_generosity: airy ? 'generous' : 'moderate',
|
|
629
|
-
margin_asymmetry: 'inner<outer',
|
|
630
|
-
running_heads: 'present',
|
|
631
|
-
folio_position: 'outside bottom',
|
|
632
|
-
caption_position: 'below',
|
|
633
|
-
confidence: 0.7,
|
|
634
|
-
},
|
|
635
|
-
running_heads: {
|
|
636
|
-
presence: 'present',
|
|
637
|
-
position: 'outer top',
|
|
638
|
-
transform: text.includes('uppercase') ? 'uppercase' : 'small-caps',
|
|
639
|
-
rule: text.includes('rule') || dramatic ? 'hairline' : 'none',
|
|
640
|
-
font_role: 'caption',
|
|
641
|
-
evidence: source,
|
|
642
|
-
confidence: 0.7,
|
|
643
|
-
},
|
|
644
|
-
folio: {
|
|
645
|
-
position: 'outside bottom',
|
|
646
|
-
style: 'plain',
|
|
647
|
-
suppress_on: ['title', 'opener'],
|
|
648
|
-
evidence: source,
|
|
649
|
-
confidence: 0.7,
|
|
650
|
-
},
|
|
651
|
-
opener: {
|
|
652
|
-
spacing: airy ? 'generous' : 'compact',
|
|
653
|
-
recto_start: true,
|
|
654
|
-
ornament: text.includes('ornament') || dramatic ? 'rule' : 'none',
|
|
655
|
-
drop_cap: false,
|
|
656
|
-
evidence: source,
|
|
657
|
-
confidence: 0.68,
|
|
658
|
-
},
|
|
659
|
-
ornaments: {
|
|
660
|
-
kind: text.includes('ornament') || dramatic ? 'rule' : 'none',
|
|
661
|
-
weight: 'hairline',
|
|
662
|
-
placement: 'chapter opener',
|
|
663
|
-
evidence: source,
|
|
664
|
-
confidence: 0.66,
|
|
665
|
-
},
|
|
666
|
-
table_treatment: {
|
|
667
|
-
rule_weight: 'hairline',
|
|
668
|
-
header_treatment: 'plain',
|
|
669
|
-
row_rules: 'header-only',
|
|
670
|
-
density: 'balanced',
|
|
671
|
-
evidence: source,
|
|
672
|
-
confidence: 0.7,
|
|
673
|
-
},
|
|
674
|
-
caption_treatment: {
|
|
675
|
-
position: 'below',
|
|
676
|
-
label_style: 'small-caps',
|
|
677
|
-
alignment: text.includes('left') ? 'left' : 'center',
|
|
678
|
-
evidence: source,
|
|
679
|
-
confidence: 0.7,
|
|
680
|
-
},
|
|
681
|
-
finish: {
|
|
682
|
-
register: 'literary',
|
|
683
|
-
confidence: 0.7,
|
|
684
|
-
},
|
|
685
|
-
},
|
|
686
|
-
rules: [
|
|
687
|
-
{
|
|
688
|
-
rule: 'emulate the uploaded book reference through safe Pure Render tokens',
|
|
689
|
-
evidence: source,
|
|
690
|
-
confidence: sample ? 0.78 : 0.68,
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
rule: prompt.trim() || 'use restrained long-form book treatment',
|
|
694
|
-
evidence: 'book design concern',
|
|
695
|
-
confidence: prompt.trim() ? 0.72 : 0.62,
|
|
696
|
-
},
|
|
697
|
-
],
|
|
698
|
-
derived: null,
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export function buildWriterPrintStyleProfileFromReference({
|
|
703
|
-
source,
|
|
704
|
-
prompt,
|
|
705
|
-
sample,
|
|
706
|
-
}: {
|
|
707
|
-
source: string
|
|
708
|
-
prompt: string
|
|
709
|
-
sample?: PrintReferenceImageSample
|
|
710
|
-
}): PrintStyleProfile {
|
|
711
|
-
const text = prompt.toLowerCase()
|
|
712
|
-
const sans = text.includes('sans') || text.includes('modern')
|
|
713
|
-
const airy = text.includes('airy') || text.includes('open')
|
|
714
|
-
const compact = text.includes('compact') || text.includes('dense')
|
|
715
|
-
const highContrast =
|
|
716
|
-
text.includes('high contrast') || text.includes('crisp')
|
|
717
|
-
const flush = !text.includes('justify')
|
|
718
|
-
return {
|
|
719
|
-
schema_version: '1.0',
|
|
720
|
-
sources: [{ id: source, note: 'writer print design reference' }],
|
|
721
|
-
exemplar_quality: {
|
|
722
|
-
usable: true,
|
|
723
|
-
reason: sample
|
|
724
|
-
? 'local pixel extraction from uploaded writer reference image'
|
|
725
|
-
: 'writer design concern inferred from description',
|
|
726
|
-
confidence: sample ? 0.74 : 0.66,
|
|
727
|
-
},
|
|
728
|
-
observations: {
|
|
729
|
-
palette: [
|
|
730
|
-
{
|
|
731
|
-
role: 'surface',
|
|
732
|
-
approx_hex:
|
|
733
|
-
sample?.paper ?? (text.includes('cream') ? '#FBFAF4' : '#FFFFFF'),
|
|
734
|
-
coverage: 'dominant',
|
|
735
|
-
where_seen: sample ? 'largest light color cluster' : 'description',
|
|
736
|
-
confidence: sample ? 0.78 : 0.62,
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
role: 'ink',
|
|
740
|
-
approx_hex: sample?.ink ?? (highContrast ? '#111111' : '#202020'),
|
|
741
|
-
coverage: 'frequent',
|
|
742
|
-
where_seen: sample ? 'dark text-like pixels' : 'description',
|
|
743
|
-
confidence: sample ? 0.74 : 0.64,
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
role: 'accent',
|
|
747
|
-
approx_hex:
|
|
748
|
-
sample?.accent ?? (text.includes('blue') ? '#335f8f' : '#5f7f6c'),
|
|
749
|
-
coverage: 'sparse',
|
|
750
|
-
where_seen: sample
|
|
751
|
-
? 'most saturated non-neutral cluster'
|
|
752
|
-
: 'description',
|
|
753
|
-
confidence: sample?.accentConfidence ?? 0.62,
|
|
754
|
-
},
|
|
755
|
-
],
|
|
756
|
-
type: [
|
|
757
|
-
{
|
|
758
|
-
role: 'display',
|
|
759
|
-
class: sans ? 'sans' : 'serif',
|
|
760
|
-
serif_detail: 'bracketed',
|
|
761
|
-
confidence: 0.68,
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
role: 'body',
|
|
765
|
-
class: sans ? 'sans' : 'serif',
|
|
766
|
-
serif_detail: 'bracketed',
|
|
767
|
-
confidence: 0.7,
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
role: 'caption',
|
|
771
|
-
class: 'sans',
|
|
772
|
-
confidence: 0.68,
|
|
773
|
-
},
|
|
774
|
-
],
|
|
775
|
-
type_pairing: {
|
|
776
|
-
size_contrast: compact ? 'subtle' : 'moderate',
|
|
777
|
-
confidence: 0.66,
|
|
778
|
-
},
|
|
779
|
-
type_scale: {
|
|
780
|
-
body_pt: compact ? 10.2 : 10.8,
|
|
781
|
-
title_scale: compact ? 1.75 : 2,
|
|
782
|
-
section_scale: compact ? 1.2 : 1.28,
|
|
783
|
-
caption_scale: 0.82,
|
|
784
|
-
footnote_scale: 0.78,
|
|
785
|
-
confidence: sample ? 0.64 : 0.6,
|
|
786
|
-
},
|
|
787
|
-
text_setting: {
|
|
788
|
-
alignment: flush ? 'flush-left' : 'justified',
|
|
789
|
-
hyphenation: flush ? 'absent' : 'present',
|
|
790
|
-
measure_cpl: compact ? 68 : 62,
|
|
791
|
-
leading_feel: airy ? 'airy' : compact ? 'tight' : 'normal',
|
|
792
|
-
confidence: 0.68,
|
|
793
|
-
},
|
|
794
|
-
page_model: {
|
|
795
|
-
columns: 1,
|
|
796
|
-
margin_generosity: airy ? 'generous' : 'moderate',
|
|
797
|
-
margin_asymmetry: 'symmetric',
|
|
798
|
-
running_heads: 'absent',
|
|
799
|
-
folio_position: 'bottom center',
|
|
800
|
-
caption_position: 'below',
|
|
801
|
-
confidence: 0.62,
|
|
802
|
-
},
|
|
803
|
-
table_treatment: {
|
|
804
|
-
rule_weight: 'hairline',
|
|
805
|
-
header_treatment: 'plain',
|
|
806
|
-
row_rules: 'header-only',
|
|
807
|
-
density: compact ? 'compact' : 'balanced',
|
|
808
|
-
evidence: source,
|
|
809
|
-
confidence: 0.66,
|
|
810
|
-
},
|
|
811
|
-
caption_treatment: {
|
|
812
|
-
position: 'below',
|
|
813
|
-
label_style: 'plain',
|
|
814
|
-
alignment: 'left',
|
|
815
|
-
evidence: source,
|
|
816
|
-
confidence: 0.66,
|
|
817
|
-
},
|
|
818
|
-
finish: {
|
|
819
|
-
register: 'draft',
|
|
820
|
-
confidence: 0.72,
|
|
821
|
-
},
|
|
822
|
-
},
|
|
823
|
-
rules: [
|
|
824
|
-
{
|
|
825
|
-
rule: 'emulate the uploaded writer reference through safe Pure Render tokens',
|
|
826
|
-
evidence: source,
|
|
827
|
-
confidence: sample ? 0.74 : 0.66,
|
|
828
|
-
},
|
|
829
|
-
{
|
|
830
|
-
rule:
|
|
831
|
-
prompt.trim() ||
|
|
832
|
-
'keep writer output simple, readable, and free of manuscript noise',
|
|
833
|
-
evidence: 'writer design concern',
|
|
834
|
-
confidence: prompt.trim() ? 0.68 : 0.62,
|
|
835
|
-
},
|
|
836
|
-
],
|
|
837
|
-
derived: null,
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function extractJsonObject(text: string): string {
|
|
842
|
-
const trimmed = text.trim()
|
|
843
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) return trimmed
|
|
844
|
-
const start = trimmed.indexOf('{')
|
|
845
|
-
const end = trimmed.lastIndexOf('}')
|
|
846
|
-
if (start === -1 || end === -1 || end <= start) {
|
|
847
|
-
throw new Error('Model response did not contain a JSON object')
|
|
848
|
-
}
|
|
849
|
-
return trimmed.slice(start, end + 1)
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function bestByRole<
|
|
853
|
-
T extends PrintStylePaletteObservation | PrintStyleTypeObservation,
|
|
854
|
-
>(items: readonly T[]): T[] {
|
|
855
|
-
const byRole = new Map<string, T>()
|
|
856
|
-
for (const item of items) {
|
|
857
|
-
const existing = byRole.get(item.role)
|
|
858
|
-
if (!existing || item.confidence > existing.confidence) {
|
|
859
|
-
byRole.set(item.role, item)
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
return Array.from(byRole.values())
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function bestByConfidence<T extends { confidence?: number }>(
|
|
866
|
-
items: readonly (T | undefined)[],
|
|
867
|
-
): T | undefined {
|
|
868
|
-
return items
|
|
869
|
-
.filter((item): item is T => Boolean(item))
|
|
870
|
-
.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))[0]
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
function reconcileRules(rules: readonly PrintStyleRule[]): PrintStyleRule[] {
|
|
874
|
-
const byRule = new Map<string, PrintStyleRule>()
|
|
875
|
-
for (const rule of rules) {
|
|
876
|
-
const key = rule.rule.trim().toLowerCase()
|
|
877
|
-
if (!key) continue
|
|
878
|
-
const existing = byRule.get(key)
|
|
879
|
-
if (!existing || rule.confidence > existing.confidence) {
|
|
880
|
-
byRule.set(key, rule)
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return Array.from(byRule.values()).sort((a, b) => b.confidence - a.confidence)
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function detectReconciliationConflicts(
|
|
887
|
-
profiles: readonly PrintStyleProfile[],
|
|
888
|
-
): PrintStyleRule[] {
|
|
889
|
-
const conflicts: PrintStyleRule[] = []
|
|
890
|
-
const addConflict = (label: string, values: readonly FieldValue[]): void => {
|
|
891
|
-
const confident = values.filter(
|
|
892
|
-
item => item.value !== undefined && item.value !== '' && item.confidence >= 0.6,
|
|
893
|
-
)
|
|
894
|
-
const unique = Array.from(
|
|
895
|
-
new Map(confident.map(item => [String(item.value), item])).values(),
|
|
896
|
-
)
|
|
897
|
-
if (unique.length < 2) return
|
|
898
|
-
conflicts.push({
|
|
899
|
-
rule: `Reference conflict: ${label} disagrees across uploaded images (${unique
|
|
900
|
-
.map(item => `${item.source}: ${item.value}`)
|
|
901
|
-
.join('; ')}). Highest-confidence value was used; review before saving as a shared theme.`,
|
|
902
|
-
evidence: unique.map(item => item.source).join(', '),
|
|
903
|
-
confidence: Math.min(0.9, average(unique.map(item => item.confidence))),
|
|
904
|
-
})
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const sourceId = (profile: PrintStyleProfile, index: number): string =>
|
|
908
|
-
profile.sources?.[0]?.id ?? `source-${index + 1}`
|
|
909
|
-
|
|
910
|
-
addConflict(
|
|
911
|
-
'body font class',
|
|
912
|
-
profiles.map((profile, index) => ({
|
|
913
|
-
source: sourceId(profile, index),
|
|
914
|
-
value: profile.observations.type?.find(item => item.role === 'body')?.class,
|
|
915
|
-
confidence:
|
|
916
|
-
profile.observations.type?.find(item => item.role === 'body')?.confidence ??
|
|
917
|
-
0,
|
|
918
|
-
})),
|
|
919
|
-
)
|
|
920
|
-
addConflict(
|
|
921
|
-
'accent color',
|
|
922
|
-
profiles.map((profile, index) => ({
|
|
923
|
-
source: sourceId(profile, index),
|
|
924
|
-
value: profile.observations.palette?.find(item => item.role === 'accent')
|
|
925
|
-
?.approx_hex,
|
|
926
|
-
confidence:
|
|
927
|
-
profile.observations.palette?.find(item => item.role === 'accent')
|
|
928
|
-
?.confidence ?? 0,
|
|
929
|
-
})),
|
|
930
|
-
)
|
|
931
|
-
addConflict(
|
|
932
|
-
'text alignment',
|
|
933
|
-
profiles.map((profile, index) => ({
|
|
934
|
-
source: sourceId(profile, index),
|
|
935
|
-
value: profile.observations.text_setting?.alignment,
|
|
936
|
-
confidence: profile.observations.text_setting?.confidence ?? 0,
|
|
937
|
-
})),
|
|
938
|
-
)
|
|
939
|
-
addConflict(
|
|
940
|
-
'page columns',
|
|
941
|
-
profiles.map((profile, index) => ({
|
|
942
|
-
source: sourceId(profile, index),
|
|
943
|
-
value: profile.observations.page_model?.columns,
|
|
944
|
-
confidence: profile.observations.page_model?.confidence ?? 0,
|
|
945
|
-
})),
|
|
946
|
-
)
|
|
947
|
-
addConflict(
|
|
948
|
-
'running heads',
|
|
949
|
-
profiles.map((profile, index) => ({
|
|
950
|
-
source: sourceId(profile, index),
|
|
951
|
-
value: profile.observations.running_heads?.presence,
|
|
952
|
-
confidence: profile.observations.running_heads?.confidence ?? 0,
|
|
953
|
-
})),
|
|
954
|
-
)
|
|
955
|
-
addConflict(
|
|
956
|
-
'opener ornament',
|
|
957
|
-
profiles.map((profile, index) => ({
|
|
958
|
-
source: sourceId(profile, index),
|
|
959
|
-
value: profile.observations.opener?.ornament,
|
|
960
|
-
confidence: profile.observations.opener?.confidence ?? 0,
|
|
961
|
-
})),
|
|
962
|
-
)
|
|
963
|
-
addConflict(
|
|
964
|
-
'table density',
|
|
965
|
-
profiles.map((profile, index) => ({
|
|
966
|
-
source: sourceId(profile, index),
|
|
967
|
-
value: profile.observations.table_treatment?.density,
|
|
968
|
-
confidence: profile.observations.table_treatment?.confidence ?? 0,
|
|
969
|
-
})),
|
|
970
|
-
)
|
|
971
|
-
addConflict(
|
|
972
|
-
'caption position',
|
|
973
|
-
profiles.map((profile, index) => ({
|
|
974
|
-
source: sourceId(profile, index),
|
|
975
|
-
value: profile.observations.caption_treatment?.position,
|
|
976
|
-
confidence: profile.observations.caption_treatment?.confidence ?? 0,
|
|
977
|
-
})),
|
|
978
|
-
)
|
|
979
|
-
|
|
980
|
-
return conflicts
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
interface FieldValue {
|
|
984
|
-
source: string
|
|
985
|
-
value: string | number | undefined
|
|
986
|
-
confidence: number
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
function average(values: readonly number[]): number {
|
|
990
|
-
if (values.length === 0) return 0
|
|
991
|
-
return values.reduce((sum, value) => sum + value, 0) / values.length
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
function defaultPrintReferenceImageSample(): PrintReferenceImageSample {
|
|
995
|
-
return {
|
|
996
|
-
paper: '#FFFFFF',
|
|
997
|
-
ink: '#1A1A1A',
|
|
998
|
-
accent: '#46584B',
|
|
999
|
-
accentConfidence: 0.45,
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
function quantizeColor(value: number | undefined): number {
|
|
1004
|
-
return Math.round((value ?? 0) / 24) * 24
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
function rgbToHex(rgb: readonly number[]): string {
|
|
1008
|
-
return `#${rgb
|
|
1009
|
-
.map(value =>
|
|
1010
|
-
Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0'),
|
|
1011
|
-
)
|
|
1012
|
-
.join('')}`
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function colorLuminance([r, g, b]: readonly number[]): number {
|
|
1016
|
-
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
function colorSaturation([r, g, b]: readonly number[]): number {
|
|
1020
|
-
const max = Math.max(r, g, b)
|
|
1021
|
-
const min = Math.min(r, g, b)
|
|
1022
|
-
return max === 0 ? 0 : (max - min) / max
|
|
1023
|
-
}
|
|
1
|
+
import type {
|
|
2
|
+
PrintStylePaletteObservation,
|
|
3
|
+
PrintStyleProfile,
|
|
4
|
+
PrintStyleRule,
|
|
5
|
+
PrintStyleTypeObservation,
|
|
6
|
+
} from './types.js'
|
|
7
|
+
|
|
8
|
+
export const PRINT_TOKEN_EXTRACTION_PROMPT = `PRINT-TOKEN EXTRACTION PROMPT - v1.0
|
|
9
|
+
=========================================================================
|
|
10
|
+
Sent to a vision model alongside one or more reference images (a journal
|
|
11
|
+
page, a book spread, a typeset PDF, a cover). Produces a STYLE PROFILE that
|
|
12
|
+
the compiler turns into a print theme.json for the Pure Render system.
|
|
13
|
+
|
|
14
|
+
Pair this prompt with: print-style-profile.schema.json (the output shape).
|
|
15
|
+
=========================================================================
|
|
16
|
+
|
|
17
|
+
ROLE
|
|
18
|
+
You are a print design-system extractor. Given one or more reference images of
|
|
19
|
+
TYPESET PAGES, reverse-engineer a reusable visual system that could set NEW text
|
|
20
|
+
in the same family. You are not describing the image; you are inferring the rules
|
|
21
|
+
a typesetter followed.
|
|
22
|
+
|
|
23
|
+
DIVISION OF LABOR (critical)
|
|
24
|
+
- You judge classification, relationships, mood, and rules - what you are good at.
|
|
25
|
+
- You do NOT report exact colors or measurements as truth. A separate pixel pass
|
|
26
|
+
refines every numeric value. Give approx values as HINTS only.
|
|
27
|
+
- You NEVER name or identify a typeface. Report ATTRIBUTES that let a substitute be
|
|
28
|
+
chosen from a licensed library.
|
|
29
|
+
- Print-specific: estimate things in RELATIVE terms (ratios, proportions of the
|
|
30
|
+
page, x-heights relative to cap height) rather than absolute points, because you
|
|
31
|
+
cannot measure the trim size from pixels.
|
|
32
|
+
|
|
33
|
+
PER IMAGE, record OBSERVATIONS (what is literally present):
|
|
34
|
+
|
|
35
|
+
1. palette - print is restrained. By ROLE, not swatch:
|
|
36
|
+
- surface (paper/background), ink (body text), accent (rules/links/numbers - note
|
|
37
|
+
how sparingly used), and any secondary (captions, folios).
|
|
38
|
+
- For each: role, approx_hex (HINT), where_seen, coverage (dominant|frequent|sparse).
|
|
39
|
+
|
|
40
|
+
2. type - by ATTRIBUTE not identity, per ROLE you can see (display/headings, body,
|
|
41
|
+
caption/folio, mono/code):
|
|
42
|
+
- class (serif|sans|slab|script), weight, width, contrast/stress (thick-thin),
|
|
43
|
+
x_height (low|medium|high), case_usage, and the SERIF DETAIL if serif
|
|
44
|
+
(bracketed|slab|hairline|wedge) - this most determines the substitute.
|
|
45
|
+
- pairing: heading/body relationship, and size_contrast (dramatic|moderate|subtle)
|
|
46
|
+
- implies the scale ratio.
|
|
47
|
+
- type_scale: relative or estimated scale for title, chapter title, section
|
|
48
|
+
heading, body, caption, and footnote. Keep absolute point sizes low-confidence
|
|
49
|
+
unless the source provides a known trim/scale.
|
|
50
|
+
|
|
51
|
+
3. text setting - the heart of print. Estimate RELATIVELY:
|
|
52
|
+
- alignment (justified|flush-left), hyphenation (present|absent),
|
|
53
|
+
- measure: approx characters per line (count a representative full line),
|
|
54
|
+
- leading_feel (tight|normal|airy) - ratio of line gap to text size,
|
|
55
|
+
- paragraphing (indented runs | space-between | both),
|
|
56
|
+
- density (airy|balanced|dense).
|
|
57
|
+
|
|
58
|
+
4. page model - what you can infer about the page:
|
|
59
|
+
- columns (1|2|n),
|
|
60
|
+
- margin_generosity (tight|moderate|generous) and which margins are larger
|
|
61
|
+
(inner/outer/top/bottom asymmetry -> suggests recto/verso canon),
|
|
62
|
+
- running heads (present|absent, position), folio (position),
|
|
63
|
+
- figure/table treatment (captions above|below, rule style, numbering visible).
|
|
64
|
+
|
|
65
|
+
5. page furniture and ornaments - manuscript/book references often depend on
|
|
66
|
+
details outside body text. Capture these as structured observations:
|
|
67
|
+
- running_heads: presence, position, transform, rule/no rule, font_role.
|
|
68
|
+
- folio: position, style, suppress_on first/opener pages if visible.
|
|
69
|
+
- opener: chapter/opening spacing, recto_start, ornament, drop_cap.
|
|
70
|
+
- ornaments: none|rule|divider|fleuron|chapter-mark, weight, placement.
|
|
71
|
+
- table_treatment: rule_weight, header_treatment, row_rules, density.
|
|
72
|
+
- caption_treatment: position, label_style, alignment.
|
|
73
|
+
|
|
74
|
+
6. finish & mood - register (scholarly|literary|editorial|technical|...),
|
|
75
|
+
temperature, era. Subjective; only you can supply it.
|
|
76
|
+
|
|
77
|
+
THEN INFER THE RULES (the most important output)
|
|
78
|
+
List the constraints the setting obeys - what must stay true for a NEW page to
|
|
79
|
+
belong to this family.
|
|
80
|
+
|
|
81
|
+
REQUIREMENTS
|
|
82
|
+
- Cite evidence for every field (where in the image you saw it).
|
|
83
|
+
- Give confidence 0..1 per field. When unsure, LOWER confidence - do NOT guess to
|
|
84
|
+
fill a slot. A low-confidence field is dropped for a safe default; a confident
|
|
85
|
+
wrong guess corrupts the system.
|
|
86
|
+
- Output OBSERVATIONS and RULES only. Do NOT output final tokens or a theme - the
|
|
87
|
+
compiler derives those. Leave "derived": null.
|
|
88
|
+
- Flag bad exemplars: if a source is a poor reference, set exemplar_quality.usable=false
|
|
89
|
+
with a reason, so it is not faithfully reproduced.
|
|
90
|
+
- Multiple images: observations PER source id. Do NOT merge or average; the compiler
|
|
91
|
+
reconciles.
|
|
92
|
+
|
|
93
|
+
OUTPUT
|
|
94
|
+
Strict JSON conforming to print-style-profile.schema.json. No prose, no markdown,
|
|
95
|
+
no commentary outside the JSON.`
|
|
96
|
+
|
|
97
|
+
export const PRINT_STYLE_PROFILE_SCHEMA = {
|
|
98
|
+
type: 'object',
|
|
99
|
+
required: [
|
|
100
|
+
'schema_version',
|
|
101
|
+
'sources',
|
|
102
|
+
'exemplar_quality',
|
|
103
|
+
'observations',
|
|
104
|
+
'rules',
|
|
105
|
+
'derived',
|
|
106
|
+
],
|
|
107
|
+
properties: {
|
|
108
|
+
schema_version: { const: '1.0' },
|
|
109
|
+
sources: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
items: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
required: ['id'],
|
|
114
|
+
properties: { id: { type: 'string' }, note: { type: 'string' } },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
exemplar_quality: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
required: ['usable', 'reason', 'confidence'],
|
|
120
|
+
properties: {
|
|
121
|
+
usable: { type: 'boolean' },
|
|
122
|
+
reason: { type: 'string' },
|
|
123
|
+
confidence: { type: 'number', minimum: 0, maximum: 1 },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
observations: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
palette: {
|
|
130
|
+
type: 'array',
|
|
131
|
+
items: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
required: ['role', 'confidence'],
|
|
134
|
+
properties: {
|
|
135
|
+
role: { enum: ['surface', 'ink', 'accent', 'secondary'] },
|
|
136
|
+
approx_hex: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'HINT only; refined by pixel pass',
|
|
139
|
+
},
|
|
140
|
+
coverage: { enum: ['dominant', 'frequent', 'sparse'] },
|
|
141
|
+
where_seen: { type: 'string' },
|
|
142
|
+
confidence: { type: 'number' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
type: {
|
|
147
|
+
type: 'array',
|
|
148
|
+
items: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
required: ['role', 'class', 'confidence'],
|
|
151
|
+
properties: {
|
|
152
|
+
role: { enum: ['display', 'body', 'caption', 'mono'] },
|
|
153
|
+
class: { enum: ['serif', 'sans', 'slab', 'script'] },
|
|
154
|
+
mood: {
|
|
155
|
+
enum: [
|
|
156
|
+
'old-style',
|
|
157
|
+
'transitional',
|
|
158
|
+
'slab',
|
|
159
|
+
'humanist-sans',
|
|
160
|
+
'grotesque-sans',
|
|
161
|
+
'geometric-sans',
|
|
162
|
+
'mono',
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
serif_detail: {
|
|
166
|
+
enum: ['bracketed', 'slab', 'hairline', 'wedge', 'none'],
|
|
167
|
+
},
|
|
168
|
+
weight: { type: 'string' },
|
|
169
|
+
width: { type: 'string' },
|
|
170
|
+
stress: { enum: ['high-contrast', 'moderate', 'low-contrast'] },
|
|
171
|
+
x_height: { enum: ['low', 'medium', 'high'] },
|
|
172
|
+
case_usage: { type: 'string' },
|
|
173
|
+
where_seen: { type: 'string' },
|
|
174
|
+
confidence: { type: 'number' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
type_pairing: {
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
size_contrast: { enum: ['dramatic', 'moderate', 'subtle'] },
|
|
182
|
+
note: { type: 'string' },
|
|
183
|
+
confidence: { type: 'number' },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
type_scale: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
body_pt: { type: 'number' },
|
|
190
|
+
title_scale: { type: 'number' },
|
|
191
|
+
chapter_title_scale: { type: 'number' },
|
|
192
|
+
section_scale: { type: 'number' },
|
|
193
|
+
caption_scale: { type: 'number' },
|
|
194
|
+
footnote_scale: { type: 'number' },
|
|
195
|
+
confidence: { type: 'number' },
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
text_setting: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {
|
|
201
|
+
alignment: { enum: ['justified', 'flush-left'] },
|
|
202
|
+
hyphenation: { enum: ['present', 'absent'] },
|
|
203
|
+
measure_cpl: { type: 'number' },
|
|
204
|
+
leading_feel: { enum: ['tight', 'normal', 'airy'] },
|
|
205
|
+
paragraphing: { enum: ['indented', 'spaced', 'both'] },
|
|
206
|
+
density: { enum: ['airy', 'balanced', 'dense'] },
|
|
207
|
+
confidence: { type: 'number' },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
page_model: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
columns: { type: 'integer' },
|
|
214
|
+
margin_generosity: { enum: ['tight', 'moderate', 'generous'] },
|
|
215
|
+
margin_asymmetry: { type: 'string' },
|
|
216
|
+
running_heads: { enum: ['present', 'absent'] },
|
|
217
|
+
folio_position: { type: 'string' },
|
|
218
|
+
caption_position: { enum: ['above', 'below'] },
|
|
219
|
+
confidence: { type: 'number' },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
running_heads: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
presence: { enum: ['present', 'absent'] },
|
|
226
|
+
position: { type: 'string' },
|
|
227
|
+
transform: {
|
|
228
|
+
enum: ['uppercase', 'small-caps', 'title-case', 'none'],
|
|
229
|
+
},
|
|
230
|
+
rule: { enum: ['none', 'hairline', 'solid'] },
|
|
231
|
+
font_role: {
|
|
232
|
+
enum: ['display', 'body', 'caption', 'sans'],
|
|
233
|
+
},
|
|
234
|
+
evidence: { type: 'string' },
|
|
235
|
+
confidence: { type: 'number' },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
folio: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
position: { type: 'string' },
|
|
242
|
+
style: { enum: ['plain', 'small-caps', 'bracketed'] },
|
|
243
|
+
suppress_on: {
|
|
244
|
+
type: 'array',
|
|
245
|
+
items: { type: 'string' },
|
|
246
|
+
},
|
|
247
|
+
evidence: { type: 'string' },
|
|
248
|
+
confidence: { type: 'number' },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
opener: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
spacing: { enum: ['compact', 'balanced', 'generous'] },
|
|
255
|
+
recto_start: { type: 'boolean' },
|
|
256
|
+
ornament: {
|
|
257
|
+
enum: ['none', 'rule', 'divider', 'fleuron', 'chapter-mark'],
|
|
258
|
+
},
|
|
259
|
+
drop_cap: { type: 'boolean' },
|
|
260
|
+
evidence: { type: 'string' },
|
|
261
|
+
confidence: { type: 'number' },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
ornaments: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
kind: {
|
|
268
|
+
enum: ['none', 'rule', 'divider', 'fleuron', 'chapter-mark'],
|
|
269
|
+
},
|
|
270
|
+
weight: { enum: ['hairline', 'light', 'medium'] },
|
|
271
|
+
placement: { type: 'string' },
|
|
272
|
+
evidence: { type: 'string' },
|
|
273
|
+
confidence: { type: 'number' },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
table_treatment: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
rule_weight: { enum: ['hairline', 'light', 'medium'] },
|
|
280
|
+
header_treatment: { type: 'string' },
|
|
281
|
+
row_rules: { enum: ['none', 'hairline', 'light'] },
|
|
282
|
+
density: { enum: ['airy', 'balanced', 'dense'] },
|
|
283
|
+
evidence: { type: 'string' },
|
|
284
|
+
confidence: { type: 'number' },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
caption_treatment: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {
|
|
290
|
+
position: { enum: ['above', 'below'] },
|
|
291
|
+
label_style: { enum: ['bold', 'small-caps', 'plain'] },
|
|
292
|
+
alignment: { enum: ['left', 'center', 'justified'] },
|
|
293
|
+
evidence: { type: 'string' },
|
|
294
|
+
confidence: { type: 'number' },
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
finish: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
register: { type: 'string' },
|
|
301
|
+
temperature: { type: 'string' },
|
|
302
|
+
era: { type: 'string' },
|
|
303
|
+
confidence: { type: 'number' },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
rules: {
|
|
309
|
+
type: 'array',
|
|
310
|
+
items: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
required: ['rule', 'confidence'],
|
|
313
|
+
properties: {
|
|
314
|
+
rule: { type: 'string' },
|
|
315
|
+
evidence: { type: 'string' },
|
|
316
|
+
confidence: { type: 'number' },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
derived: {
|
|
321
|
+
type: ['object', 'null'],
|
|
322
|
+
description: 'compiler-only; model leaves null',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
} as const
|
|
326
|
+
|
|
327
|
+
export interface PrintStyleProfilePromptInput {
|
|
328
|
+
mode: 'image' | 'description'
|
|
329
|
+
concerns?: string
|
|
330
|
+
imageName?: string
|
|
331
|
+
imageMimeType?: string
|
|
332
|
+
imagePixelSummary?: string
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function buildPrintStyleProfilePrompt(
|
|
336
|
+
input: PrintStyleProfilePromptInput,
|
|
337
|
+
): string {
|
|
338
|
+
const reference =
|
|
339
|
+
input.mode === 'image'
|
|
340
|
+
? [
|
|
341
|
+
`Reference image: ${input.imageName ?? 'uploaded image'}`,
|
|
342
|
+
input.imageMimeType ? `MIME type: ${input.imageMimeType}` : '',
|
|
343
|
+
input.imagePixelSummary
|
|
344
|
+
? `Pixel pass summary: ${input.imagePixelSummary}`
|
|
345
|
+
: 'Use the attached image as the visual source.',
|
|
346
|
+
]
|
|
347
|
+
.filter(Boolean)
|
|
348
|
+
.join('\n')
|
|
349
|
+
: [
|
|
350
|
+
'Text-only design brief. This is valid style evidence.',
|
|
351
|
+
'Do not reject the exemplar merely because no image is attached.',
|
|
352
|
+
'Infer the print style profile from the user concerns and keep visual-only fields low-confidence.',
|
|
353
|
+
].join('\n')
|
|
354
|
+
|
|
355
|
+
return [
|
|
356
|
+
PRINT_TOKEN_EXTRACTION_PROMPT,
|
|
357
|
+
'',
|
|
358
|
+
'JSON SCHEMA',
|
|
359
|
+
JSON.stringify(PRINT_STYLE_PROFILE_SCHEMA, null, 2),
|
|
360
|
+
'',
|
|
361
|
+
'USER DESIGN INPUT',
|
|
362
|
+
reference,
|
|
363
|
+
`Concerns/preferences: ${input.concerns?.trim() || 'none supplied'}`,
|
|
364
|
+
'',
|
|
365
|
+
'Return only one JSON object matching the schema.',
|
|
366
|
+
].join('\n')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function parsePrintStyleProfileResponse(
|
|
370
|
+
text: string,
|
|
371
|
+
): PrintStyleProfile {
|
|
372
|
+
const parsed = JSON.parse(extractJsonObject(text)) as unknown
|
|
373
|
+
if (!isPrintStyleProfile(parsed)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
'Model response did not match the print style profile schema',
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
return parsed
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function reconcilePrintStyleProfiles(
|
|
382
|
+
profiles: readonly PrintStyleProfile[],
|
|
383
|
+
): PrintStyleProfile {
|
|
384
|
+
const usable = profiles.filter(profile => profile.exemplar_quality.usable)
|
|
385
|
+
const inputs = usable.length > 0 ? usable : profiles
|
|
386
|
+
if (inputs.length === 0) {
|
|
387
|
+
throw new Error('No print style profiles to reconcile')
|
|
388
|
+
}
|
|
389
|
+
if (inputs.length === 1) return inputs[0]!
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
schema_version: '1.0',
|
|
393
|
+
sources: inputs.flatMap(profile => profile.sources ?? []),
|
|
394
|
+
exemplar_quality: {
|
|
395
|
+
usable: usable.length > 0,
|
|
396
|
+
reason:
|
|
397
|
+
usable.length === profiles.length
|
|
398
|
+
? 'reconciled multiple usable print references'
|
|
399
|
+
: 'reconciled usable print references; discarded unusable exemplars',
|
|
400
|
+
confidence: average(
|
|
401
|
+
inputs.map(profile => profile.exemplar_quality.confidence),
|
|
402
|
+
),
|
|
403
|
+
},
|
|
404
|
+
observations: {
|
|
405
|
+
palette: bestByRole(
|
|
406
|
+
inputs.flatMap(profile => profile.observations.palette ?? []),
|
|
407
|
+
),
|
|
408
|
+
type: bestByRole(
|
|
409
|
+
inputs.flatMap(profile => profile.observations.type ?? []),
|
|
410
|
+
),
|
|
411
|
+
type_pairing: bestByConfidence(
|
|
412
|
+
inputs.map(profile => profile.observations.type_pairing),
|
|
413
|
+
),
|
|
414
|
+
type_scale: bestByConfidence(
|
|
415
|
+
inputs.map(profile => profile.observations.type_scale),
|
|
416
|
+
),
|
|
417
|
+
text_setting: bestByConfidence(
|
|
418
|
+
inputs.map(profile => profile.observations.text_setting),
|
|
419
|
+
),
|
|
420
|
+
page_model: bestByConfidence(
|
|
421
|
+
inputs.map(profile => profile.observations.page_model),
|
|
422
|
+
),
|
|
423
|
+
running_heads: bestByConfidence(
|
|
424
|
+
inputs.map(profile => profile.observations.running_heads),
|
|
425
|
+
),
|
|
426
|
+
folio: bestByConfidence(
|
|
427
|
+
inputs.map(profile => profile.observations.folio),
|
|
428
|
+
),
|
|
429
|
+
opener: bestByConfidence(
|
|
430
|
+
inputs.map(profile => profile.observations.opener),
|
|
431
|
+
),
|
|
432
|
+
ornaments: bestByConfidence(
|
|
433
|
+
inputs.map(profile => profile.observations.ornaments),
|
|
434
|
+
),
|
|
435
|
+
table_treatment: bestByConfidence(
|
|
436
|
+
inputs.map(profile => profile.observations.table_treatment),
|
|
437
|
+
),
|
|
438
|
+
caption_treatment: bestByConfidence(
|
|
439
|
+
inputs.map(profile => profile.observations.caption_treatment),
|
|
440
|
+
),
|
|
441
|
+
finish: bestByConfidence(
|
|
442
|
+
inputs.map(profile => profile.observations.finish),
|
|
443
|
+
),
|
|
444
|
+
},
|
|
445
|
+
rules: [
|
|
446
|
+
...reconcileRules(inputs.flatMap(profile => profile.rules ?? [])),
|
|
447
|
+
...detectReconciliationConflicts(inputs),
|
|
448
|
+
],
|
|
449
|
+
derived: null,
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function isPrintStyleProfile(
|
|
454
|
+
value: unknown,
|
|
455
|
+
): value is PrintStyleProfile {
|
|
456
|
+
if (!value || typeof value !== 'object') return false
|
|
457
|
+
const candidate = value as Partial<PrintStyleProfile>
|
|
458
|
+
return (
|
|
459
|
+
typeof candidate.schema_version === 'string' &&
|
|
460
|
+
Boolean(candidate.observations) &&
|
|
461
|
+
typeof candidate.observations === 'object' &&
|
|
462
|
+
Boolean(candidate.exemplar_quality) &&
|
|
463
|
+
typeof candidate.exemplar_quality === 'object' &&
|
|
464
|
+
typeof candidate.exemplar_quality.usable === 'boolean' &&
|
|
465
|
+
typeof candidate.exemplar_quality.reason === 'string' &&
|
|
466
|
+
typeof candidate.exemplar_quality.confidence === 'number' &&
|
|
467
|
+
Array.isArray(candidate.rules) &&
|
|
468
|
+
candidate.derived === null
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export interface PrintReferenceImageSample {
|
|
473
|
+
paper: string
|
|
474
|
+
ink: string
|
|
475
|
+
accent: string
|
|
476
|
+
accentConfidence: number
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export async function samplePrintReferenceImageColors(
|
|
480
|
+
image: File,
|
|
481
|
+
): Promise<PrintReferenceImageSample> {
|
|
482
|
+
const bitmap = await createImageBitmap(image)
|
|
483
|
+
try {
|
|
484
|
+
const canvas = document.createElement('canvas')
|
|
485
|
+
canvas.width = 80
|
|
486
|
+
canvas.height = 80
|
|
487
|
+
const context = canvas.getContext('2d')
|
|
488
|
+
if (!context) return defaultPrintReferenceImageSample()
|
|
489
|
+
|
|
490
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
|
|
491
|
+
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data
|
|
492
|
+
const buckets = new Map<
|
|
493
|
+
string,
|
|
494
|
+
{ rgb: [number, number, number]; count: number }
|
|
495
|
+
>()
|
|
496
|
+
for (let index = 0; index < pixels.length; index += 16) {
|
|
497
|
+
const alpha = pixels[index + 3]
|
|
498
|
+
if (alpha < 128) continue
|
|
499
|
+
const rgb: [number, number, number] = [
|
|
500
|
+
quantizeColor(pixels[index]),
|
|
501
|
+
quantizeColor(pixels[index + 1]),
|
|
502
|
+
quantizeColor(pixels[index + 2]),
|
|
503
|
+
]
|
|
504
|
+
const key = rgb.join(',')
|
|
505
|
+
const bucket = buckets.get(key)
|
|
506
|
+
if (bucket) bucket.count += 1
|
|
507
|
+
else buckets.set(key, { rgb, count: 1 })
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const ranked = Array.from(buckets.values()).sort(
|
|
511
|
+
(a, b) => b.count - a.count,
|
|
512
|
+
)
|
|
513
|
+
const light = ranked.find(bucket => colorLuminance(bucket.rgb) > 0.72)
|
|
514
|
+
const dark =
|
|
515
|
+
ranked
|
|
516
|
+
.filter(bucket => colorLuminance(bucket.rgb) < 0.36)
|
|
517
|
+
.sort((a, b) => b.count - a.count)[0] ?? ranked[ranked.length - 1]
|
|
518
|
+
const saturated = ranked
|
|
519
|
+
.filter(bucket => colorSaturation(bucket.rgb) > 0.22)
|
|
520
|
+
.sort(
|
|
521
|
+
(a, b) =>
|
|
522
|
+
colorSaturation(b.rgb) * b.count - colorSaturation(a.rgb) * a.count,
|
|
523
|
+
)[0]
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
paper: rgbToHex(light?.rgb ?? [255, 255, 255]),
|
|
527
|
+
ink: rgbToHex(dark?.rgb ?? [26, 26, 26]),
|
|
528
|
+
accent: rgbToHex(saturated?.rgb ?? [70, 88, 75]),
|
|
529
|
+
accentConfidence: saturated ? 0.72 : 0.45,
|
|
530
|
+
}
|
|
531
|
+
} finally {
|
|
532
|
+
bitmap.close()
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function buildBookPrintStyleProfileFromReference({
|
|
537
|
+
source,
|
|
538
|
+
prompt,
|
|
539
|
+
sample,
|
|
540
|
+
}: {
|
|
541
|
+
source: string
|
|
542
|
+
prompt: string
|
|
543
|
+
sample?: PrintReferenceImageSample
|
|
544
|
+
}): PrintStyleProfile {
|
|
545
|
+
const text = prompt.toLowerCase()
|
|
546
|
+
const sans = text.includes('sans')
|
|
547
|
+
const dramatic = text.includes('dramatic') || text.includes('literary')
|
|
548
|
+
const airy = text.includes('airy') || text.includes('generous')
|
|
549
|
+
return {
|
|
550
|
+
schema_version: '1.0',
|
|
551
|
+
sources: [{ id: source, note: 'book print design reference' }],
|
|
552
|
+
exemplar_quality: {
|
|
553
|
+
usable: true,
|
|
554
|
+
reason: sample
|
|
555
|
+
? 'local pixel extraction from uploaded book reference image'
|
|
556
|
+
: 'book design concern inferred from description',
|
|
557
|
+
confidence: sample ? 0.78 : 0.68,
|
|
558
|
+
},
|
|
559
|
+
observations: {
|
|
560
|
+
palette: [
|
|
561
|
+
{
|
|
562
|
+
role: 'surface',
|
|
563
|
+
approx_hex:
|
|
564
|
+
sample?.paper ?? (text.includes('cream') ? '#FBFAF4' : '#FFFFFF'),
|
|
565
|
+
coverage: 'dominant',
|
|
566
|
+
where_seen: sample ? 'largest light color cluster' : 'description',
|
|
567
|
+
confidence: sample ? 0.82 : 0.64,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
role: 'ink',
|
|
571
|
+
approx_hex: sample?.ink ?? '#1A1A1A',
|
|
572
|
+
coverage: 'frequent',
|
|
573
|
+
where_seen: sample ? 'dark text-like pixels' : 'description',
|
|
574
|
+
confidence: sample ? 0.76 : 0.64,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
role: 'accent',
|
|
578
|
+
approx_hex:
|
|
579
|
+
sample?.accent ?? (text.includes('red') ? '#7A2634' : '#46584B'),
|
|
580
|
+
coverage: 'sparse',
|
|
581
|
+
where_seen: sample
|
|
582
|
+
? 'most saturated non-neutral cluster'
|
|
583
|
+
: 'description',
|
|
584
|
+
confidence: sample?.accentConfidence ?? 0.62,
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
type: [
|
|
588
|
+
{
|
|
589
|
+
role: 'display',
|
|
590
|
+
class: sans ? 'sans' : 'serif',
|
|
591
|
+
serif_detail: dramatic ? 'hairline' : 'bracketed',
|
|
592
|
+
confidence: 0.72,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
role: 'body',
|
|
596
|
+
class: sans ? 'sans' : 'serif',
|
|
597
|
+
serif_detail: 'bracketed',
|
|
598
|
+
confidence: 0.72,
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
role: 'caption',
|
|
602
|
+
class: 'sans',
|
|
603
|
+
confidence: 0.7,
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
type_pairing: {
|
|
607
|
+
size_contrast: dramatic ? 'dramatic' : 'moderate',
|
|
608
|
+
confidence: 0.72,
|
|
609
|
+
},
|
|
610
|
+
type_scale: {
|
|
611
|
+
body_pt: dramatic ? 10.8 : 10.6,
|
|
612
|
+
title_scale: dramatic ? 2.6 : 2.2,
|
|
613
|
+
chapter_title_scale: dramatic ? 2.2 : 1.9,
|
|
614
|
+
section_scale: dramatic ? 1.5 : 1.35,
|
|
615
|
+
caption_scale: 0.82,
|
|
616
|
+
footnote_scale: 0.78,
|
|
617
|
+
confidence: sample ? 0.68 : 0.62,
|
|
618
|
+
},
|
|
619
|
+
text_setting: {
|
|
620
|
+
alignment: text.includes('flush') ? 'flush-left' : 'justified',
|
|
621
|
+
hyphenation: text.includes('no hyphen') ? 'absent' : 'present',
|
|
622
|
+
measure_cpl: text.includes('narrow') ? 54 : 58,
|
|
623
|
+
leading_feel: airy ? 'airy' : 'normal',
|
|
624
|
+
confidence: 0.7,
|
|
625
|
+
},
|
|
626
|
+
page_model: {
|
|
627
|
+
columns: 1,
|
|
628
|
+
margin_generosity: airy ? 'generous' : 'moderate',
|
|
629
|
+
margin_asymmetry: 'inner<outer',
|
|
630
|
+
running_heads: 'present',
|
|
631
|
+
folio_position: 'outside bottom',
|
|
632
|
+
caption_position: 'below',
|
|
633
|
+
confidence: 0.7,
|
|
634
|
+
},
|
|
635
|
+
running_heads: {
|
|
636
|
+
presence: 'present',
|
|
637
|
+
position: 'outer top',
|
|
638
|
+
transform: text.includes('uppercase') ? 'uppercase' : 'small-caps',
|
|
639
|
+
rule: text.includes('rule') || dramatic ? 'hairline' : 'none',
|
|
640
|
+
font_role: 'caption',
|
|
641
|
+
evidence: source,
|
|
642
|
+
confidence: 0.7,
|
|
643
|
+
},
|
|
644
|
+
folio: {
|
|
645
|
+
position: 'outside bottom',
|
|
646
|
+
style: 'plain',
|
|
647
|
+
suppress_on: ['title', 'opener'],
|
|
648
|
+
evidence: source,
|
|
649
|
+
confidence: 0.7,
|
|
650
|
+
},
|
|
651
|
+
opener: {
|
|
652
|
+
spacing: airy ? 'generous' : 'compact',
|
|
653
|
+
recto_start: true,
|
|
654
|
+
ornament: text.includes('ornament') || dramatic ? 'rule' : 'none',
|
|
655
|
+
drop_cap: false,
|
|
656
|
+
evidence: source,
|
|
657
|
+
confidence: 0.68,
|
|
658
|
+
},
|
|
659
|
+
ornaments: {
|
|
660
|
+
kind: text.includes('ornament') || dramatic ? 'rule' : 'none',
|
|
661
|
+
weight: 'hairline',
|
|
662
|
+
placement: 'chapter opener',
|
|
663
|
+
evidence: source,
|
|
664
|
+
confidence: 0.66,
|
|
665
|
+
},
|
|
666
|
+
table_treatment: {
|
|
667
|
+
rule_weight: 'hairline',
|
|
668
|
+
header_treatment: 'plain',
|
|
669
|
+
row_rules: 'header-only',
|
|
670
|
+
density: 'balanced',
|
|
671
|
+
evidence: source,
|
|
672
|
+
confidence: 0.7,
|
|
673
|
+
},
|
|
674
|
+
caption_treatment: {
|
|
675
|
+
position: 'below',
|
|
676
|
+
label_style: 'small-caps',
|
|
677
|
+
alignment: text.includes('left') ? 'left' : 'center',
|
|
678
|
+
evidence: source,
|
|
679
|
+
confidence: 0.7,
|
|
680
|
+
},
|
|
681
|
+
finish: {
|
|
682
|
+
register: 'literary',
|
|
683
|
+
confidence: 0.7,
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
rules: [
|
|
687
|
+
{
|
|
688
|
+
rule: 'emulate the uploaded book reference through safe Pure Render tokens',
|
|
689
|
+
evidence: source,
|
|
690
|
+
confidence: sample ? 0.78 : 0.68,
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
rule: prompt.trim() || 'use restrained long-form book treatment',
|
|
694
|
+
evidence: 'book design concern',
|
|
695
|
+
confidence: prompt.trim() ? 0.72 : 0.62,
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
derived: null,
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function buildWriterPrintStyleProfileFromReference({
|
|
703
|
+
source,
|
|
704
|
+
prompt,
|
|
705
|
+
sample,
|
|
706
|
+
}: {
|
|
707
|
+
source: string
|
|
708
|
+
prompt: string
|
|
709
|
+
sample?: PrintReferenceImageSample
|
|
710
|
+
}): PrintStyleProfile {
|
|
711
|
+
const text = prompt.toLowerCase()
|
|
712
|
+
const sans = text.includes('sans') || text.includes('modern')
|
|
713
|
+
const airy = text.includes('airy') || text.includes('open')
|
|
714
|
+
const compact = text.includes('compact') || text.includes('dense')
|
|
715
|
+
const highContrast =
|
|
716
|
+
text.includes('high contrast') || text.includes('crisp')
|
|
717
|
+
const flush = !text.includes('justify')
|
|
718
|
+
return {
|
|
719
|
+
schema_version: '1.0',
|
|
720
|
+
sources: [{ id: source, note: 'writer print design reference' }],
|
|
721
|
+
exemplar_quality: {
|
|
722
|
+
usable: true,
|
|
723
|
+
reason: sample
|
|
724
|
+
? 'local pixel extraction from uploaded writer reference image'
|
|
725
|
+
: 'writer design concern inferred from description',
|
|
726
|
+
confidence: sample ? 0.74 : 0.66,
|
|
727
|
+
},
|
|
728
|
+
observations: {
|
|
729
|
+
palette: [
|
|
730
|
+
{
|
|
731
|
+
role: 'surface',
|
|
732
|
+
approx_hex:
|
|
733
|
+
sample?.paper ?? (text.includes('cream') ? '#FBFAF4' : '#FFFFFF'),
|
|
734
|
+
coverage: 'dominant',
|
|
735
|
+
where_seen: sample ? 'largest light color cluster' : 'description',
|
|
736
|
+
confidence: sample ? 0.78 : 0.62,
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
role: 'ink',
|
|
740
|
+
approx_hex: sample?.ink ?? (highContrast ? '#111111' : '#202020'),
|
|
741
|
+
coverage: 'frequent',
|
|
742
|
+
where_seen: sample ? 'dark text-like pixels' : 'description',
|
|
743
|
+
confidence: sample ? 0.74 : 0.64,
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
role: 'accent',
|
|
747
|
+
approx_hex:
|
|
748
|
+
sample?.accent ?? (text.includes('blue') ? '#335f8f' : '#5f7f6c'),
|
|
749
|
+
coverage: 'sparse',
|
|
750
|
+
where_seen: sample
|
|
751
|
+
? 'most saturated non-neutral cluster'
|
|
752
|
+
: 'description',
|
|
753
|
+
confidence: sample?.accentConfidence ?? 0.62,
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
type: [
|
|
757
|
+
{
|
|
758
|
+
role: 'display',
|
|
759
|
+
class: sans ? 'sans' : 'serif',
|
|
760
|
+
serif_detail: 'bracketed',
|
|
761
|
+
confidence: 0.68,
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
role: 'body',
|
|
765
|
+
class: sans ? 'sans' : 'serif',
|
|
766
|
+
serif_detail: 'bracketed',
|
|
767
|
+
confidence: 0.7,
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
role: 'caption',
|
|
771
|
+
class: 'sans',
|
|
772
|
+
confidence: 0.68,
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
type_pairing: {
|
|
776
|
+
size_contrast: compact ? 'subtle' : 'moderate',
|
|
777
|
+
confidence: 0.66,
|
|
778
|
+
},
|
|
779
|
+
type_scale: {
|
|
780
|
+
body_pt: compact ? 10.2 : 10.8,
|
|
781
|
+
title_scale: compact ? 1.75 : 2,
|
|
782
|
+
section_scale: compact ? 1.2 : 1.28,
|
|
783
|
+
caption_scale: 0.82,
|
|
784
|
+
footnote_scale: 0.78,
|
|
785
|
+
confidence: sample ? 0.64 : 0.6,
|
|
786
|
+
},
|
|
787
|
+
text_setting: {
|
|
788
|
+
alignment: flush ? 'flush-left' : 'justified',
|
|
789
|
+
hyphenation: flush ? 'absent' : 'present',
|
|
790
|
+
measure_cpl: compact ? 68 : 62,
|
|
791
|
+
leading_feel: airy ? 'airy' : compact ? 'tight' : 'normal',
|
|
792
|
+
confidence: 0.68,
|
|
793
|
+
},
|
|
794
|
+
page_model: {
|
|
795
|
+
columns: 1,
|
|
796
|
+
margin_generosity: airy ? 'generous' : 'moderate',
|
|
797
|
+
margin_asymmetry: 'symmetric',
|
|
798
|
+
running_heads: 'absent',
|
|
799
|
+
folio_position: 'bottom center',
|
|
800
|
+
caption_position: 'below',
|
|
801
|
+
confidence: 0.62,
|
|
802
|
+
},
|
|
803
|
+
table_treatment: {
|
|
804
|
+
rule_weight: 'hairline',
|
|
805
|
+
header_treatment: 'plain',
|
|
806
|
+
row_rules: 'header-only',
|
|
807
|
+
density: compact ? 'compact' : 'balanced',
|
|
808
|
+
evidence: source,
|
|
809
|
+
confidence: 0.66,
|
|
810
|
+
},
|
|
811
|
+
caption_treatment: {
|
|
812
|
+
position: 'below',
|
|
813
|
+
label_style: 'plain',
|
|
814
|
+
alignment: 'left',
|
|
815
|
+
evidence: source,
|
|
816
|
+
confidence: 0.66,
|
|
817
|
+
},
|
|
818
|
+
finish: {
|
|
819
|
+
register: 'draft',
|
|
820
|
+
confidence: 0.72,
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
rules: [
|
|
824
|
+
{
|
|
825
|
+
rule: 'emulate the uploaded writer reference through safe Pure Render tokens',
|
|
826
|
+
evidence: source,
|
|
827
|
+
confidence: sample ? 0.74 : 0.66,
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
rule:
|
|
831
|
+
prompt.trim() ||
|
|
832
|
+
'keep writer output simple, readable, and free of manuscript noise',
|
|
833
|
+
evidence: 'writer design concern',
|
|
834
|
+
confidence: prompt.trim() ? 0.68 : 0.62,
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
derived: null,
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function extractJsonObject(text: string): string {
|
|
842
|
+
const trimmed = text.trim()
|
|
843
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) return trimmed
|
|
844
|
+
const start = trimmed.indexOf('{')
|
|
845
|
+
const end = trimmed.lastIndexOf('}')
|
|
846
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
847
|
+
throw new Error('Model response did not contain a JSON object')
|
|
848
|
+
}
|
|
849
|
+
return trimmed.slice(start, end + 1)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function bestByRole<
|
|
853
|
+
T extends PrintStylePaletteObservation | PrintStyleTypeObservation,
|
|
854
|
+
>(items: readonly T[]): T[] {
|
|
855
|
+
const byRole = new Map<string, T>()
|
|
856
|
+
for (const item of items) {
|
|
857
|
+
const existing = byRole.get(item.role)
|
|
858
|
+
if (!existing || item.confidence > existing.confidence) {
|
|
859
|
+
byRole.set(item.role, item)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return Array.from(byRole.values())
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function bestByConfidence<T extends { confidence?: number }>(
|
|
866
|
+
items: readonly (T | undefined)[],
|
|
867
|
+
): T | undefined {
|
|
868
|
+
return items
|
|
869
|
+
.filter((item): item is T => Boolean(item))
|
|
870
|
+
.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0))[0]
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function reconcileRules(rules: readonly PrintStyleRule[]): PrintStyleRule[] {
|
|
874
|
+
const byRule = new Map<string, PrintStyleRule>()
|
|
875
|
+
for (const rule of rules) {
|
|
876
|
+
const key = rule.rule.trim().toLowerCase()
|
|
877
|
+
if (!key) continue
|
|
878
|
+
const existing = byRule.get(key)
|
|
879
|
+
if (!existing || rule.confidence > existing.confidence) {
|
|
880
|
+
byRule.set(key, rule)
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return Array.from(byRule.values()).sort((a, b) => b.confidence - a.confidence)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function detectReconciliationConflicts(
|
|
887
|
+
profiles: readonly PrintStyleProfile[],
|
|
888
|
+
): PrintStyleRule[] {
|
|
889
|
+
const conflicts: PrintStyleRule[] = []
|
|
890
|
+
const addConflict = (label: string, values: readonly FieldValue[]): void => {
|
|
891
|
+
const confident = values.filter(
|
|
892
|
+
item => item.value !== undefined && item.value !== '' && item.confidence >= 0.6,
|
|
893
|
+
)
|
|
894
|
+
const unique = Array.from(
|
|
895
|
+
new Map(confident.map(item => [String(item.value), item])).values(),
|
|
896
|
+
)
|
|
897
|
+
if (unique.length < 2) return
|
|
898
|
+
conflicts.push({
|
|
899
|
+
rule: `Reference conflict: ${label} disagrees across uploaded images (${unique
|
|
900
|
+
.map(item => `${item.source}: ${item.value}`)
|
|
901
|
+
.join('; ')}). Highest-confidence value was used; review before saving as a shared theme.`,
|
|
902
|
+
evidence: unique.map(item => item.source).join(', '),
|
|
903
|
+
confidence: Math.min(0.9, average(unique.map(item => item.confidence))),
|
|
904
|
+
})
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const sourceId = (profile: PrintStyleProfile, index: number): string =>
|
|
908
|
+
profile.sources?.[0]?.id ?? `source-${index + 1}`
|
|
909
|
+
|
|
910
|
+
addConflict(
|
|
911
|
+
'body font class',
|
|
912
|
+
profiles.map((profile, index) => ({
|
|
913
|
+
source: sourceId(profile, index),
|
|
914
|
+
value: profile.observations.type?.find(item => item.role === 'body')?.class,
|
|
915
|
+
confidence:
|
|
916
|
+
profile.observations.type?.find(item => item.role === 'body')?.confidence ??
|
|
917
|
+
0,
|
|
918
|
+
})),
|
|
919
|
+
)
|
|
920
|
+
addConflict(
|
|
921
|
+
'accent color',
|
|
922
|
+
profiles.map((profile, index) => ({
|
|
923
|
+
source: sourceId(profile, index),
|
|
924
|
+
value: profile.observations.palette?.find(item => item.role === 'accent')
|
|
925
|
+
?.approx_hex,
|
|
926
|
+
confidence:
|
|
927
|
+
profile.observations.palette?.find(item => item.role === 'accent')
|
|
928
|
+
?.confidence ?? 0,
|
|
929
|
+
})),
|
|
930
|
+
)
|
|
931
|
+
addConflict(
|
|
932
|
+
'text alignment',
|
|
933
|
+
profiles.map((profile, index) => ({
|
|
934
|
+
source: sourceId(profile, index),
|
|
935
|
+
value: profile.observations.text_setting?.alignment,
|
|
936
|
+
confidence: profile.observations.text_setting?.confidence ?? 0,
|
|
937
|
+
})),
|
|
938
|
+
)
|
|
939
|
+
addConflict(
|
|
940
|
+
'page columns',
|
|
941
|
+
profiles.map((profile, index) => ({
|
|
942
|
+
source: sourceId(profile, index),
|
|
943
|
+
value: profile.observations.page_model?.columns,
|
|
944
|
+
confidence: profile.observations.page_model?.confidence ?? 0,
|
|
945
|
+
})),
|
|
946
|
+
)
|
|
947
|
+
addConflict(
|
|
948
|
+
'running heads',
|
|
949
|
+
profiles.map((profile, index) => ({
|
|
950
|
+
source: sourceId(profile, index),
|
|
951
|
+
value: profile.observations.running_heads?.presence,
|
|
952
|
+
confidence: profile.observations.running_heads?.confidence ?? 0,
|
|
953
|
+
})),
|
|
954
|
+
)
|
|
955
|
+
addConflict(
|
|
956
|
+
'opener ornament',
|
|
957
|
+
profiles.map((profile, index) => ({
|
|
958
|
+
source: sourceId(profile, index),
|
|
959
|
+
value: profile.observations.opener?.ornament,
|
|
960
|
+
confidence: profile.observations.opener?.confidence ?? 0,
|
|
961
|
+
})),
|
|
962
|
+
)
|
|
963
|
+
addConflict(
|
|
964
|
+
'table density',
|
|
965
|
+
profiles.map((profile, index) => ({
|
|
966
|
+
source: sourceId(profile, index),
|
|
967
|
+
value: profile.observations.table_treatment?.density,
|
|
968
|
+
confidence: profile.observations.table_treatment?.confidence ?? 0,
|
|
969
|
+
})),
|
|
970
|
+
)
|
|
971
|
+
addConflict(
|
|
972
|
+
'caption position',
|
|
973
|
+
profiles.map((profile, index) => ({
|
|
974
|
+
source: sourceId(profile, index),
|
|
975
|
+
value: profile.observations.caption_treatment?.position,
|
|
976
|
+
confidence: profile.observations.caption_treatment?.confidence ?? 0,
|
|
977
|
+
})),
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
return conflicts
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
interface FieldValue {
|
|
984
|
+
source: string
|
|
985
|
+
value: string | number | undefined
|
|
986
|
+
confidence: number
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function average(values: readonly number[]): number {
|
|
990
|
+
if (values.length === 0) return 0
|
|
991
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function defaultPrintReferenceImageSample(): PrintReferenceImageSample {
|
|
995
|
+
return {
|
|
996
|
+
paper: '#FFFFFF',
|
|
997
|
+
ink: '#1A1A1A',
|
|
998
|
+
accent: '#46584B',
|
|
999
|
+
accentConfidence: 0.45,
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function quantizeColor(value: number | undefined): number {
|
|
1004
|
+
return Math.round((value ?? 0) / 24) * 24
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function rgbToHex(rgb: readonly number[]): string {
|
|
1008
|
+
return `#${rgb
|
|
1009
|
+
.map(value =>
|
|
1010
|
+
Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0'),
|
|
1011
|
+
)
|
|
1012
|
+
.join('')}`
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function colorLuminance([r, g, b]: readonly number[]): number {
|
|
1016
|
+
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function colorSaturation([r, g, b]: readonly number[]): number {
|
|
1020
|
+
const max = Math.max(r, g, b)
|
|
1021
|
+
const min = Math.min(r, g, b)
|
|
1022
|
+
return max === 0 ? 0 : (max - min) / max
|
|
1023
|
+
}
|