| 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 { |
| |
| } |
|
|
| 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); |
| } |
| |
| }, [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); } |
| }; |
|
|
| |
| const fetchGroupChat = async () => { |
| try { |
| const res = await apiFetch(`/api/chat?problemId=${encodeURIComponent(question.id)}&limit=50`); |
| if (res.ok) { |
| const data = await res.json(); |
| |
| 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); } |
| }; |
|
|
| |
| useEffect(() => { |
| if (activeTab === 'discussion') { |
| setIsChatLoading(true); |
| fetchGroupChat().finally(() => setIsChatLoading(false)); |
| groupChatPollRef.current = setInterval(fetchGroupChat, 8000); |
| } |
| return () => { |
| if (groupChatPollRef.current) clearInterval(groupChatPollRef.current); |
| }; |
| |
| }, [activeTab, question.id]); |
|
|
| |
| 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 { |
| |
| } |
|
|
| 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); |
| |
| 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> |
|
|
| {} |
| <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> |
|
|
| {} |
| <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> |
|
|
| {} |
| <div className="relative flex min-h-[420px] flex-1 min-w-0 flex-col gap-3 overflow-hidden xl:min-h-0"> |
|
|
| {} |
| <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> |
|
|
| {} |
| <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> |
|
|
| {} |
| <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> |
| ); |
| } |
|
|