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
+}