diff --git a/app/api/templates/[id]/route.ts b/app/api/templates/[id]/route.ts new file mode 100644 index 0000000..3b330dd --- /dev/null +++ b/app/api/templates/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextResponse, NextRequest } from 'next/server'; +import { curatedTemplates } from '@/src/lib/templates/curated'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + + // Lookup requested template + // In the future: also look up generated templates from the DB here + const template = curatedTemplates.find(t => t.id === id); + + if (!template) { + return NextResponse.json({ error: 'Template not found' }, { status: 404 }); + } + + return NextResponse.json({ template }); + } catch (error) { + console.error('Error fetching template:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/api/templates/route.ts b/app/api/templates/route.ts new file mode 100644 index 0000000..9d10b10 --- /dev/null +++ b/app/api/templates/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { curatedTemplates } from '@/src/lib/templates/curated'; + +export async function GET() { + try { + // Return summary of curated templates for the listing page + const summaries = curatedTemplates.map(t => ({ + id: t.id, + title: t.title, + description: t.description, + category: t.category, + difficulty: t.difficulty, + targetRps: t.targetRps + })); + + return NextResponse.json({ templates: summaries }); + } catch (error) { + console.error('Error fetching templates:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} diff --git a/app/practice/[id]/page.tsx b/app/practice/[id]/page.tsx new file mode 100644 index 0000000..0401749 --- /dev/null +++ b/app/practice/[id]/page.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState, useRef, useCallback } from 'react'; +import { DesignCanvas, CanvasNode, Connection, CanvasStateRef } from '@/components/canvas/DesignCanvas'; +import { ComponentPalette } from '@/components/canvas/ComponentPalette'; +import { CanvasPanelsProvider } from '@/components/canvas/CanvasPanelsContext'; +import { ITemplate } from '@/src/lib/templates/types'; +import { SimulationResult } from '@/src/lib/simulation/engine'; +import { + getSavedProgress, saveProgress, clearProgress, + isSolved, markSolved, unmarkSolved, +} from '@/src/lib/practice/storage'; + +export default function PracticePage() { + const params = useParams(); + const router = useRouter(); + const id = params?.id as string; + + const [template, setTemplate] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [simulationState, setSimulationState] = useState(null); + const [feedback, setFeedback] = useState<{ type: 'success' | 'error', text: string } | null>(null); + const [showSolution, setShowSolution] = useState(false); + const [solved, setSolved] = useState(false); + const [canvasVersion, setCanvasVersion] = useState(0); + + // Preserve user's canvas modifications across solution toggles + const [userNodes, setUserNodes] = useState(null); + const [userConnections, setUserConnections] = useState(null); + const canvasStateRef = useRef(null); + + // Auto-save progress periodically + const autoSaveRef = useRef(null); + + useEffect(() => { + const controller = new AbortController(); + + async function loadTemplate() { + try { + const res = await fetch(`/api/templates/${id}`, { signal: controller.signal }); + if (!res.ok) throw new Error(`Failed to load template: ${res.status} ${res.statusText}`); + const data = await res.json(); + + if (controller.signal.aborted) return; + + setTemplate(data.template); + + // Load saved progress from localStorage + const saved = getSavedProgress(id); + if (saved) { + setUserNodes(saved.nodes); + setUserConnections(saved.connections); + } + + // Check if already solved + setSolved(isSolved(id)); + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') return; + if (!controller.signal.aborted) { + setError(err instanceof Error ? err.message : 'Failed to load'); + } + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + } + if (id) loadTemplate(); + + return () => controller.abort(); + }, [id]); + + // Auto-save every 3 seconds while actively working + useEffect(() => { + if (!id || showSolution) return; + + autoSaveRef.current = setInterval(() => { + if (canvasStateRef.current) { + saveProgress(id, canvasStateRef.current.nodes, canvasStateRef.current.connections); + } + }, 3000); + + return () => { + if (autoSaveRef.current) clearInterval(autoSaveRef.current); + }; + }, [id, showSolution]); + + const handleToggleSolution = useCallback(() => { + if (!showSolution) { + // Snapshot user's work before switching to solution + if (canvasStateRef.current) { + const nodes = [...canvasStateRef.current.nodes]; + const connections = [...canvasStateRef.current.connections]; + setUserNodes(nodes); + setUserConnections(connections); + saveProgress(id, nodes, connections); + } + } + setShowSolution(prev => !prev); + setFeedback(null); + }, [showSolution, id]); + + const handleSubmit = () => { + if (!template || !simulationState) { + setFeedback({ type: 'error', text: 'Run the simulation first before submitting your fix.' }); + return; + } + + // Guard: ensure the bottleneck node is defined and has metrics + if (!template.bottleneckNodeId || !simulationState.nodeMetrics[template.bottleneckNodeId]) { + setFeedback({ type: 'error', text: 'No bottleneck node defined or metrics unavailable for the target node.' }); + return; + } + + const bottleneckNodeStatus = simulationState.nodeMetrics[template.bottleneckNodeId].status; + + if (bottleneckNodeStatus === 'normal' || bottleneckNodeStatus === 'warning') { + if (simulationState.globalStatus === 'critical') { + setFeedback({ type: 'error', text: `Bottleneck resolved on the target node, but another part of the system is now critical. Keep tweaking!` }); + } else if (simulationState.globalStatus === 'degraded') { + setFeedback({ type: 'error', text: `Target bottleneck resolved, but the system is still degraded. Other nodes are under stress — optimize further.` }); + } else { + // SUCCESS — globalStatus is 'healthy' + setSolved(true); + markSolved(id); + if (canvasStateRef.current) { + saveProgress(id, canvasStateRef.current.nodes, canvasStateRef.current.connections); + } + setFeedback({ type: 'success', text: '🎉 System stabilized! Bottleneck resolved. Excellent architectural decision.' }); + } + } else { + setFeedback({ type: 'error', text: `The bottleneck node is still overloaded under this load. Try adding a component to absorb some of the traffic.`}); + } + }; + + const handleReset = () => { + if (!template) return; + setUserNodes(null); + setUserConnections(null); + setFeedback(null); + setSolved(false); + setCanvasVersion(v => v + 1); + clearProgress(id); + unmarkSolved(id); + }; + + if (loading) { + return ( +
+
+
+

