Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 112 additions & 16 deletions packages/app/src/builder-ui/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -2404,11 +2410,12 @@ export function createDebugInjectDialog(
operation: 'step' | 'run' = 'step',
prefillValues?: Record<string, any>,
) {
const cachedInputValues = getDebugInputValues(component.uid) || {};

const createInputList = (array, type) => {
return array
.map((el, index) => {
if (el.type === 'file') {
const prefillValue = prefillValues?.[el.name] || '';
return `
<div class="mb-5">
<div class="flex">
Expand All @@ -2423,7 +2430,6 @@ export function createDebugInjectDialog(
hover:file:bg-gray-300 pl-5
" multiple>
</div>
${prefillValue ? `<div class="mt-2 text-xs text-gray-600">Previously selected: ${prefillValue.substring(0, 100)}${prefillValue.length > 100 ? '...' : ''}</div>` : ''}
</div>`;
} else {
return `
Expand Down Expand Up @@ -2543,11 +2549,15 @@ export function createDebugInjectDialog(
</p>
</div>
</div>`;

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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2789,7 +2829,9 @@ export function createDebugInjectDialog(
</span>`,
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: `<span class="flex items-center gap-2">
Expand All @@ -2798,23 +2840,29 @@ export function createDebugInjectDialog(
</span>`,
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;
}
}
Expand Down Expand Up @@ -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);
});
}
});
Expand Down
172 changes: 172 additions & 0 deletions packages/app/src/builder-ui/utils/debug-values-cache.ts
Original file line number Diff line number Diff line change
@@ -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<string, CacheEntry>();
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<string, any>) => {
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<string, any> | 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
}