-
Notifications
You must be signed in to change notification settings - Fork 0
Staging #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Staging #108
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4bf3fc3
feat(practice): implement template-based practice system with simulat…
Shashank0701-byte b59249f
Merge pull request #107 from Shashank0701-byte/feature/chaos
Shashank0701-byte 5b25990
fix: Lint and build issues
Shashank0701-byte fec4eb1
Merge pull request #109 from Shashank0701-byte/feature/chaos
Shashank0701-byte 2d288f7
fix: Lint and build issues
Shashank0701-byte ede010f
Merge pull request #110 from Shashank0701-byte/feature/chaos
Shashank0701-byte 72ad5d8
fix: Lint and build issues
Shashank0701-byte 28b7aa0
Merge pull request #111 from Shashank0701-byte/feature/chaos
Shashank0701-byte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,286 @@ | ||
| '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'; | ||
|
|
||
| // --- LocalStorage helpers --- | ||
| function getSavedProgress(templateId: string): { nodes: CanvasNode[]; connections: Connection[] } | null { | ||
| try { | ||
| const data = localStorage.getItem(`practice_progress_${templateId}`); | ||
| return data ? JSON.parse(data) : null; | ||
| } catch { return null; } | ||
| } | ||
|
|
||
| function saveProgress(templateId: string, nodes: CanvasNode[], connections: Connection[]) { | ||
| try { | ||
| localStorage.setItem(`practice_progress_${templateId}`, JSON.stringify({ nodes, connections })); | ||
| } catch { /* quota exceeded — silently fail */ } | ||
| } | ||
|
|
||
| function markSolved(templateId: string) { | ||
| try { | ||
| const solved = JSON.parse(localStorage.getItem('practice_solved') || '[]') as string[]; | ||
| if (!solved.includes(templateId)) { | ||
| solved.push(templateId); | ||
| localStorage.setItem('practice_solved', JSON.stringify(solved)); | ||
| } | ||
| } catch { /* silently fail */ } | ||
| } | ||
|
|
||
| export function isSolved(templateId: string): boolean { | ||
| try { | ||
| const solved = JSON.parse(localStorage.getItem('practice_solved') || '[]') as string[]; | ||
| return solved.includes(templateId); | ||
| } catch { return false; } | ||
| } | ||
| // --- End helpers --- | ||
|
|
||
| export default function PracticePage() { | ||
| const params = useParams(); | ||
| const router = useRouter(); | ||
| const id = params?.id as string; | ||
|
|
||
| const [template, setTemplate] = useState<ITemplate | null>(null); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| const [simulationState, setSimulationState] = useState<SimulationResult | null>(null); | ||
| const [feedback, setFeedback] = useState<{ type: 'success' | 'error', text: string } | null>(null); | ||
| const [showSolution, setShowSolution] = useState(false); | ||
| const [solved, setSolved] = useState(false); | ||
|
|
||
| // Preserve user's canvas modifications across solution toggles | ||
| const [userNodes, setUserNodes] = useState<CanvasNode[] | null>(null); | ||
| const [userConnections, setUserConnections] = useState<Connection[] | null>(null); | ||
| const canvasStateRef = useRef<CanvasStateRef | null>(null); | ||
|
|
||
| // Auto-save progress periodically | ||
| const autoSaveRef = useRef<NodeJS.Timeout | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| async function loadTemplate() { | ||
| try { | ||
| const res = await fetch(`/api/templates/${id}`); | ||
| if (!res.ok) throw new Error('Template not found'); | ||
| const data = await res.json(); | ||
| 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) { | ||
| setError(err instanceof Error ? err.message : 'Failed to load'); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| if (id) loadTemplate(); | ||
| }, [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; | ||
| } | ||
|
|
||
| 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 { | ||
| // SUCCESS — persist solved state | ||
| 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); | ||
| try { | ||
| localStorage.removeItem(`practice_progress_${id}`); | ||
| const solved = JSON.parse(localStorage.getItem('practice_solved') || '[]') as string[]; | ||
| localStorage.setItem('practice_solved', JSON.stringify(solved.filter(s => s !== id))); | ||
| } catch { /* ignore */ } | ||
| }; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (loading) { | ||
| return ( | ||
| <div className="flex h-screen items-center justify-center bg-background-dark"> | ||
| <div className="flex flex-col items-center gap-4"> | ||
| <div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" /> | ||
| <p className="text-slate-400">Loading exercise...</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| if (error || !template) { | ||
| return ( | ||
| <div className="flex h-screen items-center justify-center bg-background-dark"> | ||
| <p className="text-red-500">Error: {error}</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
Comment on lines
+160
to
+166
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error message could show "Error: null" in edge case. If the template fails to load but 🔧 Proposed fix for fallback error message if (error || !template) {
return (
<div className="flex h-screen items-center justify-center bg-background-dark">
- <p className="text-red-500">Error: {error}</p>
+ <p className="text-red-500">Error: {error ?? 'Failed to load template'}</p>
</div>
);
}🤖 Prompt for AI Agents |
||
|
|
||
| const activeNodes = showSolution | ||
| ? template.modelSolution.nodes | ||
| : (userNodes ?? template.initialNodes); | ||
| const activeConnections = showSolution | ||
| ? template.modelSolution.connections | ||
| : (userConnections ?? template.initialConnections); | ||
|
|
||
| return ( | ||
| <CanvasPanelsProvider> | ||
| <div className="flex h-screen w-full flex-col bg-background-dark text-white overflow-hidden"> | ||
| {/* Header */} | ||
| <header className="flex h-14 shrink-0 items-center justify-between border-b border-border-dark bg-sidebar-bg px-6 z-10"> | ||
| <div className="flex items-center gap-3"> | ||
| <span className="text-slate-400 cursor-pointer hover:text-white transition" onClick={() => router.push('/practice')}>←</span> | ||
| <div> | ||
| <h1 className="text-base font-semibold text-white flex items-center gap-2"> | ||
| {template.title} | ||
| {solved && ( | ||
| <span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 px-2.5 py-0.5 text-[10px] font-semibold text-emerald-400 uppercase tracking-wider"> | ||
| <span className="material-symbols-outlined" style={{ fontSize: '12px' }}>check_circle</span> | ||
| Solved | ||
| </span> | ||
| )} | ||
| </h1> | ||
| <p className="text-xs text-slate-500">{template.category} • {template.difficulty.toUpperCase()}</p> | ||
| </div> | ||
| </div> | ||
| <div className="flex gap-3"> | ||
| <button | ||
| onClick={handleReset} | ||
| className="rounded-lg px-3 py-2 text-sm text-slate-500 hover:text-slate-300 transition cursor-pointer" | ||
| title="Reset to original template" | ||
| > | ||
| Reset | ||
| </button> | ||
| <button | ||
| onClick={handleToggleSolution} | ||
| className="rounded-lg px-4 py-2 border border-border-dark text-sm hover:bg-sidebar-bg transition text-slate-300 cursor-pointer" | ||
| > | ||
| {showSolution ? 'Back to Your Work' : 'View Solution'} | ||
| </button> | ||
| {!solved ? ( | ||
| <button | ||
| onClick={handleSubmit} | ||
| disabled={showSolution} | ||
| className="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-500 transition shadow-sm disabled:opacity-50 cursor-pointer" | ||
| > | ||
| Submit Fix | ||
| </button> | ||
| ) : ( | ||
| <button | ||
| onClick={() => router.push('/practice')} | ||
| className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/80 transition shadow-sm cursor-pointer" | ||
| > | ||
| Practice More | ||
| </button> | ||
| )} | ||
| </div> | ||
| </header> | ||
|
|
||
| {/* Canvas area */} | ||
| <div className="flex flex-1 overflow-hidden relative"> | ||
| {!showSolution && <ComponentPalette />} | ||
|
|
||
| <DesignCanvas | ||
| key={`${showSolution ? 'solution' : 'active'}-${userNodes ? 'saved' : 'initial'}`} | ||
| initialNodes={activeNodes} | ||
| initialConnections={activeConnections} | ||
| initialTargetRps={template.targetRps} | ||
| onSimulationChange={setSimulationState} | ||
| readOnly={showSolution} | ||
| stateRef={showSolution ? undefined : canvasStateRef} | ||
| /> | ||
|
|
||
| {/* Problem Card overlay */} | ||
| <div className="absolute top-6 z-[60] w-80 flex flex-col gap-4 pointer-events-none" style={{ left: showSolution ? '24px' : '280px' }}> | ||
|
|
||
| <div className="bg-black/90 backdrop-blur-md rounded-xl p-5 border border-border-dark shadow-xl pointer-events-auto"> | ||
| <h3 className="font-semibold text-white mb-2">Problem Statement</h3> | ||
| <p className="text-sm text-slate-400 leading-relaxed"> | ||
| {template.description} | ||
| </p> | ||
| </div> | ||
|
|
||
| {showSolution && ( | ||
| <div className="bg-indigo-950/95 backdrop-blur-md rounded-xl p-5 border border-indigo-800 shadow-xl pointer-events-auto"> | ||
| <h3 className="font-semibold text-indigo-300 mb-2">Model Solution</h3> | ||
| <p className="text-sm text-indigo-200 leading-relaxed"> | ||
| {template.solutionExplanation} | ||
| </p> | ||
| </div> | ||
| )} | ||
|
|
||
| {feedback && !showSolution && ( | ||
| <div className={`backdrop-blur-md rounded-xl p-4 border shadow-xl pointer-events-auto ${ | ||
| feedback.type === 'success' | ||
| ? 'bg-emerald-950/95 border-emerald-800 text-emerald-300' | ||
| : 'bg-red-950/95 border-red-800 text-red-300' | ||
| }`}> | ||
| <p className="text-sm font-medium leading-relaxed"> | ||
| {feedback.text} | ||
| </p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </CanvasPanelsProvider> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.