@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.
@@ -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
+ }
@@ -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
  ],