Loading exercise...

+
+
+ ); + } + if (error || !template) { + return ( +
+

Error: {error}

+
+ ); + } + + const activeNodes = showSolution + ? template.modelSolution.nodes + : (userNodes ?? template.initialNodes); + const activeConnections = showSolution + ? template.modelSolution.connections + : (userConnections ?? template.initialConnections); + + return ( + +
+ {/* Header */} +
+
+ router.push('/practice')}>← +
+

+ {template.title} + {solved && ( + + check_circle + Solved + + )} +

+

{template.category} • {template.difficulty.toUpperCase()}

+
+
+
+ + + {!solved ? ( + + ) : ( + + )} +
+
+ + {/* Canvas area */} +
+ {!showSolution && } + + + + {/* Problem Card overlay */} +
+ +
+

Problem Statement

+

+ {template.description} +

+
+ + {showSolution && ( +
+

Model Solution

+

+ {template.solutionExplanation} +

+
+ )} + + {feedback && !showSolution && ( +
+

+ {feedback.text} +

+
+ )} +
+
+
+
+ ); +} diff --git a/app/practice/page.tsx b/app/practice/page.tsx new file mode 100644 index 0000000..34fbce9 --- /dev/null +++ b/app/practice/page.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { Sidebar } from '@/components/dashboard/Sidebar'; +import { SidebarProvider } from '@/components/dashboard/SidebarContext'; +import { getSolvedIds } from '@/src/lib/practice/storage'; + +interface TemplateSummary { + id: string; + title: string; + description: string; + category: string; + difficulty: 'easy' | 'medium' | 'hard'; + targetRps: number; +} + +export default function PracticeDirectory() { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(true); + const [solvedIds, setSolvedIds] = useState([]); + + useEffect(() => { + const controller = new AbortController(); + + async function load() { + try { + const res = await fetch('/api/templates', { signal: controller.signal }); + if (!res.ok) throw new Error(`Failed to load templates: ${res.status} ${res.statusText}`); + const data = await res.json(); + if (!controller.signal.aborted) { + setTemplates(data.templates || []); + } + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') return; + console.error(err); + } finally { + if (!controller.signal.aborted) { + setLoading(false); + } + } + } + load(); + + setSolvedIds(getSolvedIds()); + + return () => controller.abort(); + }, []); + + return ( + +
+ +
+
+
+ + {/* Hero banner */} +
+
+
+

Targeted System Design Practice

+

+ Fix broken architectures under load. Focus on specific patterns like caching, + horizontal scaling, and load balancing without the overhead of a full interview. +

+ {solvedIds.length > 0 && ( +

+ ✅ {solvedIds.length} / {templates.length || '...'} exercises completed +

+ )} +
+
+ +

Available Exercises

+ + {loading ? ( +
+ {[1, 2, 3].map(i => ( +
+ ))} +
+ ) : ( +
+ {/* AI Exercise Card */} +
+
+ smart_toy +
+

Generate Custom Exercise

+

Uses AI to generate a brand new unique bottleneck scenario.

+ + Coming Soon + +
+ + {/* Curated Templates */} + {templates.map(template => { + const templateSolved = solvedIds.includes(template.id); + return ( + + {/* Solved checkmark overlay */} + {templateSolved && ( +
+ check_circle +
+ )} +
+
+ + {template.difficulty.toUpperCase()} + + {template.category} +
+

{template.title}

+

+ {template.description} +

+
+
+ {templateSolved ? 'Review Solution' : 'Start Drill'} + arrow_forward +
+ + ); + })} +
+ )} +
+
+
+
+
+ ); +} diff --git a/components/canvas/DesignCanvas.tsx b/components/canvas/DesignCanvas.tsx index 1b9ea51..a0b74ab 100644 --- a/components/canvas/DesignCanvas.tsx +++ b/components/canvas/DesignCanvas.tsx @@ -3,6 +3,7 @@ import { useState, useRef, useId, useCallback, useEffect, useReducer, MutableRefObject } from 'react'; import { IConstraintChange } from '@/src/lib/db/models/InterviewSession'; import { useSimulationEngine } from '@/src/hooks/useSimulationEngine'; +import { SimulationResult } from '@/src/lib/simulation/engine'; import { SimulationControls } from './SimulationControls'; // Color mapping for different component types @@ -80,12 +81,15 @@ export interface CanvasStateRef { interface DesignCanvasProps { initialNodes?: CanvasNode[]; initialConnections?: Connection[]; + initialTargetRps?: number; onSave?: (nodes: CanvasNode[], connections: Connection[]) => void; readOnly?: boolean; /** Live ref to current canvas state — updated on every change */ stateRef?: MutableRefObject; /** Array of active constraint changes to visually impact the canvas */ activeConstraints?: IConstraintChange[]; + /** Callback fired when the simulation status changes so parent can validate */ + onSimulationChange?: (metrics: SimulationResult) => void; } const MAX_HISTORY = 50; @@ -142,10 +146,12 @@ function historyReducer(state: HistoryState, action: HistoryAction): HistoryStat export function DesignCanvas({ initialNodes = DEFAULT_NODES, initialConnections = DEFAULT_CONNECTIONS, + initialTargetRps = 10000, onSave, readOnly = false, stateRef, - activeConstraints = [] + activeConstraints = [], + onSimulationChange }: DesignCanvasProps) { const arrowId = useId(); const canvasRef = useRef(null); @@ -185,9 +191,22 @@ export function DesignCanvas({ // Simulation Engine State const [isSimulationRunningRaw, setIsSimulationRunning] = useState(false); const isSimulationRunning = isSimulationRunningRaw && !readOnly; - const [targetRps, setTargetRps] = useState(10000); + const [targetRps, setTargetRps] = useState(initialTargetRps); + + // Keep targetRps in sync when the prop changes (async template loads) + useEffect(() => { + setTargetRps(initialTargetRps); + }, [initialTargetRps]); + const simulationMetrics = useSimulationEngine(nodes, connections, targetRps, isSimulationRunning); + // Expose simulation changes to parent (used by Templates system) + useEffect(() => { + if (onSimulationChange) { + onSimulationChange(simulationMetrics); + } + }, [simulationMetrics, onSimulationChange]); + // Selection state const [selectedNodeId, setSelectedNodeId] = useState(null); const [selectedConnectionId, setSelectedConnectionId] = useState(null); diff --git a/components/dashboard/Hero.tsx b/components/dashboard/Hero.tsx index 82f77cf..9b3d5fa 100644 --- a/components/dashboard/Hero.tsx +++ b/components/dashboard/Hero.tsx @@ -1,3 +1,5 @@ +import Link from 'next/link'; + interface HeroProps { userName?: string; } @@ -10,9 +12,9 @@ export function Hero({ userName = 'Designer' }: HeroProps) {

Welcome back, {userName}

Ready to architect your next big system? Create a new design or continue working on existing ones.

- +
diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index 8a54fac..fb1d5e2 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -16,12 +16,13 @@ export function Sidebar() { { id: 'my-designs', href: '/dashboard', label: 'My Designs', icon: 'grid_view', filled: true }, { id: 'analytics', href: '/dashboard/analytics', label: 'Analytics', icon: 'bar_chart', filled: true }, { id: 'interview', href: '/interview', label: 'Interview Mode', icon: 'play_circle', filled: false }, - { id: 'templates', href: '#', label: 'Templates', icon: 'library_books', filled: false }, + { id: 'templates', href: '/practice', label: 'Templates', icon: 'library_books', filled: false }, ]; const isActive = (href: string) => { if (href === '#') return false; - if (href === '/dashboard') return pathname === href; + if (href === '/dashboard' || href === '/interview') return pathname === href; + if (href === '/practice') return pathname === '/practice' || pathname.startsWith('/practice/'); return pathname === href || pathname.startsWith(href + '/'); }; @@ -137,6 +138,9 @@ export function Sidebar() { {item.icon} {item.label} + {item.id === 'templates' && ( + New + )} ); })} diff --git a/src/lib/practice/storage.ts b/src/lib/practice/storage.ts new file mode 100644 index 0000000..8e016a6 --- /dev/null +++ b/src/lib/practice/storage.ts @@ -0,0 +1,63 @@ +// Practice persistence helpers +// Shared module so both the listing page and drill page use the same logic. + +import type { CanvasNode, Connection } from '@/components/canvas/DesignCanvas'; + +const SOLVED_KEY = 'practice_solved'; +const progressKey = (id: string) => `practice_progress_${id}`; + +export function getSavedProgress(templateId: string): { nodes: CanvasNode[]; connections: Connection[] } | null { + try { + const raw = localStorage.getItem(progressKey(templateId)); + if (!raw) return null; + const parsed = JSON.parse(raw); + if ( + parsed != null && + typeof parsed === 'object' && + Array.isArray(parsed.nodes) && + Array.isArray(parsed.connections) + ) { + return parsed as { nodes: CanvasNode[]; connections: Connection[] }; + } + return null; + } catch { return null; } +} + +export function saveProgress(templateId: string, nodes: CanvasNode[], connections: Connection[]) { + try { + localStorage.setItem(progressKey(templateId), JSON.stringify({ nodes, connections })); + } catch { /* quota exceeded — silently fail */ } +} + +export function clearProgress(templateId: string) { + try { localStorage.removeItem(progressKey(templateId)); } catch { /* ignore */ } +} + +export function getSolvedIds(): string[] { + try { + const parsed = JSON.parse(localStorage.getItem(SOLVED_KEY) || '[]'); + if (!Array.isArray(parsed)) return []; + return parsed.filter((v): v is string => typeof v === 'string'); + } catch { return []; } +} + +export function isSolved(templateId: string): boolean { + return getSolvedIds().includes(templateId); +} + +export function markSolved(templateId: string) { + try { + const solved = getSolvedIds(); + if (!solved.includes(templateId)) { + solved.push(templateId); + localStorage.setItem(SOLVED_KEY, JSON.stringify(solved)); + } + } catch { /* silently fail */ } +} + +export function unmarkSolved(templateId: string) { + try { + const solved = getSolvedIds().filter(s => s !== templateId); + localStorage.setItem(SOLVED_KEY, JSON.stringify(solved)); + } catch { /* ignore */ } +} diff --git a/src/lib/simulation/engine.ts b/src/lib/simulation/engine.ts index 4f0743a..a913519 100644 --- a/src/lib/simulation/engine.ts +++ b/src/lib/simulation/engine.ts @@ -1,6 +1,9 @@ import { ICanvasNode, IConnection } from '../db/models/Design'; import { NODE_CAPACITIES, NodeMetrics, EdgeMetrics } from './constants'; +/** Fraction of traffic that passes through a Cache node (cache misses). 0.1 = 90% hit rate. */ +const CACHE_MISS_RATIO = 0.1; + export interface SimulationResult { nodeMetrics: Record; edgeMetrics: Record; @@ -111,7 +114,9 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target // Distribute to children const outgoingEdges = adj[node.id]; if (outgoingEdges && outgoingEdges.length > 0) { - const flowPerEdge = outFlow / outgoingEdges.length; + // Cache nodes absorb most traffic; only cache misses flow downstream + const effectiveOut = node.type === 'Cache' ? outFlow * CACHE_MISS_RATIO : outFlow; + const flowPerEdge = effectiveOut / outgoingEdges.length; outgoingEdges.forEach(edge => { edgeMetrics[edge.id].trafficFlow += flowPerEdge; if (nextTrafficIn[edge.to] !== undefined) { diff --git a/src/lib/templates/curated.ts b/src/lib/templates/curated.ts new file mode 100644 index 0000000..589c7ab --- /dev/null +++ b/src/lib/templates/curated.ts @@ -0,0 +1,85 @@ +import { ITemplate } from './types'; + +export const curatedTemplates: ITemplate[] = [ + { + id: 'curated-cache-001', + title: 'Database Under Siege', + description: 'Your SQL database is receiving 8,000 RPS directly from the app servers. It can only handle 3,000. Users are experiencing dropped requests and timeouts. Fix the bottleneck using the canvas.', + category: 'Caching Optimization', + difficulty: 'easy', + targetRps: 8000, + initialNodes: [ + { id: 'client', type: 'Client', icon: 'person', x: 500, y: 350, label: 'Users' }, + { id: 'lb', type: 'LB', icon: 'account_tree', x: 700, y: 350, label: 'Load Balancer' }, + { id: 'srv1', type: 'Server', icon: 'dns', x: 900, y: 250, label: 'App Server 1' }, + { id: 'srv2', type: 'Server', icon: 'dns', x: 900, y: 450, label: 'App Server 2' }, + { id: 'db', type: 'SQL', icon: 'database', x: 1100, y: 350, label: 'Primary DB' } + ], + initialConnections: [ + { id: 'c1', from: 'client', to: 'lb' }, + { id: 'c2', from: 'lb', to: 'srv1' }, + { id: 'c3', from: 'lb', to: 'srv2' }, + { id: 'c4', from: 'srv1', to: 'db' }, + { id: 'c5', from: 'srv2', to: 'db' } + ], + bottleneckNodeId: 'db', + expectedSolution: ['add_cache'], + modelSolution: { + nodes: [ + { id: 'client', type: 'Client', icon: 'person', x: 500, y: 350, label: 'Users' }, + { id: 'lb', type: 'LB', icon: 'account_tree', x: 700, y: 350, label: 'Load Balancer' }, + { id: 'srv1', type: 'Server', icon: 'dns', x: 900, y: 250, label: 'App Server 1' }, + { id: 'srv2', type: 'Server', icon: 'dns', x: 900, y: 450, label: 'App Server 2' }, + { id: 'cache', type: 'Cache', icon: 'memory', x: 1100, y: 350, label: 'Redis Cache' }, + { id: 'db', type: 'SQL', icon: 'database', x: 1300, y: 350, label: 'Primary DB' } + ], + connections: [ + { id: 'c1', from: 'client', to: 'lb' }, + { id: 'c2', from: 'lb', to: 'srv1' }, + { id: 'c3', from: 'lb', to: 'srv2' }, + { id: 'c4', from: 'srv1', to: 'cache' }, + { id: 'c5', from: 'srv2', to: 'cache' }, + { id: 'c6', from: 'cache', to: 'db', label: 'Cache Misses' } + ] + }, + solutionExplanation: "Adding a Redis Cache (50k RPS capacity) in front of the SQL database absorbs 90% of read traffic. Only cache misses (10%) flow to the DB, reducing its load from 8k to ~800 RPS — well within its 3k capacity." + }, + { + id: 'curated-scale-001', + title: 'The Monolith', + description: 'A single App Server is handling all 10,000 RPS coming from the Load Balancer. It can only handle 5,000. Add another server and balance the load.', + category: 'Scaling Systems', + difficulty: 'easy', + targetRps: 10000, + initialNodes: [ + { id: 'client', type: 'Client', icon: 'person', x: 500, y: 350, label: 'Users' }, + { id: 'lb', type: 'LB', icon: 'account_tree', x: 700, y: 350, label: 'Load Balancer' }, + { id: 'srv1', type: 'Server', icon: 'dns', x: 900, y: 350, label: 'App Server 1' }, + { id: 'db', type: 'SQL', icon: 'database', x: 1100, y: 350, label: 'Primary DB' } + ], + initialConnections: [ + { id: 'c1', from: 'client', to: 'lb' }, + { id: 'c2', from: 'lb', to: 'srv1' }, + { id: 'c3', from: 'srv1', to: 'db' } + ], + bottleneckNodeId: 'srv1', + expectedSolution: ['scale_horizontally'], + modelSolution: { + nodes: [ + { id: 'client', type: 'Client', icon: 'person', x: 500, y: 350, label: 'Users' }, + { id: 'lb', type: 'LB', icon: 'account_tree', x: 700, y: 350, label: 'Load Balancer' }, + { id: 'srv1', type: 'Server', icon: 'dns', x: 900, y: 250, label: 'App Server 1' }, + { id: 'srv2', type: 'Server', icon: 'dns', x: 900, y: 450, label: 'App Server 2' }, + { id: 'db', type: 'SQL', icon: 'database', x: 1100, y: 350, label: 'Primary DB' } + ], + connections: [ + { id: 'c1', from: 'client', to: 'lb' }, + { id: 'c2', from: 'lb', to: 'srv1' }, + { id: 'c3', from: 'lb', to: 'srv2' }, + { id: 'c4', from: 'srv1', to: 'db' }, + { id: 'c5', from: 'srv2', to: 'db' } + ] + }, + solutionExplanation: "By adding a second App Server (scaling horizontally) and connecting the Load Balancer to both, the 10,000 RPS is distributed equally. Each server now handles 5,000 RPS, which is exactly within their capacity." + } +]; diff --git a/src/lib/templates/types.ts b/src/lib/templates/types.ts new file mode 100644 index 0000000..008710f --- /dev/null +++ b/src/lib/templates/types.ts @@ -0,0 +1,28 @@ +import { ICanvasNode, IConnection } from "@/src/lib/db/models/Design"; + +export type TemplateDifficulty = "easy" | "medium" | "hard"; +export type TemplateCategory = "Bottleneck Resolution" | "Scaling Systems" | "Caching Optimization" | "Load Balancing" | "Fault Tolerance"; + +export interface ITemplate { + id: string; // Unique identifier (e.g., 'curated-bottleneck-001' or 'ai-gen-123') + title: string; + description: string; + category: TemplateCategory; + difficulty: TemplateDifficulty; + targetRps: number; // For the simulation engine + + // Starting state + initialNodes: ICanvasNode[]; + initialConnections: IConnection[]; + + // For validation + bottleneckNodeId: string; // The specific node ID that is overloaded and needs fixing + + // Model Solution + expectedSolution: string[]; // Descriptive tags or rules (e.g. ['add_cache']) + modelSolution: { + nodes: ICanvasNode[]; + connections: IConnection[]; + }; + solutionExplanation: string; +}