Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions app/api/interview/[id]/chaos-timeout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/src/lib/db/mongoose';
import InterviewSession from '@/src/lib/db/models/InterviewSession';

export async function POST(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
await dbConnect();
const { id } = await context.params;
const { type, constraintId } = await req.json();

// 1. Fetch Session
const session = await InterviewSession.findOne({ id });
if (!session) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
}

// 2. Locate Constraint
const constraintChanges = session.constraintChanges || [];
const constraintIndex = constraintChanges.findIndex((c: { id: string }) => c.id === constraintId);
if (constraintIndex === -1) {
return NextResponse.json({ error: 'Constraint not found' }, { status: 404 });
}

const constraint = constraintChanges[constraintIndex];

// 3. Early Returns (already addressed or failed)
if (constraint.status === 'addressed' || (type === 'warning' && constraint.overtimeAt) || (type === 'penalty' && constraint.failedAt)) {
return NextResponse.json({ success: true, warning: 'Already processed' });
}

if (!session.aiMessages) session.aiMessages = [];

// 4. Update state depending on timeout type
if (type === 'warning') {
constraint.overtimeAt = new Date();
session.aiMessages.push({
role: 'interviewer',
content: `Hey, I noticed we still haven't addressed the ${constraint.title} issue. In a real-world scenario, leaving a failure like this unhandled could lead to a broader system outage. Could you walk me through how you'd mitigate this in the next 2 minutes? Let's treat this as a high-priority incident.`,
timestamp: new Date()
});
session.markModified('constraintChanges');
session.markModified('aiMessages');
} else if (type === 'penalty') {
constraint.failedAt = new Date();
// We keep status as 'active' so the UI node stays visibly broken
session.aiMessages.push({
role: 'interviewer',
content: `Alright, time's up on the ${constraint.title} scenario. Since we weren't able to establish a complete mitigation plan in time, I'll be noting this gap in high-availability planning for the evaluation. That said, let's keep moving forward with the rest of your system design—what were you thinking for the next component?`,
timestamp: new Date()
});
session.markModified('constraintChanges');
session.markModified('aiMessages');
}

await session.save();

return NextResponse.json({ success: true, messages: session.aiMessages, constraintChanges: session.constraintChanges });
} catch (error) {
console.error('Chaos Timeout Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
42 changes: 42 additions & 0 deletions app/api/interview/[id]/final-validation/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import dbConnect from '@/src/lib/db/mongoose';
import InterviewSession from '@/src/lib/db/models/InterviewSession';

export async function POST(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
await dbConnect();
const { id } = await context.params;

const session = await InterviewSession.findOne({ id });
if (!session) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 });
}

if (!session.aiMessages) session.aiMessages = [];

// Idempotency check: Have we already sent it?
const alreadySent = session.aiMessages.some((m: { role: string; content: string }) =>
m.role === 'interviewer' && m.content.includes("Final Validation Phase")
);

if (alreadySent || session.status !== 'in_progress') {
return NextResponse.json({ success: true, messages: session.aiMessages });
}

session.aiMessages.push({
role: 'interviewer',
content: `**Final Validation Phase**: We have roughly 10 minutes left in the interview! It's time to test your architecture's resiliency. Please turn to the **Simulation Controls** panel, hit 'Run Test', and use the **Target Throughput** slider to simulate traffic. Talk me through how your system behaves under different load scenarios, and point out any bottlenecks.`,
timestamp: new Date()
});
session.markModified('aiMessages');
await session.save();

