RYP / src /components /Compiler.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
import React, { useEffect, useMemo, useRef, useState } from 'react';
import Editor from '@monaco-editor/react';
import { CodingQuestion } from '@/data/codingQuestions';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import {
Play,
Send,
RotateCcw,
ChevronLeft,
ChevronRight,
Terminal,
Info,
CheckCircle2,
XCircle,
Sparkles,
Loader2,
X,
Lock,
User as UserIcon,
Bot,
BookOpen,
BriefcaseBusiness,
Code2,
History,
Lightbulb,
ListChecks,
BarChart3,
Gauge,
Layers3,
GripHorizontal,
Tag,
MessageSquare,
ThumbsUp,
Clock,
Trash2,
ChevronDown,
ChevronUp,
Zap,
HardDrive,
} from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { apiFetch } from '@/lib/authClient';
import { getCodeEditorFontSize } from '@/lib/preferences';
import ComplexityGrowthChart from '@/components/ComplexityGrowthChart';
import { useSubscription } from '@/contexts/SubscriptionContext';
import useResizable from './Compiler/useResizable';
interface CompilerProps {
question: CodingQuestion;
onBack: () => void;
onSolved?: (language?: string) => void;
fontSize?: 'small' | 'medium' | 'large';
}
type ExecutionResult = {
passed: boolean;
input: string;
expected: string;
actual: string;
error?: string;
};
type StarterLanguage = keyof CodingQuestion['starterCode'];
type SolutionLanguage = Exclude<keyof CodingQuestion['solution'], 'explanation'>;
type Language = StarterLanguage | 'python3' | 'c';
type LanguageOption = {
value: Language;
label: string;
monacoLanguage: string;
backendLanguage: string;
starterKey?: StarterLanguage;
solutionKey?: SolutionLanguage;
};
const LANGUAGE_OPTIONS: LanguageOption[] = [
{ value: 'javascript', label: 'JavaScript', monacoLanguage: 'javascript', backendLanguage: 'javascript', starterKey: 'javascript', solutionKey: 'javascript' },
{ value: 'python', label: 'Python', monacoLanguage: 'python', backendLanguage: 'python', starterKey: 'python', solutionKey: 'python' },
{ value: 'python3', label: 'Python 3', monacoLanguage: 'python', backendLanguage: 'python3', starterKey: 'python', solutionKey: 'python' },
{ value: 'cpp', label: 'C++', monacoLanguage: 'cpp', backendLanguage: 'cpp', starterKey: 'cpp', solutionKey: 'cpp' },
{ value: 'c', label: 'C', monacoLanguage: 'c', backendLanguage: 'c' },
{ value: 'java', label: 'Java', monacoLanguage: 'java', backendLanguage: 'java', starterKey: 'java', solutionKey: 'java' },
{ value: 'go', label: 'Go', monacoLanguage: 'go', backendLanguage: 'go', starterKey: 'go', solutionKey: 'go' },
];
const getLanguageOption = (language: Language) =>
LANGUAGE_OPTIONS.find((option) => option.value === language) ?? LANGUAGE_OPTIONS[0];
const getTitleFunctionName = (title: string) => {
const words = title.match(/[A-Za-z0-9]+/g) ?? [];
if (words.length === 0) return 'solve';
const [first, ...rest] = words;
const camelName = [
first.toLowerCase(),
...rest.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()),
].join('');
return /^[A-Za-z_]/.test(camelName) ? camelName : `solve${camelName}`;
};
const extractFunctionNameFromSource = (source: string) => {
const patterns = [
/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?function\b/,
/\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/,
/\bdef\s+([A-Za-z_]\w*)\s*\(/,
/\bfunc\s+([A-Za-z_]\w*)\s*\(/,
/\b(?:public|private|protected|static|\s)+[\w<>\[\]]+\s+([A-Za-z_]\w*)\s*\([^;]*\)\s*\{/,
/\b[\w:<>,\s*&]+\s+([A-Za-z_]\w*)\s*\([^;]*\)\s*\{/,
];
for (const pattern of patterns) {
const match = source.match(pattern);
if (match?.[1] && !['if', 'for', 'while', 'switch', 'catch'].includes(match[1])) {
return match[1];
}
}
return '';
};
const getPrimaryFunctionName = (question: CodingQuestion) => {
const sources = [
question.starterCode.javascript,
question.starterCode.python,
question.starterCode.cpp,
question.starterCode.java,
question.starterCode.go,
];
for (const source of sources) {
const name = extractFunctionNameFromSource(source);
if (name) return name;
}
return getTitleFunctionName(question.title);
};
const C_RESERVED_WORDS = new Set([
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum',
'extern', 'float', 'for', 'goto', 'if', 'inline', 'int', 'long', 'register', 'restrict', 'return',
'short', 'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void',
'volatile', 'while',
]);
const sanitizeCIdentifier = (value: string, fallback: string) => {
const sanitized = value.trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^[^A-Za-z_]+/, '');
if (!sanitized || C_RESERVED_WORDS.has(sanitized)) return fallback;
return sanitized;
};
const extractInputParamNames = (input: string) => {
const names = [...input.matchAll(/(?:^|,\s*)([A-Za-z_]\w*)\s*=/g)].map((match) => match[1]);
return names;
};
const extractSignatureParamNames = (source: string) => {
const signature = source.match(/\(([^)]*)\)/)?.[1];
if (!signature) return [];
return signature
.split(',')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => part.replace(/=.*/, '').replace(/:.*/, '').trim().split(/\s+/).pop() ?? '')
.map((name) => name.replace(/[*&\[\]]/g, '').trim())
.filter((name) => name && name !== 'self' && name !== 'this');
};
const getParamNames = (question: CodingQuestion) => {
const expectedCount = question.testCases[0]?.params.length ?? 0;
const sources = [
extractInputParamNames(question.testCases[0]?.input ?? ''),
extractSignatureParamNames(question.starterCode.python),
extractSignatureParamNames(question.starterCode.javascript),
extractSignatureParamNames(question.starterCode.go),
extractSignatureParamNames(question.starterCode.java),
extractSignatureParamNames(question.starterCode.cpp),
];
const names = sources.find((candidate) => candidate.length >= expectedCount) ?? [];
const used = new Set<string>();
return Array.from({ length: expectedCount }, (_, index) => {
let name = sanitizeCIdentifier(names[index] ?? '', `arg${index + 1}`);
while (used.has(name)) {
name = `${name}_${index + 1}`;
}
used.add(name);
return name;
});
};
const isNumberArray = (value: unknown): value is number[] =>
Array.isArray(value) && value.every((item) => typeof item === 'number');
const getCParamDeclaration = (value: unknown, name: string) => {
if (isNumberArray(value)) return `int* ${name}, int ${name}Size`;
if (Array.isArray(value)) return `int* ${name}, int ${name}Size`;
if (typeof value === 'string') return `const char* ${name}`;
if (typeof value === 'boolean') return `int ${name}`;
return `long long ${name}`;
};
const getCReturnType = (expected: unknown) => {
if (isNumberArray(expected)) return 'int*';
if (typeof expected === 'string') return 'const char*';
if (typeof expected === 'boolean') return 'int';
return 'long long';
};
const getCDefaultReturn = (expected: unknown) => {
if (isNumberArray(expected)) return ' *returnSize = 0;\n return NULL;';
if (typeof expected === 'string') return ' return "";';
return ' return 0;';
};
const buildCStarterCode = (question: CodingQuestion) => {
const sample = question.testCases[0];
const functionName = sanitizeCIdentifier(getPrimaryFunctionName(question), 'solve');
const params = sample?.params ?? [];
const paramNames = getParamNames(question);
const declarations = params.map((param, index) => getCParamDeclaration(param, paramNames[index] ?? `arg${index + 1}`));
const expected = sample?.expected;
const returnType = getCReturnType(expected);
if (isNumberArray(expected)) {
declarations.push('int* returnSize');
}
const declarationText = declarations.length ? declarations.join(', ') : 'void';
return `${returnType} ${functionName}(${declarationText}) {\n${getCDefaultReturn(expected)}\n}`;
};
const isNumberMatrix = (value: unknown): value is number[][] =>
Array.isArray(value) && value.every(isNumberArray);
const isStringArray = (value: unknown): value is string[] =>
Array.isArray(value) && value.every((item) => typeof item === 'string');
const getJSDocType = (value: unknown): string => {
if (isNumberMatrix(value)) return 'number[][]';
if (isNumberArray(value)) return 'number[]';
if (isStringArray(value)) return 'string[]';
if (Array.isArray(value)) return 'any[]';
if (typeof value === 'string') return 'string';
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'number') return 'number';
return 'any';
};
const getPythonType = (value: unknown): string => {
if (isNumberMatrix(value)) return 'List[List[int]]';
if (isNumberArray(value)) return 'List[int]';
if (isStringArray(value)) return 'List[str]';
if (Array.isArray(value)) return 'List[Any]';
if (typeof value === 'string') return 'str';
if (typeof value === 'boolean') return 'bool';
if (typeof value === 'number') return Number.isInteger(value) ? 'int' : 'float';
return 'Any';
};
const getCppType = (value: unknown, forParam = false): string => {
if (isNumberMatrix(value)) return forParam ? 'vector<vector<int>>&' : 'vector<vector<int>>';
if (isNumberArray(value)) return forParam ? 'vector<int>&' : 'vector<int>';
if (isStringArray(value)) return forParam ? 'vector<string>&' : 'vector<string>';
if (Array.isArray(value)) return forParam ? 'vector<int>&' : 'vector<int>';
if (typeof value === 'string') return 'string';
if (typeof value === 'boolean') return 'bool';
if (typeof value === 'number') return Number.isInteger(value) ? 'int' : 'double';
return 'long long';
};
const getJavaType = (value: unknown): string => {
if (isNumberMatrix(value)) return 'int[][]';
if (isNumberArray(value)) return 'int[]';
if (isStringArray(value)) return 'String[]';
if (Array.isArray(value)) return 'int[]';
if (typeof value === 'string') return 'String';
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'number') return Number.isInteger(value) ? 'int' : 'double';
return 'Object';
};
const getGoType = (value: unknown): string => {
if (isNumberMatrix(value)) return '[][]int';
if (isNumberArray(value)) return '[]int';
if (isStringArray(value)) return '[]string';
if (Array.isArray(value)) return '[]int';
if (typeof value === 'string') return 'string';
if (typeof value === 'boolean') return 'bool';
if (typeof value === 'number') return Number.isInteger(value) ? 'int' : 'float64';
return 'interface{}';
};
const buildJavaScriptStarterCode = (question: CodingQuestion) => {
const sample = question.testCases[0];
const functionName = sanitizeCIdentifier(getPrimaryFunctionName(question), 'solve');
const params = sample?.params ?? [];
const paramNames = getParamNames(question);
const jsDocParams = params.map((param, index) => ` * @param {${getJSDocType(param)}} ${paramNames[index] ?? `arg${index + 1}`}`);
const returnType = getJSDocType(sample?.expected);
return `/**\n${jsDocParams.join('\n')}${jsDocParams.length ? '\n' : ''} * @return {${returnType}}\n */\nvar ${functionName} = function(${paramNames.join(', ')}) {\n \n};`;
};
const buildPythonStarterCode = (question: CodingQuestion) => {
const sample = question.testCases[0];
const functionName = sanitizeCIdentifier(getPrimaryFunctionName(question), 'solve');
const params = sample?.params ?? [];
const paramNames = getParamNames(question);
const typedParams = params.map((param, index) => `${paramNames[index] ?? `arg${index + 1}`}: ${getPythonType(param)}`);
const returnType = getPythonType(sample?.expected);
return `class Solution:\n def ${functionName}(self${typedParams.length ? `, ${typedParams.join(', ')}` : ''}) -> ${returnType}:\n pass`;
};
const buildCppStarterCode = (question: CodingQuestion) => {
const sample = question.testCases[0];
const functionName = sanitizeCIdentifier(getPrimaryFunctionName(question), 'solve');
const params = sample?.params ?? [];
const paramNames = getParamNames(question);
const typedParams = params.map((param, index) => `${getCppType(param, true)} ${paramNames[index] ?? `arg${index + 1}`}`);
const returnType = getCppType(sample?.expected);
return `class Solution {\npublic:\n ${returnType} ${functionName}(${typedParams.join(', ')}) {\n \n }\n};`;
};
const buildJavaStarterCode = (question: CodingQuestion) => {
const sample = question.testCases[0];
const functionName = sanitizeCIdentifier(getPrimaryFunctionName(question), 'solve');
const params = sample?.params ?? [];
const paramNames = getParamNames(question);
const typedParams = params.map((param, index) => `${getJavaType(param)} ${paramNames[index] ?? `arg${index + 1}`}`);
const returnType = getJavaType(sample?.expected);
return `class Solution {\n public ${returnType} ${functionName}(${typedParams.join(', ')}) {\n \n }\n}`;
};
const buildGoStarterCode = (question: CodingQuestion) => {
const sample = question.testCases[0];
const functionName = sanitizeCIdentifier(getPrimaryFunctionName(question), 'solve');
const params = sample?.params ?? [];
const paramNames = getParamNames(question);
const typedParams = params.map((param, index) => `${paramNames[index] ?? `arg${index + 1}`} ${getGoType(param)}`);
const returnType = getGoType(sample?.expected);
return `func ${functionName}(${typedParams.join(', ')}) ${returnType} {\n \n}`;
};
const buildFallbackStarterCode = (question: CodingQuestion, language: Language) => {
switch (language) {
case 'javascript':
return buildJavaScriptStarterCode(question);
case 'python':
case 'python3':
return buildPythonStarterCode(question);
case 'cpp':
return buildCppStarterCode(question);
case 'java':
return buildJavaStarterCode(question);
case 'go':
return buildGoStarterCode(question);
case 'c':
return buildCStarterCode(question);
default:
return '';
}
};
const getStarterCode = (question: CodingQuestion, language: Language) => {
if (language === 'c') return buildCStarterCode(question);
const starterKey = getLanguageOption(language).starterKey;
const existingStarter = starterKey ? question.starterCode[starterKey]?.trim() : '';
return existingStarter || buildFallbackStarterCode(question, language);
};
const getSolutionCode = (question: CodingQuestion, language: Language) => {
const solutionKey = getLanguageOption(language).solutionKey;
return solutionKey ? question.solution[solutionKey] ?? '' : '';
};
const stringifyValue = (value: unknown) => JSON.stringify(value) ?? 'null';
const buildDefaultCustomInput = (question: CodingQuestion) => {
const sample = question.testCases[0];
if (!sample) return '';
return sample.params.map((param) => stringifyValue(param)).join('\n');
};
const parseCustomInput = (raw: string, expectedCount: number) => {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error('Enter custom input first.');
}
try {
const parsed = JSON.parse(trimmed);
if (expectedCount <= 1) {
return [parsed];
}
if (Array.isArray(parsed) && parsed.length === expectedCount) {
return parsed;
}
} catch {
/* fall back to line-based parsing */
}
const lines = trimmed
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const params = lines.map((line, index) => {
try {
return JSON.parse(line);
} catch {
throw new Error(`Line ${index + 1} is not valid JSON.`);
}
});
if (params.length !== expectedCount) {
throw new Error(`Expected ${expectedCount} argument${expectedCount === 1 ? '' : 's'}, but got ${params.length}.`);
}
return params;
};
const prettyPrintExecutionValue = (raw: string) => {
if (raw === 'Error') return raw;
try {
return JSON.stringify(JSON.parse(raw), null, 2);
} catch {
return raw;
}
};
type DescriptionExample = {
input: string;
output: string;
explanation?: string;
};
type ParsedDescription = {
statement: string[];
examples: DescriptionExample[];
constraints: string[];
hint?: string;
askedBy?: string;
};
type CodingTestCase = CodingQuestion['testCases'][number] & {
explanation?: string;
};
const formatExpectedValue = (value: unknown, fallback: string) => {
if (typeof value === 'undefined') return fallback;
if (typeof value === 'string') return `"${value}"`;
return JSON.stringify(value) ?? fallback;
};
const extractQuotedInput = (input: string) => input.match(/"([^"]*)"/)?.[1];
const decodeRunLengthString = (encoded: string) => {
const matches = [...encoded.matchAll(/([a-zA-Z])(\d+)/g)];
if (!matches.length || matches.map((match) => match[0]).join('') !== encoded) {
return null;
}
const decodedParts: string[] = [];
const vowels: string[] = [];
for (const match of matches) {
const char = match[1].toLowerCase();
const count = Number(match[2]);
if (!Number.isFinite(count) || count < 0) return null;
decodedParts.push(char.repeat(count));
if ('aeiou'.includes(char)) {
vowels.push(...Array.from({ length: count }, () => char));
}
}
return {
decoded: decodedParts.join(''),
vowels,
};
};
const summarizeVowels = (vowels: string[]) => {
if (vowels.length === 0) return 'No vowels are present, so the answer is 0.';
if (vowels.length <= 12) return `Vowels: ${vowels.join(', ')} -> ${vowels.length}.`;
const counts = vowels.reduce<Record<string, number>>((acc, vowel) => {
acc[vowel] = (acc[vowel] ?? 0) + 1;
return acc;
}, {});
const summary = ['a', 'e', 'i', 'o', 'u']
.filter((vowel) => counts[vowel])
.map((vowel) => `${vowel} x ${counts[vowel]}`)
.join(', ');
return `Vowel counts: ${summary} -> ${vowels.length}.`;
};
const buildTestCaseExplanation = (
questionTitle: string,
category: string,
description: string,
testCase: CodingTestCase,
) => {
if (testCase.explanation?.trim()) {
return testCase.explanation.trim();
}
const problemText = `${questionTitle} ${category} ${description}`;
const isCompressedVowelProblem = /vowels?/i.test(problemText) && /(compressed|run-length|encoded)/i.test(problemText);
if (isCompressedVowelProblem) {
const encoded = typeof testCase.params?.[0] === 'string' ? testCase.params[0] : extractQuotedInput(testCase.input);
const decoded = encoded ? decodeRunLengthString(encoded) : null;
if (encoded && decoded) {
const decodedText = decoded.decoded.length <= 48
? `Decoded string is "${decoded.decoded}".`
: `Decoded string has ${decoded.decoded.length} characters.`;
return `${decodedText} ${summarizeVowels(decoded.vowels)}`;
}
}
const expected = formatExpectedValue(testCase.expected, testCase.output);
if (typeof testCase.expected === 'boolean') {
return `For this input, the required condition is ${testCase.expected ? 'true' : 'false'}, so the expected output is \`${testCase.output}\`.`;
}
if (typeof testCase.expected === 'number') {
return `Evaluating this input gives \`${expected}\`, so the expected output is \`${testCase.output}\`.`;
}
if (Array.isArray(testCase.expected)) {
if (testCase.expected.length === 0) {
return `No valid result exists for this input, so the expected output is \`${testCase.output}\`.`;
}
return `The valid result for this input is \`${testCase.output}\`.`;
}
return `The expected returned value is \`${expected}\`, so the output is \`${testCase.output}\`.`;
};
type ComplexityFamily =
| 'constant'
| 'logarithmic'
| 'linear'
| 'linearithmic'
| 'heap'
| 'quadratic'
| 'cubic'
| 'exponential';
type ComplexityReport = {
time: string;
space: string;
family: ComplexityFamily;
confidence: 'Low' | 'Medium' | 'High';
summary: string;
signals: string[];
};
const sectionHeader = (value: string, label: string) =>
new RegExp(`^${label}:?\\s*$`, 'i').test(value.trim());
const parseProblemDescription = (
description: string,
testCases: CodingQuestion['testCases'],
questionTitle: string,
category: string,
): ParsedDescription => {
const lines = description.replace(/\r\n/g, '\n').split('\n');
const statementLines: string[] = [];
const parsedExamples: DescriptionExample[] = [];
const constraints: string[] = [];
const hintLines: string[] = [];
const askedByLines: string[] = [];
let mode: 'statement' | 'examples' | 'constraints' | 'hint' | 'askedBy' = 'statement';
let currentExample: DescriptionExample | null = null;
const commitExample = () => {
if (currentExample && (currentExample.input || currentExample.output || currentExample.explanation)) {
parsedExamples.push(currentExample);
}
currentExample = null;
};
for (const line of lines) {
const trimmed = line.trim();
if (sectionHeader(trimmed, 'Examples?')) {
commitExample();
mode = 'examples';
continue;
}
if (sectionHeader(trimmed, 'Constraints?')) {
commitExample();
mode = 'constraints';
continue;
}
const hintMatch = trimmed.match(/^Hint:\s*(.*)$/i);
if (hintMatch) {
commitExample();
mode = 'hint';
if (hintMatch[1]) hintLines.push(hintMatch[1]);
continue;
}
const askedByMatch = trimmed.match(/^Asked by:\s*(.*)$/i);
if (askedByMatch) {
commitExample();
mode = 'askedBy';
if (askedByMatch[1]) askedByLines.push(askedByMatch[1]);
continue;
}
if (mode === 'examples') {
const inputMatch = trimmed.match(/^(?:\d+\.\s*)?Input:\s*(.*)$/i);
const outputMatch = trimmed.match(/^Output:\s*(.*)$/i);
const explanationMatch = trimmed.match(/^Explanation:\s*(.*)$/i);
if (inputMatch) {
commitExample();
currentExample = { input: inputMatch[1], output: '' };
} else if (outputMatch) {
currentExample = currentExample ?? { input: '', output: '' };
currentExample.output = outputMatch[1];
} else if (explanationMatch) {
currentExample = currentExample ?? { input: '', output: '' };
currentExample.explanation = explanationMatch[1];
} else if (trimmed && currentExample?.explanation) {
currentExample.explanation = `${currentExample.explanation} ${trimmed}`;
}
continue;
}
if (mode === 'constraints') {
if (trimmed) {
constraints.push(trimmed.replace(/^[-*]\s*/, ''));
}
continue;
}
if (mode === 'hint') {
if (trimmed) hintLines.push(trimmed);
continue;
}
if (mode === 'askedBy') {
if (trimmed) askedByLines.push(trimmed);
continue;
}
statementLines.push(line);
}
commitExample();
const statement = statementLines
.join('\n')
.split(/\n{2,}/)
.map((part) => part.trim())
.filter(Boolean);
const examples: DescriptionExample[] = testCases.map((testCase, index) => ({
input: parsedExamples[index]?.input || testCase.input,
output: parsedExamples[index]?.output || testCase.output,
explanation: parsedExamples[index]?.explanation || buildTestCaseExplanation(questionTitle, category, description, testCase),
}));
for (const example of parsedExamples.slice(examples.length)) {
examples.push(example);
}
return {
statement,
examples,
constraints,
hint: hintLines.join(' ').trim() || undefined,
askedBy: askedByLines.join(' ').trim() || undefined,
};
};
const splitTopLevelInput = (input: string) => {
const parts: string[] = [];
let depth = 0;
let start = 0;
let quote: '"' | "'" | null = null;
let escaped = false;
for (let index = 0; index < input.length; index += 1) {
const char = input[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (char === '\\') {
escaped = true;
} else if (char === quote) {
quote = null;
}
continue;
}
if (char === '"' || char === "'") {
quote = char;
continue;
}
if (char === '[' || char === '(' || char === '{') {
depth += 1;
continue;
}
if (char === ']' || char === ')' || char === '}') {
depth = Math.max(0, depth - 1);
continue;
}
if (char === ',' && depth === 0) {
parts.push(input.slice(start, index).trim());
start = index + 1;
}
}
parts.push(input.slice(start).trim());
return parts.filter(Boolean);
};
const formatInputFields = (input: string) => {
const assignments = splitTopLevelInput(input).map((part) => {
const match = part.match(/^([A-Za-z_]\w*)\s*=\s*([\s\S]*)$/);
return match ? { name: match[1], value: match[2].trim() } : null;
});
if (assignments.length > 0 && assignments.every(Boolean)) {
return assignments as { name: string; value: string }[];
}
return [{ name: 'Input', value: input }];
};
const renderInlineCode = (text: string) =>
text.split(/(`[^`]+`)/g).map((part, index) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code
key={index}
className="rounded border border-white/10 bg-white/10 px-1.5 py-0.5 font-mono text-[0.9em] text-zinc-100"
>
{part.slice(1, -1)}
</code>
);
}
return <React.Fragment key={index}>{part}</React.Fragment>;
});
const stripCodeForAnalysis = (source: string, lang: Language) => {
let cleaned = source.replace(/\/\*[\s\S]*?\*\//g, '');
if (lang === 'python' || lang === 'python3') {
cleaned = cleaned.replace(/#.*$/gm, '');
} else {
cleaned = cleaned.replace(/\/\/.*$/gm, '');
}
return cleaned;
};
const countMatches = (value: string, pattern: RegExp) => value.match(pattern)?.length ?? 0;
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const estimatePythonLoopDepth = (cleaned: string) => {
const activeLoopIndents: number[] = [];
let maxDepth = 0;
let loopCount = 0;
for (const line of cleaned.split('\n')) {
if (!line.trim()) continue;
const indent = line.match(/^\s*/)?.[0].replace(/\t/g, ' ').length ?? 0;
while (activeLoopIndents.length && indent <= activeLoopIndents[activeLoopIndents.length - 1]) {
activeLoopIndents.pop();
}
if (/^(for|while)\b/.test(line.trim())) {
activeLoopIndents.push(indent);
loopCount += 1;
maxDepth = Math.max(maxDepth, activeLoopIndents.length);
}
}
return { loopCount, maxDepth };
};
const estimateBraceLoopDepth = (cleaned: string) => {
const loopPattern = /\b(for|while)\b|\.forEach\s*\(|\.map\s*\(|\.filter\s*\(|\.reduce\s*\(|\.some\s*\(|\.every\s*\(|\.find\s*\(/;
const activeLoopDepths: number[] = [];
let braceDepth = 0;
let loopCount = 0;
let maxDepth = 0;
for (const line of cleaned.split('\n')) {
const leadingCloseCount = line.match(/^\s*}+/)?.[0].split('').filter((char) => char === '}').length ?? 0;
const adjustedDepth = Math.max(0, braceDepth - leadingCloseCount);
while (activeLoopDepths.length && activeLoopDepths[activeLoopDepths.length - 1] > adjustedDepth) {
activeLoopDepths.pop();
}
const hasLoop = loopPattern.test(line);
if (hasLoop) {
loopCount += 1;
maxDepth = Math.max(maxDepth, activeLoopDepths.length + 1);
}
const opens = countMatches(line, /{/g);
const closes = countMatches(line, /}/g);
const nextDepth = Math.max(0, braceDepth + opens - closes);
if (hasLoop) {
activeLoopDepths.push(opens > 0 ? braceDepth + opens : braceDepth + 1);
}
braceDepth = nextDepth;
while (activeLoopDepths.length && activeLoopDepths[activeLoopDepths.length - 1] > braceDepth) {
activeLoopDepths.pop();
}
}
return { loopCount, maxDepth };
};
const extractFunctionNames = (cleaned: string) => {
const names = new Set<string>();
const patterns = [
/function\s+([A-Za-z_$][\w$]*)\s*\(/g,
/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?function\b/g,
/\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/g,
/\bdef\s+([A-Za-z_]\w*)\s*\(/g,
/\bfunc\s+([A-Za-z_]\w*)\s*\(/g,
/\b(?:public|private|protected|static)?\s*[\w<>\[\]]+\s+([A-Za-z_]\w*)\s*\([^;]*\)\s*{/g,
];
for (const pattern of patterns) {
for (const match of cleaned.matchAll(pattern)) {
if (match[1] && !['if', 'for', 'while', 'switch', 'catch'].includes(match[1])) {
names.add(match[1]);
}
}
}
return Array.from(names);
};
const countHeapOperations = (cleaned: string) => {
const pythonHeapOps = countMatches(cleaned, /\b(?:heapq\.)?(?:heappush|heappop|heapreplace|heappushpop|heapify)\s*\(/g);
const namedHeapOps = countMatches(
cleaned,
/\b(?:heap|pq|queue|priorityQueue|minHeap|maxHeap)\.(?:push|pop|offer|poll|add|remove|enqueue|dequeue)\s*\(/g,
);
const cppPriorityQueueOps = /\bpriority_queue\s*</.test(cleaned)
? countMatches(cleaned, /\.(?:push|pop|emplace)\s*\(/g)
: 0;
const javaPriorityQueueOps = /\bPriorityQueue\s*</.test(cleaned)
? countMatches(cleaned, /\.(?:offer|poll|add|remove)\s*\(/g)
: 0;
return pythonHeapOps + namedHeapOps + cppPriorityQueueOps + javaPriorityQueueOps;
};
const hasKBoundedHeapSignal = (cleaned: string, question: CodingQuestion) => {
const context = `${cleaned}\n${question.title}\n${question.description}\n${question.category}`;
return /\bk\b|merge\s+k|k\s+sorted|lists?\s*\.length|lists?\s*\.size\s*\(|len\s*\(\s*lists?\s*\)|enumerate\s*\(\s*lists?\s*\)|for\s+\w+\s+in\s+lists?\b/i.test(
context,
);
};
const analyzeCodeComplexity = (
sourceCode: string,
lang: Language,
question: CodingQuestion,
): ComplexityReport => {
const cleaned = stripCodeForAnalysis(sourceCode, lang);
const meaningfulLines = cleaned.split('\n').filter((line) => line.trim()).length;
const loopEstimate = lang === 'python' || lang === 'python3' ? estimatePythonLoopDepth(cleaned) : estimateBraceLoopDepth(cleaned);
const iteratorCount = countMatches(cleaned, /\.(?:forEach|map|filter|reduce|some|every|find)\s*\(/g);
const loopCount = Math.max(loopEstimate.loopCount, iteratorCount);
const maxLoopDepth = Math.max(loopEstimate.maxDepth, iteratorCount > 0 ? 1 : 0);
const sortDetected = /\.sort\s*\(|\bsort\s*\(|Arrays\.sort|Collections\.sort/.test(cleaned);
const heapOperationCount = countHeapOperations(cleaned);
const heapDetected = heapOperationCount > 0 || /\bheapq\b|\bPriorityQueue\b|\bpriority_queue\s*</.test(cleaned);
const heapWorkDetected = heapOperationCount > 0;
const kBoundedHeap = heapDetected && hasKBoundedHeapSignal(cleaned, question);
const functionNames = extractFunctionNames(cleaned);
const recursiveNames = functionNames.filter((name) => {
const calls = countMatches(cleaned, new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`, 'g'));
return calls > 1;
});
const recursionDetected = recursiveNames.length > 0;
const branchingRecursion = recursiveNames.some((name) => {
const calls = countMatches(cleaned, new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`, 'g'));
return calls > 2;
});
const divideAndConquerSignal = /\/\s*2|>>\s*1|\bmid\b|Math\.floor|\/\/\s*2/.test(cleaned);
const matrixSignal = /matrix|mat\b|grid|rows?|cols?|m\s*[=;]|n\s*[=;]/i.test(
`${cleaned}\n${question.title}\n${question.category}`,
);
const growsSpace = /new\s+(?:Array|Map|Set)|\b(?:Map|Set|HashMap|HashSet|ArrayList|vector|unordered_map|unordered_set)\b|\[\]|\{\}|\bdict\b|\blist\b|make\s*\(\s*(?:\[\]|map)/.test(
cleaned,
);
let family: ComplexityFamily = 'constant';
let time = 'O(1)';
let summary = 'No repeated traversal is obvious, so the current code looks constant time.';
if (recursionDetected && branchingRecursion) {
family = 'exponential';
time = 'O(2^n)';
summary = 'The code appears to make multiple recursive calls from the same function, which can grow exponentially.';
} else if (heapWorkDetected && loopCount > 0) {
family = 'heap';
time = kBoundedHeap ? 'O(N log k)' : 'O(n log n)';
summary = kBoundedHeap
? 'A heap is used while merging/traversing k input lists, so each of the N output elements pays a log k heap operation.'
: 'A heap is used inside traversal, so heap operations add a logarithmic factor to the repeated work.';
} else if (recursionDetected && divideAndConquerSignal && maxLoopDepth === 0) {
family = 'logarithmic';
time = 'O(log n)';
summary = 'A self-call with a halving pattern usually grows logarithmically.';
} else if (maxLoopDepth >= 3) {
family = 'cubic';
time = 'O(n^3)';
summary = 'Three nested loop levels dominate the runtime.';
} else if (maxLoopDepth >= 2) {
family = 'quadratic';
time = matrixSignal ? 'O(m * n)' : 'O(n^2)';
summary = matrixSignal
? 'Nested loops over row and column dimensions make the runtime grow with every matrix cell.'
: 'Two nested loop levels dominate the runtime.';
} else if (sortDetected) {
family = 'linearithmic';
time = 'O(n log n)';
summary = 'Sorting is the dominant visible operation.';
} else if (maxLoopDepth === 1 || recursionDetected) {
family = 'linear';
time = 'O(n)';
summary = recursionDetected
? 'A single recursive path usually grows linearly with input depth.'
: 'A single traversal over the input dominates the runtime.';
}
let space = 'O(1)';
if (recursionDetected) {
space = 'O(n)';
}
if (heapWorkDetected) {
space = kBoundedHeap ? 'O(k)' : 'O(n)';
} else if (growsSpace) {
space = matrixSignal && time === 'O(m * n)' ? 'O(m * n)' : 'O(n)';
}
const signals = [
loopCount > 0 ? `${loopCount} loop-like construct${loopCount === 1 ? '' : 's'} detected` : '',
maxLoopDepth > 0 ? `Maximum visible nesting depth: ${maxLoopDepth}` : '',
sortDetected ? 'Sorting call detected' : '',
heapDetected ? `${Math.max(heapOperationCount, 1)} heap/priority-queue operation${heapOperationCount === 1 ? '' : 's'} detected` : '',
kBoundedHeap ? 'Heap size is bounded by k input lists' : '',
recursionDetected ? `Recursive call detected${recursiveNames.length ? `: ${recursiveNames.join(', ')}` : ''}` : '',
growsSpace ? 'Growing data structure allocation detected' : '',
].filter(Boolean);
if (signals.length === 0) {
signals.push('No obvious loops, recursion, sorting, or growing containers detected');
}
const confidence: ComplexityReport['confidence'] =
meaningfulLines < 4 ? 'Low' : maxLoopDepth >= 2 || sortDetected || recursionDetected || heapWorkDetected ? 'High' : 'Medium';
return {
time,
space,
family,
confidence,
summary,
signals,
};
};
export default function Compiler({ question, onBack, onSolved, fontSize = 'medium' }: CompilerProps) {
const { tier } = useSubscription();
const availableLanguages = LANGUAGE_OPTIONS;
const defaultLanguage: Language = 'javascript';
const customInputExample = question.testCases[0]?.input || 'Enter one JSON argument per line';
const [code, setCode] = useState(getStarterCode(question, defaultLanguage));
const [language, setLanguage] = useState<Language>(defaultLanguage);
const isLanguageAvailable = availableLanguages.some(({ value }) => value === language);
const [output, setOutput] = useState<string>('');
const [isRunning, setIsRunning] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [results, setResults] = useState<ExecutionResult[] | null>(null);
const [isAiLoading, setIsAiLoading] = useState(false);
const [activeTab, setActiveTab] = useState('description');
const [bottomTab, setBottomTab] = useState<'testcase' | 'result' | 'analysis'>('testcase');
const [isChatOpen, setIsChatOpen] = useState(false);
const [chatMessages, setChatMessages] = useState<{ role: 'user' | 'assistant', content: string }[]>([]);
const [chatInput, setChatInput] = useState('');
const [chatPosition, setChatPosition] = useState<{ x: number; y: number } | null>(null);
const [isDraggingChat, setIsDraggingChat] = useState(false);
const [activeTestCaseIndex, setActiveTestCaseIndex] = useState(0);
const [useCustomInput, setUseCustomInput] = useState(false);
const [customInput, setCustomInput] = useState(buildDefaultCustomInput(question));
const [customOutput, setCustomOutput] = useState('');
const [complexityReport, setComplexityReport] = useState<ComplexityReport | null>(null);
const [discussions, setDiscussions] = useState<any[]>([]);
const [isDiscussionsLoading, setIsDiscussionsLoading] = useState(false);
const [discussionPage, setDiscussionPage] = useState(1);
const [discussionTotalPages, setDiscussionTotalPages] = useState(1);
const [expandedCards, setExpandedCards] = useState<Set<string>>(new Set());
const [groupChatMsgs, setGroupChatMsgs] = useState<any[]>([]);
const [chatMsgInput, setChatMsgInput] = useState('');
const [isChatLoading, setIsChatLoading] = useState(false);
const [isChatSending, setIsChatSending] = useState(false);
const groupChatEndRef = useRef<HTMLDivElement>(null);
const groupChatPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const editorRef = useRef<any>(null);
const bottomResizer = useResizable({
direction: 'horizontal',
initialSize: 280,
minSize: 44,
maxSize: 800,
reverse: true,
});
const leftPanelResizer = useResizable({
direction: 'vertical',
initialSize: 450,
minSize: 320,
maxSize: 1000,
});
const currentUserId = useMemo(() => {
try {
const token = localStorage.getItem('ryp_auth_token');
if (!token) return '';
if (token.startsWith('local-demo:')) return token.slice('local-demo:'.length);
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.sub || payload.id || '';
} catch { return ''; }
}, []);
const currentUsername = useMemo(() => {
try {
const raw = localStorage.getItem('ryp_local_users');
if (!raw) return 'User';
const users = JSON.parse(raw);
const me = users.find((u: any) => u.id === currentUserId);
return me?.displayName || 'User';
} catch { return 'User'; }
}, [currentUserId]);
const fetchDiscussions = async (page = 1) => {
setIsDiscussionsLoading(true);
try {
const res = await apiFetch(`/api/discussions?problemId=${encodeURIComponent(question.id)}&page=${page}&limit=10`);
if (res.ok) {
const data = await res.json();
setDiscussions(data.posts || []);
setDiscussionTotalPages(data.totalPages || 1);
setDiscussionPage(data.currentPage || 1);
}
} catch (e) {
console.error('Failed to fetch discussions', e);
} finally {
setIsDiscussionsLoading(false);
}
};
useEffect(() => {
if (activeTab === 'discussion') {
fetchDiscussions(discussionPage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, question.id]);
const toggleUpvote = async (postId: string) => {
try {
const res = await apiFetch(`/api/discussions/upvote?id=${postId}&userId=${encodeURIComponent(currentUserId)}`, { method: 'PATCH' });
if (res.ok) {
const data = await res.json();
setDiscussions(prev => prev.map(d =>
d.id === postId ? { ...d, upvotes: data.upvotes, upvotedBy: data.upvotedBy } : d
));
}
} catch (e) { console.error('Upvote failed', e); }
};
const deleteDiscussion = async (postId: string) => {
try {
const res = await apiFetch(`/api/discussions?id=${postId}&userId=${encodeURIComponent(currentUserId)}`, { method: 'DELETE' });
if (res.ok) {
setDiscussions(prev => prev.filter(d => d.id !== postId));
}
} catch (e) { console.error('Delete failed', e); }
};
// ── Chat helpers ───────────────────────────────────────────────────
const fetchGroupChat = async () => {
try {
const res = await apiFetch(`/api/chat?problemId=${encodeURIComponent(question.id)}&limit=50`);
if (res.ok) {
const data = await res.json();
// API returns newest first, reverse so oldest is at top
setGroupChatMsgs((data.messages || []).reverse());
}
} catch (e) { console.error('Failed to fetch chat', e); }
};
const sendGroupChatMsg = async () => {
const msg = chatMsgInput.trim();
if (!msg || isChatSending) return;
setIsChatSending(true);
try {
const res = await apiFetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
problemId: question.id,
userId: currentUserId,
username: currentUsername,
message: msg,
}),
});
if (res.ok) {
const newMsg = await res.json();
setGroupChatMsgs(prev => [...prev, newMsg]);
setChatMsgInput('');
setTimeout(() => groupChatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
}
} catch (e) { console.error('Send chat failed', e); }
setIsChatSending(false);
};
const deleteGroupChatMsg = async (msgId: string) => {
try {
const res = await apiFetch(`/api/chat?id=${msgId}&userId=${encodeURIComponent(currentUserId)}`, { method: 'DELETE' });
if (res.ok) {
setGroupChatMsgs(prev => prev.filter(m => m.id !== msgId));
}
} catch (e) { console.error('Delete chat failed', e); }
};
// Load chat when Discussion tab is active and poll every 8s
useEffect(() => {
if (activeTab === 'discussion') {
setIsChatLoading(true);
fetchGroupChat().finally(() => setIsChatLoading(false));
groupChatPollRef.current = setInterval(fetchGroupChat, 8000);
}
return () => {
if (groupChatPollRef.current) clearInterval(groupChatPollRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, question.id]);
// Auto-scroll chat on new messages
useEffect(() => {
if (groupChatMsgs.length > 0) {
groupChatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [groupChatMsgs.length]);
const timeAgo = (dateStr: string) => {
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString();
};
const chatDragRef = useRef<{
pointerId: number;
startX: number;
startY: number;
originX: number;
originY: number;
} | null>(null);
const descriptionSections = useMemo(
() => parseProblemDescription(question.description, question.testCases, question.title, question.category),
[question.category, question.description, question.testCases, question.title],
);
const getChatSize = () => {
if (typeof window === 'undefined') return { width: 384, height: 512 };
return {
width: Math.min(384, Math.max(288, window.innerWidth - 32)),
height: Math.min(512, Math.max(320, window.innerHeight - 96)),
};
};
const clampChatPosition = (position: { x: number; y: number }) => {
if (typeof window === 'undefined') return position;
const margin = 12;
const { width, height } = getChatSize();
const maxX = Math.max(margin, window.innerWidth - width - margin);
const maxY = Math.max(margin, window.innerHeight - height - margin);
return {
x: Math.min(Math.max(position.x, margin), maxX),
y: Math.min(Math.max(position.y, margin), maxY),
};
};
const getDefaultChatPosition = () => {
if (typeof window === 'undefined') return { x: 24, y: 24 };
const { width, height } = getChatSize();
return clampChatPosition({
x: window.innerWidth - width - 24,
y: window.innerHeight - height - 24,
});
};
useEffect(() => {
if (!isLanguageAvailable) {
setLanguage(defaultLanguage);
return;
}
setCode(getStarterCode(question, language));
setComplexityReport(null);
}, [defaultLanguage, isLanguageAvailable, language, question]);
useEffect(() => {
setUseCustomInput(false);
setCustomInput(buildDefaultCustomInput(question));
setCustomOutput('');
setComplexityReport(null);
setBottomTab('testcase');
setActiveTestCaseIndex(0);
}, [question]);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const frame = window.requestAnimationFrame(() => {
editorRef.current?.layout();
});
const timeout = window.setTimeout(() => {
editorRef.current?.layout();
}, 220);
return () => {
window.cancelAnimationFrame(frame);
window.clearTimeout(timeout);
};
}, [isChatOpen, language, useCustomInput, bottomResizer.size, leftPanelResizer.size]);
useEffect(() => {
if (!isChatOpen || typeof window === 'undefined') {
return;
}
const handleResize = () => {
setChatPosition((position) => position ? clampChatPosition(position) : getDefaultChatPosition());
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [isChatOpen]);
const executeCode = async (
userCode: string,
testCases: CodingQuestion['testCases'],
mode: 'judge' | 'custom' = 'judge'
) => {
const res = await apiFetch('/api/code/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
language: getLanguageOption(language).backendLanguage,
code: userCode,
mode,
starterCode: getStarterCode(question, language),
questionTitle: question.title,
testCases,
}),
});
let data: { error?: string; results?: ExecutionResult[]; summary?: string } = {};
try {
data = await res.json();
} catch {
/* ignore */
}
if (!res.ok) {
throw new Error(data.error || 'Code execution failed. Check that the API server, Docker, and the runner images are available.');
}
if (!data.results || !Array.isArray(data.results)) {
throw new Error('Invalid response from execution engine.');
}
return { results: data.results, summary: data.summary ?? '' };
};
const runJavaScript = async (userCode: string, testCases: CodingQuestion['testCases']) => {
const execution = await executeCode(userCode, testCases);
return execution.results;
};
const runCodeWithAi = async (userCode: string, testCases: CodingQuestion['testCases']) => {
return executeCode(userCode, testCases);
};
const handleRun = async () => {
setIsRunning(true);
setResults(null);
setOutput('Compiling and running public test cases...');
setActiveTab('submissions');
setBottomTab('result');
try {
const execution = await executeCode(code, question.testCases);
setResults(execution.results);
const allPassed = execution.results.every((result) => result.passed);
setOutput(allPassed ? 'All public cases passed.' : 'Some public cases failed.');
} catch (err: any) {
setOutput(`Error: ${err.message}`);
} finally {
setIsRunning(false);
}
};
const handleSubmit = async () => {
setIsSubmitting(true);
setResults(null);
setOutput('Submitting and running all test cases (including hidden)...');
setActiveTab('submissions');
setBottomTab('result');
try {
const executionCases = [...question.testCases, ...question.hiddenTestCases];
const execution = await executeCode(code, executionCases);
setResults(execution.results);
const allPassed = execution.results.every((result) => result.passed);
if (allPassed) {
setOutput('Accepted! All tests passed.');
if (onSolved) onSolved(language);
// Auto-post to Discussion
try {
await apiFetch('/api/discussions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
problemId: question.id,
problemTitle: question.title,
userId: currentUserId,
username: currentUsername,
language,
code,
approach: '',
executionTime: execution.summary || '—',
memoryUsed: '—',
tags: [question.category, question.difficulty],
}),
});
} catch (postErr) {
console.error('Failed to post to discussion', postErr);
}
} else {
setOutput('Wrong answer.');
}
} catch (err: any) {
setOutput(`Submission Error: ${err.message}`);
} finally {
setIsSubmitting(false);
}
};
const handleCustomRun = async () => {
setIsRunning(true);
setResults(null);
setCustomOutput('');
setOutput('Running code with custom input...');
setActiveTab('submissions');
setBottomTab('result');
try {
const expectedCount = question.testCases[0]?.params.length ?? 0;
if (expectedCount === 0) {
throw new Error('Custom input is not available for this problem yet.');
}
const params = parseCustomInput(customInput, expectedCount);
const execution = await executeCode(
code,
[
{
input: params.map((param) => stringifyValue(param)).join('\n'),
output: '-',
params,
expected: null,
},
],
'custom'
);
const customResult = execution.results[0];
setResults(execution.results);
setCustomOutput(customResult.error ? `Error: ${customResult.error}` : prettyPrintExecutionValue(customResult.actual));
setOutput(customResult.error ? `Custom input failed for ${language}.` : `Custom input executed for ${language}.`);
} catch (err: any) {
setCustomOutput(`Error: ${err.message}`);
setOutput(`Error: ${err.message}`);
} finally {
setIsRunning(false);
}
};
const sendChatMessage = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!chatInput.trim() || isAiLoading) return;
const userMessage = chatInput.trim();
setChatInput('');
const newMessages = [...chatMessages, { role: 'user' as const, content: userMessage }];
setChatMessages(newMessages);
setIsAiLoading(true);
try {
const response = await apiFetch('/api/ai/hint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: newMessages,
questionContext: {
title: question.title,
description: question.description,
language,
code
}
})
});
if (!response.ok) throw new Error('Failed to get hint');
const data = await response.json();
setChatMessages([...newMessages, { role: 'assistant' as const, content: data.message }]);
} catch (error) {
console.warn('AI chat service unavailable, showing fallback message');
setChatMessages([...newMessages, { role: 'assistant' as const, content: "I'm having trouble connecting right now. Please try again in a moment." }]);
} finally {
setIsAiLoading(false);
}
};
const openAiChat = () => {
setChatPosition((position) => position ?? getDefaultChatPosition());
setIsChatOpen(true);
if (chatMessages.length === 0) {
setChatMessages([
{ role: 'assistant', content: `I can help with hints for "${question.title}".` }
]);
}
};
const closeAiChat = () => {
chatDragRef.current = null;
setIsDraggingChat(false);
setIsChatOpen(false);
};
const handleChatDragStart = (event: React.PointerEvent<HTMLDivElement>) => {
if ((event.target as HTMLElement).closest('button, input, textarea')) {
return;
}
const position = chatPosition ?? getDefaultChatPosition();
setChatPosition(position);
chatDragRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
originX: position.x,
originY: position.y,
};
setIsDraggingChat(true);
event.currentTarget.setPointerCapture(event.pointerId);
};
const handleChatDragMove = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = chatDragRef.current;
if (!drag || drag.pointerId !== event.pointerId) {
return;
}
setChatPosition(clampChatPosition({
x: drag.originX + event.clientX - drag.startX,
y: drag.originY + event.clientY - drag.startY,
}));
};
const handleChatDragEnd = (event: React.PointerEvent<HTMLDivElement>) => {
const drag = chatDragRef.current;
if (!drag || drag.pointerId !== event.pointerId) {
return;
}
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
chatDragRef.current = null;
setIsDraggingChat(false);
};
const handleComplexityAnalysis = () => {
const report = analyzeCodeComplexity(code, language, question);
setComplexityReport(report);
setBottomTab('analysis');
};
const selectedLanguageOption = getLanguageOption(language);
const selectedLanguageLabel = selectedLanguageOption.label;
const selectedSolutionCode = getSolutionCode(question, language);
const companies = (question.companies ?? []).filter(Boolean);
const askedByText = descriptionSections.askedBy || companies.join(', ');
const visibleCompanies = companies.slice(0, 3);
const extraCompanyCount = Math.max(companies.length - visibleCompanies.length, 0);
const visibleTestCases = question.testCases.slice(0, 4);
const activeTestCase = visibleTestCases[Math.min(activeTestCaseIndex, Math.max(visibleTestCases.length - 1, 0))];
const activeDescriptionExample = descriptionSections.examples[activeTestCaseIndex];
const activeTestCaseExplanation = activeTestCase
? activeDescriptionExample?.explanation
|| buildTestCaseExplanation(question.title, question.category, question.description, activeTestCase)
: '';
const passedCount = results?.filter((result) => result.passed).length ?? 0;
const resultHasFailure = Boolean(results?.some((result) => !result.passed));
const difficultyClass =
question.difficulty === 'Easy'
? 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20'
: question.difficulty === 'Medium'
? 'bg-amber-500/15 text-amber-300 border-amber-500/20'
: 'bg-rose-500/15 text-rose-400 border-rose-500/20';
const renderCompactResults = () => {
if (isRunning || isSubmitting) {
return (
<div className="flex h-full min-h-[150px] flex-col items-center justify-center text-zinc-500">
<Loader2 size={28} className="mb-3 animate-spin text-emerald-500" />
<p className="text-sm font-semibold">{output}</p>
</div>
);
}
if (!output && !results && !customOutput) {
return (
<div className="flex h-full min-h-[150px] flex-col items-center justify-center text-center text-zinc-500">
<Terminal size={34} className="mb-3 opacity-30" />
<p className="text-sm">You must run your code first</p>
</div>
);
}
return (
<div className="space-y-3">
{output && (
<div
className={cn(
'flex items-start gap-3 rounded-md border px-4 py-3 text-sm',
resultHasFailure || output.startsWith('Error') || output.includes('Wrong')
? 'border-rose-500/25 bg-rose-500/10 text-rose-200'
: 'border-emerald-500/20 bg-emerald-500/10 text-emerald-200'
)}
>
{resultHasFailure || output.startsWith('Error') || output.includes('Wrong') ? (
<XCircle size={18} className="mt-0.5 shrink-0 text-rose-400" />
) : (
<CheckCircle2 size={18} className="mt-0.5 shrink-0 text-emerald-400" />
)}
<div>
<div className="font-semibold">{output}</div>
{results && (
<div className="mt-1 text-xs opacity-70">
{passedCount}/{results.length} cases passed
</div>
)}
</div>
</div>
)}
{results?.map((result, index) => (
<div key={index} className="rounded-md border border-zinc-800 bg-[#262626] p-3 text-xs">
<div className="mb-3 flex items-center justify-between">
<span className="font-semibold text-zinc-300">Case {index + 1}</span>
<span className={cn('font-semibold', result.passed ? 'text-emerald-400' : 'text-rose-400')}>
{result.passed ? 'Accepted' : 'Wrong Answer'}
</span>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div>
<div className="mb-1 text-[10px] font-bold uppercase tracking-widest text-zinc-500">Input</div>
<pre className="max-h-24 overflow-auto rounded bg-[#1a1a1a] p-2 text-zinc-300">{result.input}</pre>
</div>
<div>
<div className="mb-1 text-[10px] font-bold uppercase tracking-widest text-zinc-500">Expected</div>
<pre className="max-h-24 overflow-auto rounded bg-[#1a1a1a] p-2 text-zinc-300">{result.expected}</pre>
</div>
<div>
<div className="mb-1 text-[10px] font-bold uppercase tracking-widest text-zinc-500">Output</div>
<pre className={cn('max-h-24 overflow-auto rounded p-2', result.passed ? 'bg-emerald-500/10 text-emerald-300' : 'bg-rose-500/10 text-rose-300')}>
{result.actual}
{result.error && `\n${result.error}`}
</pre>
</div>
</div>
</div>
))}
{customOutput && (
<pre className={cn('max-h-36 overflow-auto whitespace-pre-wrap rounded-md border border-zinc-800 bg-[#1a1a1a] p-3 text-sm', customOutput.startsWith('Error:') ? 'text-rose-300' : 'text-emerald-300')}>
{customOutput}
</pre>
)}
</div>
);
};
const renderComplexityAnalysis = () => {
if (!complexityReport) {
return (
<div className="flex min-h-[160px] flex-col items-center justify-center text-center text-zinc-500">
<BarChart3 size={36} className="mb-3 opacity-35" />
<p className="text-sm font-semibold text-zinc-300">Write code, then click Analysis.</p>
<p className="mt-1 max-w-md text-xs leading-5">
The analyzer reads your current editor code and estimates runtime from visible loops, recursion, sorting, and allocated data structures.
</p>
</div>
);
}
return (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-md border border-emerald-500/20 bg-emerald-500/10 p-3">
<div className="mb-2 flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-emerald-300/80">
<Gauge size={14} /> Time
</div>
<div className="font-mono text-2xl font-black text-emerald-200">{complexityReport.time}</div>
</div>
<div className="rounded-md border border-cyan-500/20 bg-cyan-500/10 p-3">
<div className="mb-2 flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-cyan-300/80">
<Layers3 size={14} /> Space
</div>
<div className="font-mono text-2xl font-black text-cyan-100">{complexityReport.space}</div>
</div>
<div className="rounded-md border border-violet-500/20 bg-violet-500/10 p-3">
<div className="mb-2 flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-violet-300/80">
<BarChart3 size={14} /> Confidence
</div>
<div className="text-2xl font-black text-violet-100">{complexityReport.confidence}</div>
</div>
</div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(260px,0.8fr)]">
<div className="rounded-md border border-[#333] bg-[#181818] p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<div>
<div className="text-sm font-semibold text-white">Big O Graph</div>
<p className="mt-1 text-xs text-zinc-500">Calculated growth curve for the detected estimate</p>
</div>
<Badge variant="outline" className="rounded-full border-violet-500/20 bg-violet-500/10 px-2.5 py-1 font-mono text-[11px] text-violet-200">
{complexityReport.time}
</Badge>
</div>
<div className="h-64 overflow-hidden rounded-md bg-white p-3">
<ComplexityGrowthChart complexity={complexityReport.time} theme="light" />
</div>
<p className="mt-3 text-xs leading-5 text-zinc-400">
Plotted from the calculated estimate {complexityReport.time}; values are normalized so the growth shape stays readable.
</p>
</div>
<div className="space-y-3 rounded-md border border-[#333] bg-[#181818] p-4">
<div>
<div className="text-sm font-semibold text-white">Code Signals</div>
<p className="mt-2 text-sm leading-6 text-zinc-300">{complexityReport.summary}</p>
</div>
<div className="space-y-2">
{complexityReport.signals.map((signal) => (
<div key={signal} className="flex items-start gap-2 rounded-md bg-[#232323] px-3 py-2 text-xs text-zinc-300">
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-emerald-400" />
<span>{signal}</span>
</div>
))}
</div>
<div className="rounded-md border border-amber-500/15 bg-amber-500/10 px-3 py-2 text-xs leading-5 text-amber-100/80">
This is a static estimate from your code shape, so unusual library calls or hidden input constraints may need manual review.
</div>
</div>
</div>
</div>
);
};
return (
<div className="relative flex h-screen flex-col overflow-hidden bg-[#0f0f0f] text-zinc-100">
<div className="pointer-events-none fixed inset-0 z-0 bg-[linear-gradient(180deg,#111_0%,#0b0b0b_100%)]" />
{/* Header */}
<header className="z-10 h-14 shrink-0 border-b border-[#2d2d2d] bg-[#111] px-3 shadow-lg">
<div className="flex h-full flex-wrap items-center justify-between gap-3">
<div className="flex min-w-0 flex-wrap items-center gap-3 sm:gap-4">
<Button variant="ghost" size="sm" onClick={onBack} className="h-9 rounded-md px-2 text-zinc-400 hover:bg-[#2a2a2a] hover:text-white">
<ChevronLeft size={18} className="mr-1" /> Back
</Button>
<div className="hidden h-6 w-px bg-[#333] sm:block" />
<h2 className="min-w-0 max-w-full truncate text-base font-semibold tracking-tight text-zinc-100">{question.title}</h2>
<Badge variant="outline" className={cn("shrink-0 rounded-full px-2.5 py-1 text-[11px] font-bold uppercase tracking-wide", difficultyClass)}>
{question.difficulty}
</Badge>
</div>
<div className="flex flex-wrap items-center justify-end gap-2 sm:gap-3">
<select
value={language}
onChange={(e) => setLanguage(e.target.value as Language)}
className="h-10 min-w-[130px] rounded-full border border-[#333] bg-[#0b0b0b] px-4 py-1.5 text-sm font-semibold text-white transition-colors focus:outline-none focus:ring-1 focus:ring-emerald-500"
>
{availableLanguages.map((option) => (
<option key={option.value} value={option.value} className="bg-[#0c0c0c]">{option.label}</option>
))}
</select>
<Button
variant="outline"
size="sm"
onClick={() => {
setCode(getStarterCode(question, language));
setComplexityReport(null);
}}
className="h-10 rounded-full border-[#333] bg-[#1f1f1f] px-4 text-zinc-300 hover:bg-[#2b2b2b] hover:text-white"
>
<RotateCcw size={14} className="mr-2" /> Reset
</Button>
<Button
variant="outline"
size="sm"
onClick={handleComplexityAnalysis}
disabled={!code.trim() || tier === 'free'}
className="h-10 rounded-full border-violet-500/25 bg-violet-500/15 px-4 font-bold text-violet-100 hover:border-violet-400/50 hover:bg-violet-500/25 hover:text-white"
>
<BarChart3 size={14} className="mr-2" />
{tier === 'free' ? <><Lock size={12} className="mr-1 inline" /> Pro Only</> : 'Analysis'}
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleRun}
disabled={isRunning || isSubmitting}
className="h-10 min-w-[84px] rounded-full bg-[#2a2a2a] px-5 font-bold text-white hover:bg-[#3a3a3a]"
>
{isRunning ? (
<Loader2 size={14} className="mr-2 animate-spin" />
) : (
<Play size={14} className="mr-2" />
)}
Run
</Button>
<Button
size="sm"
className="h-10 min-w-[112px] rounded-full bg-emerald-600 px-5 font-bold text-white shadow-lg shadow-emerald-600/20 hover:bg-emerald-500"
onClick={handleSubmit}
disabled={isRunning || isSubmitting}
>
{isSubmitting ? (
<Loader2 size={16} className="mr-2 animate-spin" />
) : (
<Send size={16} className="mr-2" />
)}
Submit
</Button>
</div>
</div>
</header>
{/* Main Content */}
<div className="relative z-10 min-h-0 flex-1 overflow-hidden p-3">
<div className="flex h-full min-h-0 flex-col gap-3 overflow-hidden xl:flex-row xl:gap-0">
{/* Left Panel: Description */}
<div
style={{ width: typeof window !== 'undefined' && window.innerWidth >= 1280 ? leftPanelResizer.size : undefined }}
className="flex h-full min-h-[320px] min-w-0 flex-col overflow-hidden rounded-lg border border-[#303030] bg-[#1f1f1f] shadow-2xl xl:min-h-0"
>
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex h-full min-h-0 flex-col">
<TabsList className="h-11 shrink-0 justify-start gap-1 overflow-x-auto rounded-none border-b border-[#303030] bg-[#2a2a2a] px-2 scrollbar-auto-hide">
<TabsTrigger value="description" className="h-8 shrink-0 rounded-md px-3 text-sm font-semibold text-zinc-400 transition-all data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<Info size={14} className="mr-2" /> Description
</TabsTrigger>
<TabsTrigger value="discussion" className="h-8 shrink-0 rounded-md px-3 text-sm font-semibold text-zinc-400 transition-all data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<MessageSquare size={14} className="mr-2" /> Discussion
</TabsTrigger>
<TabsTrigger value="solutions" className="h-8 shrink-0 rounded-md px-3 text-sm font-semibold text-zinc-400 transition-all data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<Code2 size={14} className="mr-2" /> Solutions
</TabsTrigger>
<TabsTrigger value="submissions" className="h-8 shrink-0 rounded-md px-3 text-sm font-semibold text-zinc-400 transition-all data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<History size={14} className="mr-2" /> Submissions
</TabsTrigger>
</TabsList>
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain custom-scrollbar [scrollbar-gutter:stable]">
<TabsContent value="description" className="mt-0 min-h-full space-y-8 px-6 py-6 pb-10">
<section className="space-y-5">
<h1 className="text-[28px] font-semibold leading-tight tracking-normal text-white sm:text-[32px]">{question.title}</h1>
<Button
type="button"
onClick={openAiChat}
className="h-10 rounded-lg border border-[#343434] bg-[#0f1117] px-3.5 text-sm font-semibold text-white shadow-none hover:bg-[#191b22]"
>
<Sparkles size={16} className="mr-2 text-emerald-300" />
Discuss Approach
<ChevronRight size={16} className="ml-1.5 text-zinc-400" />
</Button>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className={cn('rounded-full border px-2.5 py-1 text-xs font-semibold', difficultyClass)}>
{question.difficulty}
</Badge>
<Badge variant="outline" className="rounded-full border-[#3a3a3a] bg-[#2b2b2b] px-2.5 py-1 text-xs font-medium text-zinc-200">
<Tag size={13} className="mr-1.5" /> {question.category}
</Badge>
{companies.length > 0 && (
<Badge variant="outline" className="rounded-full border-[#3a3a3a] bg-[#2b2b2b] px-2.5 py-1 text-xs font-medium text-amber-300">
<BriefcaseBusiness size={13} className="mr-1.5" />
{visibleCompanies.join(', ')}
{extraCompanyCount > 0 ? ` +${extraCompanyCount}` : ''}
</Badge>
)}
{descriptionSections.hint && (
<Badge variant="outline" className="rounded-full border-[#3a3a3a] bg-[#2b2b2b] px-2.5 py-1 text-xs font-medium text-zinc-200">
<Lightbulb size={13} className="mr-1.5 text-amber-300" /> Hint
</Badge>
)}
</div>
<div className="space-y-4 text-[16px] leading-7 text-zinc-100">
{(descriptionSections.statement.length ? descriptionSections.statement : [question.description]).map((paragraph, index) => (
<p key={index}>{renderInlineCode(paragraph)}</p>
))}
</div>
</section>
{descriptionSections.examples.length > 0 && (
<section className="space-y-6">
{descriptionSections.examples.map((example, index) => (
<div key={`${example.input}-${index}`} className="space-y-3">
<h3 className="text-[15px] font-semibold text-white">Example {index + 1}:</h3>
<div className="space-y-1 border-l-2 border-[#3f3f46] pl-4 font-mono text-[13px] leading-6 text-zinc-300">
<div className="whitespace-pre-wrap break-words">
<span className="font-semibold text-white">Input:</span> {example.input}
</div>
<div className="whitespace-pre-wrap break-words">
<span className="font-semibold text-white">Output:</span> {example.output}
</div>
{(example.explanation || example.output) && (
<div className="whitespace-pre-wrap break-words">
<span className="font-semibold text-white">Explanation:</span>{' '}
{renderInlineCode(example.explanation || `The expected output is \`${example.output}\`.`)}
</div>
)}
</div>
</div>
))}
</section>
)}
{descriptionSections.constraints.length > 0 && (
<section className="space-y-3">
<h2 className="text-base font-semibold text-white">Constraints:</h2>
<ul className="space-y-3 text-[15px] leading-7 text-zinc-100">
{descriptionSections.constraints.map((constraint) => (
<li key={constraint} className="ml-5 list-disc marker:text-zinc-300">
<span>{renderInlineCode(constraint)}</span>
</li>
))}
</ul>
</section>
)}
{(descriptionSections.hint || askedByText) && (
<section className="space-y-4 border-t border-[#303030] pt-5">
{descriptionSections.hint && (
<div>
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-white">
<Lightbulb size={15} className="text-amber-300" /> Hint
</div>
<p className="text-sm leading-6 text-zinc-300">{renderInlineCode(descriptionSections.hint)}</p>
</div>
)}
{askedByText && (
<div>
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-white">
<BriefcaseBusiness size={15} className="text-amber-300" /> Companies
</div>
<p className="text-sm leading-6 text-zinc-300">{askedByText}</p>
</div>
)}
</section>
)}
</TabsContent>
<TabsContent value="discussion" className="mt-0 min-h-full px-6 py-6 pb-10">
<div className="space-y-6">
{/* ── Group Chat Box ────────────────────────────── */}
<div className="overflow-hidden rounded-xl border border-purple-500/20 bg-[#14142a]">
{/* Chat Header */}
<div className="flex items-center gap-2 border-b border-purple-500/15 bg-purple-500/5 px-4 py-3">
<MessageSquare size={16} className="text-purple-400" />
<span className="text-sm font-bold text-white">Group Chat</span>
<span className="ml-auto flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-purple-400/60">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />
Live
</span>
</div>
{/* Messages Area */}
<div className="h-[280px] overflow-y-auto custom-scrollbar px-4 py-3 space-y-3">
{isChatLoading ? (
<div className="flex h-full items-center justify-center">
<Loader2 size={24} className="animate-spin text-purple-500" />
</div>
) : groupChatMsgs.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center text-center">
<MessageSquare size={36} className="mb-3 text-zinc-700" />
<p className="text-sm font-semibold text-zinc-500">No messages yet</p>
<p className="text-xs text-zinc-600">Start the conversation about this problem!</p>
</div>
) : (
<>
{groupChatMsgs.map((msg) => {
const isOwn = msg.userId === currentUserId;
return (
<div key={msg.id} className={cn('flex gap-2.5 group', isOwn ? 'flex-row-reverse' : 'flex-row')}>
{/* Avatar */}
<div className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-black',
isOwn ? 'bg-emerald-500/20 text-emerald-400' : 'bg-purple-500/20 text-purple-400'
)}>
{(msg.username || 'U').charAt(0).toUpperCase()}
</div>
{/* Bubble */}
<div className={cn(
'max-w-[75%] rounded-2xl px-3.5 py-2 text-sm leading-relaxed',
isOwn
? 'rounded-br-md bg-emerald-500/15 text-emerald-100 border border-emerald-500/20'
: 'rounded-bl-md bg-[#1e1e3a] text-zinc-200 border border-purple-500/10'
)}>
<div className="flex items-center gap-2 mb-0.5">
<span className={cn('text-[11px] font-bold', isOwn ? 'text-emerald-400' : 'text-purple-400')}>
{msg.username || 'User'}
</span>
<span className="text-[10px] text-zinc-600">{timeAgo(msg.createdAt)}</span>
</div>
<p className="text-[13px] break-words">{msg.message}</p>
{isOwn && (
<button
onClick={() => deleteGroupChatMsg(msg.id)}
className="mt-1 flex items-center gap-1 text-[10px] text-zinc-600 opacity-0 transition-all group-hover:opacity-100 hover:text-rose-400"
>
<Trash2 size={10} /> Delete
</button>
)}
</div>
</div>
);
})}
<div ref={groupChatEndRef} />
</>
)}
</div>
{/* Input Bar */}
<div className="flex items-center gap-2 border-t border-purple-500/15 bg-[#0f0f22] px-3 py-2.5">
<input
type="text"
value={chatMsgInput}
onChange={(e) => setChatMsgInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendGroupChatMsg(); } }}
placeholder="Type a message..."
maxLength={500}
className="flex-1 rounded-lg border border-zinc-800 bg-zinc-950/60 px-3 py-2 text-sm text-white placeholder-zinc-600 outline-none transition-all focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/30"
/>
<button
onClick={sendGroupChatMsg}
disabled={!chatMsgInput.trim() || isChatSending}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-purple-500/80 text-white transition-all hover:bg-purple-500 disabled:opacity-30 disabled:hover:bg-purple-500/80"
>
{isChatSending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
</button>
</div>
</div>
{/* ── Divider ───────────────────────────────────── */}
<div className="flex items-center gap-3">
<div className="h-px flex-1 bg-zinc-800" />
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-600">Accepted Solutions</span>
<div className="h-px flex-1 bg-zinc-800" />
</div>
{/* ── Existing Discussion Posts ─────────────────── */}
{isDiscussionsLoading ? (
<div className="flex flex-col items-center justify-center py-16">
<Loader2 size={36} className="mb-4 animate-spin text-purple-500" />
<p className="text-sm font-medium text-zinc-500">Loading discussions…</p>
</div>
) : discussions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 text-center">
<Code2 size={36} className="mb-3 text-zinc-700" />
<p className="text-sm font-bold text-zinc-500">No accepted solutions yet</p>
<p className="mt-1 text-xs text-zinc-600">Be the first to solve it and share your approach!</p>
</div>
) : (
<>
{discussions.map((post) => {
const isExpanded = expandedCards.has(post.id);
const codeLines = (post.code || '').split('\n');
const shouldCollapse = codeLines.length > 10;
const displayCode = isExpanded ? post.code : codeLines.slice(0, 10).join('\n') + (shouldCollapse ? '\n// ...' : '');
const isUpvoted = (post.upvotedBy || []).includes(currentUserId);
const isOwn = post.userId === currentUserId;
return (
<div key={post.id} className="rounded-lg border border-[#2a2a3e] bg-[#1a1a2e] p-5 transition-all hover:border-[#3a3a5e]">
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-purple-500/20 text-sm font-black text-purple-400">
{(post.username || 'U').charAt(0).toUpperCase()}
</div>
<div>
<span className="text-sm font-bold text-white">{post.username || 'User'}</span>
<div className="flex items-center gap-1 text-xs text-zinc-500">
<Clock size={11} /> {timeAgo(post.createdAt)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="rounded-full border-purple-500/30 bg-purple-500/10 px-2 py-0.5 text-[11px] font-bold text-purple-300">
{post.language}
</Badge>
{post.executionTime && post.executionTime !== '—' && (
<span className="flex items-center gap-1 text-[11px] text-emerald-400">
<Zap size={11} /> {post.executionTime}
</span>
)}
{post.memoryUsed && post.memoryUsed !== '—' && (
<span className="flex items-center gap-1 text-[11px] text-sky-400">
<HardDrive size={11} /> {post.memoryUsed}
</span>
)}
</div>
</div>
{/* Code Block */}
<div className="rounded-md border border-[#2a2a3e] bg-[#0f0f1a] overflow-hidden">
<pre className="overflow-x-auto p-4 font-mono text-[13px] leading-relaxed text-zinc-300">
<code>{displayCode}</code>
</pre>
{shouldCollapse && (
<button
onClick={() => {
setExpandedCards(prev => {
const next = new Set(prev);
if (next.has(post.id)) next.delete(post.id);
else next.add(post.id);
return next;
});
}}
className="flex w-full items-center justify-center gap-1 border-t border-[#2a2a3e] py-2 text-xs font-medium text-purple-400 transition-colors hover:bg-purple-500/5 hover:text-purple-300"
>
{isExpanded ? <><ChevronUp size={14} /> Show less</> : <><ChevronDown size={14} /> Show all {codeLines.length} lines</>}
</button>
)}
</div>
{/* Approach */}
{post.approach && (
<div className="mt-3 rounded-md border border-[#2a2a3e] bg-[#12122a] p-3">
<p className="mb-1 text-[10px] font-black uppercase tracking-widest text-zinc-500">Approach</p>
<p className="text-sm leading-relaxed text-zinc-300">{post.approach}</p>
</div>
)}
{/* Footer: upvote + delete */}
<div className="mt-4 flex items-center justify-between">
<button
onClick={() => toggleUpvote(post.id)}
className={cn(
'flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-bold transition-all',
isUpvoted
? 'border-purple-500/40 bg-purple-500/15 text-purple-300'
: 'border-[#2a2a3e] bg-transparent text-zinc-500 hover:border-purple-500/30 hover:text-purple-400'
)}
>
<ThumbsUp size={13} className={isUpvoted ? 'fill-purple-400' : ''} />
{post.upvotes || 0}
</button>
{isOwn && (
<button
onClick={() => deleteDiscussion(post.id)}
className="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-zinc-600 transition-colors hover:bg-rose-500/10 hover:text-rose-400"
>
<Trash2 size={13} /> Delete
</button>
)}
</div>
</div>
);
})}
{/* Pagination */}
{discussionTotalPages > 1 && (
<div className="flex items-center justify-center gap-3 pt-4">
<button
disabled={discussionPage <= 1}
onClick={() => fetchDiscussions(discussionPage - 1)}
className="rounded-md border border-[#333] bg-[#262626] px-3 py-1.5 text-xs font-bold text-zinc-400 transition-colors hover:text-white disabled:opacity-40"
>
← Prev
</button>
<span className="text-xs text-zinc-500">
Page {discussionPage} of {discussionTotalPages}
</span>
<button
disabled={discussionPage >= discussionTotalPages}
onClick={() => fetchDiscussions(discussionPage + 1)}
className="rounded-md border border-[#333] bg-[#262626] px-3 py-1.5 text-xs font-bold text-zinc-400 transition-colors hover:text-white disabled:opacity-40"
>
Next →
</button>
</div>
)}
</>
)}
</div>
</TabsContent>
<TabsContent value="submissions" className="mt-0 min-h-full px-6 py-6 pb-10">
<div className="space-y-5">
{isRunning || isSubmitting ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-500">
<Loader2 size={40} className="mb-4 animate-spin text-emerald-500" />
<p className="text-sm font-bold tracking-wide">{output}</p>
</div>
) : (
<>
{output && (
<pre className={cn(
"whitespace-pre-wrap p-5 rounded-[16px] border text-sm font-mono shadow-xl",
(output.includes('Accepted') || output.includes('passed') || output.includes('executed'))
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
: "bg-rose-500/10 border-rose-500/20 text-rose-400"
)}>
{output}
</pre>
)}
{results && (
<div className="space-y-4">
<div className="text-[10px] font-black text-slate-500 uppercase tracking-[0.2em]">Test Case Results</div>
{results.map((res, i) => (
<div key={i} className={`p-5 rounded-[16px] border shadow-xl ${res.passed ? 'bg-white/5 border-white/10' : 'bg-rose-500/5 border-rose-500/20'}`}>
<div className="flex items-center justify-between mb-4 border-b border-white/5 pb-3">
<span className="text-[10px] font-black tracking-widest text-slate-400">CASE {i + 1}</span>
{res.passed ? (
<span className="text-[10px] font-black tracking-widest text-emerald-500 bg-emerald-500/10 px-2 py-1 rounded-md">PASSED</span>
) : (
<span className="text-[10px] font-black tracking-widest text-rose-500 bg-rose-500/10 px-2 py-1 rounded-md">FAILED</span>
)}
</div>
<div className="grid grid-cols-2 gap-4 text-[11px] font-mono">
<div>
<div className="text-slate-500 font-bold tracking-wider mb-1 uppercase">Input</div>
<div className="whitespace-pre-wrap break-words text-slate-300 bg-black/40 px-2 py-1 rounded-md">{res.input}</div>
</div>
<div>
<div className="text-slate-500 font-bold tracking-wider mb-1 uppercase">Expected</div>
<div className="whitespace-pre-wrap break-words text-slate-300 bg-black/40 px-2 py-1 rounded-md">{res.expected}</div>
</div>
<div className="col-span-2">
<div className="text-slate-500 font-bold tracking-wider mb-1 uppercase">Actual</div>
<div className={cn("whitespace-pre-wrap break-words px-2 py-1 rounded-md", res.passed ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400")}>
{res.actual}
{res.error && <div className="text-rose-500 mt-2 font-sans font-medium">{res.error}</div>}
</div>
</div>
</div>
</div>
))}
</div>
)}
{!output && !results && (
<div className="flex flex-col items-center justify-center py-16 text-slate-500 text-center">
<Terminal size={48} className="mb-4 opacity-20" />
<p className="text-sm font-bold tracking-wide">Run code to see results.</p>
</div>
)}
</>
)}
</div>
</TabsContent>
<TabsContent value="solutions" className="mt-0 min-h-full px-6 py-6 pb-10">
<div className="rounded-md border border-[#333] bg-[#262626] p-5">
<h3 className="mb-4 text-lg font-semibold text-white">Optimal Solution ({selectedLanguageLabel})</h3>
<pre className="overflow-x-auto rounded-md border border-[#333] bg-[#1a1a1a] p-4 font-mono text-[13px] leading-relaxed text-emerald-300">
{selectedSolutionCode || "Solution not available for this language."}
</pre>
<div className="mt-4 rounded-md border border-[#333] bg-[#1f1f1f] p-4 text-sm leading-7 text-zinc-300">
<p className="mb-2 text-xs font-bold uppercase tracking-widest text-zinc-500">Notes</p>
{question.solution.explanation}
</div>
</div>
</TabsContent>
</div>
</Tabs>
</div>
{/* Vertical Resizer Handle */}
<div
onMouseDown={leftPanelResizer.onMouseDown}
className={cn(
"group relative hidden w-1.5 cursor-col-resize shrink-0 transition-all hover:w-2 xl:block",
leftPanelResizer.isDragging ? "bg-emerald-500/40 w-2" : "bg-transparent hover:bg-emerald-500/20"
)}
>
<div className="absolute left-1/2 top-1/2 h-8 w-1 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-600/50 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
{/* Right Panel: Editor & Testcases */}
<div className="relative flex min-h-[420px] flex-1 min-w-0 flex-col gap-3 overflow-hidden xl:min-h-0">
{/* Editor Box */}
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-[#303030] bg-[#1f1f1f] shadow-2xl">
<div className="flex h-11 shrink-0 items-center justify-between border-b border-[#303030] bg-[#2a2a2a] px-3">
<div className="flex items-center gap-2 text-sm font-semibold text-white">
<Code2 size={17} className="text-emerald-400" />
Code
</div>
<div className="flex items-center gap-3 text-sm text-zinc-400">
<span className="font-medium text-zinc-200">{selectedLanguageLabel}</span>
<span className="hidden h-4 w-px bg-[#444] sm:block" />
<span className="hidden text-xs sm:inline">Saved</span>
</div>
</div>
<div className="relative min-h-0 flex-1">
<Editor
height="100%"
theme="vs-dark"
language={selectedLanguageOption.monacoLanguage}
value={code}
onChange={(value) => {
setCode(value || '');
setComplexityReport(null);
}}
onMount={(editorInstance) => {
editorRef.current = editorInstance;
editorInstance.layout();
}}
options={{
fontSize: getCodeEditorFontSize(fontSize),
lineHeight: 21,
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
fontLigatures: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 14, bottom: 16 },
smoothScrolling: true,
cursorSmoothCaretAnimation: 'on',
cursorBlinking: 'smooth',
renderLineHighlight: 'all',
renderWhitespace: 'selection',
stickyScroll: { enabled: true },
scrollbar: {
vertical: 'auto',
horizontal: 'auto',
verticalScrollbarSize: 14,
horizontalScrollbarSize: 14,
useShadows: false,
},
lineNumbers: 'on',
lineNumbersMinChars: 3,
wordWrap: 'on',
wordWrapColumn: 80,
wrappingIndent: 'same',
folding: true,
foldingHighlight: true,
foldingStrategy: 'auto',
showFoldingControls: 'always',
bracketPairColorization: { enabled: true },
guides: {
bracketPairs: true,
indentation: true,
highlightActiveIndentation: true,
},
hover: { enabled: true },
suggest: {
showKeywords: true,
showSnippets: true,
showFunctions: true,
showConstructors: true,
showFields: true,
showVariables: true,
showClasses: true,
showStructs: true,
showInterfaces: true,
showModules: true,
showProperties: true,
showEvents: true,
showOperators: true,
showUnits: true,
showValues: true,
showConstants: true,
showEnums: true,
showEnumMembers: true,
showColors: true,
showFiles: true,
showReferences: true,
showFolders: true,
showTypeParameters: true,
showUsers: true,
showIssues: true,
},
quickSuggestions: {
other: true,
comments: false,
strings: true,
},
acceptSuggestionOnCommitCharacter: true,
acceptSuggestionOnEnter: 'on',
tabCompletion: 'on',
parameterHints: {
enabled: true,
cycle: true,
},
autoClosingBrackets: 'always',
autoClosingQuotes: 'always',
autoSurround: 'languageDefined',
formatOnPaste: true,
formatOnType: true,
autoIndent: 'full',
matchBrackets: 'always',
autoClosingDelete: 'auto',
trimAutoWhitespace: true,
renderControlCharacters: false,
contextmenu: true,
mouseWheelZoom: true,
multiCursorModifier: 'ctrlCmd',
selectionHighlight: true,
occurrencesHighlight: 'multiFile',
}}
/>
{/* AI Assistant Button */}
<div className="absolute bottom-4 left-4 z-20">
<Button
onClick={openAiChat}
className="h-11 rounded-lg border border-[#3a3a3a] bg-[#111] px-4 font-semibold text-white shadow-lg hover:bg-[#2a2a2a]"
>
<Sparkles size={18} className="mr-2" />
Help in coding
</Button>
</div>
{/* AI Chat Interface */}
<AnimatePresence>
{isChatOpen && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
style={{
left: chatPosition?.x ?? 24,
top: chatPosition?.y ?? 24,
width: 'min(24rem, calc(100vw - 2rem))',
height: 'min(32rem, calc(100vh - 6rem))',
}}
className={cn(
'fixed z-[80] flex max-w-full flex-col overflow-hidden rounded-2xl border border-zinc-800 bg-zinc-900 shadow-2xl',
isDraggingChat && 'border-emerald-500/30 shadow-emerald-500/10',
)}
>
{/* Chat Header */}
<div
onPointerDown={handleChatDragStart}
onPointerMove={handleChatDragMove}
onPointerUp={handleChatDragEnd}
onPointerCancel={handleChatDragEnd}
className={cn(
'flex touch-none select-none items-center justify-between border-b border-zinc-800 bg-zinc-900/70 p-4 cursor-grab active:cursor-grabbing',
isDraggingChat && 'bg-zinc-900',
)}
title="Drag to move"
>
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-500">
<Bot size={18} />
</div>
<div>
<div className="text-sm font-bold text-white">AI Mentor</div>
<div className="text-[10px] text-emerald-500 flex items-center gap-1">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" /> Online
</div>
</div>
</div>
<GripHorizontal size={16} className="mx-2 shrink-0 text-zinc-600" />
<button
onClick={closeAiChat}
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-zinc-700 bg-zinc-800 text-zinc-300 transition-colors hover:border-rose-500/50 hover:bg-rose-500/15 hover:text-rose-100"
title="Close chat"
aria-label="Close chat"
>
<X size={18} />
</button>
</div>
{/* Chat Messages */}
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{chatMessages.map((msg, i) => (
<div
key={i}
className={cn(
"flex gap-3 max-w-[85%]",
msg.role === 'user' ? "ml-auto flex-row-reverse" : "mr-auto"
)}
>
<div className={cn(
"w-7 h-7 rounded-full flex items-center justify-center shrink-0",
msg.role === 'user' ? "bg-indigo-500/10 text-indigo-400" : "bg-emerald-500/10 text-emerald-400"
)}>
{msg.role === 'user' ? <UserIcon size={14} /> : <Bot size={14} />}
</div>
<div className={cn(
"p-3 rounded-2xl text-sm leading-relaxed",
msg.role === 'user'
? "bg-indigo-600 text-white rounded-tr-none"
: "bg-zinc-800 text-zinc-200 rounded-tl-none border border-zinc-700"
)}>
{msg.content}
</div>
</div>
))}
{isAiLoading && (
<div className="flex gap-3 mr-auto max-w-[85%]">
<div className="w-7 h-7 rounded-full bg-emerald-500/10 text-emerald-400 flex items-center justify-center shrink-0">
<Bot size={14} />
</div>
<div className="bg-zinc-800 text-zinc-200 p-3 rounded-2xl rounded-tl-none border border-zinc-700 flex gap-1">
<div className="w-1.5 h-1.5 bg-zinc-500 rounded-full animate-bounce" />
<div className="w-1.5 h-1.5 bg-zinc-500 rounded-full animate-bounce [animation-delay:0.2s]" />
<div className="w-1.5 h-1.5 bg-zinc-500 rounded-full animate-bounce [animation-delay:0.4s]" />
</div>
</div>
)}
</div>
</ScrollArea>
{/* Chat Input */}
<form onSubmit={sendChatMessage} className="p-4 border-t border-zinc-800 bg-zinc-900/50">
<div className="relative">
<input
type="text"
value={chatInput}
onChange={(e) => setChatInput(e.target.value)}
placeholder="Ask for a hint..."
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl py-2.5 pl-4 pr-12 text-sm text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all"
/>
<button
type="submit"
disabled={!chatInput.trim() || isAiLoading}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:hover:bg-emerald-600 text-white rounded-lg transition-all"
>
<Send size={16} />
</button>
</div>
<div className="mt-2 text-[10px] text-zinc-500 text-center">
Hints only.
</div>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* Resizer Handle */}
<div
onMouseDown={bottomResizer.onMouseDown}
className={cn(
"group relative h-1.5 w-full cursor-ns-resize shrink-0 transition-all hover:h-2",
bottomResizer.isDragging ? "bg-emerald-500/40 h-2" : "bg-transparent hover:bg-emerald-500/20"
)}
>
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<div className="h-1 w-8 rounded-full bg-zinc-600/50" />
</div>
</div>
{/* Testcase / Result Panel */}
<div
style={{ height: bottomResizer.size }}
className="shrink-0 overflow-hidden rounded-lg border border-[#303030] bg-[#1f1f1f] shadow-2xl"
>
<Tabs value={bottomTab} onValueChange={(value) => setBottomTab(value as 'testcase' | 'result' | 'analysis')} className="flex h-full min-h-0 flex-col">
<div className="flex h-11 shrink-0 items-center justify-between border-b border-[#303030] bg-[#2a2a2a] px-2">
<TabsList className="h-9 justify-start gap-1 rounded-none bg-transparent p-0">
<TabsTrigger value="testcase" className="h-8 rounded-md px-3 text-sm font-semibold text-zinc-400 data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<ListChecks size={15} className="mr-2 text-emerald-400" /> Testcase
</TabsTrigger>
<TabsTrigger value="result" className="h-8 rounded-md px-3 text-sm font-semibold text-zinc-400 data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<Terminal size={15} className="mr-2 text-emerald-400" /> Test Result
</TabsTrigger>
<TabsTrigger value="analysis" className="h-8 rounded-md px-3 text-sm font-semibold text-zinc-400 data-[state=active]:bg-[#1f1f1f] data-[state=active]:text-white">
<BarChart3 size={15} className="mr-2 text-violet-400" /> Analysis
</TabsTrigger>
</TabsList>
<Button
variant="ghost"
size="sm"
onClick={() => setUseCustomInput((value) => !value)}
className={cn(
'h-8 rounded-md px-3 text-xs font-semibold',
useCustomInput ? 'bg-[#3a3a3a] text-white hover:bg-[#444]' : 'text-zinc-400 hover:bg-[#333] hover:text-white'
)}
>
Custom
</Button>
</div>
<TabsContent value="testcase" className="mt-0 min-h-0 flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="space-y-4 p-4">
<div className="flex flex-wrap gap-2">
{visibleTestCases.map((testCase, index) => (
<button
key={`${testCase.input}-${index}`}
type="button"
onClick={() => setActiveTestCaseIndex(index)}
className={cn(
'h-8 rounded-md px-3 text-sm font-semibold transition-colors',
activeTestCaseIndex === index
? 'bg-[#3a3a3a] text-white'
: 'bg-transparent text-zinc-400 hover:bg-[#303030] hover:text-zinc-100'
)}
>
Case {index + 1}
</button>
))}
</div>
{activeTestCase && (
<div className="space-y-3">
{formatInputFields(activeTestCase.input).map((field) => (
<div key={`${field.name}-${field.value}`} className="space-y-1.5">
<div className="text-xs font-semibold text-zinc-400">{field.name} =</div>
<pre className="max-h-24 min-h-10 overflow-auto whitespace-pre-wrap break-words rounded-md border border-[#343434] bg-[#262626] px-3 py-2 font-mono text-sm leading-6 text-zinc-100">
{field.value}
</pre>
</div>
))}
<div className="space-y-1.5">
<div className="text-xs font-semibold text-zinc-400">Expected</div>
<pre className="max-h-24 min-h-10 overflow-auto whitespace-pre-wrap break-words rounded-md border border-emerald-500/15 bg-emerald-500/10 px-3 py-2 font-mono text-sm leading-6 text-emerald-200">
{activeTestCase.output}
</pre>
</div>
<div className="space-y-1.5">
<div className="text-xs font-semibold text-zinc-400">Explanation</div>
<div className="rounded-md border border-[#343434] bg-[#262626] px-3 py-2 text-sm leading-6 text-zinc-300">
{renderInlineCode(activeTestCaseExplanation)}
</div>
</div>
</div>
)}
<AnimatePresence initial={false}>
{useCustomInput && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="grid gap-4 border-t border-[#333] pt-4 xl:grid-cols-[1.2fr_0.8fr]">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-semibold text-white">Custom Input</div>
<div className="text-[10px] font-bold uppercase tracking-widest text-zinc-500">{selectedLanguageLabel}</div>
</div>
<textarea
value={customInput}
onChange={(event) => setCustomInput(event.target.value)}
placeholder={buildDefaultCustomInput(question)}
className="h-32 w-full resize-none rounded-md border border-[#333] bg-[#181818] px-3 py-2 font-mono text-sm text-zinc-200 outline-none transition focus:border-emerald-500/60"
/>
<div className="flex flex-wrap items-center justify-between gap-3 text-[11px] text-zinc-500">
<span className="max-w-full break-words font-mono">Example: {customInputExample}</span>
<Button
size="sm"
onClick={handleCustomRun}
disabled={isRunning || isSubmitting}
className="rounded-md bg-[#333] font-bold text-white hover:bg-[#444]"
>
{isRunning ? <Loader2 size={16} className="mr-2 animate-spin" /> : <Play size={16} className="mr-2" />}
Run Custom
</Button>
</div>
</div>
<div className="space-y-3">
<div className="text-sm font-semibold text-white">Output</div>
<pre className={cn(
'h-32 overflow-auto whitespace-pre-wrap rounded-md border border-[#333] bg-[#181818] px-3 py-2 text-sm',
customOutput.startsWith('Error:') ? 'text-rose-300' : 'text-emerald-300'
)}>
{customOutput || 'No custom output yet.'}
</pre>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="result" className="mt-0 min-h-0 flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="p-4">
{renderCompactResults()}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="analysis" className="mt-0 min-h-0 flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="p-4">
{renderComplexityAnalysis()}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</div>
</div>
</div>
</div>
</div>
);
}