diff --git a/src/blockpy.js b/src/blockpy.js index 23b43481..7e86c29b 100644 --- a/src/blockpy.js +++ b/src/blockpy.js @@ -873,6 +873,10 @@ export class BlockPy { } }), }, + // Method to view exact value of complex data structures + viewExactValue: function(variableName, type, exactValue) { + self.components.dialog.DATA_EXPLORER(variableName, type, exactValue); + }, files: { visible: ko.pureComputed(() => model.display.instructor() || !model.assignment.settings.hideFiles() || model.assignment.settings.preloadAllFiles() diff --git a/src/dialog.js b/src/dialog.js index a6905569..f420a306 100644 --- a/src/dialog.js +++ b/src/dialog.js @@ -241,4 +241,273 @@ BlockPyDialog.prototype.START_SHARE = function (url, wasPrompted) { this.tag.find(".blockpy-copy-share-link").html("Copied!"); }); }); +}; + +/** + * Enhanced State Explorer dialog for viewing complex data structures + * Supports nested exploration similar to Spyder's variable explorer + */ +BlockPyDialog.prototype.DATA_EXPLORER = function (variableName, type, exactValue) { + const explorerContent = this.buildDataExplorerContent(variableName, type, exactValue); + + this.show(`Data Explorer: ${variableName} (${type})`, explorerContent, () => { + // Cleanup when dialog closes + this.tag.find(".data-explorer-item").off("click"); + }); + + // Make dialog larger for better data viewing + this.tag.find(".modal-dialog").addClass("modal-xl"); + + // Set up click handlers for nested exploration + this.setupDataExplorerHandlers(); +}; + +/** + * Build HTML content for data explorer based on data type and value + */ +BlockPyDialog.prototype.buildDataExplorerContent = function(variableName, type, exactValue) { + let content = "
"; + + try { + switch (type) { + case "List": + content += this.buildListExplorer(variableName, exactValue); + break; + case "Dictionary": + content += this.buildDictExplorer(variableName, exactValue); + break; + case "Tuple": + content += this.buildTupleExplorer(variableName, exactValue); + break; + default: + content += this.buildSimpleValueExplorer(variableName, type, exactValue); + } + } catch (error) { + content += `
Error exploring ${type}: ${error.message}
`; + content += `
${exactValue.$r ? exactValue.$r().v : exactValue.toString()}
`; + } + + content += "
"; + return content; +}; + +/** + * Build explorer content for List data structures + */ +BlockPyDialog.prototype.buildListExplorer = function(variableName, listValue) { + let content = `
List Contents (${listValue.v.length} items)
`; + content += "
"; + content += ""; + + for (let i = 0; i < Math.min(listValue.v.length, 100); i++) { + const item = listValue.v[i]; + const itemInfo = this.parseDataValue(`${variableName}[${i}]`, item); + const canExplore = this.canExploreValue(item); + + content += ""; + content += ``; + content += ``; + content += ``; + content += ""; + } + + if (listValue.v.length > 100) { + content += ``; + } + + content += "
IndexTypeValueExplore
${i}${itemInfo.type}${this.escapeHtml(itemInfo.value)}"; + if (canExplore) { + content += ``; + } + content += "
... and ${listValue.v.length - 100} more items
"; + return content; +}; + +/** + * Build explorer content for Dictionary data structures + */ +BlockPyDialog.prototype.buildDictExplorer = function(variableName, dictValue) { + let content = "
Dictionary Contents
"; + content += "
"; + content += ""; + + const entries = Object.entries(dictValue.v); + for (let i = 0; i < Math.min(entries.length, 100); i++) { + const [key, value] = entries[i]; + const keyInfo = this.parseDataValue("key", key); + const valueInfo = this.parseDataValue(`${variableName}[${keyInfo.value}]`, value); + const canExplore = this.canExploreValue(value); + + content += ""; + content += ``; + content += ``; + content += ``; + content += ""; + } + + if (entries.length > 100) { + content += ``; + } + + content += "
KeyTypeValueExplore
${this.escapeHtml(keyInfo.value)}${valueInfo.type}${this.escapeHtml(valueInfo.value)}"; + if (canExplore) { + content += ``; + } + content += "
... and ${entries.length - 100} more entries
"; + return content; +}; + +/** + * Build explorer content for Tuple data structures + */ +BlockPyDialog.prototype.buildTupleExplorer = function(variableName, tupleValue) { + let content = `
Tuple Contents (${tupleValue.v.length} items)
`; + content += "
"; + content += ""; + + for (let i = 0; i < Math.min(tupleValue.v.length, 100); i++) { + const item = tupleValue.v[i]; + const itemInfo = this.parseDataValue(`${variableName}[${i}]`, item); + const canExplore = this.canExploreValue(item); + + content += ""; + content += ``; + content += ``; + content += ``; + content += ""; + } + + if (tupleValue.v.length > 100) { + content += ``; + } + + content += "
IndexTypeValueExplore
${i}${itemInfo.type}${this.escapeHtml(itemInfo.value)}"; + if (canExplore) { + content += ``; + } + content += "
... and ${tupleValue.v.length - 100} more items
"; + return content; +}; + +/** + * Build explorer content for simple values + */ +BlockPyDialog.prototype.buildSimpleValueExplorer = function(variableName, type, exactValue) { + let content = `
${type} Value
`; + content += "
"; + content += ""; + content += `
${this.escapeHtml(exactValue.$r ? exactValue.$r().v : exactValue.toString())}
`; + content += "
"; + + if (exactValue.$r) { + content += "
"; + content += ""; + content += `
${this.escapeHtml(exactValue.$r().v)}
`; + content += "
"; + } + + return content; +}; + +/** + * Parse a Skulpt value to get readable information + */ +BlockPyDialog.prototype.parseDataValue = function(name, value) { + if (value === undefined) { + return { name, type: "Unknown", value: "Undefined" }; + } + + // Use existing parsing logic from BlockPyTrace + if (window.BlockPyTrace && window.BlockPyTrace.parseValue) { + const parsed = window.BlockPyTrace.parseValue(name, value, true); + return parsed || { name, type: "Unknown", value: "Unknown" }; + } + + // Fallback parsing + if (value.constructor === Sk.builtin.str) { + return { name, type: "String", value: value.$r().v }; + } else if (value.constructor === Sk.builtin.int_) { + return { name, type: "Integer", value: value.$r().v }; + } else if (value.constructor === Sk.builtin.float_) { + return { name, type: "Float", value: value.$r().v }; + } else if (value.constructor === Sk.builtin.bool) { + return { name, type: "Boolean", value: value.$r().v }; + } else if (value.constructor === Sk.builtin.list) { + return { name, type: "List", value: `[${value.v.length} items]` }; + } else if (value.constructor === Sk.builtin.dict) { + return { name, type: "Dictionary", value: `{${Object.keys(value.v).length} keys}` }; + } else if (value.constructor === Sk.builtin.tuple) { + return { name, type: "Tuple", value: `(${value.v.length} items)` }; + } else { + return { name, type: value.tp$name || "Object", value: value.$r ? value.$r().v : value.toString() }; + } +}; + +/** + * Check if a value can be explored further + */ +BlockPyDialog.prototype.canExploreValue = function(value) { + if (!value) {return false;} + + return value.constructor === Sk.builtin.list || + value.constructor === Sk.builtin.dict || + value.constructor === Sk.builtin.tuple || + (value.v && (Array.isArray(value.v) || typeof value.v === "object")); +}; + +/** + * Set up click handlers for nested data exploration + */ +BlockPyDialog.prototype.setupDataExplorerHandlers = function() { + const self = this; + + this.tag.find(".data-explorer-item").on("click", function() { + const button = $(this); + const itemName = button.data("name"); + const itemType = button.data("type"); + const index = button.data("index"); + const key = button.data("key"); + + // Get the current dialog's exact value and navigate to the item + // This is a simplified approach - in a full implementation, we'd store references + // to the original data structure to allow deep navigation + + // For now, create a new dialog with placeholder content + const newDialog = new BlockPyDialog(self.main, $("
").appendTo("body")); + newDialog.show(`Data Explorer: ${itemName} (${itemType})`, + `
+ Nested exploration of ${itemName} would open here. +

+ Item: ${itemName}
+ Type: ${itemType}
+ ${index !== undefined ? `Index: ${index}
` : ""} + ${key !== undefined ? `Key: ${key}
` : ""} +
`); + + newDialog.tag.find(".modal-dialog").addClass("modal-lg"); + }); +}; + +/** + * Utility function to escape HTML + */ +BlockPyDialog.prototype.escapeHtml = function(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; }; \ No newline at end of file diff --git a/src/trace.js b/src/trace.js index ea8b75dd..9bafee2e 100644 --- a/src/trace.js +++ b/src/trace.js @@ -56,10 +56,10 @@ export const TRACE_HTML = ` - + - - + + @@ -294,7 +294,8 @@ export class BlockPyTrace { case Sk.builtin.tuple: return {"name": property, "type": "Tuple", - "value": value.$r().v + "value": value.$r().v, + "exact_value": value }; case Sk.builtin.list: if (value.v.length <= 20) { @@ -313,7 +314,8 @@ export class BlockPyTrace { case Sk.builtin.dict: return {"name": property, "type": "Dictionary", - "value": value.$r().v + "value": value.$r().v, + "exact_value": value }; case Number: return {"name": property,