return NextResponse.json({ success: true, messages: session.aiMessages });
} catch (error) {
console.error('Final Validation Error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
7 changes: 5 additions & 2 deletions app/api/interview/[id]/hint/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,15 @@ function generateConstraintChange(
impactAreas: ['scalability', 'caching', 'load balancing'],
};
} else if ((databaseCount <= 1 || mentionsDisconnected) && difficulty !== 'easy') {
const candidateNodes = nodes.filter((n) => ['SQL', 'Blob', 'Cache', 'Server'].includes(n.type));
const victim = candidateNodes[Math.floor(Math.random() * candidateNodes.length)];
selected = {
type: 'reliability',
title: 'Regional Failover',
description: 'The system must continue serving users during a full regional outage with minimal disruption.',
title: 'Node Failure',
description: victim ? `The ${victim.label || victim.type} component just went offline unexpectedly. The system must continue serving users with minimal disruption.` : 'The system must continue serving users during a full regional outage with minimal disruption.',
severity: difficulty === 'hard' ? 'high' : 'moderate',
impactAreas: ['availability', 'replication', 'disaster recovery'],
impactedNodeId: victim?.id,
};
} else if (!hasQueue && (hasRealtimePrompt || nodeTypes.has('Server') || nodeTypes.has('Function'))) {
selected = {
Expand Down
40 changes: 39 additions & 1 deletion app/interview/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default function InterviewCanvasPage({ params }: PageProps) {
const [showHints, setShowHints] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [isInterviewPanelOpen, setIsInterviewPanelOpen] = useState(false);
const [finalValidationTriggered, setFinalValidationTriggered] = useState(false);

// Refs for save logic
const isSavingRef = useRef(false);
Expand Down Expand Up @@ -100,6 +101,23 @@ export default function InterviewCanvasPage({ params }: PageProps) {
onConstraintChange: handleConstraintChange
});

// Final Validation Phase Trigger automatically checks and dispatches.
useEffect(() => {
if (!finalValidationTriggered && timer.minutes !== undefined && timer.minutes <= 10 && session?.status === 'in_progress') {
setFinalValidationTriggered(true);

authFetch(`/api/interview/${id}/final-validation`, { method: 'POST' })
.then(res => res.json())
.then(data => {
if (data.success && data.messages) {
if (ai.setMessages) ai.setMessages(data.messages);
setIsInterviewPanelOpen(true);
}
})
.catch(err => console.error('Final validation trigger failed:', err));
}
}, [timer.minutes, finalValidationTriggered, session?.status, id, ai]);

// Fetch session data
const fetchSession = useCallback(async () => {
if (!user?.uid || !id) return;
Expand Down Expand Up @@ -284,6 +302,24 @@ export default function InterviewCanvasPage({ params }: PageProps) {
};
}, [id]);

const handleChaosTimeout = useCallback(async (type: 'warning' | 'penalty', constraintId: string) => {
try {
const res = await authFetch(`/api/interview/${id}/chaos-timeout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, constraintId })
});
const data = await res.json();
if (data.success && data.messages) {
if (ai.setMessages) ai.setMessages(data.messages);
setSession(prev => prev ? { ...prev, constraintChanges: data.constraintChanges } : null);
setIsInterviewPanelOpen(true); // Automatically slide open the interviewer panel
}
} catch (err) {
console.error('Chaos timeout failed:', err);
}
}, [id, ai]);

// Loading state
if (authLoading || isLoading) {
return (
Expand Down Expand Up @@ -330,13 +366,14 @@ export default function InterviewCanvasPage({ params }: PageProps) {
<InterviewHeader
difficulty={session.difficulty}
constraintChangeCount={session.constraintChanges?.length || 0}
latestConstraintTitle={session.constraintChanges?.at(-1)?.title}
latestConstraint={session.constraintChanges?.at(-1)}
saveStatus={saveStatus}
timer={timer}
status={session.status}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
sessionId={id}
onChaosTimeout={handleChaosTimeout}
/>

{/* Submit Error Banner */}
Expand Down Expand Up @@ -377,6 +414,7 @@ export default function InterviewCanvasPage({ params }: PageProps) {
onSave={isReadOnly ? undefined : saveDesign}
readOnly={isReadOnly}
stateRef={isReadOnly ? undefined : canvasStateRef}
activeConstraints={session.constraintChanges || []}
/>

{/* Properties Panel */}
Expand Down
Loading
Loading