@viji-dev/sdk 1.0.0 → 1.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.
Files changed (77) hide show
  1. package/README.md +70 -63
  2. package/bin/viji.js +9 -29
  3. package/dist/assets/artist-dts-BHUsvSI6.js +613 -0
  4. package/dist/assets/artist-dts-p5-Cyw8vmy_.js +736 -0
  5. package/dist/assets/core-CiQx3w0t.js +12 -0
  6. package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
  7. package/dist/assets/docs-api-PBLtY4Ni.js +12381 -0
  8. package/dist/assets/engine-javascript-CXyY7cc8.js +141 -0
  9. package/dist/assets/essentia-wasm.web-0S-sW98u-CYV1l1zv.js +38 -0
  10. package/dist/assets/essentia.js-core.es-DnrJE0uR-DOSrF5_G.js +32 -0
  11. package/dist/assets/glsl-DMyvO4G4.js +1 -0
  12. package/dist/assets/index-BhFxsauQ.js +215 -0
  13. package/dist/assets/index-BqhVeA7U.css +1 -0
  14. package/dist/assets/index-T4TOjvD0.js +1 -0
  15. package/dist/assets/index-Wz9WqGqz.js +52 -0
  16. package/dist/assets/index-t24aGwla.js +1 -0
  17. package/dist/assets/javascript-wDzz0qaB.js +1 -0
  18. package/dist/assets/shader-uniforms-GdaUkQPK.js +1 -0
  19. package/dist/assets/typescript-BPQ3VLAy.js +1 -0
  20. package/dist/assets/viji.worker-CQSJ0SiO-ljtBlcNZ.js +27018 -0
  21. package/{index.html → dist/index.html} +2 -1
  22. package/package.json +31 -35
  23. package/src/cli/commands/build.js +50 -99
  24. package/src/cli/commands/create.js +49 -46
  25. package/src/cli/commands/dev.js +30 -97
  26. package/src/cli/server/dev-server.js +233 -0
  27. package/src/cli/server/scene-scanner.js +93 -0
  28. package/src/cli/server/vite-scene-plugin.d.ts +2 -0
  29. package/src/cli/server/vite-scene-plugin.js +134 -0
  30. package/src/cli/utils/cli-utils.js +29 -139
  31. package/src/cli/utils/scene-compiler.js +10 -17
  32. package/src/templates/scene-templates.js +85 -0
  33. package/.gitignore +0 -29
  34. package/eslint.config.js +0 -37
  35. package/postcss.config.js +0 -6
  36. package/scenes/audio-visualizer/main.js +0 -287
  37. package/scenes/core-demo/main.js +0 -532
  38. package/scenes/demo-scene/main.js +0 -619
  39. package/scenes/global.d.ts +0 -15
  40. package/scenes/particle-system/main.js +0 -349
  41. package/scenes/tsconfig.json +0 -12
  42. package/scenes/video-mirror/main.ts +0 -436
  43. package/src/App.css +0 -42
  44. package/src/App.tsx +0 -279
  45. package/src/cli/commands/init.js +0 -262
  46. package/src/components/SDKPage.tsx +0 -337
  47. package/src/components/core/CoreContainer.tsx +0 -126
  48. package/src/components/ui/DeviceSelectionList.tsx +0 -137
  49. package/src/components/ui/FPSCounter.tsx +0 -78
  50. package/src/components/ui/FileDropzonePanel.tsx +0 -120
  51. package/src/components/ui/FileListPanel.tsx +0 -285
  52. package/src/components/ui/InputExpansionPanel.tsx +0 -31
  53. package/src/components/ui/MediaPlayerControls.tsx +0 -191
  54. package/src/components/ui/MenuContainer.tsx +0 -71
  55. package/src/components/ui/ParametersMenu.tsx +0 -797
  56. package/src/components/ui/ProjectSwitcherMenu.tsx +0 -192
  57. package/src/components/ui/QuickInputControls.tsx +0 -542
  58. package/src/components/ui/SDKMenuSystem.tsx +0 -96
  59. package/src/components/ui/SettingsMenu.tsx +0 -346
  60. package/src/components/ui/SimpleInputControls.tsx +0 -137
  61. package/src/index.css +0 -68
  62. package/src/main.tsx +0 -10
  63. package/src/scenes-hmr.ts +0 -158
  64. package/src/services/project-filesystem.ts +0 -436
  65. package/src/stores/scene-player/index.ts +0 -3
  66. package/src/stores/scene-player/input-manager.store.ts +0 -1045
  67. package/src/stores/scene-player/scene-session.store.ts +0 -659
  68. package/src/styles/globals.css +0 -111
  69. package/src/templates/minimal-template.js +0 -11
  70. package/src/utils/debounce.js +0 -34
  71. package/src/vite-env.d.ts +0 -1
  72. package/tailwind.config.js +0 -18
  73. package/tsconfig.app.json +0 -27
  74. package/tsconfig.json +0 -27
  75. package/tsconfig.node.json +0 -27
  76. package/vite.config.ts +0 -54
  77. /package/{public → dist}/favicon.png +0 -0
