@invect/ui 0.0.1 → 0.0.3
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 +0 -4
- package/dist/{Invect-CWpIwZ5F.js → Invect-CJSKm2Aq.js} +28086 -27414
- package/dist/Invect.d.ts +16 -4
- package/dist/components/flow-editor/FlowCommandPalette.d.ts +19 -0
- package/dist/components/flow-editor/ShortcutsHelpDialog.d.ts +6 -0
- package/dist/components/flow-editor/keyboard-shortcuts.d.ts +161 -0
- package/dist/components/flow-editor/serialize-to-sdk.d.ts +12 -12
- package/dist/components/flow-editor/use-keyboard-shortcuts.d.ts +14 -0
- package/dist/demo.js +159 -145
- package/dist/index.css +1 -1
- package/dist/index.js +175 -175
- package/dist/styles.d.ts +2 -0
- package/package.json +21 -17
- package/src/Invect.tsx +24 -6
- package/src/components/flow-editor/FlowCommandPalette.tsx +136 -0
- package/src/components/flow-editor/FlowEditor.tsx +18 -0
- package/src/components/flow-editor/ShortcutsHelpDialog.tsx +68 -0
- package/src/components/flow-editor/keyboard-shortcuts.ts +203 -0
- package/src/components/flow-editor/serialize-to-sdk.ts +20 -19
- package/src/components/flow-editor/use-keyboard-shortcuts.ts +280 -0
- package/src/demo/DemoInvect.tsx +10 -1
- package/src/demo/demo-api-client.ts +1 -0
- package/src/styles.d.ts +2 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { useHotkeys } from 'react-hotkeys-hook';
|
|
3
|
+
import { useReactFlow } from '@xyflow/react';
|
|
4
|
+
import { useFlowEditorStore } from './flow-editor.store';
|
|
5
|
+
import { useFlowActions } from '../../routes/flow-route-layout';
|
|
6
|
+
import { useUIStore } from '~/stores/uiStore';
|
|
7
|
+
import { useTheme } from '~/contexts/ThemeProvider';
|
|
8
|
+
import { useChatStore } from '~/components/chat/chat.store';
|
|
9
|
+
import { SHORTCUTS, getShortcutDisplay } from './keyboard-shortcuts';
|
|
10
|
+
import type { CommandPaletteAction } from './FlowCommandPalette';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Hook that registers all keyboard shortcuts for the flow editor
|
|
14
|
+
// and exposes command palette state + actions.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Options to control which canvas-level shortcuts to skip (handled elsewhere) */
|
|
18
|
+
interface UseKeyboardShortcutsOptions {
|
|
19
|
+
/** Whether copy/paste shortcuts are handled by useCopyPaste (avoid double-binding) */
|
|
20
|
+
copyPasteHandledExternally?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useKeyboardShortcuts(opts: UseKeyboardShortcutsOptions = {}) {
|
|
24
|
+
const { copyPasteHandledExternally = true } = opts;
|
|
25
|
+
|
|
26
|
+
// --- State for command palette and help dialog ---
|
|
27
|
+
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
28
|
+
const [shortcutsHelpOpen, setShortcutsHelpOpen] = useState(false);
|
|
29
|
+
|
|
30
|
+
// --- External hooks ---
|
|
31
|
+
const reactFlow = useReactFlow();
|
|
32
|
+
const flowActions = useFlowActions();
|
|
33
|
+
const toggleNodeSidebar = useUIStore((s) => s.toggleNodeSidebar);
|
|
34
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
35
|
+
const toggleChat = useChatStore((s) => s.togglePanel);
|
|
36
|
+
|
|
37
|
+
// --- Actions ---
|
|
38
|
+
const handleSave = useCallback(() => {
|
|
39
|
+
if (flowActions?.onSave) {
|
|
40
|
+
flowActions.onSave();
|
|
41
|
+
}
|
|
42
|
+
}, [flowActions]);
|
|
43
|
+
|
|
44
|
+
const handleExecute = useCallback(() => {
|
|
45
|
+
if (flowActions?.onExecute) {
|
|
46
|
+
flowActions.onExecute();
|
|
47
|
+
}
|
|
48
|
+
}, [flowActions]);
|
|
49
|
+
|
|
50
|
+
const handleFitView = useCallback(() => {
|
|
51
|
+
reactFlow.fitView({ padding: 0.2, duration: 200 });
|
|
52
|
+
}, [reactFlow]);
|
|
53
|
+
|
|
54
|
+
const handleZoomIn = useCallback(() => {
|
|
55
|
+
reactFlow.zoomIn({ duration: 200 });
|
|
56
|
+
}, [reactFlow]);
|
|
57
|
+
|
|
58
|
+
const handleZoomOut = useCallback(() => {
|
|
59
|
+
reactFlow.zoomOut({ duration: 200 });
|
|
60
|
+
}, [reactFlow]);
|
|
61
|
+
|
|
62
|
+
const handleSelectAll = useCallback(() => {
|
|
63
|
+
const nodes = useFlowEditorStore.getState().nodes;
|
|
64
|
+
const changes = nodes.map((n) => ({
|
|
65
|
+
id: n.id,
|
|
66
|
+
type: 'select' as const,
|
|
67
|
+
selected: true,
|
|
68
|
+
}));
|
|
69
|
+
useFlowEditorStore.getState().applyNodeChanges(changes);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const handleToggleTheme = useCallback(() => {
|
|
73
|
+
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
|
|
74
|
+
}, [resolvedTheme, setTheme]);
|
|
75
|
+
|
|
76
|
+
const openCommandPalette = useCallback(() => setCommandPaletteOpen(true), []);
|
|
77
|
+
const openShortcutsHelp = useCallback(() => setShortcutsHelpOpen(true), []);
|
|
78
|
+
|
|
79
|
+
// --- Register hotkeys ---
|
|
80
|
+
|
|
81
|
+
// General
|
|
82
|
+
useHotkeys(
|
|
83
|
+
SHORTCUTS.commandPalette.keys,
|
|
84
|
+
(e) => {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
openCommandPalette();
|
|
87
|
+
},
|
|
88
|
+
{ enableOnFormTags: true, enableOnContentEditable: true },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useHotkeys(
|
|
92
|
+
SHORTCUTS.save.keys,
|
|
93
|
+
(e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
handleSave();
|
|
96
|
+
},
|
|
97
|
+
{ enableOnFormTags: true, enableOnContentEditable: true },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
useHotkeys(
|
|
101
|
+
SHORTCUTS.executeFlow.keys,
|
|
102
|
+
(e) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
handleExecute();
|
|
105
|
+
},
|
|
106
|
+
{ enableOnFormTags: true, enableOnContentEditable: true },
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
useHotkeys(SHORTCUTS.showShortcuts.keys, (e) => {
|
|
110
|
+
// Don't fire when typing in inputs — only from canvas
|
|
111
|
+
const el = e.target as HTMLElement;
|
|
112
|
+
if (
|
|
113
|
+
el.tagName === 'INPUT' ||
|
|
114
|
+
el.tagName === 'TEXTAREA' ||
|
|
115
|
+
el.isContentEditable ||
|
|
116
|
+
el.closest('.cm-editor') ||
|
|
117
|
+
el.closest('[role="dialog"]')
|
|
118
|
+
) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
openShortcutsHelp();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Navigation
|
|
126
|
+
useHotkeys(SHORTCUTS.fitView.keys, (e) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
handleFitView();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
useHotkeys(SHORTCUTS.zoomIn.keys, (e) => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
handleZoomIn();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
useHotkeys(SHORTCUTS.zoomOut.keys, (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
handleZoomOut();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Editing (only if not handled by useCopyPaste)
|
|
142
|
+
if (!copyPasteHandledExternally) {
|
|
143
|
+
// These would be registered here if useCopyPaste was removed.
|
|
144
|
+
// For now, copy/paste/cut/duplicate/delete are in useCopyPaste.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Select all
|
|
148
|
+
useHotkeys(SHORTCUTS.selectAll.keys, (e) => {
|
|
149
|
+
const el = e.target as HTMLElement;
|
|
150
|
+
if (
|
|
151
|
+
el.tagName === 'INPUT' ||
|
|
152
|
+
el.tagName === 'TEXTAREA' ||
|
|
153
|
+
el.isContentEditable ||
|
|
154
|
+
el.closest('.cm-editor') ||
|
|
155
|
+
el.closest('[role="dialog"]')
|
|
156
|
+
) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
handleSelectAll();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// View
|
|
164
|
+
useHotkeys(SHORTCUTS.toggleSidebar.keys, (e) => {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
toggleNodeSidebar();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
useHotkeys(SHORTCUTS.toggleTheme.keys, (e) => {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
handleToggleTheme();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
useHotkeys(SHORTCUTS.toggleChat.keys, (e) => {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
toggleChat();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- Build command palette actions ---
|
|
180
|
+
const commandPaletteActions: CommandPaletteAction[] = useMemo(
|
|
181
|
+
() => [
|
|
182
|
+
{
|
|
183
|
+
id: SHORTCUTS.save.id,
|
|
184
|
+
label: SHORTCUTS.save.label,
|
|
185
|
+
category: SHORTCUTS.save.category,
|
|
186
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.save),
|
|
187
|
+
onSelect: handleSave,
|
|
188
|
+
disabled: !flowActions?.onSave,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: SHORTCUTS.executeFlow.id,
|
|
192
|
+
label: SHORTCUTS.executeFlow.label,
|
|
193
|
+
category: SHORTCUTS.executeFlow.category,
|
|
194
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.executeFlow),
|
|
195
|
+
onSelect: handleExecute,
|
|
196
|
+
disabled: !flowActions?.onExecute,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: SHORTCUTS.showShortcuts.id,
|
|
200
|
+
label: SHORTCUTS.showShortcuts.label,
|
|
201
|
+
category: SHORTCUTS.showShortcuts.category,
|
|
202
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.showShortcuts),
|
|
203
|
+
onSelect: openShortcutsHelp,
|
|
204
|
+
},
|
|
205
|
+
// Editing
|
|
206
|
+
{
|
|
207
|
+
id: SHORTCUTS.selectAll.id,
|
|
208
|
+
label: SHORTCUTS.selectAll.label,
|
|
209
|
+
category: SHORTCUTS.selectAll.category,
|
|
210
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.selectAll),
|
|
211
|
+
onSelect: handleSelectAll,
|
|
212
|
+
},
|
|
213
|
+
// Navigation
|
|
214
|
+
{
|
|
215
|
+
id: SHORTCUTS.fitView.id,
|
|
216
|
+
label: SHORTCUTS.fitView.label,
|
|
217
|
+
category: SHORTCUTS.fitView.category,
|
|
218
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.fitView),
|
|
219
|
+
onSelect: handleFitView,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: SHORTCUTS.zoomIn.id,
|
|
223
|
+
label: SHORTCUTS.zoomIn.label,
|
|
224
|
+
category: SHORTCUTS.zoomIn.category,
|
|
225
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.zoomIn),
|
|
226
|
+
onSelect: handleZoomIn,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: SHORTCUTS.zoomOut.id,
|
|
230
|
+
label: SHORTCUTS.zoomOut.label,
|
|
231
|
+
category: SHORTCUTS.zoomOut.category,
|
|
232
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.zoomOut),
|
|
233
|
+
onSelect: handleZoomOut,
|
|
234
|
+
},
|
|
235
|
+
// View
|
|
236
|
+
{
|
|
237
|
+
id: SHORTCUTS.toggleSidebar.id,
|
|
238
|
+
label: SHORTCUTS.toggleSidebar.label,
|
|
239
|
+
category: SHORTCUTS.toggleSidebar.category,
|
|
240
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.toggleSidebar),
|
|
241
|
+
onSelect: toggleNodeSidebar,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: SHORTCUTS.toggleTheme.id,
|
|
245
|
+
label: SHORTCUTS.toggleTheme.label,
|
|
246
|
+
category: SHORTCUTS.toggleTheme.category,
|
|
247
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.toggleTheme),
|
|
248
|
+
onSelect: handleToggleTheme,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: SHORTCUTS.toggleChat.id,
|
|
252
|
+
label: SHORTCUTS.toggleChat.label,
|
|
253
|
+
category: SHORTCUTS.toggleChat.category,
|
|
254
|
+
shortcutDisplay: getShortcutDisplay(SHORTCUTS.toggleChat),
|
|
255
|
+
onSelect: toggleChat,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
handleSave,
|
|
260
|
+
handleExecute,
|
|
261
|
+
openShortcutsHelp,
|
|
262
|
+
handleSelectAll,
|
|
263
|
+
handleFitView,
|
|
264
|
+
handleZoomIn,
|
|
265
|
+
handleZoomOut,
|
|
266
|
+
toggleNodeSidebar,
|
|
267
|
+
handleToggleTheme,
|
|
268
|
+
toggleChat,
|
|
269
|
+
flowActions,
|
|
270
|
+
],
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
commandPaletteOpen,
|
|
275
|
+
setCommandPaletteOpen,
|
|
276
|
+
shortcutsHelpOpen,
|
|
277
|
+
setShortcutsHelpOpen,
|
|
278
|
+
commandPaletteActions,
|
|
279
|
+
};
|
|
280
|
+
}
|
package/src/demo/DemoInvect.tsx
CHANGED
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import React, { useMemo } from 'react';
|
|
24
|
+
import React, { useEffect, useMemo } from 'react';
|
|
25
25
|
import { Invect, type InvectProps } from '../Invect';
|
|
26
26
|
import { createDemoApiClient, type DemoData } from './demo-api-client';
|
|
27
27
|
import type { ApiClient } from '../api/client';
|
|
28
|
+
import { useChatStore } from '../components/chat/chat.store';
|
|
28
29
|
|
|
29
30
|
export interface DemoInvectProps extends Omit<InvectProps, 'apiBaseUrl' | 'apiClient'> {
|
|
30
31
|
/** Static data to power the demo UI */
|
|
@@ -38,5 +39,13 @@ export interface DemoInvectProps extends Omit<InvectProps, 'apiBaseUrl' | 'apiCl
|
|
|
38
39
|
export function DemoInvect({ data, useMemoryRouter = true, ...rest }: DemoInvectProps) {
|
|
39
40
|
const mockClient = useMemo(() => createDemoApiClient(data) as unknown as ApiClient, [data]);
|
|
40
41
|
|
|
42
|
+
// Open chat panel and pre-select Anthropic + Claude Sonnet 4.6 for the demo
|
|
43
|
+
const setOpen = useChatStore((s) => s.setOpen);
|
|
44
|
+
const updateSettings = useChatStore((s) => s.updateSettings);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setOpen(true);
|
|
47
|
+
updateSettings({ credentialId: 'cred-anthropic', model: 'claude-sonnet-4-6' });
|
|
48
|
+
}, [setOpen, updateSettings]);
|
|
49
|
+
|
|
41
50
|
return <Invect apiClient={mockClient} useMemoryRouter={useMemoryRouter} {...rest} />;
|
|
42
51
|
}
|
|
@@ -191,6 +191,7 @@ export function createDemoApiClient(data: DemoData = {}): Record<string, unknown
|
|
|
191
191
|
// Chat
|
|
192
192
|
getChatStatus: async () => ({ enabled: true }),
|
|
193
193
|
getChatModels: async () => [
|
|
194
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
|
|
194
195
|
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' },
|
|
195
196
|
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
|
|
196
197
|
],
|
package/src/styles.d.ts
ADDED