@genfeedai/workflow-ui 0.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.
Files changed (86) hide show
  1. package/dist/canvas.d.mts +27 -0
  2. package/dist/canvas.d.ts +27 -0
  3. package/dist/canvas.js +45 -0
  4. package/dist/canvas.mjs +16 -0
  5. package/dist/chunk-22PDGHNQ.mjs +737 -0
  6. package/dist/chunk-3SPPKCWR.js +458 -0
  7. package/dist/chunk-3YFFDHC5.js +300 -0
  8. package/dist/chunk-5HJFQVUR.js +61 -0
  9. package/dist/chunk-5LQ4QBR5.js +2 -0
  10. package/dist/chunk-6DOEUDD5.js +254 -0
  11. package/dist/chunk-7SKSRSS7.mjs +57 -0
  12. package/dist/chunk-AC6TWLRT.mjs +27 -0
  13. package/dist/chunk-ADWNF7V3.js +120 -0
  14. package/dist/chunk-BJ3R5R32.mjs +2163 -0
  15. package/dist/chunk-CETJJ73S.js +1555 -0
  16. package/dist/chunk-CSUBLSKZ.mjs +1002 -0
  17. package/dist/chunk-CV4M7CNU.mjs +251 -0
  18. package/dist/chunk-E323WAZG.mjs +272 -0
  19. package/dist/chunk-E544XUBL.js +378 -0
  20. package/dist/chunk-EC2ZIWOK.js +1007 -0
  21. package/dist/chunk-EFXQT23N.mjs +99 -0
  22. package/dist/chunk-EMUMKW5C.js +107 -0
  23. package/dist/chunk-FOMOOERN.js +2 -0
  24. package/dist/chunk-FT33LFII.mjs +21 -0
  25. package/dist/chunk-FT64PCUP.mjs +533 -0
  26. package/dist/chunk-H6LZKSLY.js +5678 -0
  27. package/dist/chunk-HPQT36RR.js +543 -0
  28. package/dist/chunk-JLWKW3G5.js +2 -0
  29. package/dist/chunk-L5TF4EHW.mjs +1 -0
  30. package/dist/chunk-LAJ34AH2.mjs +374 -0
  31. package/dist/chunk-LDN7IX4Y.mjs +1 -0
  32. package/dist/chunk-MLJJBBTB.mjs +1 -0
  33. package/dist/chunk-NSDLGLAQ.js +2166 -0
  34. package/dist/chunk-RJ262NXS.js +24 -0
  35. package/dist/chunk-RXNEDWK2.js +141 -0
  36. package/dist/chunk-SW7QNEZU.js +744 -0
  37. package/dist/chunk-UQQUWGHW.mjs +118 -0
  38. package/dist/chunk-VOGL2WCE.mjs +1542 -0
  39. package/dist/chunk-VRN3UWE5.mjs +138 -0
  40. package/dist/chunk-XV5Z5XYR.mjs +5640 -0
  41. package/dist/chunk-Z7PWFZG5.js +30 -0
  42. package/dist/chunk-ZJD5WMR3.mjs +418 -0
  43. package/dist/hooks.d.mts +255 -0
  44. package/dist/hooks.d.ts +255 -0
  45. package/dist/hooks.js +56 -0
  46. package/dist/hooks.mjs +11 -0
  47. package/dist/index.d.mts +29 -0
  48. package/dist/index.d.ts +29 -0
  49. package/dist/index.js +180 -0
  50. package/dist/index.mjs +19 -0
  51. package/dist/lib.d.mts +164 -0
  52. package/dist/lib.d.ts +164 -0
  53. package/dist/lib.js +144 -0
  54. package/dist/lib.mjs +3 -0
  55. package/dist/nodes.d.mts +128 -0
  56. package/dist/nodes.d.ts +128 -0
  57. package/dist/nodes.js +151 -0
  58. package/dist/nodes.mjs +14 -0
  59. package/dist/panels.d.mts +22 -0
  60. package/dist/panels.d.ts +22 -0
  61. package/dist/panels.js +21 -0
  62. package/dist/panels.mjs +4 -0
  63. package/dist/promptLibraryStore-BZnfmEkc.d.ts +464 -0
  64. package/dist/promptLibraryStore-zqb59nsu.d.mts +464 -0
  65. package/dist/provider.d.mts +29 -0
  66. package/dist/provider.d.ts +29 -0
  67. package/dist/provider.js +17 -0
  68. package/dist/provider.mjs +4 -0
  69. package/dist/stores.d.mts +96 -0
  70. package/dist/stores.d.ts +96 -0
  71. package/dist/stores.js +113 -0
  72. package/dist/stores.mjs +43 -0
  73. package/dist/toolbar.d.mts +73 -0
  74. package/dist/toolbar.d.ts +73 -0
  75. package/dist/toolbar.js +34 -0
  76. package/dist/toolbar.mjs +5 -0
  77. package/dist/types-ipAnBzAJ.d.mts +46 -0
  78. package/dist/types-ipAnBzAJ.d.ts +46 -0
  79. package/dist/ui.d.mts +67 -0
  80. package/dist/ui.d.ts +67 -0
  81. package/dist/ui.js +84 -0
  82. package/dist/ui.mjs +3 -0
  83. package/dist/workflowStore-4EGKJLYK.mjs +3 -0
  84. package/dist/workflowStore-KM32FDL7.js +12 -0
  85. package/package.json +117 -0
  86. package/src/styles/workflow-ui.css +186 -0
