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 += "| Index | Type | Value | Explore |
";
+
+ 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 += `| ${i} | `;
+ content += `${itemInfo.type} | `;
+ content += `${this.escapeHtml(itemInfo.value)} | `;
+ content += "";
+ if (canExplore) {
+ content += ``;
+ }
+ content += " |
";
+ }
+
+ if (listValue.v.length > 100) {
+ content += `| ... and ${listValue.v.length - 100} more items |
`;
+ }
+
+ content += "
";
+ return content;
+};
+
+/**
+ * Build explorer content for Dictionary data structures
+ */
+BlockPyDialog.prototype.buildDictExplorer = function(variableName, dictValue) {
+ let content = "Dictionary Contents
";
+ content += "";
+ content += "| Key | Type | Value | Explore |
";
+
+ 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 += `${this.escapeHtml(keyInfo.value)} | `;
+ content += `${valueInfo.type} | `;
+ content += `${this.escapeHtml(valueInfo.value)} | `;
+ content += "";
+ if (canExplore) {
+ content += ``;
+ }
+ content += " |
";
+ }
+
+ if (entries.length > 100) {
+ content += `| ... and ${entries.length - 100} more entries |
`;
+ }
+
+ content += "
";
+ 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 += "| Index | Type | Value | Explore |
";
+
+ 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 += `| ${i} | `;
+ content += `${itemInfo.type} | `;
+ content += `${this.escapeHtml(itemInfo.value)} | `;
+ content += "";
+ if (canExplore) {
+ content += ``;
+ }
+ content += " |
";
+ }
+
+ if (tupleValue.v.length > 100) {
+ content += `| ... and ${tupleValue.v.length - 100} more items |
`;
+ }
+
+ content += "
";
+ 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,