@invect/ui 0.0.1 → 0.0.2
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/api/node-data.api.d.ts +1 -1
- 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/package.json +4 -3
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invect/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Complete React components and routes for Invect workflow management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|
|
@@ -74,12 +74,13 @@
|
|
|
74
74
|
"codemirror": "^6.0.2",
|
|
75
75
|
"nanoid": "^5.1.6",
|
|
76
76
|
"prettier": "^3.8.1",
|
|
77
|
+
"react-hotkeys-hook": "^5.2.4",
|
|
77
78
|
"react-markdown": "^10.1.0",
|
|
78
79
|
"react-resizable-panels": "^3.0.6",
|
|
79
80
|
"remark-gfm": "^4.0.1",
|
|
80
81
|
"zustand": "^5.0.9",
|
|
81
|
-
"@invect/core": "0.0.
|
|
82
|
-
"@invect/layouts": "0.0.
|
|
82
|
+
"@invect/core": "0.0.2",
|
|
83
|
+
"@invect/layouts": "0.0.2"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
86
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
package/src/Invect.tsx
CHANGED
|
@@ -26,13 +26,23 @@ import { AppSideMenu } from './components/side-menu/side-menu';
|
|
|
26
26
|
|
|
27
27
|
export interface InvectProps {
|
|
28
28
|
reactQueryClient?: QueryClient;
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
apiPath?: string;
|
|
30
|
+
frontendPath?: string;
|
|
31
31
|
useMemoryRouter?: boolean; // Use MemoryRouter instead of BrowserRouter (useful for testing)
|
|
32
32
|
/** Frontend plugins that contribute sidebar items, routes, panel tabs, header actions, etc. */
|
|
33
33
|
plugins?: InvectFrontendPlugin[];
|
|
34
|
-
/** Pre-configured API client instance (e.g. for demo mode). When provided,
|
|
34
|
+
/** Pre-configured API client instance (e.g. for demo mode). When provided, apiPath is ignored. */
|
|
35
35
|
apiClient?: ApiClient;
|
|
36
|
+
/**
|
|
37
|
+
* Invect configuration object (from defineConfig).
|
|
38
|
+
* When provided, `apiPath` and `frontendPath` are read from the config
|
|
39
|
+
* unless explicitly overridden by the individual props.
|
|
40
|
+
*/
|
|
41
|
+
config?: {
|
|
42
|
+
apiPath?: string;
|
|
43
|
+
frontendPath?: string;
|
|
44
|
+
plugins?: Array<{ frontend?: InvectFrontendPlugin }>;
|
|
45
|
+
};
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
// Create a default QueryClient if none is provided
|
|
@@ -193,12 +203,20 @@ InvectRoutes.displayName = 'InvectRoutes';
|
|
|
193
203
|
export const Invect = React.memo(
|
|
194
204
|
({
|
|
195
205
|
reactQueryClient,
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
apiPath: apiPathProp,
|
|
207
|
+
frontendPath: frontendPathProp,
|
|
198
208
|
useMemoryRouter = false,
|
|
199
209
|
plugins,
|
|
200
210
|
apiClient,
|
|
211
|
+
config,
|
|
201
212
|
}: InvectProps) => {
|
|
213
|
+
const apiBaseUrl = apiPathProp ?? config?.apiPath ?? 'http://localhost:3000/invect';
|
|
214
|
+
const basePath = frontendPathProp ?? config?.frontendPath ?? '/invect';
|
|
215
|
+
const resolvedPlugins =
|
|
216
|
+
plugins ??
|
|
217
|
+
config?.plugins
|
|
218
|
+
?.map((p) => p.frontend)
|
|
219
|
+
.filter((p): p is InvectFrontendPlugin => p !== null && p !== undefined);
|
|
202
220
|
const client = reactQueryClient || createDefaultQueryClient();
|
|
203
221
|
const hasRouter = useHasRouterContext();
|
|
204
222
|
|
|
@@ -208,7 +226,7 @@ export const Invect = React.memo(
|
|
|
208
226
|
apiBaseUrl={apiBaseUrl}
|
|
209
227
|
apiClient={apiClient}
|
|
210
228
|
basePath={basePath}
|
|
211
|
-
plugins={
|
|
229
|
+
plugins={resolvedPlugins}
|
|
212
230
|
/>
|
|
213
231
|
);
|
|
214
232
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
CommandDialog,
|
|
4
|
+
CommandInput,
|
|
5
|
+
CommandList,
|
|
6
|
+
CommandEmpty,
|
|
7
|
+
CommandGroup,
|
|
8
|
+
CommandItem,
|
|
9
|
+
CommandShortcut,
|
|
10
|
+
CommandSeparator,
|
|
11
|
+
} from '~/components/ui/command';
|
|
12
|
+
import { CATEGORY_LABELS, type ShortcutCategory } from './keyboard-shortcuts';
|
|
13
|
+
import {
|
|
14
|
+
Save,
|
|
15
|
+
Play,
|
|
16
|
+
Copy,
|
|
17
|
+
Scissors,
|
|
18
|
+
ClipboardPaste,
|
|
19
|
+
CopyPlus,
|
|
20
|
+
Trash2,
|
|
21
|
+
BoxSelect,
|
|
22
|
+
Maximize,
|
|
23
|
+
ZoomIn,
|
|
24
|
+
ZoomOut,
|
|
25
|
+
PanelLeft,
|
|
26
|
+
Moon,
|
|
27
|
+
Keyboard,
|
|
28
|
+
Search,
|
|
29
|
+
MessageSquare,
|
|
30
|
+
} from 'lucide-react';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Command palette for the flow editor (⌘K)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface CommandPaletteAction {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
category: ShortcutCategory;
|
|
41
|
+
icon?: React.ReactNode;
|
|
42
|
+
shortcutDisplay?: string;
|
|
43
|
+
onSelect: () => void;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface FlowCommandPaletteProps {
|
|
48
|
+
open: boolean;
|
|
49
|
+
onOpenChange: (open: boolean) => void;
|
|
50
|
+
actions: CommandPaletteAction[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Icons mapped to shortcut IDs for visual consistency */
|
|
54
|
+
const SHORTCUT_ICONS: Record<string, React.ReactNode> = {
|
|
55
|
+
'command-palette': <Search className="size-4" />,
|
|
56
|
+
save: <Save className="size-4" />,
|
|
57
|
+
'execute-flow': <Play className="size-4" />,
|
|
58
|
+
'show-shortcuts': <Keyboard className="size-4" />,
|
|
59
|
+
copy: <Copy className="size-4" />,
|
|
60
|
+
cut: <Scissors className="size-4" />,
|
|
61
|
+
paste: <ClipboardPaste className="size-4" />,
|
|
62
|
+
duplicate: <CopyPlus className="size-4" />,
|
|
63
|
+
'delete-selection': <Trash2 className="size-4" />,
|
|
64
|
+
'select-all': <BoxSelect className="size-4" />,
|
|
65
|
+
'fit-view': <Maximize className="size-4" />,
|
|
66
|
+
'zoom-in': <ZoomIn className="size-4" />,
|
|
67
|
+
'zoom-out': <ZoomOut className="size-4" />,
|
|
68
|
+
'toggle-sidebar': <PanelLeft className="size-4" />,
|
|
69
|
+
'toggle-theme': <Moon className="size-4" />,
|
|
70
|
+
'toggle-chat': <MessageSquare className="size-4" />,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function FlowCommandPalette({ open, onOpenChange, actions }: FlowCommandPaletteProps) {
|
|
74
|
+
// Group actions by category
|
|
75
|
+
const grouped = React.useMemo(() => {
|
|
76
|
+
const groups: Record<ShortcutCategory, CommandPaletteAction[]> = {
|
|
77
|
+
general: [],
|
|
78
|
+
editing: [],
|
|
79
|
+
navigation: [],
|
|
80
|
+
view: [],
|
|
81
|
+
};
|
|
82
|
+
for (const action of actions) {
|
|
83
|
+
groups[action.category].push(action);
|
|
84
|
+
}
|
|
85
|
+
return groups;
|
|
86
|
+
}, [actions]);
|
|
87
|
+
|
|
88
|
+
const handleSelect = React.useCallback(
|
|
89
|
+
(action: CommandPaletteAction) => {
|
|
90
|
+
onOpenChange(false);
|
|
91
|
+
// Defer execution to let the dialog close first
|
|
92
|
+
requestAnimationFrame(() => {
|
|
93
|
+
action.onSelect();
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
[onOpenChange],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const categoryOrder: ShortcutCategory[] = ['general', 'editing', 'navigation', 'view'];
|
|
100
|
+
const nonEmptyCategories = categoryOrder.filter((cat) => grouped[cat].length > 0);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<CommandDialog
|
|
104
|
+
open={open}
|
|
105
|
+
onOpenChange={onOpenChange}
|
|
106
|
+
title="Command Palette"
|
|
107
|
+
description="Search for commands..."
|
|
108
|
+
>
|
|
109
|
+
<CommandInput placeholder="Type a command or search..." />
|
|
110
|
+
<CommandList>
|
|
111
|
+
<CommandEmpty>No commands found.</CommandEmpty>
|
|
112
|
+
{nonEmptyCategories.map((category, index) => (
|
|
113
|
+
<React.Fragment key={category}>
|
|
114
|
+
{index > 0 && <CommandSeparator />}
|
|
115
|
+
<CommandGroup heading={CATEGORY_LABELS[category]}>
|
|
116
|
+
{grouped[category].map((action) => (
|
|
117
|
+
<CommandItem
|
|
118
|
+
key={action.id}
|
|
119
|
+
value={`${action.label} ${action.description ?? ''}`}
|
|
120
|
+
onSelect={() => handleSelect(action)}
|
|
121
|
+
disabled={action.disabled}
|
|
122
|
+
>
|
|
123
|
+
{action.icon ?? SHORTCUT_ICONS[action.id] ?? null}
|
|
124
|
+
<span>{action.label}</span>
|
|
125
|
+
{action.shortcutDisplay && (
|
|
126
|
+
<CommandShortcut>{action.shortcutDisplay}</CommandShortcut>
|
|
127
|
+
)}
|
|
128
|
+
</CommandItem>
|
|
129
|
+
))}
|
|
130
|
+
</CommandGroup>
|
|
131
|
+
</React.Fragment>
|
|
132
|
+
))}
|
|
133
|
+
</CommandList>
|
|
134
|
+
</CommandDialog>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -42,6 +42,9 @@ import { useNodeExecutions } from '~/api/executions.api';
|
|
|
42
42
|
import { extractOutputValue } from './node-config-panel/utils';
|
|
43
43
|
import { nanoid } from 'nanoid';
|
|
44
44
|
import { useCopyPaste } from './use-copy-paste';
|
|
45
|
+
import { useKeyboardShortcuts } from './use-keyboard-shortcuts';
|
|
46
|
+
import { FlowCommandPalette } from './FlowCommandPalette';
|
|
47
|
+
import { ShortcutsHelpDialog } from './ShortcutsHelpDialog';
|
|
45
48
|
|
|
46
49
|
// Stable references for React Flow - defined at module scope to avoid re-renders
|
|
47
50
|
const EDGE_TYPES: EdgeTypes = {
|
|
@@ -251,6 +254,15 @@ export function FlowWorkbenchView({
|
|
|
251
254
|
// Copy/paste/cut/duplicate/delete keyboard shortcuts
|
|
252
255
|
useCopyPaste({ flowId, reactFlowInstance });
|
|
253
256
|
|
|
257
|
+
// Command palette, keyboard shortcuts, and help dialog
|
|
258
|
+
const {
|
|
259
|
+
commandPaletteOpen,
|
|
260
|
+
setCommandPaletteOpen,
|
|
261
|
+
shortcutsHelpOpen,
|
|
262
|
+
setShortcutsHelpOpen,
|
|
263
|
+
commandPaletteActions,
|
|
264
|
+
} = useKeyboardShortcuts();
|
|
265
|
+
|
|
254
266
|
// Use React Flow's built-in hook to detect when nodes have been initialized
|
|
255
267
|
// This is true when all nodes have been measured and their handles registered
|
|
256
268
|
const nodesInitializedFromHook = useNodesInitialized();
|
|
@@ -997,6 +1009,12 @@ export function FlowWorkbenchView({
|
|
|
997
1009
|
availableTools={availableTools}
|
|
998
1010
|
initialToolInstanceId={configPanelToolInstanceId}
|
|
999
1011
|
/>
|
|
1012
|
+
<FlowCommandPalette
|
|
1013
|
+
open={commandPaletteOpen}
|
|
1014
|
+
onOpenChange={setCommandPaletteOpen}
|
|
1015
|
+
actions={commandPaletteActions}
|
|
1016
|
+
/>
|
|
1017
|
+
<ShortcutsHelpDialog open={shortcutsHelpOpen} onOpenChange={setShortcutsHelpOpen} />
|
|
1000
1018
|
</>
|
|
1001
1019
|
);
|
|
1002
1020
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogHeader,
|
|
6
|
+
DialogTitle,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
} from '~/components/ui/dialog';
|
|
9
|
+
import {
|
|
10
|
+
CATEGORY_LABELS,
|
|
11
|
+
getShortcutDisplay,
|
|
12
|
+
getShortcutsByCategory,
|
|
13
|
+
type ShortcutCategory,
|
|
14
|
+
} from './keyboard-shortcuts';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Keyboard shortcuts help overlay (? key)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
interface ShortcutsHelpDialogProps {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onOpenChange: (open: boolean) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ShortcutsHelpDialog({ open, onOpenChange }: ShortcutsHelpDialogProps) {
|
|
26
|
+
const grouped = React.useMemo(() => getShortcutsByCategory(), []);
|
|
27
|
+
const categoryOrder: ShortcutCategory[] = ['general', 'editing', 'navigation', 'view'];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
31
|
+
<DialogHeader className="sr-only">
|
|
32
|
+
<DialogTitle>Keyboard Shortcuts</DialogTitle>
|
|
33
|
+
<DialogDescription>A reference of all available keyboard shortcuts.</DialogDescription>
|
|
34
|
+
</DialogHeader>
|
|
35
|
+
<DialogContent className="max-w-lg" showCloseButton>
|
|
36
|
+
<div className="space-y-4">
|
|
37
|
+
<h2 className="text-lg font-semibold">Keyboard Shortcuts</h2>
|
|
38
|
+
{categoryOrder.map((category) => {
|
|
39
|
+
const shortcuts = grouped[category];
|
|
40
|
+
if (shortcuts.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
<div key={category}>
|
|
45
|
+
<h3 className="mb-2 text-xs font-medium tracking-wider uppercase text-muted-foreground">
|
|
46
|
+
{CATEGORY_LABELS[category]}
|
|
47
|
+
</h3>
|
|
48
|
+
<div className="space-y-1">
|
|
49
|
+
{shortcuts.map((shortcut) => (
|
|
50
|
+
<div
|
|
51
|
+
key={shortcut.id}
|
|
52
|
+
className="flex items-center justify-between py-1.5 px-2 rounded-md text-sm"
|
|
53
|
+
>
|
|
54
|
+
<span>{shortcut.label}</span>
|
|
55
|
+
<kbd className="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-mono rounded bg-muted text-muted-foreground border border-border">
|
|
56
|
+
{getShortcutDisplay(shortcut)}
|
|
57
|
+
</kbd>
|
|
58
|
+
</div>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</div>
|
|
65
|
+
</DialogContent>
|
|
66
|
+
</Dialog>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Keyboard shortcut definitions for the flow editor
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface KeyboardShortcut {
|
|
6
|
+
/** Unique identifier */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Human-readable label shown in command palette and help overlay */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Key combo in react-hotkeys-hook format (e.g. "mod+k", "shift+a") */
|
|
11
|
+
keys: string;
|
|
12
|
+
/** Display string for macOS (e.g. "⌘K") */
|
|
13
|
+
macDisplay: string;
|
|
14
|
+
/** Display string for Windows/Linux (e.g. "Ctrl+K") */
|
|
15
|
+
winDisplay: string;
|
|
16
|
+
/** Category for grouping in the command palette */
|
|
17
|
+
category: ShortcutCategory;
|
|
18
|
+
/** Optional description for the command palette */
|
|
19
|
+
description?: string;
|
|
20
|
+
/** If true, this shortcut is also available when editing text inputs */
|
|
21
|
+
enableOnFormTags?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ShortcutCategory = 'general' | 'editing' | 'navigation' | 'view';
|
|
25
|
+
|
|
26
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
27
|
+
|
|
28
|
+
/** Get the platform-appropriate display string for a shortcut */
|
|
29
|
+
export function getShortcutDisplay(shortcut: KeyboardShortcut): string {
|
|
30
|
+
return isMac ? shortcut.macDisplay : shortcut.winDisplay;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Shortcut definitions
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export const SHORTCUTS = {
|
|
38
|
+
// === General ===
|
|
39
|
+
commandPalette: {
|
|
40
|
+
id: 'command-palette',
|
|
41
|
+
label: 'Open Command Palette',
|
|
42
|
+
keys: 'mod+k',
|
|
43
|
+
macDisplay: '⌘K',
|
|
44
|
+
winDisplay: 'Ctrl+K',
|
|
45
|
+
category: 'general',
|
|
46
|
+
enableOnFormTags: true,
|
|
47
|
+
},
|
|
48
|
+
save: {
|
|
49
|
+
id: 'save',
|
|
50
|
+
label: 'Save Flow',
|
|
51
|
+
keys: 'mod+s',
|
|
52
|
+
macDisplay: '⌘S',
|
|
53
|
+
winDisplay: 'Ctrl+S',
|
|
54
|
+
category: 'general',
|
|
55
|
+
enableOnFormTags: true,
|
|
56
|
+
},
|
|
57
|
+
executeFlow: {
|
|
58
|
+
id: 'execute-flow',
|
|
59
|
+
label: 'Run Flow',
|
|
60
|
+
keys: 'mod+enter',
|
|
61
|
+
macDisplay: '⌘↵',
|
|
62
|
+
winDisplay: 'Ctrl+Enter',
|
|
63
|
+
category: 'general',
|
|
64
|
+
enableOnFormTags: true,
|
|
65
|
+
},
|
|
66
|
+
showShortcuts: {
|
|
67
|
+
id: 'show-shortcuts',
|
|
68
|
+
label: 'Show Keyboard Shortcuts',
|
|
69
|
+
keys: 'shift+/',
|
|
70
|
+
macDisplay: '?',
|
|
71
|
+
winDisplay: '?',
|
|
72
|
+
category: 'general',
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// === Editing ===
|
|
76
|
+
copy: {
|
|
77
|
+
id: 'copy',
|
|
78
|
+
label: 'Copy Selected Nodes',
|
|
79
|
+
keys: 'mod+c',
|
|
80
|
+
macDisplay: '⌘C',
|
|
81
|
+
winDisplay: 'Ctrl+C',
|
|
82
|
+
category: 'editing',
|
|
83
|
+
},
|
|
84
|
+
cut: {
|
|
85
|
+
id: 'cut',
|
|
86
|
+
label: 'Cut Selected Nodes',
|
|
87
|
+
keys: 'mod+x',
|
|
88
|
+
macDisplay: '⌘X',
|
|
89
|
+
winDisplay: 'Ctrl+X',
|
|
90
|
+
category: 'editing',
|
|
91
|
+
},
|
|
92
|
+
paste: {
|
|
93
|
+
id: 'paste',
|
|
94
|
+
label: 'Paste Nodes',
|
|
95
|
+
keys: 'mod+v',
|
|
96
|
+
macDisplay: '⌘V',
|
|
97
|
+
winDisplay: 'Ctrl+V',
|
|
98
|
+
category: 'editing',
|
|
99
|
+
},
|
|
100
|
+
duplicate: {
|
|
101
|
+
id: 'duplicate',
|
|
102
|
+
label: 'Duplicate Selected Nodes',
|
|
103
|
+
keys: 'mod+d',
|
|
104
|
+
macDisplay: '⌘D',
|
|
105
|
+
winDisplay: 'Ctrl+D',
|
|
106
|
+
category: 'editing',
|
|
107
|
+
},
|
|
108
|
+
deleteSelection: {
|
|
109
|
+
id: 'delete-selection',
|
|
110
|
+
label: 'Delete Selected Nodes',
|
|
111
|
+
keys: 'backspace',
|
|
112
|
+
macDisplay: '⌫',
|
|
113
|
+
winDisplay: 'Delete',
|
|
114
|
+
category: 'editing',
|
|
115
|
+
},
|
|
116
|
+
selectAll: {
|
|
117
|
+
id: 'select-all',
|
|
118
|
+
label: 'Select All Nodes',
|
|
119
|
+
keys: 'mod+a',
|
|
120
|
+
macDisplay: '⌘A',
|
|
121
|
+
winDisplay: 'Ctrl+A',
|
|
122
|
+
category: 'editing',
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// === Navigation ===
|
|
126
|
+
fitView: {
|
|
127
|
+
id: 'fit-view',
|
|
128
|
+
label: 'Fit View',
|
|
129
|
+
keys: 'mod+shift+f',
|
|
130
|
+
macDisplay: '⌘⇧F',
|
|
131
|
+
winDisplay: 'Ctrl+Shift+F',
|
|
132
|
+
category: 'navigation',
|
|
133
|
+
},
|
|
134
|
+
zoomIn: {
|
|
135
|
+
id: 'zoom-in',
|
|
136
|
+
label: 'Zoom In',
|
|
137
|
+
keys: 'mod+=',
|
|
138
|
+
macDisplay: '⌘+',
|
|
139
|
+
winDisplay: 'Ctrl++',
|
|
140
|
+
category: 'navigation',
|
|
141
|
+
},
|
|
142
|
+
zoomOut: {
|
|
143
|
+
id: 'zoom-out',
|
|
144
|
+
label: 'Zoom Out',
|
|
145
|
+
keys: 'mod+-',
|
|
146
|
+
macDisplay: '⌘−',
|
|
147
|
+
winDisplay: 'Ctrl+-',
|
|
148
|
+
category: 'navigation',
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// === View ===
|
|
152
|
+
toggleSidebar: {
|
|
153
|
+
id: 'toggle-sidebar',
|
|
154
|
+
label: 'Toggle Node Sidebar',
|
|
155
|
+
keys: 'mod+b',
|
|
156
|
+
macDisplay: '⌘B',
|
|
157
|
+
winDisplay: 'Ctrl+B',
|
|
158
|
+
category: 'view',
|
|
159
|
+
},
|
|
160
|
+
toggleTheme: {
|
|
161
|
+
id: 'toggle-theme',
|
|
162
|
+
label: 'Toggle Dark/Light Mode',
|
|
163
|
+
keys: 'mod+shift+l',
|
|
164
|
+
macDisplay: '⌘⇧L',
|
|
165
|
+
winDisplay: 'Ctrl+Shift+L',
|
|
166
|
+
category: 'view',
|
|
167
|
+
},
|
|
168
|
+
toggleChat: {
|
|
169
|
+
id: 'toggle-chat',
|
|
170
|
+
label: 'Toggle AI Chat Assistant',
|
|
171
|
+
keys: 'mod+shift+a',
|
|
172
|
+
macDisplay: '⌘⇧A',
|
|
173
|
+
winDisplay: 'Ctrl+Shift+A',
|
|
174
|
+
category: 'view',
|
|
175
|
+
},
|
|
176
|
+
} as const satisfies Record<string, KeyboardShortcut>;
|
|
177
|
+
|
|
178
|
+
export type ShortcutId = keyof typeof SHORTCUTS;
|
|
179
|
+
|
|
180
|
+
/** All shortcuts as a flat array, for iteration */
|
|
181
|
+
export const ALL_SHORTCUTS: KeyboardShortcut[] = Object.values(SHORTCUTS);
|
|
182
|
+
|
|
183
|
+
/** Shortcuts grouped by category */
|
|
184
|
+
export function getShortcutsByCategory(): Record<ShortcutCategory, KeyboardShortcut[]> {
|
|
185
|
+
const grouped: Record<ShortcutCategory, KeyboardShortcut[]> = {
|
|
186
|
+
general: [],
|
|
187
|
+
editing: [],
|
|
188
|
+
navigation: [],
|
|
189
|
+
view: [],
|
|
190
|
+
};
|
|
191
|
+
for (const shortcut of ALL_SHORTCUTS) {
|
|
192
|
+
grouped[shortcut.category].push(shortcut);
|
|
193
|
+
}
|
|
194
|
+
return grouped;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Human-readable category labels */
|
|
198
|
+
export const CATEGORY_LABELS: Record<ShortcutCategory, string> = {
|
|
199
|
+
general: 'General',
|
|
200
|
+
editing: 'Editing',
|
|
201
|
+
navigation: 'Navigation',
|
|
202
|
+
view: 'View',
|
|
203
|
+
};
|
|
@@ -155,19 +155,19 @@ function serializeEdge(edge: ClipboardEdge, nodeIdToRef: Map<string, string>): s
|
|
|
155
155
|
/**
|
|
156
156
|
* Convert clipboard nodes and edges into SDK source code text.
|
|
157
157
|
*
|
|
158
|
-
* Returns a string like:
|
|
158
|
+
* Returns a string shaped like a `defineFlow()` body:
|
|
159
159
|
* ```
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
160
|
+
* nodes: [
|
|
161
|
+
* input('query', { variableName: 'query' }),
|
|
162
|
+
* model('answer', {
|
|
163
|
+
* credentialId: 'cred-abc',
|
|
164
|
+
* model: 'gpt-4o-mini',
|
|
165
|
+
* prompt: '{{ query }}',
|
|
166
|
+
* }),
|
|
167
|
+
* ],
|
|
168
|
+
* edges: [
|
|
169
|
+
* ['query', 'answer'],
|
|
170
|
+
* ],
|
|
171
171
|
* ```
|
|
172
172
|
*/
|
|
173
173
|
export function serializeToSDK(nodes: ClipboardNode[], edges: ClipboardEdge[]): string {
|
|
@@ -179,25 +179,26 @@ export function serializeToSDK(nodes: ClipboardNode[], edges: ClipboardEdge[]):
|
|
|
179
179
|
|
|
180
180
|
const parts: string[] = [];
|
|
181
181
|
|
|
182
|
-
// Nodes
|
|
182
|
+
// Nodes array
|
|
183
183
|
if (nodes.length > 0) {
|
|
184
|
-
parts.push('
|
|
184
|
+
parts.push('nodes: [');
|
|
185
185
|
for (const n of nodes) {
|
|
186
|
-
parts.push(serializeNode(n) + ',');
|
|
187
|
-
parts.push('');
|
|
186
|
+
parts.push(' ' + serializeNode(n) + ',');
|
|
188
187
|
}
|
|
188
|
+
parts.push('],');
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// Edges
|
|
191
|
+
// Edges array
|
|
192
192
|
const serializedEdges = edges
|
|
193
193
|
.map((e) => serializeEdge(e, nodeIdToRef))
|
|
194
194
|
.filter((e): e is string => e !== null);
|
|
195
195
|
|
|
196
196
|
if (serializedEdges.length > 0) {
|
|
197
|
-
parts.push('
|
|
197
|
+
parts.push('edges: [');
|
|
198
198
|
for (const e of serializedEdges) {
|
|
199
|
-
parts.push(e + ',');
|
|
199
|
+
parts.push(' ' + e + ',');
|
|
200
200
|
}
|
|
201
|
+
parts.push('],');
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
return parts.join('\n').trimEnd();
|