@@ -0,0 +1,533 @@
1
+ import { Button } from './chunk-7SKSRSS7.mjs';
2
+ import { extractEnumValues, supportsImageInput, validateRequiredSchemaFields, CONNECTION_FIELDS, getSchemaDefaults, getImageDimensions, getVideoMetadata } from './chunk-EFXQT23N.mjs';
3
+ import { useExecutionStore } from './chunk-CSUBLSKZ.mjs';
4
+ import { require_dist, useWorkflowStore } from './chunk-BJ3R5R32.mjs';
5
+ import { useWorkflowUIConfig } from './chunk-FT33LFII.mjs';
6
+ import { __toESM } from './chunk-AC6TWLRT.mjs';
7
+ import { useMemo, useCallback, useRef, useEffect, useState } from 'react';
8
+ import { ChevronDown, Expand, Square, Play } from 'lucide-react';
9
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
+ import { useShallow } from 'zustand/react/shallow';
11
+
12
+ function useAIGenNode({
13
+ nodeId,
14
+ selectedModel,
15
+ schemaParams
16
+ }) {
17
+ const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
18
+ const schemaProperties = useMemo(() => {
19
+ const schema = selectedModel?.inputSchema;
20
+ return schema?.properties;
21
+ }, [selectedModel?.inputSchema]);
22
+ const enumValues = useMemo(
23
+ () => extractEnumValues(
24
+ selectedModel?.componentSchemas
25
+ ),
26
+ [selectedModel?.componentSchemas]
27
+ );
28
+ const modelSupportsImageInput = useMemo(
29
+ () => supportsImageInput(selectedModel?.inputSchema),
30
+ [selectedModel?.inputSchema]
31
+ );
32
+ const componentSchemas = selectedModel?.componentSchemas;
33
+ const handleSchemaParamChange = useCallback(
34
+ (key, value) => {
35
+ const currentNode = useWorkflowStore.getState().getNodeById(nodeId);
36
+ const currentData = currentNode?.data;
37
+ updateNodeData(nodeId, {
38
+ schemaParams: {
39
+ ...currentData?.schemaParams ?? {},
40
+ [key]: value
41
+ }
42
+ });
43
+ },
44
+ [nodeId, updateNodeData]
45
+ );
46
+ return {
47
+ schemaProperties,
48
+ enumValues,
49
+ modelSupportsImageInput,
50
+ handleSchemaParamChange,
51
+ componentSchemas
52
+ };
53
+ }
54
+ function useAIGenNodeHeader({
55
+ modelDisplayName,
56
+ isProcessing,
57
+ canGenerate,
58
+ hasOutput,
59
+ onModelBrowse,
60
+ onGenerate,
61
+ onStop,
62
+ onExpand
63
+ }) {
64
+ const titleElement = useMemo(
65
+ () => /* @__PURE__ */ jsxs(
66
+ "button",
67
+ {
68
+ className: `flex flex-1 items-center gap-1 text-sm font-medium text-left text-foreground ${isProcessing ? "opacity-50 cursor-default" : "hover:text-foreground/80 cursor-pointer"}`,
69
+ onClick: () => !isProcessing && onModelBrowse(),
70
+ title: "Browse models",
71
+ disabled: isProcessing,
72
+ children: [
73
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: modelDisplayName }),
74
+ /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" })
75
+ ]
76
+ }
77
+ ),
78
+ [modelDisplayName, isProcessing, onModelBrowse]
79
+ );
80
+ const headerActions = useMemo(
81
+ () => /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ hasOutput && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onExpand, title: "Expand preview", children: /* @__PURE__ */ jsx(Expand, { className: "h-3 w-3" }) }),
83
+ isProcessing ? /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: onStop, children: [
84
+ /* @__PURE__ */ jsx(Square, { className: "h-4 w-4 fill-current" }),
85
+ "Generating"
86
+ ] }) : /* @__PURE__ */ jsxs(
87
+ Button,
88
+ {
89
+ variant: canGenerate ? "default" : "secondary",
90
+ size: "sm",
91
+ onClick: onGenerate,
92
+ disabled: !canGenerate,
93
+ children: [
94
+ /* @__PURE__ */ jsx(Play, { className: "h-4 w-4 fill-current" }),
95
+ "Generate"
96
+ ]
97
+ }
98
+ )
99
+ ] }),
100
+ [hasOutput, isProcessing, canGenerate, onGenerate, onStop, onExpand]
101
+ );
102
+ return { titleElement, headerActions };
103
+ }
104
+ function useAutoLoadModelSchema({
105
+ currentModel,
106
+ selectedModel,
107
+ modelIdMap,
108
+ onModelSelect
109
+ }) {
110
+ const { modelSchema } = useWorkflowUIConfig();
111
+ const hasAttemptedSchemaLoad = useRef(false);
112
+ useEffect(() => {
113
+ if (hasAttemptedSchemaLoad.current || selectedModel || !currentModel || !modelSchema) {
114
+ return;
115
+ }
116
+ const modelId = modelIdMap[currentModel];
117
+ if (!modelId) return;
118
+ const controller = new AbortController();
119
+ let isCancelled = false;
120
+ const loadSchema = async () => {
121
+ try {
122
+ const model = await modelSchema.fetchModelSchema(modelId, controller.signal);
123
+ if (model && !isCancelled) {
124
+ hasAttemptedSchemaLoad.current = true;
125
+ onModelSelect(model);
126
+ }
127
+ } catch {
128
+ }
129
+ };
130
+ loadSchema();
131
+ return () => {
132
+ isCancelled = true;
133
+ controller.abort();
134
+ };
135
+ }, [currentModel, selectedModel, modelIdMap, onModelSelect, modelSchema]);
136
+ }
137
+
138
+ // src/hooks/useRequiredInputs.ts
139
+ var import_types = __toESM(require_dist());
140
+ function useRequiredInputs(nodeId, nodeType) {
141
+ const edges = useWorkflowStore((state) => state.edges);
142
+ return useMemo(() => {
143
+ const nodeDef = import_types.NODE_DEFINITIONS[nodeType];
144
+ if (!nodeDef) {
145
+ return { hasRequiredInputs: true, missingInputs: [], connectionStatus: /* @__PURE__ */ new Map() };
146
+ }
147
+ const incomingEdges = edges.filter((e) => e.target === nodeId);
148
+ const connectedHandles = new Set(incomingEdges.map((e) => e.targetHandle).filter(Boolean));
149
+ const connectionStatus = /* @__PURE__ */ new Map();
150
+ const missingInputs = [];
151
+ for (const input of nodeDef.inputs) {
152
+ const isConnected = connectedHandles.has(input.id);
153
+ connectionStatus.set(input.id, isConnected);
154
+ if (input.required && !isConnected) {
155
+ missingInputs.push(input.id);
156
+ }
157
+ }
158
+ return {
159
+ hasRequiredInputs: missingInputs.length === 0,
160
+ missingInputs,
161
+ connectionStatus
162
+ };
163
+ }, [nodeId, nodeType, edges]);
164
+ }
165
+
166
+ // src/hooks/useCanGenerate.ts
167
+ var import_types2 = __toESM(require_dist());
168
+ function extractOutputValue(node, handleType) {
169
+ const data = node.data;
170
+ if (handleType === "text") {
171
+ return data.outputText ?? data.prompt;
172
+ } else if (handleType === "image") {
173
+ return data.outputImage ?? data.image;
174
+ } else if (handleType === "video") {
175
+ return data.outputVideo ?? data.video;
176
+ } else if (handleType === "audio") {
177
+ return data.outputAudio ?? data.audio;
178
+ }
179
+ return void 0;
180
+ }
181
+ function useCanGenerate({
182
+ nodeId,
183
+ nodeType,
184
+ inputSchema,
185
+ schemaParams
186
+ }) {
187
+ const { hasRequiredInputs, missingInputs } = useRequiredInputs(nodeId, nodeType);
188
+ const getConnectedInputs = useWorkflowStore((state) => state.getConnectedInputs);
189
+ const incomingEdgesSelector = useCallback(
190
+ (state) => state.edges.filter((e) => e.target === nodeId),
191
+ [nodeId]
192
+ );
193
+ const incomingEdges = useWorkflowStore(useShallow(incomingEdgesSelector));
194
+ const connectedOutputsSelector = useCallback(
195
+ (state) => {
196
+ const outputs = {};
197
+ for (const edge of incomingEdges) {
198
+ const sourceNode = state.nodes.find((n) => n.id === edge.source);
199
+ if (sourceNode) {
200
+ outputs[edge.source] = extractOutputValue(sourceNode, edge.sourceHandle);
201
+ }
202
+ }
203
+ return outputs;
204
+ },
205
+ [incomingEdges]
206
+ );
207
+ useWorkflowStore(useShallow(connectedOutputsSelector));
208
+ return useMemo(() => {
209
+ const missingItems = [];
210
+ for (const inputId of missingInputs) {
211
+ missingItems.push({
212
+ type: "connection",
213
+ field: inputId,
214
+ message: `Missing connection: ${inputId}`
215
+ });
216
+ }
217
+ const connectedInputs = getConnectedInputs(nodeId);
218
+ const nodeDef = import_types2.NODE_DEFINITIONS[nodeType];
219
+ const requiredHandleIds = new Set(
220
+ nodeDef?.inputs.filter((h) => h.required).map((h) => h.id) ?? []
221
+ );
222
+ let hasRequiredData = true;
223
+ let hasConnectedData = true;
224
+ if (hasRequiredInputs) {
225
+ for (const edge of incomingEdges) {
226
+ const handleId = edge.targetHandle;
227
+ if (!handleId) continue;
228
+ if (!connectedInputs.has(handleId)) {
229
+ hasConnectedData = false;
230
+ if (requiredHandleIds.has(handleId)) {
231
+ hasRequiredData = false;
232
+ }
233
+ }
234
+ }
235
+ }
236
+ const schemaValidation = validateRequiredSchemaFields(
237
+ inputSchema,
238
+ schemaParams ?? {},
239
+ CONNECTION_FIELDS
240
+ );
241
+ for (const field of schemaValidation.missingFields) {
242
+ missingItems.push({
243
+ type: "schema",
244
+ field,
245
+ message: `Required field: ${field}`
246
+ });
247
+ }
248
+ const canGenerate = hasRequiredInputs && hasRequiredData && schemaValidation.isValid;
249
+ return {
250
+ canGenerate,
251
+ missingItems,
252
+ hasRequiredConnections: hasRequiredInputs,
253
+ hasConnectedData,
254
+ hasRequiredSchemaFields: schemaValidation.isValid
255
+ };
256
+ }, [
257
+ nodeId,
258
+ nodeType,
259
+ hasRequiredInputs,
260
+ missingInputs,
261
+ inputSchema,
262
+ schemaParams,
263
+ getConnectedInputs,
264
+ incomingEdges
265
+ ]);
266
+ }
267
+ function useModelSelection({ nodeId, modelMap, fallbackModel }) {
268
+ const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
269
+ const handleModelSelect = useCallback(
270
+ (model) => {
271
+ const internalModel = modelMap[model.id] ?? fallbackModel;
272
+ const schemaDefaults = getSchemaDefaults(model.inputSchema);
273
+ const selectedModel = {
274
+ provider: model.provider,
275
+ modelId: model.id,
276
+ displayName: model.displayName,
277
+ inputSchema: model.inputSchema,
278
+ componentSchemas: model.componentSchemas
279
+ };
280
+ updateNodeData(nodeId, {
281
+ model: internalModel,
282
+ provider: model.provider,
283
+ selectedModel,
284
+ schemaParams: schemaDefaults
285
+ });
286
+ },
287
+ [nodeId, modelMap, fallbackModel, updateNodeData]
288
+ );
289
+ return { handleModelSelect };
290
+ }
291
+
292
+ // src/hooks/useNodeExecution.ts
293
+ var import_types3 = __toESM(require_dist());
294
+ function useNodeExecution(nodeId) {
295
+ const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
296
+ const executeNode = useExecutionStore((state) => state.executeNode);
297
+ const stopNodeExecution = useExecutionStore((state) => state.stopNodeExecution);
298
+ const handleGenerate = useCallback(() => {
299
+ updateNodeData(nodeId, { status: import_types3.NodeStatusEnum.PROCESSING });
300
+ executeNode(nodeId);
301
+ }, [nodeId, executeNode, updateNodeData]);
302
+ const handleStop = useCallback(() => {
303
+ stopNodeExecution(nodeId);
304
+ }, [nodeId, stopNodeExecution]);
305
+ return { handleGenerate, handleStop };
306
+ }
307
+ function readFileAsBase64(file, nodeId, getMetadata, buildUploadUpdate, updateNodeData, onComplete) {
308
+ const reader = new FileReader();
309
+ reader.onload = async (event) => {
310
+ const dataUrl = event.target?.result;
311
+ const metadata = await getMetadata(dataUrl);
312
+ updateNodeData(nodeId, buildUploadUpdate(dataUrl, file.name, metadata));
313
+ };
314
+ reader.readAsDataURL(file);
315
+ }
316
+ function useMediaUpload({
317
+ nodeId,
318
+ mediaType,
319
+ initialUrl = "",
320
+ getMetadata,
321
+ buildUploadUpdate,
322
+ buildUrlUpdate,
323
+ buildRemoveUpdate
324
+ }) {
325
+ const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
326
+ const workflowId = useWorkflowStore((state) => state.workflowId);
327
+ const { fileUpload } = useWorkflowUIConfig();
328
+ const fileInputRef = useRef(null);
329
+ const [showUrlInput, setShowUrlInput] = useState(false);
330
+ const [urlValue, setUrlValue] = useState(initialUrl);
331
+ const [isUploading, setIsUploading] = useState(false);
332
+ const getMetadataRef = useRef(getMetadata);
333
+ getMetadataRef.current = getMetadata;
334
+ const buildUploadUpdateRef = useRef(buildUploadUpdate);
335
+ buildUploadUpdateRef.current = buildUploadUpdate;
336
+ const buildUrlUpdateRef = useRef(buildUrlUpdate);
337
+ buildUrlUpdateRef.current = buildUrlUpdate;
338
+ const buildRemoveUpdateRef = useRef(buildRemoveUpdate);
339
+ buildRemoveUpdateRef.current = buildRemoveUpdate;
340
+ const handleFileSelect = useCallback(
341
+ async (e) => {
342
+ const file = e.target.files?.[0];
343
+ if (!file) return;
344
+ if (workflowId && fileUpload) {
345
+ setIsUploading(true);
346
+ try {
347
+ const result = await fileUpload.uploadFile(
348
+ `/files/workflows/${workflowId}/input/${mediaType}`,
349
+ file
350
+ );
351
+ const metadata = await getMetadataRef.current(result.url);
352
+ updateNodeData(
353
+ nodeId,
354
+ buildUploadUpdateRef.current(result.url, result.filename, metadata)
355
+ );
356
+ } catch (_error) {
357
+ readFileAsBase64(
358
+ file,
359
+ nodeId,
360
+ getMetadataRef.current,
361
+ buildUploadUpdateRef.current,
362
+ updateNodeData
363
+ );
364
+ } finally {
365
+ setIsUploading(false);
366
+ }
367
+ } else {
368
+ readFileAsBase64(
369
+ file,
370
+ nodeId,
371
+ getMetadataRef.current,
372
+ buildUploadUpdateRef.current,
373
+ updateNodeData
374
+ );
375
+ }
376
+ },
377
+ [nodeId, updateNodeData, workflowId, mediaType, fileUpload]
378
+ );
379
+ const handleRemove = useCallback(() => {
380
+ updateNodeData(nodeId, buildRemoveUpdateRef.current());
381
+ setUrlValue("");
382
+ }, [nodeId, updateNodeData]);
383
+ const handleUrlSubmit = useCallback(async () => {
384
+ if (!urlValue.trim()) return;
385
+ try {
386
+ let metadata;
387
+ if (mediaType === "image") {
388
+ metadata = await getImageDimensions(urlValue);
389
+ } else {
390
+ const meta = await getVideoMetadata(urlValue);
391
+ metadata = {
392
+ duration: meta.duration,
393
+ width: meta.dimensions.width,
394
+ height: meta.dimensions.height
395
+ };
396
+ }
397
+ updateNodeData(nodeId, buildUrlUpdateRef.current(urlValue, metadata));
398
+ } catch (_error) {
399
+ updateNodeData(nodeId, buildUrlUpdateRef.current(urlValue, null));
400
+ }
401
+ setShowUrlInput(false);
402
+ }, [nodeId, updateNodeData, urlValue, mediaType]);
403
+ const handleUrlKeyDown = useCallback(
404
+ (e) => {
405
+ if (e.key === "Enter") {
406
+ handleUrlSubmit();
407
+ } else if (e.key === "Escape") {
408
+ setShowUrlInput(false);
409
+ setUrlValue(initialUrl);
410
+ }
411
+ },
412
+ [handleUrlSubmit, initialUrl]
413
+ );
414
+ return {
415
+ fileInputRef,
416
+ showUrlInput,
417
+ setShowUrlInput,
418
+ urlValue,
419
+ setUrlValue,
420
+ isUploading,
421
+ handleFileSelect,
422
+ handleRemove,
423
+ handleUrlSubmit,
424
+ handleUrlKeyDown
425
+ };
426
+ }
427
+ function usePromptAutocomplete({
428
+ availableVariables,
429
+ textareaRef,
430
+ localTemplate,
431
+ setLocalTemplate,
432
+ onTemplateCommit
433
+ }) {
434
+ const [showAutocomplete, setShowAutocomplete] = useState(false);
435
+ const [autocompletePosition, setAutocompletePosition] = useState({ top: 0, left: 0 });
436
+ const [autocompleteFilter, setAutocompleteFilter] = useState("");
437
+ const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] = useState(0);
438
+ const filteredAutocompleteVars = useMemo(() => {
439
+ return availableVariables.filter(
440
+ (v) => v.name.toLowerCase().includes(autocompleteFilter.toLowerCase())
441
+ );
442
+ }, [availableVariables, autocompleteFilter]);
443
+ const handleAutocompleteSelect = useCallback(
444
+ (varName) => {
445
+ if (!textareaRef.current) return;
446
+ const cursorPos = textareaRef.current.selectionStart;
447
+ const textBeforeCursor = localTemplate.slice(0, cursorPos);
448
+ const textAfterCursor = localTemplate.slice(cursorPos);
449
+ const match = textBeforeCursor.match(/@(\w*)$/);
450
+ if (!match) return;
451
+ const atPosition = cursorPos - match[0].length;
452
+ const newTemplate = `${localTemplate.slice(0, atPosition)}@${varName}${textAfterCursor}`;
453
+ setLocalTemplate(newTemplate);
454
+ onTemplateCommit?.(newTemplate);
455
+ setShowAutocomplete(false);
456
+ const newCursorPos = atPosition + varName.length + 1;
457
+ setTimeout(() => {
458
+ if (textareaRef.current) {
459
+ textareaRef.current.focus();
460
+ textareaRef.current.setSelectionRange(newCursorPos, newCursorPos);
461
+ }
462
+ }, 0);
463
+ },
464
+ [localTemplate, textareaRef, setLocalTemplate, onTemplateCommit]
465
+ );
466
+ const handleChange = useCallback(
467
+ (e) => {
468
+ const newValue = e.target.value;
469
+ setLocalTemplate(newValue);
470
+ const cursorPos = e.target.selectionStart;
471
+ const textBeforeCursor = newValue.slice(0, cursorPos);
472
+ const match = textBeforeCursor.match(/@(\w*)$/);
473
+ if (match && textareaRef.current) {
474
+ setAutocompleteFilter(match[1] || "");
475
+ setSelectedAutocompleteIndex(0);
476
+ const lineHeight = 20;
477
+ const lines = textBeforeCursor.split("\n");
478
+ const currentLine = lines.length - 1;
479
+ const top = currentLine * lineHeight + 30;
480
+ const left = 10;
481
+ setAutocompletePosition({ top, left });
482
+ setShowAutocomplete(true);
483
+ } else {
484
+ setShowAutocomplete(false);
485
+ }
486
+ },
487
+ [textareaRef, setLocalTemplate]
488
+ );
489
+ const handleKeyDown = useCallback(
490
+ (e) => {
491
+ if (!showAutocomplete) return;
492
+ if (e.key === "ArrowDown") {
493
+ e.preventDefault();
494
+ setSelectedAutocompleteIndex((prev) => (prev + 1) % filteredAutocompleteVars.length);
495
+ } else if (e.key === "ArrowUp") {
496
+ e.preventDefault();
497
+ setSelectedAutocompleteIndex(
498
+ (prev) => (prev - 1 + filteredAutocompleteVars.length) % filteredAutocompleteVars.length
499
+ );
500
+ } else if (e.key === "Enter" || e.key === "Tab") {
501
+ if (filteredAutocompleteVars.length > 0) {
502
+ e.preventDefault();
503
+ handleAutocompleteSelect(filteredAutocompleteVars[selectedAutocompleteIndex].name);
504
+ }
505
+ } else if (e.key === "Escape") {
506
+ e.preventDefault();
507
+ e.stopPropagation();
508
+ setShowAutocomplete(false);
509
+ }
510
+ },
511
+ [
512
+ showAutocomplete,
513
+ filteredAutocompleteVars,
514
+ selectedAutocompleteIndex,
515
+ handleAutocompleteSelect
516
+ ]
517
+ );
518
+ const closeAutocomplete = useCallback(() => {
519
+ setShowAutocomplete(false);
520
+ }, []);
521
+ return {
522
+ showAutocomplete,
523
+ autocompletePosition,
524
+ filteredAutocompleteVars,
525
+ selectedAutocompleteIndex,
526
+ handleChange,
527
+ handleKeyDown,
528
+ handleAutocompleteSelect,
529
+ closeAutocomplete
530
+ };
531
+ }
532
+
533
+ export { useAIGenNode, useAIGenNodeHeader, useAutoLoadModelSchema, useCanGenerate, useMediaUpload, useModelSelection, useNodeExecution, usePromptAutocomplete, useRequiredInputs };