diff --git a/packages/app/src/builder-ui/debugger.ts b/packages/app/src/builder-ui/debugger.ts index 8bcd786e5..cebc1e3ed 100644 --- a/packages/app/src/builder-ui/debugger.ts +++ b/packages/app/src/builder-ui/debugger.ts @@ -7,6 +7,12 @@ import SmythFile from './lib/SmythFile.class'; import { alert, modalDialog } from './ui/dialogs'; import { twModalDialog } from './ui/tw-dialogs'; import { delay } from './utils'; +import { + cacheFileObjects, + getCachedFileObjects, + getDebugInputValues, + saveDebugInputValues, +} from './utils/debug-values-cache'; import { getFileCategory, getMimeTypeFromUrl, isURL } from './utils/general.utils'; import { Workspace } from './workspace/Workspace.class'; // import microlight from 'microlight'; @@ -2404,11 +2410,12 @@ export function createDebugInjectDialog( operation: 'step' | 'run' = 'step', prefillValues?: Record, ) { + const cachedInputValues = getDebugInputValues(component.uid) || {}; + const createInputList = (array, type) => { return array .map((el, index) => { if (el.type === 'file') { - const prefillValue = prefillValues?.[el.name] || ''; return `
@@ -2423,7 +2430,6 @@ export function createDebugInjectDialog( hover:file:bg-gray-300 pl-5 " multiple>
- ${prefillValue ? `
Previously selected: ${prefillValue.substring(0, 100)}${prefillValue.length > 100 ? '...' : ''}
` : ''}
`; } else { return ` @@ -2543,11 +2549,15 @@ export function createDebugInjectDialog(

`; - - const handleFileInput = async (fileInput: HTMLInputElement, element: any) => { + const handleFileInput = async (fileInput: HTMLInputElement, element: any, inputName?: string) => { const files = Array.from(fileInput.files || []); element.files = files; + // Store files directly in memory cache for fast access + if (inputName) { + cacheFileObjects(component.uid, inputName, files); + } + // Create base64 data URLs for debug system (back to original format) const fileValues = await Promise.all( files.map((file) => { return new Promise((resolve) => { @@ -2661,7 +2671,37 @@ export function createDebugInjectDialog( debugInputs[component.uid].inputs[inputs[index].name] = inputVal; return obj; }, {}); + // Save input values to session storage for persistence (including files) + const inputValuesToSave = inputElementsArray.reduce((obj, el: any, index) => { + const fieldName = inputs[index].name; + const fieldValue = el.value; + const inputType = inputs[index].type; + + // Handle text inputs + if (fieldValue && fieldValue.trim() !== '') { + obj[fieldName] = fieldValue; + } + + // Handle file inputs - store file metadata for cache (actual files stored separately in memory) + if (inputType === 'file' && inputFileValue[index]?.files?.length > 0) { + const fileData = inputFileValue[index]; + + // Store file metadata for cache (actual file data is already cached via cacheFileObjects) + obj[fieldName] = { + type: 'file_metadata', + files: fileData.files.map((file: File) => ({ + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified, + })), + // Note: Actual file data is stored separately in memory cache + }; + } + return obj; + }, {}); + saveDebugInputValues(component.uid, inputValuesToSave); // --- Add this check for empty inputs --- let allInputsEmpty = true; // @ts-ignore @@ -2789,7 +2829,9 @@ export function createDebugInjectDialog( `, cssClass: 'border border-[#3b82f6] bg-white hover:bg-[#3b82f6] group transition-all duration-200 !text-[#3b82f6] hover:!text-white', - callback: (dialog) => handleDebugAction(component, dialog, 'step'), + callback: (dialog) => { + return handleDebugAction(component, dialog, 'step'); + }, }, { label: ` @@ -2798,23 +2840,29 @@ export function createDebugInjectDialog( `, cssClass: 'border border-[#3b82f6] bg-white hover:bg-[#3b82f6] group transition-all duration-200 !text-[#3b82f6] hover:!text-white', - callback: (dialog) => handleDebugAction(component, dialog, 'run'), + callback: (dialog) => { + return handleDebugAction(component, dialog, 'run'); + }, }, ], onCloseClick: () => {}, onDOMReady: function (dialog) { - if (debugInputs[component.uid]?.inputs) { - //console.log('debugInputs', debugInputs[component.uid]?.inputs); - for (let inputName in debugInputs[component.uid]?.inputs) { + // Load cached input values from memory cache + const cachedInputValues = getDebugInputValues(component.uid); + + if (cachedInputValues) { + for (let inputName in cachedInputValues) { const inputElement: HTMLInputElement = dialog.querySelector( `.inputs-input[name="${inputName}"]`, ); - - if (!inputElement) continue; + if (!inputElement) { + console.warn(`Input element not found for: ${inputName}`); + continue; + } // setting value to the file type input leads to error if (inputElement.type !== 'file') { - const value = debugInputs[component.uid]?.inputs[inputName] || ''; + const value = cachedInputValues[inputName] || ''; inputElement.value = value; } } @@ -2843,22 +2891,70 @@ export function createDebugInjectDialog( inputs.forEach((input, index) => { if (input.type === 'file') { - const inputFile = inputsContainer.querySelector(`#inputs-input-${index}`); + const inputFile = inputsContainer.querySelector( + `#inputs-input-${index}`, + ) as HTMLInputElement; const element = { domElement: inputFile, value: null, files: [] }; + + // Restore cached files directly from memory + const cachedFileObjects = getCachedFileObjects(component.uid, input.name); + + if (cachedFileObjects?.length > 0) { + try { + // Set files directly to DOM input using DataTransfer + const dataTransfer = new DataTransfer(); + cachedFileObjects.forEach((file) => dataTransfer.items.add(file)); + inputFile.files = dataTransfer.files; + + // Convert cached files back to base64 data URLs for debug system + Promise.all( + cachedFileObjects.map((file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + let result = e.target.result as string; + // Windows markdown file type fix + if ( + navigator.userAgent.includes('Windows') && + file.type === '' && + file.name.toLowerCase().endsWith('.md') + ) { + result = result.replace( + 'data:application/octet-stream;', + 'data:text/markdown;', + ); + } + resolve(result); + }; + reader.readAsDataURL(file); + }); + }), + ).then((fileDataUrls) => { + element.value = fileDataUrls.length === 1 ? fileDataUrls[0] : fileDataUrls; + }); + + element.files = cachedFileObjects; + } catch (error) { + console.warn('Failed to restore cached files:', error); + } + } + inputFileValue[index] = element; inputFile.addEventListener('change', (e: any) => { - handleFileInput(e.target, element); + handleFileInput(e.target, element, input.name); }); } }); outputs.forEach((output, index) => { if (output.type === 'file') { - const outputFile = outputsContainer.querySelector(`#outputs-input-${index}`); + const outputFile = outputsContainer.querySelector( + `#outputs-input-${index}`, + ) as HTMLInputElement; const element = { domElement: outputFile, value: null, files: [] }; outputFileValue[index] = element; outputFile.addEventListener('change', (e: any) => { - handleFileInput(e.target, element); + handleFileInput(e.target, element, output.name); }); } }); diff --git a/packages/app/src/builder-ui/utils/debug-values-cache.ts b/packages/app/src/builder-ui/utils/debug-values-cache.ts new file mode 100644 index 000000000..67366b25c --- /dev/null +++ b/packages/app/src/builder-ui/utils/debug-values-cache.ts @@ -0,0 +1,172 @@ +import type { Workspace } from '../workspace/Workspace.class'; + +declare var workspace: Workspace; + +interface CacheEntry { + data: any; + timestamp: number; + size: number; +} + +// Cache configuration and state +const debugInputsCache = new Map(); +const MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100MB +const MAX_SINGLE_FILE_SIZE = 20 * 1024 * 1024; // 20MB +const CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000; // 4 hours +let currentCacheSize = 0; + +// Utility functions +const calculateDataSize = (data: any): number => { + if (data instanceof File) return data.size; + if (typeof data !== 'object' || data === null) return JSON.stringify(data).length * 2; + + return Object.entries(data).reduce((total, [, value]) => { + if (typeof value === 'object' && value !== null) { + if ('type' in value && value.type === 'file_reference' && 'size' in value) + return total + (value.size as number); + if ('files' in value && Array.isArray(value.files)) + return total + value.files.reduce((sum: number, f: any) => sum + (f.size || 0), 0); + return total + JSON.stringify(value).length * 2; + } + return total + String(value).length * 2; + }, 0); +}; + +const isExpired = (timestamp: number) => Date.now() - timestamp > CACHE_EXPIRY_TIME; + +const generateVersionInfo = (componentId: string) => + `${btoa(componentId).slice(0, 10)}-${workspace?.agent?.data?.version || '1.0.0'}-${Math.floor(Date.now() / (1000 * 60 * 60))}`; + +// Cache management operations +const removeEntry = (key: string) => { + const entry = debugInputsCache.get(key); + if (entry) { + currentCacheSize -= entry.size; + debugInputsCache.delete(key); + } + return !!entry; +}; + +const addEntry = (key: string, data: any, size = calculateDataSize(data)) => { + removeEntry(key); // Remove existing if present + ensureCacheSpace(size); + + const entry: CacheEntry = { data, timestamp: Date.now(), size }; + debugInputsCache.set(key, entry); + currentCacheSize += size; + return entry; +}; + +const cleanupExpiredEntries = () => { + [...debugInputsCache.entries()] + .filter(([, entry]) => isExpired(entry.timestamp)) + .forEach(([key]) => removeEntry(key)); +}; + +const evictOldestEntries = (requiredSize: number) => { + const sortedEntries = [...debugInputsCache.entries()].sort( + ([, a], [, b]) => a.timestamp - b.timestamp, + ); + let freedSize = 0; + + for (const [key] of sortedEntries) { + const entry = debugInputsCache.get(key); + if (entry) { + freedSize += entry.size; + removeEntry(key); + if (freedSize >= requiredSize) break; + } + } +}; + +const ensureCacheSpace = (newEntrySize: number) => { + cleanupExpiredEntries(); + const spaceNeeded = currentCacheSize + newEntrySize - MAX_CACHE_SIZE; + if (spaceNeeded > 0) evictOldestEntries(spaceNeeded); +}; + +// Public API +export const saveDebugInputValues = (componentId: string, inputValues: Record) => { + try { + const dataToCache = { + version: generateVersionInfo(componentId), + timestamp: Date.now(), + values: inputValues, + }; + addEntry(`debug-inputs-${componentId}`, dataToCache); + } catch (error) { + console.error('Error saving debug input values:', error); + } +}; + +export const getDebugInputValues = (componentId: string): Record | null => { + try { + const entry = debugInputsCache.get(`debug-inputs-${componentId}`); + if (!entry) return null; + + if (isExpired(entry.timestamp)) { + removeEntry(`debug-inputs-${componentId}`); + return null; + } + + if (entry.data.version !== generateVersionInfo(componentId)) { + removeEntry(`debug-inputs-${componentId}`); + return null; + } + + return entry.data.values; + } catch (error) { + console.error('Error retrieving debug input values:', error); + return null; + } +}; + +export const cacheFileObjects = (componentId: string, inputName: string, files: File[]) => { + const validFiles = files.filter((file) => file.size <= MAX_SINGLE_FILE_SIZE); + if (validFiles.length === 0) return; + + const size = validFiles.reduce((total, file) => total + file.size, 0); + addEntry(`file-objects-${componentId}-${inputName}`, validFiles, size); +}; + +export const getCachedFileObjects = (componentId: string, inputName: string): File[] | null => { + const entry = debugInputsCache.get(`file-objects-${componentId}-${inputName}`); + if (!entry) return null; + + if (isExpired(entry.timestamp)) { + removeEntry(`file-objects-${componentId}-${inputName}`); + return null; + } + + return entry.data; +}; + +export const clearComponentCache = (componentId: string) => { + [...debugInputsCache.keys()] + .filter((key) => key.includes(componentId)) + .forEach((key) => removeEntry(key)); +}; + +export const clearAllCache = () => { + debugInputsCache.clear(); + currentCacheSize = 0; +}; + +export const getCacheStats = () => { + if (debugInputsCache.size === 0) { + return { entriesCount: 0, totalSize: '0 MB', oldestEntry: 'None', newestEntry: 'None' }; + } + + const timestamps = [...debugInputsCache.values()].map((entry) => entry.timestamp); + return { + entriesCount: debugInputsCache.size, + totalSize: `${(currentCacheSize / 1024 / 1024).toFixed(2)} MB`, + oldestEntry: new Date(Math.min(...timestamps)).toLocaleString(), + newestEntry: new Date(Math.max(...timestamps)).toLocaleString(), + }; +}; + +// Periodic cleanup +if (typeof window !== 'undefined') { + setInterval(cleanupExpiredEntries, 30 * 60 * 1000); // Every 30 minutes +}