- {items.map(([keyId, valId]: [ID, ID], idx: number) => {
+ {items.map(([keyId, valId]: [ID | string, ID], idx: number) => {
const keyHasError = isIdInvalid(validationErrors, keyId);
const valHasError = isIdInvalid(validationErrors, valId);
const keyErrors = getErrorsForId(validationErrors, keyId);
const valErrors = getErrorsForId(validationErrors, valId);
+ const legacyKeyId =
+ typeof keyId === "number" || keyId === "_" ? keyId : "_";
return (
{/* KEY ID */}
-
-
- setItems((prev) =>
- prev.map((p, i) => (i === idx ? [picked, p[1]] : p))
- )
- }
- onRemove={removeId}
- buttonClassName={`${styles.collectionIdBox} ${
- keyHasError ? styles.errorId : ""
- }`}
- editable={true}
- sandbox={sandbox}
+ {useInlineTargetEditor ? (
+ {
+ const nextPairs = items.map((pair, pairIndex) =>
+ pairIndex === idx ? [picked as ID | string, pair[1]] : pair
+ );
+ setItems(nextPairs);
+ commitPairs(nextPairs);
+ }}
+ addId={addId}
elements={elements}
+ onElementsChange={onElementsChange}
+ validationErrors={keyErrors}
/>
-
+ ) : (
+
+
+ setItems((prev) =>
+ prev.map((p, i) => (i === idx ? [picked, p[1]] : p))
+ )
+ }
+ onRemove={removeId}
+ buttonClassName={`${styles.collectionIdBox} ${
+ keyHasError ? styles.errorId : ""
+ }`}
+ editable={true}
+ sandbox={sandbox}
+ elements={elements}
+ />
+
+ )}
:
{/* VALUE ID + REMOVE */}
-
-
- setItems((prev) =>
- prev.map((p, i) => (i === idx ? [p[0], picked] : p))
- )
- }
- onRemove={removeId}
- buttonClassName={`${styles.collectionIdBox} ${
- valHasError ? styles.errorId : ""
- }`}
- editable={true}
- sandbox={sandbox}
+ {useInlineTargetEditor ? (
+ {
+ const nextPairs = items.map((pair, pairIndex) =>
+ pairIndex === idx ? [pair[0], picked as ID] : pair
+ );
+ setItems(nextPairs);
+ commitPairs(nextPairs);
+ }}
+ addId={addId}
elements={elements}
+ onElementsChange={onElementsChange}
+ validationErrors={valErrors}
/>
-
+ ) : (
+
+
+ setItems((prev) =>
+ prev.map((p, i) => (i === idx ? [p[0], picked] : p))
+ )
+ }
+ onRemove={removeId}
+ buttonClassName={`${styles.collectionIdBox} ${
+ valHasError ? styles.errorId : ""
+ }`}
+ editable={true}
+ sandbox={sandbox}
+ elements={elements}
+ />
+
+ )}
@@ -222,6 +269,16 @@ export default function Palette({
onScaleChange={onScaleChange}
editorScale={editorScale}
onEditorScaleChange={onEditorScaleChange}
+ visualStyle={visualStyle}
+ onVisualStyleChange={onVisualStyleChange}
+ pythonTutorReferenceArrows={pythonTutorReferenceArrows}
+ onPythonTutorReferenceArrowsChange={
+ onPythonTutorReferenceArrowsChange
+ }
+ pythonTutorStandalonePrimitives={pythonTutorStandalonePrimitives}
+ onPythonTutorStandalonePrimitivesChange={
+ onPythonTutorStandalonePrimitivesChange
+ }
fontScale={fontScale}
onFontScaleChange={onFontScaleChange}
/>
diff --git a/frontend/src/features/palette/components/PaletteBox.tsx b/frontend/src/features/palette/components/PaletteBox.tsx
index 95a335b..784b5af 100644
--- a/frontend/src/features/palette/components/PaletteBox.tsx
+++ b/frontend/src/features/palette/components/PaletteBox.tsx
@@ -1,10 +1,11 @@
import { useRef } from "react";
import { usePaletteBoxEffect } from "../hooks/usePalette";
-import { BoxTypeName } from "../../shared/types";
+import { BoxTypeName, VisualStyle } from "../../shared/types";
interface PaletteBoxProps {
/** The type of box to render (e.g., "primitive", "list", "dict") */
boxType: BoxTypeName;
+ visualStyle: VisualStyle;
}
/**
@@ -13,12 +14,12 @@ interface PaletteBoxProps {
* preview using `usePaletteBoxEffect`, and supports drag-and-drop to
* create a new box on the canvas.
*/
-export default function PaletteBox({ boxType }: PaletteBoxProps) {
+export default function PaletteBox({ boxType, visualStyle }: PaletteBoxProps) {
/** Ref to the div container that will hold the rendered SVG */
const containerRef = useRef
(null);
/** Custom hook to render the box preview inside the container */
- usePaletteBoxEffect(containerRef, boxType);
+ usePaletteBoxEffect(containerRef, boxType, visualStyle);
/** Drag-and-drop: set box type on drag start */
const handleDragStart = (event: React.DragEvent) => {
diff --git a/frontend/src/features/palette/hooks/usePalette.tsx b/frontend/src/features/palette/hooks/usePalette.tsx
index 5f6a9f7..53be004 100644
--- a/frontend/src/features/palette/hooks/usePalette.tsx
+++ b/frontend/src/features/palette/hooks/usePalette.tsx
@@ -5,7 +5,7 @@
import { useEffect, RefObject } from "react";
import { createBoxRenderer } from "../utils/BoxRenderer";
-import { BoxTypeName } from "../../shared/types";
+import { BoxTypeName, VisualStyle } from "../../shared/types";
/**
* Renders a static preview SVG for a palette box type.
@@ -21,14 +21,15 @@ import { BoxTypeName } from "../../shared/types";
*/
export const usePaletteBoxEffect = (
containerRef: RefObject,
- boxType: BoxTypeName
+ boxType: BoxTypeName,
+ visualStyle: VisualStyle
) => {
useEffect(() => {
const container = containerRef.current;
if (!container) return;
try {
- const svg = createBoxRenderer(boxType);
+ const svg = createBoxRenderer(boxType, visualStyle);
container.innerHTML = "";
container.appendChild(svg);
@@ -40,5 +41,5 @@ export const usePaletteBoxEffect = (
} catch (error) {
console.error(`Failed to render box type ${boxType}:`, error);
}
- }, [boxType, containerRef]);
+ }, [boxType, containerRef, visualStyle]);
};
diff --git a/frontend/src/features/palette/utils/BoxRenderer.ts b/frontend/src/features/palette/utils/BoxRenderer.ts
index c684828..6542f8e 100644
--- a/frontend/src/features/palette/utils/BoxRenderer.ts
+++ b/frontend/src/features/palette/utils/BoxRenderer.ts
@@ -1,8 +1,21 @@
import MemoryViz from "memory-viz";
-import { BoxTypeName } from "../../shared/types";
+import { BoxTypeName, VisualStyle } from "../../shared/types";
import { getBoxConfig, DEFAULT_PALETTE_STYLE } from "../../shared/boxConfig";
+import { createPythonTutorBoxRenderer } from "../../canvas/utils/pythonTutorRenderer";
+import { createPythonTutorPalettePreview } from "../../canvas/utils/pythonTutorReferences";
+
+export function createBoxRenderer(
+ boxType: BoxTypeName,
+ visualStyle: VisualStyle = "memoryviz"
+): SVGSVGElement {
+ if (visualStyle === "pythonTutor") {
+ const preview = createPythonTutorPalettePreview(boxType);
+ return createPythonTutorBoxRenderer(preview.element, {
+ elementsById: preview.elementsById,
+ renderMode: preview.renderMode,
+ });
+ }
-export function createBoxRenderer(boxType: BoxTypeName): SVGSVGElement {
const { MemoryModel } = MemoryViz;
const config = getBoxConfig(boxType);
diff --git a/frontend/src/features/shared/types.ts b/frontend/src/features/shared/types.ts
index 1d4c0ca..e331e7e 100644
--- a/frontend/src/features/shared/types.ts
+++ b/frontend/src/features/shared/types.ts
@@ -1,6 +1,10 @@
+import type { Dispatch, SetStateAction } from "react";
+
export type PrimitiveType = "NoneType" | "int" | "float" | "str" | "bool";
export type CollectionType = "list" | "tuple" | "set" | "dict";
export type SpecialType = "function" | "class";
+export type VisualStyle = "memoryviz" | "pythonTutor";
+export type RenderMode = "canvas" | "palette";
/**
* Box type names (used for palette and box configuration lookup)
@@ -92,6 +96,7 @@ export interface CanvasElement {
x: number;
y: number;
kind: BoxType;
+ generatedInlinePrimitive?: boolean;
invalidated?: boolean;
errors?: ElementError[]; // Unified error system (validation + feedback)
color?: string; // Optional color to apply to the element (e.g., for errors, warnings, etc.)
@@ -151,14 +156,8 @@ export type ID = number | "_";
export type ClassID = string | "_";
export interface BoxEditorType {
- metadata: {
- id: ID;
- kind: BoxType;
- className?: ClassID;
- errors?: ElementError[]; // Updated to use unified error system
- invalidated?: boolean;
- };
- onSave: (id: ID, kind: BoxType) => void;
+ metadata: CanvasElement;
+ onSave: (id: ID, kind: BoxType, invalidated?: boolean) => void;
onRemove: () => void;
onClose: () => void;
@@ -175,6 +174,9 @@ export interface BoxEditorType {
canManageFunctions?: boolean;
elements?: any[];
questionFunctionNames?: string[];
+ visualStyle?: VisualStyle;
+ pythonTutorStandalonePrimitives?: boolean;
+ onElementsChange?: Dispatch>;
}
export type Tab = "feedback" | "question";
diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css
index 09fdb1a..20b425d 100644
--- a/frontend/src/styles/theme.css
+++ b/frontend/src/styles/theme.css
@@ -66,6 +66,15 @@
--fade-box-fill: #f8fafc;
--fade-box-line-color: #e2e8f0;
--hide-box-fill: #ffffff;
+
+ /* Python Tutor styling */
+ --python-tutor-box-bg: #f1efc6;
+ --python-tutor-border: #7d7b63;
+ --python-tutor-header-text: #666666;
+ --python-tutor-body-text: #111111;
+ --python-tutor-reference-text: #1f4f82;
+ --python-tutor-selected-frame-bg: #d8dee8;
+ --python-tutor-frame-line: #a8a8a8;
}
:root[data-theme="dark"] {
@@ -132,6 +141,15 @@
--fade-box-fill: #2d2d2d;
--fade-box-line-color: #3a3a3a;
--hide-box-fill: #2d2d2d;
+
+ /* Python Tutor styling */
+ --python-tutor-box-bg: #f1efc6;
+ --python-tutor-border: #7d7b63;
+ --python-tutor-header-text: #666666;
+ --python-tutor-body-text: #111111;
+ --python-tutor-reference-text: #1f4f82;
+ --python-tutor-selected-frame-bg: #d8dee8;
+ --python-tutor-frame-line: #a8a8a8;
}
/* Apply theme to body */