@@ -1,797 +0,0 @@
1
- import React, { useState, useMemo, useCallback, useEffect } from 'react';
2
- import {
3
- Button,
4
- Input,
5
- Slider,
6
- Switch,
7
- Select,
8
- SelectItem,
9
- Card,
10
- CardBody,
11
- Modal,
12
- ModalContent,
13
- ModalHeader,
14
- ModalBody,
15
- ModalFooter,
16
- useDisclosure,
17
- Spinner,
18
- Tooltip,
19
- } from '@heroui/react';
20
- import {
21
- PlusIcon,
22
- PencilIcon,
23
- TrashIcon,
24
- ArrowLeftIcon,
25
- } from '@heroicons/react/24/outline';
26
- import { useSceneSessionStore } from '../../stores/scene-player/scene-session.store';
27
- import { useInputManagerStore, AudioInputType, VideoInputType } from '../../stores/scene-player/input-manager.store';
28
-
29
-
30
- interface ParametersMenuProps {
31
- className?: string;
32
- hidePresetsControls?: boolean; // when true, hide presets column and save-as-preset UI (used by editor)
33
- }
34
-
35
- // Removed debouncing - it was making parameter updates slower
36
-
37
- // Helper function to convert thumbnail storage keys to URLs
38
- const getThumbnailUrl = (thumbnail: string): string => {
39
- // If it's already a full URL, return as-is
40
- if (thumbnail.startsWith('http://') || thumbnail.startsWith('https://')) {
41
- return thumbnail;
42
- }
43
-
44
- // If it's a storage key (like 'thumbnails/presets/id/image.webp'),
45
- // we need the backend to provide the CDN base URL
46
- // For now, return the original value and let the backend handle URL construction
47
- return thumbnail;
48
- };
49
-
50
- const ParametersMenu: React.FC<ParametersMenuProps> = ({ className = '', hidePresetsControls = false }) => {
51
- const [isPresetManagementOpen, setIsPresetManagementOpen] = useState(false);
52
- const [presetToDelete, setPresetToDelete] = useState<string | null>(null);
53
- const [editingPresetId, setEditingPresetId] = useState<string | null>(null);
54
- const [editingName, setEditingName] = useState<string>('');
55
- const [draggingId, setDraggingId] = useState<string | null>(null);
56
- const [orderedIds, setOrderedIds] = useState<string[]>([]);
57
-
58
- const { isOpen: isDeleteModalOpen, onOpen: onDeleteModalOpen, onClose: onDeleteModalClose } = useDisclosure();
59
-
60
- // Store selectors - using stable selectors to prevent infinite loops
61
- const parameters = useSceneSessionStore((state) => state.session?.parameters || {});
62
- const parameterGroups = useSceneSessionStore((state) => state.session?.parameterGroups || []);
63
- const availablePresets = useSceneSessionStore((state) => state.session?.availablePresets || []);
64
- const activePresetId = useSceneSessionStore((state) => state.session?.activePresetId);
65
- const hasUnsavedChanges = useSceneSessionStore((state) => state.session?.hasUnsavedChanges || false);
66
- const isLoadingPresets = useSceneSessionStore((state) => state.isLoadingPresets);
67
- const isSavingPreset = useSceneSessionStore((state) => state.isSavingPreset);
68
- const presetError = useSceneSessionStore((state) => state.presetError);
69
- const session = useSceneSessionStore((state) => state.session);
70
-
71
- // Store actions
72
- const updateParameters = useSceneSessionStore((state) => state.updateParameters);
73
- const applyPreset = useSceneSessionStore((state) => state.applyPreset);
74
- const saveCurrentAsPreset = useSceneSessionStore((state) => state.saveCurrentAsPreset);
75
- const deletePreset = useSceneSessionStore((state) => state.deletePreset);
76
- const loadPresets = useSceneSessionStore((state) => state.loadPresets);
77
- const renamePreset = useSceneSessionStore((state) => state.renamePreset);
78
- // Note: reorderPresets not implemented in SDK store yet
79
- const reorderPresets = () => console.log('reorderPresets not implemented in SDK');
80
-
81
- // SDK doesn't have authentication - use mock user
82
- const currentUser = { id: 'sdk-user', username: 'SDK User' };
83
-
84
- // Input state selectors
85
- const inputConfiguration = useInputManagerStore((state) => state.inputConfiguration);
86
-
87
- // Get capabilities from core and input state
88
- const getCapabilities = () => {
89
- // Compute capabilities based on current input configuration (immediate)
90
- const configBasedAudio = inputConfiguration.audio.enabled && inputConfiguration.audio.type !== AudioInputType.NONE;
91
- const configBasedVideo = inputConfiguration.video.enabled && inputConfiguration.video.type !== VideoInputType.NONE;
92
- const configBasedInteraction = inputConfiguration.interactionEnabled;
93
-
94
- // Use configuration-based capabilities for immediate UI updates
95
- return {
96
- hasGeneral: true, // General parameters are always available
97
- hasAudio: configBasedAudio, // Use config state for immediate response
98
- hasVideo: configBasedVideo, // Use config state for immediate response
99
- hasInteraction: configBasedInteraction, // Use config state for immediate response
100
- };
101
- };
102
-
103
- // Memoize capabilities to prevent infinite loops - include input state dependencies
104
- const capabilities = useMemo(() => getCapabilities(), [
105
- session?.coreInstance,
106
- inputConfiguration.audio.enabled,
107
- inputConfiguration.audio.type,
108
- inputConfiguration.video.enabled,
109
- inputConfiguration.video.type,
110
- inputConfiguration.interactionEnabled
111
- ]);
112
-
113
- // Removed excessive debug logging that was causing performance issues on every parameter change
114
-
115
-
116
-
117
- // Memoize parameter structure only (values will be read on-demand)
118
- const processedGroups = useMemo(() => parameterGroups.map((group: any) => {
119
- const processedParameters: any[] = [];
120
-
121
- // Only handle object format (Record<string, ParameterDefinition>)
122
- if (group.parameters && typeof group.parameters === 'object' && !Array.isArray(group.parameters)) {
123
- Object.entries(group.parameters).forEach(([name, param]: [string, any]) => {
124
- processedParameters.push({
125
- name,
126
- definition: param,
127
- type: param.type || 'text',
128
- min: param.config?.min,
129
- max: param.config?.max,
130
- step: param.config?.step,
131
- options: param.config?.options || param.options,
132
- label: param.label || name.replace(/([A-Z])/g, ' $1').replace(/^./, (str: string) => str.toUpperCase()),
133
- // Remove value from processing - read it on-demand in render
134
- });
135
- });
136
- }
137
-
138
- return {
139
- ...group,
140
- processedParameters,
141
- };
142
- }), [parameterGroups]); // Removed parameters dependency - huge performance gain!
143
-
144
- // PURE CORE DEMO APPROACH: Only immediate core updates, no store updates
145
- const handleParameterChange = useCallback((paramName: string, value: any) => {
146
- // 🚀 IMMEDIATE: Update core directly (no await, no constraints) - exactly like the demo
147
- if (session?.coreInstance) {
148
- session.coreInstance.setParameter(paramName, value);
149
- }
150
- // No store updates at all - inputs manage their own state naturally
151
- }, [session?.coreInstance]);
152
-
153
- // No cleanup needed for immediate updates
154
-
155
- const handleApplyPreset = async (presetId: string) => {
156
- try {
157
- await applyPreset(presetId);
158
- } catch (error) {
159
- console.error('Failed to apply preset:', error);
160
- }
161
- };
162
-
163
- const handleSaveAsPreset = async () => {
164
- try {
165
- // determine default name: Preset N (next number)
166
- const base = 'Preset ';
167
- let maxNum = 0;
168
- availablePresets.forEach((p: any) => {
169
- const m = /^Preset\s+(\d+)$/.exec(p.name.trim());
170
- if (m) {
171
- const n = parseInt(m[1], 10);
172
- if (!Number.isNaN(n)) maxNum = Math.max(maxNum, n);
173
- }
174
- });
175
- const defaultName = `${base}${maxNum + 1}`;
176
-
177
- // SDK doesn't support thumbnail capture yet - skip thumbnails
178
- await saveCurrentAsPreset(defaultName);
179
- } catch (error) {
180
- console.error('Failed to save preset:', error);
181
- }
182
- };
183
-
184
- const handleDeletePreset = async () => {
185
- if (!presetToDelete) return;
186
-
187
- try {
188
- await deletePreset(presetToDelete);
189
- setPresetToDelete(null);
190
- onDeleteModalClose();
191
- } catch (error) {
192
- console.error('Failed to delete preset:', error);
193
- }
194
- };
195
-
196
- const confirmDeletePreset = (presetId: string) => {
197
- setPresetToDelete(presetId);
198
- onDeleteModalOpen();
199
- };
200
-
201
- // Render capabilities info (like in the example)
202
- const renderCapabilitiesInfo = () => (
203
- <div className="mb-4 p-3 bg-black/30 rounded-lg border border-white/10">
204
- <div className="text-white text-sm font-medium mb-2">📊 Active Capabilities</div>
205
- <div className="space-y-1 text-xs">
206
- <div className={`flex items-center space-x-2 ${capabilities.hasGeneral ? 'text-green-400' : 'text-gray-500'}`}>
207
- <span>🔧</span>
208
- <span>General: {capabilities.hasGeneral ? 'Always Active' : 'Inactive'}</span>
209
- </div>
210
- <div className={`flex items-center space-x-2 ${capabilities.hasAudio ? 'text-green-400' : 'text-gray-500'}`}>
211
- <span>🎵</span>
212
- <span>Audio: {capabilities.hasAudio ? 'Connected' : 'Not Connected'}</span>
213
- </div>
214
- <div className={`flex items-center space-x-2 ${capabilities.hasVideo ? 'text-green-400' : 'text-gray-500'}`}>
215
- <span>📹</span>
216
- <span>Video: {capabilities.hasVideo ? 'Connected' : 'Not Connected'}</span>
217
- </div>
218
- <div className={`flex items-center space-x-2 ${capabilities.hasInteraction ? 'text-green-400' : 'text-gray-500'}`}>
219
- <span>🖱️</span>
220
- <span>Interaction: {capabilities.hasInteraction ? 'Enabled' : 'Disabled'}</span>
221
- </div>
222
- </div>
223
- <div className="text-white/60 text-xs mt-2">
224
- Only parameters for active capabilities are shown below.
225
- </div>
226
- </div>
227
- );
228
-
229
- // Drag-and-drop ordering (HTML5 DnD)
230
- useEffect(() => {
231
- if (!draggingId) {
232
- setOrderedIds(availablePresets.map((p: any) => p.id));
233
- }
234
- }, [availablePresets, draggingId]);
235
-
236
- const onDragStart = (id: string) => {
237
- setDraggingId(id);
238
- setOrderedIds(availablePresets.map((p: any) => p.id));
239
- };
240
- const onDragOver = (e: React.DragEvent, overId: string) => {
241
- e.preventDefault();
242
- if (!draggingId || draggingId === overId) return;
243
- setOrderedIds(prev => {
244
- const next = [...prev];
245
- const from = next.indexOf(draggingId);
246
- const to = next.indexOf(overId);
247
- if (from === -1 || to === -1) return prev;
248
- next.splice(from, 1);
249
- next.splice(to, 0, draggingId);
250
- return next;
251
- });
252
- };
253
- const onDrop = () => {
254
- if (orderedIds.length > 0) {
255
- console.log('Reorder presets - not implemented in SDK yet:', orderedIds);
256
- // reorderPresets(orderedIds); // Not implemented yet
257
- }
258
- setDraggingId(null);
259
- };
260
-
261
- const renderParameterControl = (param: any) => {
262
- const { name, type, min, max, step, options, label } = param;
263
- // Read current value from store or default (inputs will handle their own responsiveness)
264
- const value = parameters[name] !== undefined ? parameters[name] : param.definition?.defaultValue;
265
-
266
- switch (type) {
267
- case 'slider':
268
- return (
269
- <div key={`${name}-${value ?? ''}`} className="mb-3">
270
- <div className="flex items-center justify-between mb-2">
271
- <label className="text-white text-sm">{label || name}</label>
272
- <span className="text-white/70 text-xs">{value}</span>
273
- </div>
274
- <Slider
275
- defaultValue={value}
276
- onChange={(val: number | number[]) => {
277
- const numValue = Array.isArray(val) ? val[0] : val;
278
- // Immediate core update for smooth scene response
279
- handleParameterChange(name, numValue);
280
- }}
281
- onChangeEnd={(val: number | number[]) => {
282
- const numValue = Array.isArray(val) ? val[0] : val;
283
- // Commit to store after interaction ends to avoid UI thrash
284
- updateParameters({ [name]: numValue }, true);
285
- }}
286
- minValue={min ?? 0}
287
- maxValue={max ?? 100}
288
- step={step ?? 1}
289
- className="w-full"
290
- color="primary"
291
- size="sm"
292
- aria-label={label || name}
293
- />
294
- </div>
295
- );
296
-
297
- case 'toggle':
298
- return (
299
- <div key={`${name}-${value ?? ''}`} className="flex items-center justify-between mb-3">
300
- <span className="text-white text-sm">{label || name}</span>
301
- <Switch
302
- defaultSelected={value}
303
- onValueChange={(val) => {
304
- handleParameterChange(name, val);
305
- updateParameters({ [name]: val }, true);
306
- }}
307
- color="primary"
308
- size="sm"
309
- />
310
- </div>
311
- );
312
-
313
- case 'color':
314
- return (
315
- <div key={`${name}-${value ?? ''}`} className="mb-3">
316
- <label className="text-white text-sm block mb-2">{label || name}</label>
317
- <div className="flex items-center space-x-2">
318
- <Input
319
- type="color"
320
- key={`${name}-colorPicker-${value ?? ''}`}
321
- defaultValue={value}
322
- onChange={(e) => handleParameterChange(name, e.target.value)}
323
- onBlur={(e) => updateParameters({ [name]: e.target.value }, true)}
324
- className="w-16 h-8"
325
- size="sm"
326
- />
327
- <Input
328
- key={`${name}-colorText-${value ?? ''}`}
329
- defaultValue={String(value)}
330
- onChange={(e) => handleParameterChange(name, e.target.value)}
331
- onBlur={(e) => updateParameters({ [name]: e.target.value }, true)}
332
- className="flex-1 text-white"
333
- size="sm"
334
- variant="bordered"
335
- classNames={{
336
- input: "text-white",
337
- inputWrapper: "bg-black/20 border-white/20"
338
- }}
339
- />
340
- </div>
341
- </div>
342
- );
343
-
344
- case 'select':
345
- // Only set selectedKeys if the value exists in options to prevent HeroUI warnings
346
- const validOptions = options || [];
347
- const currentValueStr = String(value);
348
- const selectedKeys = validOptions.includes(currentValueStr) ? [currentValueStr] : [];
349
-
350
- return (
351
- <div key={name} className="mb-3">
352
- <Select
353
- key={`${name}-${validOptions.join('-')}`}
354
- label={label || name}
355
- selectedKeys={selectedKeys}
356
- onSelectionChange={(keys) => {
357
- const selectedValue = Array.from(keys)[0];
358
- handleParameterChange(name, selectedValue);
359
- updateParameters({ [name]: selectedValue }, true);
360
- }}
361
- className="w-full visualizer-select [&_[data-slot=value]]:!text-white [&_[data-slot=value]]:!opacity-100"
362
- size="sm"
363
- variant="bordered"
364
- classNames={{
365
- trigger: "bg-black/20 border-white/20 text-white data-[hover=true]:bg-black/30",
366
- label: "text-white/80",
367
- value: "!text-white !opacity-100",
368
- innerWrapper: "text-white",
369
- selectorIcon: "text-white/60",
370
- listbox: "bg-black/90",
371
- popoverContent: "bg-black/90 border border-white/20"
372
- }}
373
- style={{
374
- // @ts-ignore - Force override HeroUI CSS variables
375
- "--nextui-content1": "white",
376
- "--nextui-content1-foreground": "white",
377
- "--nextui-foreground": "white"
378
- } as any}
379
- >
380
- {validOptions.map((option: string) => (
381
- <SelectItem
382
- key={option}
383
- className="text-white data-[hover=true]:bg-white/10"
384
-
385
- >
386
- {option}
387
- </SelectItem>
388
- ))}
389
- </Select>
390
- </div>
391
- );
392
-
393
- case 'number':
394
- return (
395
- <div key={`${name}-${value ?? ''}`} className="mb-3">
396
- <Input
397
- type="number"
398
- label={label || name}
399
- defaultValue={String(value)}
400
- onChange={(e) => handleParameterChange(name, parseFloat(e.target.value) || 0)}
401
- onBlur={(e) => updateParameters({ [name]: parseFloat(e.target.value) || 0 }, true)}
402
- min={min}
403
- max={max}
404
- step={step}
405
- className="w-full"
406
- size="sm"
407
- variant="bordered"
408
- classNames={{
409
- input: "text-white",
410
- inputWrapper: "bg-black/20 border-white/20",
411
- label: "text-white/80"
412
- }}
413
- />
414
- </div>
415
- );
416
-
417
- case 'text':
418
- default:
419
- return (
420
- <div key={`${name}-${value ?? ''}`} className="mb-3">
421
- <Input
422
- label={label || name}
423
- defaultValue={String(value)}
424
- onChange={(e) => handleParameterChange(name, e.target.value)}
425
- onBlur={(e) => updateParameters({ [name]: e.target.value }, true)}
426
- className="w-full"
427
- size="sm"
428
- variant="bordered"
429
- classNames={{
430
- input: "text-white",
431
- inputWrapper: "bg-black/20 border-white/20",
432
- label: "text-white/80"
433
- }}
434
- />
435
- </div>
436
- );
437
- }
438
- };
439
-
440
- // Render parameter group (like in the example)
441
- const renderParameterGroup = (group: any) => {
442
- if (group.processedParameters.length === 0) return null;
443
-
444
- return (
445
- <div key={group.groupName} className="mb-4">
446
- <div className="bg-black/40 border border-white/20 rounded-lg">
447
- <div className="px-3 py-2 border-b border-white/10 flex items-center justify-between">
448
- <span className="text-white font-medium text-sm">{group.groupName}</span>
449
- <span className="text-white/60 text-xs">−</span>
450
- </div>
451
- <div className="p-3 space-y-2">
452
- {group.processedParameters.map((param: any) => renderParameterControl(param))}
453
- </div>
454
- </div>
455
- </div>
456
- );
457
- };
458
-
459
- if (!session) {
460
- return (
461
- <div className="p-4 text-center text-white/60">
462
- <p>No active scene</p>
463
- </div>
464
- );
465
- }
466
-
467
- if (processedGroups.length === 0) {
468
- return (
469
- <div className="p-4 text-center text-white/60">
470
- <p>Loading parameters...</p>
471
- </div>
472
- );
473
- }
474
-
475
- return (
476
- <div className={`flex h-full ${className}`}>
477
- {/* Presets Panel */}
478
- {!hidePresetsControls && (
479
- <div className={`${isPresetManagementOpen ? 'w-full' : 'w-24'} bg-black/50 border-r border-white/10 transition-all duration-300`}>
480
- {isPresetManagementOpen ? (
481
- /* Full Preset Management View */
482
- <div className="p-4 h-full flex flex-col">
483
- {/* Header */}
484
- <div className="flex items-center justify-between mb-4">
485
- <Button
486
- isIconOnly
487
- variant="flat"
488
- size="sm"
489
- className="text-white/60 hover:text-white"
490
- onPress={() => setIsPresetManagementOpen(false)}
491
- >
492
- <ArrowLeftIcon className="w-4 h-4" />
493
- </Button>
494
- <h3 className="text-white font-medium">Manage Presets</h3>
495
- <div className="w-8" /> {/* Spacer */}
496
- </div>
497
-
498
- {/* Preset List */}
499
- <div className="flex-1 overflow-y-auto space-y-2">
500
- {isLoadingPresets ? (
501
- <div className="text-center py-8">
502
- <Spinner color="primary" size="sm" />
503
- <p className="text-white/60 text-sm mt-2">Loading presets...</p>
504
- </div>
505
- ) : presetError ? (
506
- <div className="text-center py-8 text-red-400">
507
- <p className="text-sm">Failed to load presets</p>
508
- <p className="text-xs text-white/60 mt-1">{presetError}</p>
509
- <Button
510
- size="sm"
511
- variant="flat"
512
- className="mt-3 bg-white/10 text-white"
513
- onPress={() => session?.sceneId && loadPresets(session.sceneId)}
514
- isLoading={isLoadingPresets}
515
- >
516
- Retry
517
- </Button>
518
- </div>
519
- ) : availablePresets.length === 0 ? (
520
- <div className="text-center py-8 text-white/60">
521
- <p className="text-sm">No presets available</p>
522
- <p className="text-xs mt-1">Create your first preset to get started</p>
523
- </div>
524
- ) : (
525
- (availablePresets as any[])
526
- .slice()
527
- .sort((a: any, b: any) => orderedIds.indexOf(a.id) - orderedIds.indexOf(b.id))
528
- .map((preset: any) => (
529
- <Card
530
- key={preset.id}
531
- className={`w-full bg-white/5 hover:bg-white/10 transition-colors ${
532
- activePresetId === preset.id ? 'ring-1 ring-primary-400' : ''
533
- }`}
534
- isPressable
535
- onPress={() => handleApplyPreset(preset.id)}
536
- draggable
537
- onDragStart={() => onDragStart(preset.id)}
538
- onDragOver={(e) => onDragOver(e, preset.id)}
539
- onDrop={onDrop}
540
- >
541
- <CardBody className="p-3">
542
- <div className="flex items-center justify-between gap-3">
543
- {/* Thumbnail on the left */}
544
- <div className="w-14 h-14 rounded border border-white/10 flex-shrink-0 overflow-hidden bg-viji-pattern">
545
- {preset.thumbnail ? (
546
- <img
547
- src={getThumbnailUrl(preset.thumbnail)}
548
- alt={preset.name}
549
- className="w-full h-full object-cover"
550
- onError={(e) => {
551
- const target = e.currentTarget as HTMLImageElement;
552
- target.style.display = 'none';
553
- }}
554
- />
555
- ) : null}
556
- </div>
557
-
558
- {/* Title/Name center (click to apply) */}
559
- <div
560
- className="flex-1 min-w-0"
561
- >
562
- {editingPresetId === preset.id ? (
563
- <Input
564
- size="sm"
565
- variant="bordered"
566
- classNames={{ input: 'text-white', inputWrapper: 'bg-black/20 border-white/20' }}
567
- value={editingName}
568
- autoFocus
569
- onChange={(e) => setEditingName(e.target.value)}
570
- onClick={(e) => e.stopPropagation()}
571
- onBlur={async () => {
572
- const name = editingName.trim();
573
- setEditingPresetId(null);
574
- if (name && name !== preset.name) {
575
- await renamePreset(preset.id, name);
576
- }
577
- }}
578
- onKeyDown={async (e) => {
579
- if (e.key === 'Enter') {
580
- const name = editingName.trim();
581
- (e.target as HTMLInputElement).blur();
582
- if (name && name !== preset.name) {
583
- await renamePreset(preset.id, name);
584
- }
585
- } else if (e.key === 'Escape') {
586
- setEditingPresetId(null);
587
- }
588
- }}
589
- />
590
- ) : (
591
- <>
592
- <h4 className="text-white text-sm font-medium truncate">{preset.name}</h4>
593
- <p className="text-white/60 text-xs">{new Date(preset.created).toLocaleDateString()}</p>
594
- </>
595
- )}
596
- </div>
597
-
598
- {/* Actions on the right in one row */}
599
- <div className="flex items-center gap-1 ml-2" onClick={(e) => e.stopPropagation()}>
600
- <Tooltip content="Edit name">
601
- <Button
602
- isIconOnly
603
- size="sm"
604
- variant="flat"
605
- className="text-white/60 hover:text-white min-w-6 h-6"
606
- onPress={() => { setEditingPresetId(preset.id); setEditingName(preset.name); }}
607
- >
608
- <PencilIcon className="w-3 h-3" />
609
- </Button>
610
- </Tooltip>
611
-
612
- <Tooltip content="Delete">
613
- <Button
614
- isIconOnly
615
- size="sm"
616
- variant="flat"
617
- className="text-red-400 hover:text-red-300 min-w-6 h-6"
618
- onPress={() => { confirmDeletePreset(preset.id); }}
619
- >
620
- <TrashIcon className="w-3 h-3" />
621
- </Button>
622
- </Tooltip>
623
- </div>
624
- </div>
625
- </CardBody>
626
- </Card>
627
- ))
628
- )}
629
- </div>
630
- </div>
631
- ) : (
632
- /* Narrow Preset Thumbnails View */
633
- <div className="p-2 h-full flex flex-col">
634
- {/* Edit Button */}
635
- <Tooltip content="Manage Presets" placement="right">
636
- <Button
637
- isIconOnly
638
- variant="flat"
639
- size="sm"
640
- className="text-white/60 hover:text-white mb-3"
641
- onPress={() => setIsPresetManagementOpen(true)}
642
- >
643
- <PencilIcon className="w-4 h-4" />
644
- </Button>
645
- </Tooltip>
646
-
647
- {/* Preset Thumbnails */}
648
- <div className="flex-1 overflow-y-auto space-y-2">
649
- {availablePresets.map((preset: any) => (
650
- <Tooltip key={preset.id} content={preset.name} placement="right">
651
- <Card
652
- className={`bg-white/5 hover:bg-white/10 cursor-pointer transition-colors aspect-square ${
653
- activePresetId === preset.id ? 'ring-1 ring-primary-400' : ''
654
- }`}
655
- isPressable
656
- onPress={() => handleApplyPreset(preset.id)}
657
- >
658
- <CardBody className="p-2">
659
- <div className="w-full h-full rounded overflow-hidden bg-viji-pattern">
660
- {preset.thumbnail ? (
661
- <img
662
- src={getThumbnailUrl(preset.thumbnail)}
663
- alt={preset.name}
664
- className="w-full h-full object-cover"
665
- onError={(e) => {
666
- const target = e.currentTarget as HTMLImageElement;
667
- target.style.display = 'none';
668
- }}
669
- onLoad={() => {
670
- console.debug('🖼️ [PRESET] Thumbnail loaded successfully:', {
671
- presetName: preset.name,
672
- originalThumbnail: preset.thumbnail,
673
- resolvedUrl: getThumbnailUrl(preset.thumbnail)
674
- });
675
- }}
676
- />
677
- ) : null}
678
- </div>
679
- </CardBody>
680
- </Card>
681
- </Tooltip>
682
- ))}
683
- </div>
684
- </div>
685
- )}
686
- </div>
687
- )}
688
-
689
- {/* Parameters Panel - Like the example */}
690
- {(!isPresetManagementOpen || hidePresetsControls) && (
691
- <div className="flex-1 p-4 flex flex-col relative">
692
- {processedGroups.length === 0 ? (
693
- <div className="text-center py-8 text-white/60">
694
- <p className="text-sm">No parameters available</p>
695
- <p className="text-xs mt-1">Scene may still be loading...</p>
696
- </div>
697
- ) : (
698
- <div className="flex-1 overflow-y-auto">
699
- <div className="space-y-4">
700
- {/* Capabilities Info (like in the example) */}
701
- {renderCapabilitiesInfo()}
702
-
703
- {/* General Group Parameters (rendered individually, not in a group) */}
704
- {(() => {
705
- const generalGroup = processedGroups.find(group => group.groupName === 'general');
706
- if (generalGroup && generalGroup.processedParameters.length > 0) {
707
- return (
708
- <div className="mb-6">
709
- <div className="mb-3 pb-2 border-b border-white/10">
710
- <span className="text-white font-medium text-sm">General Controls</span>
711
- </div>
712
- <div className="space-y-3">
713
- {generalGroup.processedParameters.map(renderParameterControl)}
714
- </div>
715
- </div>
716
- );
717
- }
718
- return null;
719
- })()}
720
-
721
- {/* Other Groups (excluding general) */}
722
- {processedGroups
723
- .filter((group: any) => group.groupName !== 'general')
724
- .map(renderParameterGroup)}
725
- </div>
726
- </div>
727
- )}
728
- {/* Pinned Save Area (non-scrolling) */}
729
- {hasUnsavedChanges && !hidePresetsControls && (
730
- <div className="p-3">
731
- <Button
732
- color="primary"
733
- variant="flat"
734
- className="w-full"
735
- startContent={<PlusIcon className="w-4 h-4" />}
736
- onPress={handleSaveAsPreset}
737
- isDisabled={!currentUser}
738
- isLoading={isSavingPreset}
739
- >
740
- Save as Preset
741
- </Button>
742
- {!currentUser && (
743
- <p className="text-white/60 text-xs text-center mt-2">
744
- Login required to save presets
745
- </p>
746
- )}
747
- </div>
748
- )}
749
- {isSavingPreset && (
750
- <div className="absolute inset-0 z-20 flex items-center justify-center">
751
- <Spinner color="primary" size="lg" />
752
- </div>
753
- )}
754
- </div>
755
- )}
756
-
757
- {/* No create preset modal (auto-named) */}
758
-
759
- {/* Delete Preset Modal */}
760
- {!hidePresetsControls && (
761
- <Modal isOpen={isDeleteModalOpen} onClose={onDeleteModalClose}>
762
- <ModalContent className="bg-black border border-white/10">
763
- <ModalHeader>
764
- <h3 className="text-white">Delete Preset</h3>
765
- </ModalHeader>
766
- <ModalBody>
767
- <p className="text-white/80">
768
- {(() => {
769
- const p = availablePresets.find((x: any) => x.id === presetToDelete);
770
- const name = p?.name ? `"${p.name}"` : 'this preset';
771
- return `This will permanently delete ${name}. This action cannot be undone.`;
772
- })()}
773
- </p>
774
- </ModalBody>
775
- <ModalFooter>
776
- <Button
777
- variant="flat"
778
- onPress={onDeleteModalClose}
779
- >
780
- Cancel
781
- </Button>
782
- <Button
783
- color="danger"
784
- onPress={handleDeletePreset}
785
- isLoading={isSavingPreset}
786
- >
787
- Delete
788
- </Button>
789
- </ModalFooter>
790
- </ModalContent>
791
- </Modal>
792
- )}
793
- </div>
794
- );
795
- };
796
-
797
- export default ParametersMenu;