@fgv/ts-app-shell 5.1.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.browser.js +3 -0
- package/dist/index.js +43 -0
- package/dist/packlets/ai-assist/index.js +6 -0
- package/dist/packlets/ai-assist/useAiAssist.js +219 -0
- package/dist/packlets/cascade/CascadeContainer.js +83 -0
- package/dist/packlets/cascade/ComparisonView.js +48 -0
- package/dist/packlets/cascade/EntityTabLayout.js +104 -0
- package/dist/packlets/cascade/MobileCascadeStack.js +63 -0
- package/dist/packlets/cascade/index.js +37 -0
- package/dist/packlets/cascade/model.js +30 -0
- package/dist/packlets/cascade/useCascadeOps.js +206 -0
- package/dist/packlets/cascade/useCascadeTransitions.js +58 -0
- package/dist/packlets/detail/DetailHelpers.js +103 -0
- package/dist/packlets/detail/index.js +6 -0
- package/dist/packlets/drop-zone/JsonDropZone.js +112 -0
- package/dist/packlets/drop-zone/index.js +6 -0
- package/dist/packlets/editing/EditFieldHelpers.js +130 -0
- package/dist/packlets/editing/MultiActionButton.js +73 -0
- package/dist/packlets/editing/NumericInput.js +119 -0
- package/dist/packlets/editing/TypeaheadInput.js +207 -0
- package/dist/packlets/editing/index.js +10 -0
- package/dist/packlets/editing/useTypeaheadMatch.js +102 -0
- package/dist/packlets/keyboard/index.js +7 -0
- package/dist/packlets/keyboard/registry.js +133 -0
- package/dist/packlets/keyboard/useKeyboardShortcuts.js +117 -0
- package/dist/packlets/messages/MessagesContext.js +76 -0
- package/dist/packlets/messages/MessagesLogger.js +103 -0
- package/dist/packlets/messages/StatusBar.js +154 -0
- package/dist/packlets/messages/Toast.js +68 -0
- package/dist/packlets/messages/index.js +11 -0
- package/dist/packlets/messages/model.js +56 -0
- package/dist/packlets/messages/useLogReporter.js +66 -0
- package/dist/packlets/modal/ConfirmDialog.js +78 -0
- package/dist/packlets/modal/Modal.js +55 -0
- package/dist/packlets/modal/index.js +7 -0
- package/dist/packlets/print/PrintEnclosure.js +60 -0
- package/dist/packlets/print/index.js +7 -0
- package/dist/packlets/print/openPrintWindow.js +112 -0
- package/dist/packlets/responsive/ResponsiveProvider.js +56 -0
- package/dist/packlets/responsive/index.js +7 -0
- package/dist/packlets/responsive/useResponsiveLayout.js +118 -0
- package/dist/packlets/selectors/EntityRow.js +276 -0
- package/dist/packlets/selectors/PreferredSelector.js +251 -0
- package/dist/packlets/selectors/index.js +24 -0
- package/dist/packlets/sidebar/CollectionSection.js +107 -0
- package/dist/packlets/sidebar/EntityList.js +164 -0
- package/dist/packlets/sidebar/FilterBar.js +42 -0
- package/dist/packlets/sidebar/FilterRow.js +182 -0
- package/dist/packlets/sidebar/GroupedEntityList.js +183 -0
- package/dist/packlets/sidebar/SearchBar.js +34 -0
- package/dist/packlets/sidebar/SidebarLayout.js +62 -0
- package/dist/packlets/sidebar/index.js +12 -0
- package/dist/packlets/theme/ThemeProvider.js +141 -0
- package/dist/packlets/theme/index.js +6 -0
- package/dist/packlets/top-bar/ModeSelector.js +46 -0
- package/dist/packlets/top-bar/TabBar.js +37 -0
- package/dist/packlets/top-bar/index.js +7 -0
- package/dist/packlets/url-sync/index.js +6 -0
- package/dist/packlets/url-sync/useUrlSync.js +157 -0
- package/eslint.config.js +22 -0
- package/lib/index.browser.d.ts +2 -0
- package/lib/index.browser.js +19 -0
- package/lib/index.d.ts +28 -0
- package/lib/index.js +59 -0
- package/lib/packlets/ai-assist/index.d.ts +6 -0
- package/lib/packlets/ai-assist/index.js +11 -0
- package/lib/packlets/ai-assist/useAiAssist.d.ts +77 -0
- package/lib/packlets/ai-assist/useAiAssist.js +223 -0
- package/lib/packlets/cascade/CascadeContainer.d.ts +44 -0
- package/lib/packlets/cascade/CascadeContainer.js +119 -0
- package/lib/packlets/cascade/ComparisonView.d.ts +35 -0
- package/lib/packlets/cascade/ComparisonView.js +54 -0
- package/lib/packlets/cascade/EntityTabLayout.d.ts +47 -0
- package/lib/packlets/cascade/EntityTabLayout.js +110 -0
- package/lib/packlets/cascade/MobileCascadeStack.d.ts +20 -0
- package/lib/packlets/cascade/MobileCascadeStack.js +99 -0
- package/lib/packlets/cascade/index.d.ts +12 -0
- package/lib/packlets/cascade/index.js +48 -0
- package/lib/packlets/cascade/model.d.ts +57 -0
- package/lib/packlets/cascade/model.js +33 -0
- package/lib/packlets/cascade/useCascadeOps.d.ts +111 -0
- package/lib/packlets/cascade/useCascadeOps.js +209 -0
- package/lib/packlets/cascade/useCascadeTransitions.d.ts +19 -0
- package/lib/packlets/cascade/useCascadeTransitions.js +62 -0
- package/lib/packlets/detail/DetailHelpers.d.ts +83 -0
- package/lib/packlets/detail/DetailHelpers.js +113 -0
- package/lib/packlets/detail/index.d.ts +6 -0
- package/lib/packlets/detail/index.js +14 -0
- package/lib/packlets/drop-zone/JsonDropZone.d.ts +40 -0
- package/lib/packlets/drop-zone/JsonDropZone.js +149 -0
- package/lib/packlets/drop-zone/index.d.ts +6 -0
- package/lib/packlets/drop-zone/index.js +10 -0
- package/lib/packlets/editing/EditFieldHelpers.d.ts +171 -0
- package/lib/packlets/editing/EditFieldHelpers.js +144 -0
- package/lib/packlets/editing/MultiActionButton.d.ts +45 -0
- package/lib/packlets/editing/MultiActionButton.js +109 -0
- package/lib/packlets/editing/NumericInput.d.ts +47 -0
- package/lib/packlets/editing/NumericInput.js +155 -0
- package/lib/packlets/editing/TypeaheadInput.d.ts +46 -0
- package/lib/packlets/editing/TypeaheadInput.js +243 -0
- package/lib/packlets/editing/index.d.ts +10 -0
- package/lib/packlets/editing/index.js +26 -0
- package/lib/packlets/editing/useTypeaheadMatch.d.ts +42 -0
- package/lib/packlets/editing/useTypeaheadMatch.js +105 -0
- package/lib/packlets/keyboard/index.d.ts +7 -0
- package/lib/packlets/keyboard/index.js +15 -0
- package/lib/packlets/keyboard/registry.d.ts +92 -0
- package/lib/packlets/keyboard/registry.js +138 -0
- package/lib/packlets/keyboard/useKeyboardShortcuts.d.ts +50 -0
- package/lib/packlets/keyboard/useKeyboardShortcuts.js +155 -0
- package/lib/packlets/messages/MessagesContext.d.ts +40 -0
- package/lib/packlets/messages/MessagesContext.js +113 -0
- package/lib/packlets/messages/MessagesLogger.d.ts +50 -0
- package/lib/packlets/messages/MessagesLogger.js +107 -0
- package/lib/packlets/messages/StatusBar.d.ts +22 -0
- package/lib/packlets/messages/StatusBar.js +190 -0
- package/lib/packlets/messages/Toast.d.ts +31 -0
- package/lib/packlets/messages/Toast.js +105 -0
- package/lib/packlets/messages/index.d.ts +11 -0
- package/lib/packlets/messages/index.js +24 -0
- package/lib/packlets/messages/model.d.ts +59 -0
- package/lib/packlets/messages/model.js +61 -0
- package/lib/packlets/messages/useLogReporter.d.ts +22 -0
- package/lib/packlets/messages/useLogReporter.js +69 -0
- package/lib/packlets/modal/ConfirmDialog.d.ts +39 -0
- package/lib/packlets/modal/ConfirmDialog.js +114 -0
- package/lib/packlets/modal/Modal.d.ts +22 -0
- package/lib/packlets/modal/Modal.js +91 -0
- package/lib/packlets/modal/index.d.ts +7 -0
- package/lib/packlets/modal/index.js +12 -0
- package/lib/packlets/print/PrintEnclosure.d.ts +33 -0
- package/lib/packlets/print/PrintEnclosure.js +96 -0
- package/lib/packlets/print/index.d.ts +7 -0
- package/lib/packlets/print/index.js +12 -0
- package/lib/packlets/print/openPrintWindow.d.ts +35 -0
- package/lib/packlets/print/openPrintWindow.js +118 -0
- package/lib/packlets/responsive/ResponsiveProvider.d.ts +35 -0
- package/lib/packlets/responsive/ResponsiveProvider.js +93 -0
- package/lib/packlets/responsive/index.d.ts +7 -0
- package/lib/packlets/responsive/index.js +13 -0
- package/lib/packlets/responsive/useResponsiveLayout.d.ts +48 -0
- package/lib/packlets/responsive/useResponsiveLayout.js +121 -0
- package/lib/packlets/selectors/EntityRow.d.ts +45 -0
- package/lib/packlets/selectors/EntityRow.js +315 -0
- package/lib/packlets/selectors/PreferredSelector.d.ts +50 -0
- package/lib/packlets/selectors/PreferredSelector.js +287 -0
- package/lib/packlets/selectors/index.d.ts +5 -0
- package/lib/packlets/selectors/index.js +29 -0
- package/lib/packlets/sidebar/CollectionSection.d.ts +82 -0
- package/lib/packlets/sidebar/CollectionSection.js +143 -0
- package/lib/packlets/sidebar/EntityList.d.ts +105 -0
- package/lib/packlets/sidebar/EntityList.js +200 -0
- package/lib/packlets/sidebar/FilterBar.d.ts +26 -0
- package/lib/packlets/sidebar/FilterBar.js +48 -0
- package/lib/packlets/sidebar/FilterRow.d.ts +42 -0
- package/lib/packlets/sidebar/FilterRow.js +218 -0
- package/lib/packlets/sidebar/GroupedEntityList.d.ts +59 -0
- package/lib/packlets/sidebar/GroupedEntityList.js +219 -0
- package/lib/packlets/sidebar/SearchBar.d.ts +19 -0
- package/lib/packlets/sidebar/SearchBar.js +40 -0
- package/lib/packlets/sidebar/SidebarLayout.d.ts +28 -0
- package/lib/packlets/sidebar/SidebarLayout.js +98 -0
- package/lib/packlets/sidebar/index.d.ts +12 -0
- package/lib/packlets/sidebar/index.js +22 -0
- package/lib/packlets/theme/ThemeProvider.d.ts +68 -0
- package/lib/packlets/theme/ThemeProvider.js +178 -0
- package/lib/packlets/theme/index.d.ts +6 -0
- package/lib/packlets/theme/index.js +11 -0
- package/lib/packlets/top-bar/ModeSelector.d.ts +38 -0
- package/lib/packlets/top-bar/ModeSelector.js +52 -0
- package/lib/packlets/top-bar/TabBar.d.ts +31 -0
- package/lib/packlets/top-bar/TabBar.js +43 -0
- package/lib/packlets/top-bar/index.d.ts +7 -0
- package/lib/packlets/top-bar/index.js +12 -0
- package/lib/packlets/url-sync/index.d.ts +6 -0
- package/lib/packlets/url-sync/index.js +12 -0
- package/lib/packlets/url-sync/useUrlSync.d.ts +75 -0
- package/lib/packlets/url-sync/useUrlSync.js +162 -0
- package/package.json +82 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Generic URL hash synchronization for two-tier mode/tab navigation.
|
|
24
|
+
*
|
|
25
|
+
* Encodes mode + tab in the URL hash: `#/{mode}/{tab}`
|
|
26
|
+
* Syncs bidirectionally between application state and the URL.
|
|
27
|
+
* Handles browser back/forward via popstate.
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
import { useEffect, useRef } from 'react';
|
|
32
|
+
/**
|
|
33
|
+
* Encodes mode + tab into a URL hash string.
|
|
34
|
+
* @param mode - The current mode
|
|
35
|
+
* @param tab - The current tab
|
|
36
|
+
* @returns Hash string (e.g., '#/library/ingredients')
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export function encodeUrlHash(mode, tab) {
|
|
40
|
+
return `#/${mode}/${tab}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parses a URL hash string into mode + tab, validated against the config.
|
|
44
|
+
* Returns undefined if the hash is invalid or empty.
|
|
45
|
+
* @param hash - The hash string (with or without leading '#')
|
|
46
|
+
* @param config - Validation configuration
|
|
47
|
+
* @returns Parsed mode and tab, or undefined
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export function parseUrlHash(hash, config) {
|
|
51
|
+
const cleaned = hash.replace(/^#\/?/, '');
|
|
52
|
+
if (cleaned.length === 0) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const parts = cleaned.split('/');
|
|
56
|
+
if (parts.length < 1) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const mode = parts[0];
|
|
60
|
+
if (!config.validModes.includes(mode)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const validTabs = config.validTabs[mode];
|
|
64
|
+
if (parts.length >= 2) {
|
|
65
|
+
const tab = parts[1];
|
|
66
|
+
if (validTabs.includes(tab)) {
|
|
67
|
+
return { mode, tab };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Valid mode but no valid tab — use default
|
|
71
|
+
return { mode, tab: config.defaultTabs[mode] };
|
|
72
|
+
}
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// URL Sync Hook
|
|
75
|
+
// ============================================================================
|
|
76
|
+
/**
|
|
77
|
+
* Hook that synchronizes two-tier mode/tab navigation state with the URL hash.
|
|
78
|
+
*
|
|
79
|
+
* - On mount: reads the URL hash and applies it to the store
|
|
80
|
+
* - On state change: updates the URL hash (pushes history entry)
|
|
81
|
+
* - On popstate (back/forward): reads the URL hash and applies it to the store
|
|
82
|
+
*
|
|
83
|
+
* Should be called once at the app root level.
|
|
84
|
+
*
|
|
85
|
+
* @param config - Validation configuration (modes, tabs, defaults)
|
|
86
|
+
* @param state - Current navigation state
|
|
87
|
+
* @param callbacks - State mutation callbacks
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export function useUrlSync(config, state, callbacks) {
|
|
91
|
+
const { mode, activeTab } = state;
|
|
92
|
+
const { setMode, setTab } = callbacks;
|
|
93
|
+
// Track whether we're currently applying a URL change to avoid circular updates
|
|
94
|
+
const isApplyingUrl = useRef(false);
|
|
95
|
+
// Track whether we've done the initial load
|
|
96
|
+
const initialized = useRef(false);
|
|
97
|
+
// Keep refs to the latest values so the mount effect captures them without
|
|
98
|
+
// needing them in its dependency array (the effect must only run once).
|
|
99
|
+
const configRef = useRef(config);
|
|
100
|
+
configRef.current = config;
|
|
101
|
+
const modeRef = useRef(mode);
|
|
102
|
+
modeRef.current = mode;
|
|
103
|
+
const activeTabRef = useRef(activeTab);
|
|
104
|
+
activeTabRef.current = activeTab;
|
|
105
|
+
const setModeRef = useRef(setMode);
|
|
106
|
+
setModeRef.current = setMode;
|
|
107
|
+
const setTabRef = useRef(setTab);
|
|
108
|
+
setTabRef.current = setTab;
|
|
109
|
+
// ---- Initial load: URL → store ----
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (initialized.current) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
initialized.current = true;
|
|
115
|
+
const parsed = parseUrlHash(window.location.hash, configRef.current);
|
|
116
|
+
if (parsed) {
|
|
117
|
+
isApplyingUrl.current = true;
|
|
118
|
+
setModeRef.current(parsed.mode);
|
|
119
|
+
setTabRef.current(parsed.tab);
|
|
120
|
+
isApplyingUrl.current = false;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// No valid hash — write current state to URL
|
|
124
|
+
const hash = encodeUrlHash(modeRef.current, activeTabRef.current);
|
|
125
|
+
window.history.replaceState(null, '', hash);
|
|
126
|
+
}
|
|
127
|
+
// Intentionally empty: this effect must run exactly once on mount.
|
|
128
|
+
// All values are accessed via refs to avoid stale closures.
|
|
129
|
+
}, []);
|
|
130
|
+
// ---- Store → URL: push history on navigation changes ----
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (isApplyingUrl.current) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const hash = encodeUrlHash(mode, activeTab);
|
|
136
|
+
if (window.location.hash !== hash) {
|
|
137
|
+
window.history.pushState(null, '', hash);
|
|
138
|
+
}
|
|
139
|
+
}, [mode, activeTab]);
|
|
140
|
+
// ---- URL → store: handle popstate (back/forward) ----
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const handlePopState = () => {
|
|
143
|
+
const parsed = parseUrlHash(window.location.hash, config);
|
|
144
|
+
if (parsed) {
|
|
145
|
+
isApplyingUrl.current = true;
|
|
146
|
+
setMode(parsed.mode);
|
|
147
|
+
setTab(parsed.tab);
|
|
148
|
+
isApplyingUrl.current = false;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
window.addEventListener('popstate', handlePopState);
|
|
152
|
+
return () => {
|
|
153
|
+
window.removeEventListener('popstate', handlePopState);
|
|
154
|
+
};
|
|
155
|
+
}, [config, setMode, setTab]);
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=useUrlSync.js.map
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// ESLint 9 flat config
|
|
2
|
+
const nodeProfile = require('@rushstack/eslint-config/flat/profile/node');
|
|
3
|
+
const packletsPlugin = require('@rushstack/eslint-config/flat/mixins/packlets');
|
|
4
|
+
const tsdocPlugin = require('@rushstack/eslint-config/flat/mixins/tsdoc');
|
|
5
|
+
const reactHooksPlugin = require('eslint-plugin-react-hooks');
|
|
6
|
+
|
|
7
|
+
module.exports = [
|
|
8
|
+
...nodeProfile,
|
|
9
|
+
packletsPlugin,
|
|
10
|
+
...tsdocPlugin,
|
|
11
|
+
{
|
|
12
|
+
plugins: {
|
|
13
|
+
'react-hooks': reactHooksPlugin
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
...reactHooksPlugin.configs.recommended.rules,
|
|
17
|
+
'@rushstack/packlets/mechanics': 'warn',
|
|
18
|
+
// Disable no-new-null rule for React components where null is conventional
|
|
19
|
+
'@rushstack/no-new-null': 'off'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Browser entry point - re-exports everything from main index
|
|
18
|
+
__exportStar(require("./index"), exports);
|
|
19
|
+
//# sourceMappingURL=index.browser.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* \@fgv/ts-app-shell - Shared React UI primitives for application shells.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable components for:
|
|
5
|
+
* - Column cascade (master-detail drill-down)
|
|
6
|
+
* - Compact sidebar with flyout filter selectors
|
|
7
|
+
* - Toast notifications and log message panel
|
|
8
|
+
* - Command palette
|
|
9
|
+
* - Keybinding registry
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
export * from './packlets/top-bar';
|
|
14
|
+
export * from './packlets/messages';
|
|
15
|
+
export * from './packlets/url-sync';
|
|
16
|
+
export * from './packlets/modal';
|
|
17
|
+
export * from './packlets/keyboard';
|
|
18
|
+
export * from './packlets/sidebar';
|
|
19
|
+
export * from './packlets/cascade';
|
|
20
|
+
export * from './packlets/selectors';
|
|
21
|
+
export * from './packlets/drop-zone';
|
|
22
|
+
export * from './packlets/editing';
|
|
23
|
+
export * from './packlets/detail';
|
|
24
|
+
export * from './packlets/ai-assist';
|
|
25
|
+
export * from './packlets/print';
|
|
26
|
+
export * from './packlets/theme';
|
|
27
|
+
export * from './packlets/responsive';
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* \@fgv/ts-app-shell - Shared React UI primitives for application shells.
|
|
4
|
+
*
|
|
5
|
+
* Provides reusable components for:
|
|
6
|
+
* - Column cascade (master-detail drill-down)
|
|
7
|
+
* - Compact sidebar with flyout filter selectors
|
|
8
|
+
* - Toast notifications and log message panel
|
|
9
|
+
* - Command palette
|
|
10
|
+
* - Keybinding registry
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
// Top bar components
|
|
30
|
+
__exportStar(require("./packlets/top-bar"), exports);
|
|
31
|
+
// Messages infrastructure (context, toasts, status bar)
|
|
32
|
+
__exportStar(require("./packlets/messages"), exports);
|
|
33
|
+
// URL hash synchronization for mode/tab navigation
|
|
34
|
+
__exportStar(require("./packlets/url-sync"), exports);
|
|
35
|
+
// Modal dialog
|
|
36
|
+
__exportStar(require("./packlets/modal"), exports);
|
|
37
|
+
// Keyboard shortcut registry
|
|
38
|
+
__exportStar(require("./packlets/keyboard"), exports);
|
|
39
|
+
// Sidebar layout, search, filters, and entity list
|
|
40
|
+
__exportStar(require("./packlets/sidebar"), exports);
|
|
41
|
+
// Column cascade container
|
|
42
|
+
__exportStar(require("./packlets/cascade"), exports);
|
|
43
|
+
// Selectors (PreferredSelector popover)
|
|
44
|
+
__exportStar(require("./packlets/selectors"), exports);
|
|
45
|
+
// JSON drop zone (generic drop/paste target with converter validation)
|
|
46
|
+
__exportStar(require("./packlets/drop-zone"), exports);
|
|
47
|
+
// Generic edit field primitives for entity editors
|
|
48
|
+
__exportStar(require("./packlets/editing"), exports);
|
|
49
|
+
// Generic detail-view primitive components
|
|
50
|
+
__exportStar(require("./packlets/detail"), exports);
|
|
51
|
+
// AI assist hook (generic, context-free)
|
|
52
|
+
__exportStar(require("./packlets/ai-assist"), exports);
|
|
53
|
+
// Print popup window enclosure
|
|
54
|
+
__exportStar(require("./packlets/print"), exports);
|
|
55
|
+
// Theme provider and hook
|
|
56
|
+
__exportStar(require("./packlets/theme"), exports);
|
|
57
|
+
// Responsive layout detection and context
|
|
58
|
+
__exportStar(require("./packlets/responsive"), exports);
|
|
59
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI assist packlet — generic hook for AI-assisted entity generation.
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
export { checkForAiErrorObject, useAiAssist, type IAiAssistAction, type IAiAssistResult, type IUseAiAssistParams, type IUseAiAssistResult } from './useAiAssist';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AI assist packlet — generic hook for AI-assisted entity generation.
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.useAiAssist = exports.checkForAiErrorObject = void 0;
|
|
8
|
+
var useAiAssist_1 = require("./useAiAssist");
|
|
9
|
+
Object.defineProperty(exports, "checkForAiErrorObject", { enumerable: true, get: function () { return useAiAssist_1.checkForAiErrorObject; } });
|
|
10
|
+
Object.defineProperty(exports, "useAiAssist", { enumerable: true, get: function () { return useAiAssist_1.useAiAssist; } });
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { AiAssist } from '@fgv/ts-extras';
|
|
2
|
+
import { type Logging, Result } from '@fgv/ts-utils';
|
|
3
|
+
/**
|
|
4
|
+
* Parameters for the generic useAiAssist hook.
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export interface IUseAiAssistParams {
|
|
8
|
+
/** Resolved AI assist settings (provider list + default). */
|
|
9
|
+
readonly settings: AiAssist.IAiAssistSettings | undefined;
|
|
10
|
+
/** Keystore for API key resolution (or undefined if no keystore). */
|
|
11
|
+
readonly keyStore: AiAssist.IAiAssistKeyStore | undefined;
|
|
12
|
+
/** Optional logger for request/response observability. */
|
|
13
|
+
readonly logger?: Logging.ILogger;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A single available AI assist action.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export interface IAiAssistAction {
|
|
20
|
+
/** The provider identifier */
|
|
21
|
+
readonly provider: AiAssist.AiProviderId;
|
|
22
|
+
/** Display label (e.g. "AI Assist | Copy", "AI Assist | Grok") */
|
|
23
|
+
readonly label: string;
|
|
24
|
+
/** Whether this is the default (first) action */
|
|
25
|
+
readonly isDefault: boolean;
|
|
26
|
+
/** Whether the provider is currently available (keystore unlocked, secret present) */
|
|
27
|
+
readonly isAvailable: boolean;
|
|
28
|
+
/** Reason the provider is unavailable, if any */
|
|
29
|
+
readonly unavailableReason?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Result of executing an AI assist action.
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export interface IAiAssistResult<TEntity> {
|
|
36
|
+
/** The generated entity */
|
|
37
|
+
readonly entity: TEntity;
|
|
38
|
+
/** Source indicator */
|
|
39
|
+
readonly source: 'ai';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return type of the useAiAssist hook.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export interface IUseAiAssistResult {
|
|
46
|
+
/** Available actions based on settings + keystore state */
|
|
47
|
+
readonly actions: ReadonlyArray<IAiAssistAction>;
|
|
48
|
+
/** Whether a direct assist call is in progress */
|
|
49
|
+
readonly isWorking: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Execute a copy-paste action: copies the combined prompt to clipboard.
|
|
52
|
+
* @returns Success with `'copied'`, or failure.
|
|
53
|
+
*/
|
|
54
|
+
readonly copyPrompt: (prompt: AiAssist.AiPrompt) => Promise<Result<'copied'>>;
|
|
55
|
+
/**
|
|
56
|
+
* Execute a direct API action: calls the provider, validates the response, returns the entity.
|
|
57
|
+
* @returns Success with the validated entity, or failure.
|
|
58
|
+
*/
|
|
59
|
+
readonly generateDirect: <TEntity>(provider: AiAssist.AiProviderId, prompt: AiAssist.AiPrompt, convert: (from: unknown) => Result<TEntity>, tools?: ReadonlyArray<AiAssist.AiServerToolConfig>) => Promise<Result<IAiAssistResult<TEntity>>>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks whether a parsed AI response is an error object (with an "error" field)
|
|
63
|
+
* rather than a valid entity. AI prompts instruct the model to return
|
|
64
|
+
* `{ "error": "...", "term": "..." }` when it cannot confidently generate the entity.
|
|
65
|
+
*
|
|
66
|
+
* @param parsed - The parsed JSON from the AI response
|
|
67
|
+
* @returns A failure Result with the error message if it's an error object, or undefined if not
|
|
68
|
+
*/
|
|
69
|
+
export declare function checkForAiErrorObject(parsed: unknown): Result<never> | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Generic hook providing AI assist actions based on supplied settings and keystore.
|
|
72
|
+
* @param params - Settings and keystore to use (typically from a workspace or app context)
|
|
73
|
+
* @returns Available actions and execution functions
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
export declare function useAiAssist(params: IUseAiAssistParams): IUseAiAssistResult;
|
|
77
|
+
//# sourceMappingURL=useAiAssist.d.ts.map
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2026 Erik Fortune
|
|
4
|
+
*
|
|
5
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
* in the Software without restriction, including without limitation the rights
|
|
8
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
* furnished to do so, subject to the following conditions:
|
|
11
|
+
*
|
|
12
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
* copies or substantial portions of the Software.
|
|
14
|
+
*
|
|
15
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
* SOFTWARE.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.checkForAiErrorObject = checkForAiErrorObject;
|
|
25
|
+
exports.useAiAssist = useAiAssist;
|
|
26
|
+
/**
|
|
27
|
+
* Generic AI assist hook — parameterized by settings and keystore, no app-specific context.
|
|
28
|
+
*
|
|
29
|
+
* Provides a list of available AI assist actions and functions for copy-paste
|
|
30
|
+
* and direct API generation flows.
|
|
31
|
+
*
|
|
32
|
+
* @packageDocumentation
|
|
33
|
+
*/
|
|
34
|
+
const react_1 = require("react");
|
|
35
|
+
const ts_extras_1 = require("@fgv/ts-extras");
|
|
36
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Helpers
|
|
39
|
+
// ============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Checks whether a parsed AI response is an error object (with an "error" field)
|
|
42
|
+
* rather than a valid entity. AI prompts instruct the model to return
|
|
43
|
+
* `{ "error": "...", "term": "..." }` when it cannot confidently generate the entity.
|
|
44
|
+
*
|
|
45
|
+
* @param parsed - The parsed JSON from the AI response
|
|
46
|
+
* @returns A failure Result with the error message if it's an error object, or undefined if not
|
|
47
|
+
*/
|
|
48
|
+
function checkForAiErrorObject(parsed) {
|
|
49
|
+
if (typeof parsed === 'object' &&
|
|
50
|
+
parsed !== null &&
|
|
51
|
+
'error' in parsed &&
|
|
52
|
+
typeof parsed.error === 'string') {
|
|
53
|
+
const errorObj = parsed;
|
|
54
|
+
const term = typeof errorObj.term === 'string' ? ` (term: "${errorObj.term}")` : '';
|
|
55
|
+
return (0, ts_utils_1.fail)(`AI declined to generate${term}: ${errorObj.error}`);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Hook
|
|
61
|
+
// ============================================================================
|
|
62
|
+
/**
|
|
63
|
+
* Generic hook providing AI assist actions based on supplied settings and keystore.
|
|
64
|
+
* @param params - Settings and keystore to use (typically from a workspace or app context)
|
|
65
|
+
* @returns Available actions and execution functions
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
function useAiAssist(params) {
|
|
69
|
+
const { settings, keyStore, logger } = params;
|
|
70
|
+
const [isWorking, setIsWorking] = (0, react_1.useState)(false);
|
|
71
|
+
const actions = (0, react_1.useMemo)(() => {
|
|
72
|
+
var _a, _b, _c;
|
|
73
|
+
const providers = (_a = settings === null || settings === void 0 ? void 0 : settings.providers) !== null && _a !== void 0 ? _a : [{ provider: 'copy-paste' }];
|
|
74
|
+
const enabledSet = new Set(providers.map((p) => p.provider));
|
|
75
|
+
const defaultProvider = (settings === null || settings === void 0 ? void 0 : settings.defaultProvider) && enabledSet.has(settings.defaultProvider)
|
|
76
|
+
? settings.defaultProvider
|
|
77
|
+
: (_c = (_b = providers[0]) === null || _b === void 0 ? void 0 : _b.provider) !== null && _c !== void 0 ? _c : 'copy-paste';
|
|
78
|
+
return providers.map((config) => {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
let isAvailable = true;
|
|
81
|
+
let unavailableReason;
|
|
82
|
+
if (config.provider !== 'copy-paste') {
|
|
83
|
+
// API-based providers need a secret name and unlocked keystore with that secret
|
|
84
|
+
if (!config.secretName) {
|
|
85
|
+
isAvailable = false;
|
|
86
|
+
unavailableReason = 'No API key secret configured';
|
|
87
|
+
}
|
|
88
|
+
else if (!keyStore) {
|
|
89
|
+
isAvailable = false;
|
|
90
|
+
unavailableReason = 'No keystore available';
|
|
91
|
+
}
|
|
92
|
+
else if (!keyStore.isUnlocked) {
|
|
93
|
+
isAvailable = false;
|
|
94
|
+
unavailableReason = 'Keystore is locked';
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const hasSecret = keyStore.hasSecret(config.secretName);
|
|
98
|
+
if (hasSecret.isFailure() || !hasSecret.value) {
|
|
99
|
+
isAvailable = false;
|
|
100
|
+
unavailableReason = `API key "${config.secretName}" not found in keystore`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const label = (_b = (_a = ts_extras_1.AiAssist.getProviderDescriptor(config.provider).orDefault()) === null || _a === void 0 ? void 0 : _a.buttonLabel) !== null && _b !== void 0 ? _b : config.provider;
|
|
105
|
+
return {
|
|
106
|
+
provider: config.provider,
|
|
107
|
+
label,
|
|
108
|
+
isDefault: config.provider === defaultProvider,
|
|
109
|
+
isAvailable,
|
|
110
|
+
unavailableReason
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}, [settings, keyStore]);
|
|
114
|
+
const copyPrompt = (0, react_1.useCallback)(async (prompt) => {
|
|
115
|
+
try {
|
|
116
|
+
await navigator.clipboard.writeText(prompt.combined);
|
|
117
|
+
return (0, ts_utils_1.succeed)('copied');
|
|
118
|
+
}
|
|
119
|
+
catch (_a) {
|
|
120
|
+
return (0, ts_utils_1.fail)('Failed to copy prompt to clipboard');
|
|
121
|
+
}
|
|
122
|
+
}, []);
|
|
123
|
+
const generateDirect = (0, react_1.useCallback)(async (provider, prompt, convert, tools) => {
|
|
124
|
+
// Find the provider config and descriptor
|
|
125
|
+
const providerConfig = settings === null || settings === void 0 ? void 0 : settings.providers.find((p) => p.provider === provider);
|
|
126
|
+
if (!providerConfig) {
|
|
127
|
+
return (0, ts_utils_1.fail)(`Provider "${provider}" not configured`);
|
|
128
|
+
}
|
|
129
|
+
const descriptorResult = ts_extras_1.AiAssist.getProviderDescriptor(provider);
|
|
130
|
+
if (descriptorResult.isFailure()) {
|
|
131
|
+
return (0, ts_utils_1.fail)(descriptorResult.message);
|
|
132
|
+
}
|
|
133
|
+
const descriptor = descriptorResult.value;
|
|
134
|
+
if (!providerConfig.secretName) {
|
|
135
|
+
return (0, ts_utils_1.fail)(`Provider "${provider}" has no secret name configured`);
|
|
136
|
+
}
|
|
137
|
+
if (!keyStore) {
|
|
138
|
+
return (0, ts_utils_1.fail)('No keystore available');
|
|
139
|
+
}
|
|
140
|
+
// Get API key from keystore
|
|
141
|
+
const apiKeyResult = keyStore.getApiKey(providerConfig.secretName);
|
|
142
|
+
if (apiKeyResult.isFailure()) {
|
|
143
|
+
return (0, ts_utils_1.fail)(`Failed to get API key: ${apiKeyResult.message}`);
|
|
144
|
+
}
|
|
145
|
+
setIsWorking(true);
|
|
146
|
+
try {
|
|
147
|
+
const maxAttempts = 3;
|
|
148
|
+
const correctionMessages = [];
|
|
149
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
150
|
+
// Resolve effective tools: per-call override > settings defaults > none
|
|
151
|
+
const effectiveTools = ts_extras_1.AiAssist.resolveEffectiveTools(descriptor, providerConfig.tools, tools);
|
|
152
|
+
// Call the API — through proxy if configured, otherwise direct
|
|
153
|
+
const completionParams = {
|
|
154
|
+
descriptor,
|
|
155
|
+
apiKey: apiKeyResult.value,
|
|
156
|
+
prompt,
|
|
157
|
+
additionalMessages: correctionMessages.length > 0 ? correctionMessages : undefined,
|
|
158
|
+
modelOverride: providerConfig.model,
|
|
159
|
+
logger,
|
|
160
|
+
tools: effectiveTools.length > 0 ? effectiveTools : undefined
|
|
161
|
+
};
|
|
162
|
+
const useProxy = !!(settings === null || settings === void 0 ? void 0 : settings.proxyUrl) && (settings.proxyAllProviders === true || descriptor.corsRestricted);
|
|
163
|
+
const responseResult = useProxy
|
|
164
|
+
? await ts_extras_1.AiAssist.callProxiedCompletion(settings.proxyUrl, completionParams)
|
|
165
|
+
: await ts_extras_1.AiAssist.callProviderCompletion(completionParams);
|
|
166
|
+
if (responseResult.isFailure()) {
|
|
167
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`AI completion failed: ${responseResult.message}`);
|
|
168
|
+
return (0, ts_utils_1.fail)(responseResult.message);
|
|
169
|
+
}
|
|
170
|
+
const { content: rawResponse, truncated } = responseResult.value;
|
|
171
|
+
logger === null || logger === void 0 ? void 0 : logger.detail(`AI response received (${rawResponse.length} chars, truncated=${truncated})`);
|
|
172
|
+
// Truncated responses are almost certainly malformed JSON — fail early
|
|
173
|
+
if (truncated) {
|
|
174
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('AI response truncated due to token limits');
|
|
175
|
+
return (0, ts_utils_1.fail)('AI response was truncated due to token limits — try a shorter prompt');
|
|
176
|
+
}
|
|
177
|
+
// Strip markdown code fences and parse JSON
|
|
178
|
+
const stripped = rawResponse
|
|
179
|
+
.trim()
|
|
180
|
+
.replace(/^```(?:\w+)?\s*\n?([\s\S]*?)\n?\s*```$/, '$1')
|
|
181
|
+
.trim();
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(stripped);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
188
|
+
// JSON parse failures are not retryable — the model isn't producing valid JSON at all
|
|
189
|
+
return (0, ts_utils_1.fail)(`AI returned invalid JSON: ${detail}`);
|
|
190
|
+
}
|
|
191
|
+
// Check for AI error object (model declined to generate)
|
|
192
|
+
const aiError = checkForAiErrorObject(parsed);
|
|
193
|
+
if (aiError !== undefined) {
|
|
194
|
+
return aiError;
|
|
195
|
+
}
|
|
196
|
+
// Validate with the provided converter
|
|
197
|
+
const entityResult = convert(parsed);
|
|
198
|
+
if (entityResult.isSuccess()) {
|
|
199
|
+
return (0, ts_utils_1.succeed)({ entity: entityResult.value, source: 'ai' });
|
|
200
|
+
}
|
|
201
|
+
// Validation failed — if we have retries left, send a correction
|
|
202
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`AI response validation failed (attempt ${attempt + 1}/${maxAttempts}): ${entityResult.message}`);
|
|
203
|
+
if (attempt < maxAttempts - 1) {
|
|
204
|
+
correctionMessages.push({ role: 'assistant', content: rawResponse }, {
|
|
205
|
+
role: 'user',
|
|
206
|
+
content: `The JSON you returned failed validation with the following error:\n\n${entityResult.message}\n\nPlease fix the JSON and return ONLY the corrected JSON object, nothing else.`
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Out of retries
|
|
211
|
+
return (0, ts_utils_1.fail)(`AI response validation failed after ${maxAttempts} attempts: ${entityResult.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Unreachable, but TypeScript needs it
|
|
215
|
+
return (0, ts_utils_1.fail)('AI generation failed');
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
setIsWorking(false);
|
|
219
|
+
}
|
|
220
|
+
}, [settings, keyStore, logger]);
|
|
221
|
+
return { actions, isWorking, copyPrompt, generateDirect };
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=useAiAssist.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column cascade container — horizontal scroll of detail columns.
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
/**
|
|
7
|
+
* Describes a single column in the cascade.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface ICascadeColumn {
|
|
11
|
+
/** Unique key for React reconciliation */
|
|
12
|
+
readonly key: string;
|
|
13
|
+
/** Breadcrumb label for this column */
|
|
14
|
+
readonly label: string;
|
|
15
|
+
/** Column content */
|
|
16
|
+
readonly content: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Props for the CascadeContainer component.
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export interface ICascadeContainerProps {
|
|
23
|
+
/** Ordered columns (left-to-right) */
|
|
24
|
+
readonly columns: ReadonlyArray<ICascadeColumn>;
|
|
25
|
+
/** Callback to pop the cascade back to a specific depth (0 = clear all) */
|
|
26
|
+
readonly onPopTo: (depth: number) => void;
|
|
27
|
+
/** Minimum column width in CSS units (default: '400px') */
|
|
28
|
+
readonly minColumnWidth?: string;
|
|
29
|
+
/** Callback when the user clicks inside a cascade column (signals list should collapse) */
|
|
30
|
+
readonly onFocus?: () => void;
|
|
31
|
+
/** Label for the breadcrumb root link (default: 'List') */
|
|
32
|
+
readonly rootLabel?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Horizontal scroll container for the column cascade.
|
|
36
|
+
*
|
|
37
|
+
* Renders a breadcrumb trail at the top and horizontally-scrollable
|
|
38
|
+
* detail columns below. Auto-scrolls to the rightmost column when
|
|
39
|
+
* a new column is pushed.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export declare function CascadeContainer(props: ICascadeContainerProps): React.ReactElement | null;
|
|
44
|
+
//# sourceMappingURL=CascadeContainer.d.ts.map
|