diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..45c3e28 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +echo "🔧 Running pre-commit checks..." + +# Run comprehensive security check +echo "🔒 Checking for secrets..." +if ! ./scripts/check-secrets.sh; then + echo "❌ Security check failed!" + exit 1 +fi + +# Run basic formatting on staged files only +echo "💅 Formatting staged files..." +STAGED_FILES=$(git diff --cached --name-only) +if [ -n "$STAGED_FILES" ]; then + npx prettier --write --ignore-unknown $STAGED_FILES + # Add formatted files back to staging + git add $STAGED_FILES +fi + +echo "✅ Pre-commit checks passed!" diff --git a/bun.lockb b/bun.lockb index 2ce621a..4a8b9c2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/code-executor-api/server.js b/code-executor-api/server.js index 8a3bca5..b07d0ec 100644 --- a/code-executor-api/server.js +++ b/code-executor-api/server.js @@ -115,10 +115,10 @@ async function fetchTestCasesFromDB(problemId) { console.log(`Found problem: ${problem.title}`); console.log(`Function signature: ${problem.function_signature}`); - // Fetch test cases for this problem + // Fetch test cases for this problem (include both legacy and JSON columns) const { data: testCases, error: testCasesError } = await supabase .from('test_cases') - .select('input, expected_output, is_example') + .select('input, expected_output, input_json, expected_json, is_example') .eq('problem_id', problemId) .order('is_example', { ascending: false }); @@ -147,21 +147,34 @@ async function fetchTestCasesFromDB(problemId) { // Convert database format to our expected format const formattedTestCases = testCases.map((tc, index) => { console.log(`\n--- Processing test case ${index} ---`); - console.log('Raw input:', JSON.stringify(tc.input)); - console.log('Raw expected_output:', JSON.stringify(tc.expected_output)); - // Parse the input string to extract parameters - console.log('About to parse input with function signature:', problem.function_signature); - const inputParams = parseTestCaseInput(tc.input, problem.function_signature); - console.log('Parsed input params result:', JSON.stringify(inputParams, null, 2)); + let inputParams, expectedOutput; - // Parse expected output (handle different types) - let expectedOutput; - try { - expectedOutput = JSON.parse(tc.expected_output); - } catch { - // If JSON parsing fails, treat as string/number - expectedOutput = tc.expected_output; + // Prefer JSON columns if available + if (tc.input_json && tc.expected_json) { + console.log('Using JSON-native columns'); + console.log('JSON input:', JSON.stringify(tc.input_json)); + console.log('JSON expected:', JSON.stringify(tc.expected_json)); + + inputParams = tc.input_json; + expectedOutput = tc.expected_json; + } else { + console.log('Using legacy text parsing'); + console.log('Raw input:', JSON.stringify(tc.input)); + console.log('Raw expected_output:', JSON.stringify(tc.expected_output)); + + // Parse the input string to extract parameters (legacy method) + console.log('About to parse input with function signature:', problem.function_signature); + inputParams = parseTestCaseInput(tc.input, problem.function_signature); + console.log('Parsed input params result:', JSON.stringify(inputParams, null, 2)); + + // Parse expected output (handle different types) + try { + expectedOutput = JSON.parse(tc.expected_output); + } catch { + // If JSON parsing fails, treat as string/number + expectedOutput = tc.expected_output; + } } console.log('Final inputParams:', inputParams); @@ -223,15 +236,22 @@ function parseTestCaseInput(inputString, functionSignature) { // BUT NOT arrays like: "strs = [\"eat\",\"tea\"]" const line = lines[0]; console.log('Parsing single line with comma separation:', line); + console.log('Line length:', line.length); + console.log('Line characters:', [...line].map((c, i) => `${i}:${c}`).join(' ')); - // Split by comma, but be careful with commas inside quoted strings + // Split by comma, but be careful with commas inside quoted strings and arrays const parts = []; let current = ''; let insideQuotes = false; let escapeNext = false; + let bracketDepth = 0; + let squareBracketDepth = 0; + let curlyBracketDepth = 0; + for (let i = 0; i < line.length; i++) { const char = line[i]; + console.log(`Character ${i}: '${char}', bracketDepth: ${bracketDepth}, squareBracketDepth: ${squareBracketDepth}, insideQuotes: ${insideQuotes}`); if (escapeNext) { current += char; @@ -239,10 +259,44 @@ function parseTestCaseInput(inputString, functionSignature) { } else if (char === '\\') { current += char; escapeNext = true; - } else if (char === '"') { + } else if (char === '"' && !escapeNext) { current += char; insideQuotes = !insideQuotes; - } else if (char === ',' && !insideQuotes) { + console.log(` Quote toggled, insideQuotes now: ${insideQuotes}`); + } else if (char === '[' && !insideQuotes) { + current += char; + squareBracketDepth++; + bracketDepth++; + console.log(` Opening bracket, depths: square=${squareBracketDepth}, total=${bracketDepth}`); + } else if (char === ']' && !insideQuotes) { + current += char; + // Prevent underflow on unmatched closing bracket + const hadSquare = squareBracketDepth > 0; + squareBracketDepth = Math.max(0, squareBracketDepth - 1); + if (hadSquare) { + bracketDepth = Math.max(0, bracketDepth - 1); + } else { + console.warn(`Unmatched ']' at index ${i}; depths unchanged`); + } + console.log(` Closing bracket, depths: square=${squareBracketDepth}, total=${bracketDepth}`); + } else if (char === '{' && !insideQuotes) { + current += char; + curlyBracketDepth++; + bracketDepth++; + console.log(` Opening brace, depths: curly=${curlyBracketDepth}, total=${bracketDepth}`); + } else if (char === '}' && !insideQuotes) { + current += char; + // Prevent underflow on unmatched closing brace + const hadCurly = curlyBracketDepth > 0; + curlyBracketDepth = Math.max(0, curlyBracketDepth - 1); + if (hadCurly) { + bracketDepth = Math.max(0, bracketDepth - 1); + } else { + console.warn(`Unmatched '}' at index ${i}; depths unchanged`); + } + console.log(` Closing brace, depths: curly=${curlyBracketDepth}, total=${bracketDepth}`); + } else if (char === ',' && !insideQuotes && bracketDepth === 0) { + console.log(` SPLIT POINT! Current part: '${current.trim()}'`); parts.push(current.trim()); current = ''; } else { @@ -250,6 +304,7 @@ function parseTestCaseInput(inputString, functionSignature) { } } if (current.trim()) { + console.log(` Final part: '${current.trim()}'`); parts.push(current.trim()); } @@ -335,28 +390,130 @@ function processPythonCode(userCode, testCases) { processedCode = `from typing import List, Dict, Set, Tuple, Optional, Union\n${userCode}`; } + // Add ListNode definition if needed + console.log('Checking if ListNode definition is needed...'); + console.log('User code contains ListNode:', /\bListNode\b/.test(userCode)); + console.log('User code already has ListNode class:', userCode.includes('class ListNode')); + + const needsListNode = /\bListNode\b/.test(userCode); + if (needsListNode && !userCode.includes('class ListNode')) { + console.log('Adding ListNode class definition'); + const listNodeDef = `# Definition for singly-linked list. +class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +`; + processedCode = listNodeDef + processedCode; + console.log('ListNode definition added'); + } + + // Add helper functions for ListNode operations if needed (check both userCode and signature) + console.log('Checking if ListNode helper functions are needed...'); + console.log('User code contains ListNode:', /\bListNode\b/.test(userCode)); + console.log('Processed code contains ListNode:', /\bListNode\b/.test(processedCode)); + console.log('Processed code already has array_to_listnode:', processedCode.includes('def array_to_listnode')); + + const needsListNodeHelpers = /\bListNode\b/.test(userCode) || /\bListNode\b/.test(processedCode); + console.log('Needs ListNode helpers:', needsListNodeHelpers); + + if (needsListNodeHelpers && !processedCode.includes('def array_to_listnode')) { + console.log('Adding ListNode helper functions'); + const helperFunctions = `# Helper functions for ListNode operations +def array_to_listnode(arr): + if not arr: + return None + head = ListNode(arr[0]) + current = head + for val in arr[1:]: + current.next = ListNode(val) + current = current.next + return head + +def listnode_to_array(head): + result = [] + current = head + while current: + result.append(current.val) + current = current.next + return result + +`; + processedCode = helperFunctions + processedCode; + console.log('ListNode helper functions added'); + } else { + console.log('ListNode helper functions not added - either not needed or already present'); + } + // Check if code has methods with 'self' parameter - wrap in Solution class const hasSelfParam = /def\s+\w+\s*\([^)]*self[^)]*\)/.test(userCode); if (hasSelfParam && !userCode.includes('class Solution')) { - // Indent all function definitions to be inside Solution class - const indentedCode = processedCode - .split('\n') - .map(line => { - // Only indent lines that are function definitions or their content - if (line.trim().startsWith('def ') || (line.startsWith(' ') && line.trim() !== '')) { - return ' ' + line; - } else if (line.trim() === '' || line.startsWith('from ') || line.startsWith('import ')) { - return line; // Keep imports and empty lines as-is - } else { - return ' ' + line; // Indent everything else + console.log('Code has self parameter, wrapping in Solution class'); + + // Extract imports, ListNode definition, and helper functions to keep them global + const lines = processedCode.split('\n'); + const imports = []; + const listNodeDef = []; + const helperFunctions = []; + const userCodeLines = []; + + let inListNodeDef = false; + let inHelperFunctions = false; + let inUserCode = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.startsWith('from ') || line.startsWith('import ')) { + imports.push(line); + } else if (line.includes('# Definition for singly-linked list')) { + inListNodeDef = true; + listNodeDef.push(line); + } else if (inListNodeDef && (line.startsWith('class ListNode') || line.startsWith(' ') || line.trim() === '')) { + listNodeDef.push(line); + if (line.trim() === '' && i < lines.length - 1 && !lines[i + 1].startsWith(' ') && lines[i + 1].trim() !== '') { + inListNodeDef = false; } + } else if (line.includes('# Helper functions for ListNode operations')) { + inHelperFunctions = true; + helperFunctions.push(line); + } else if (inHelperFunctions && (line.startsWith('def ') || line.startsWith(' ') || line.trim() === '')) { + helperFunctions.push(line); + if (line.trim() === '' && i < lines.length - 1 && !lines[i + 1].startsWith(' ') && lines[i + 1].trim() !== '' && !lines[i + 1].startsWith('def ')) { + inHelperFunctions = false; + } + } else if (line.trim() !== '') { + inUserCode = true; + userCodeLines.push(line); + } else if (inUserCode) { + userCodeLines.push(line); + } + } + + console.log('Extracted sections:'); + console.log('- Imports:', imports.length); + console.log('- ListNode definition:', listNodeDef.length); + console.log('- Helper functions:', helperFunctions.length); + console.log('- User code:', userCodeLines.length); + + // Indent only the user code for the Solution class + const indentedUserCode = userCodeLines + .map(line => { + if (line.trim() === '') return line; + return ' ' + line; }) .join('\n'); - processedCode = `${processedCode.split('\n').filter(line => line.startsWith('from ') || line.startsWith('import ')).join('\n')} - -class Solution: -${indentedCode.split('\n').filter(line => !line.startsWith('from ') && !line.startsWith('import ')).join('\n')}`; + // Reconstruct the code with proper structure + const sections = []; + if (imports.length > 0) sections.push(imports.join('\n')); + if (listNodeDef.length > 0) sections.push(listNodeDef.join('\n')); + if (helperFunctions.length > 0) sections.push(helperFunctions.join('\n')); + + processedCode = sections.join('\n\n') + '\n\nclass Solution:\n' + indentedUserCode; + + console.log('Restructured code with Solution class'); } // Extract function name from the code @@ -387,10 +544,14 @@ ${testExecutionCode}`; // Generate test execution code with dynamic test cases function generateTestExecutionCode(functionName, signature, testCases) { // Convert test cases to Python format - const pythonTestCases = testCases.map(tc => ({ - ...tc.input, - expected: tc.expected - })); + const pythonTestCases = testCases.map(tc => { + console.log('Test case expected output (raw):', tc.expected, 'type:', typeof tc.expected); + + return { + ...tc.input, + expected: tc.expected // Keep as-is - don't convert to string! + }; + }); // Convert JavaScript booleans to Python booleans in JSON string let testCasesJson = JSON.stringify(pythonTestCases, null, 2); @@ -410,10 +571,23 @@ function generateTestExecutionCode(functionName, signature, testCases) { const originalSignature = signature; const hasSelfParam = originalSignature.includes('self'); + // Check if this is a ListNode problem + console.log('Function signature for ListNode detection:', signature); + const isListNodeProblem = signature.includes('ListNode'); + console.log('Is ListNode problem:', isListNodeProblem); + console.log('Function has self param:', hasSelfParam); + console.log('Function parameters:', params); + if (hasSelfParam) { // If function was defined with 'self', we need to create a class instance and call it as a method const className = 'Solution'; - if (params.length === 1) { + if (isListNodeProblem) { + console.log('Generating ListNode function call for method'); + // Convert arrays to ListNodes for function call + const paramList = params.map(p => `array_to_listnode(tc["${p}"])`).join(', '); + functionCall = `listnode_to_array(${className}().${functionName}(${paramList}))`; + console.log('Generated function call:', functionCall); + } else if (params.length === 1) { functionCall = `${className}().${functionName}(tc["${params[0]}"])`; } else if (params.length === 2) { functionCall = `${className}().${functionName}(tc["${params[0]}"], tc["${params[1]}"])`; @@ -423,7 +597,11 @@ function generateTestExecutionCode(functionName, signature, testCases) { } } else { // Standalone function call - if (params.length === 1) { + if (isListNodeProblem) { + // Convert arrays to ListNodes for function call + const paramList = params.map(p => `array_to_listnode(tc["${p}"])`).join(', '); + functionCall = `listnode_to_array(${functionName}(${paramList}))`; + } else if (params.length === 1) { functionCall = `${functionName}(tc["${params[0]}"])`; } else if (params.length === 2) { functionCall = `${functionName}(tc["${params[0]}"], tc["${params[1]}"])`; @@ -443,10 +621,45 @@ test_case_index = int(sys.stdin.read().strip()) # Dynamic test cases from database/API test_cases = ${testCasesJson} +# Helpers for common LeetCode structures (ListNode) +class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +def array_to_listnode(arr): + if arr is None: + return None + dummy = ListNode(0) + cur = dummy + for x in arr: + cur.next = ListNode(x) + cur = cur.next + return dummy.next + +def listnode_to_array(head): + res = [] + cur = head + while cur is not None: + res.append(cur.val) + cur = cur.next + return res + if 0 <= test_case_index < len(test_cases): tc = test_cases[test_case_index] result = ${functionCall} - print(json.dumps(result)) + # Auto-convert ListNode outputs + try: + if isinstance(result, ListNode) or (hasattr(result, 'val') and hasattr(result, 'next')): + out = listnode_to_array(result) + else: + out = result + print(json.dumps(out)) + except Exception: + try: + print(json.dumps(result)) + except Exception: + print("null") else: print("Invalid test case index")`; } @@ -604,6 +817,7 @@ app.post('/execute', async (req, res) => { // Decode base64 outputs const stdout = result.stdout ? Buffer.from(result.stdout, 'base64').toString().trim() : ''; const stderr = result.stderr ? Buffer.from(result.stderr, 'base64').toString().trim() : ''; + const statusDesc = result.status && result.status.description ? String(result.status.description) : ''; // Parse actual output as JSON if possible let actualOutput; @@ -612,6 +826,15 @@ app.post('/execute', async (req, res) => { } catch { actualOutput = stdout; } + + // Friendly error hints when there is no output or timeouts + let friendlyError = null; + if (!stdout && !stderr) { + friendlyError = 'No output produced. Common causes: returning None, printing instead of returning, or an infinite loop (e.g., not advancing pointers).'; + } + if (/time limit exceeded|timeout/i.test(statusDesc)) { + friendlyError = 'Execution timed out. For pointer-based problems, ensure pointers advance each iteration (e.g., ptr1 = ptr1.next or ptr2 = ptr2.next).'; + } // Compare actual vs expected with conditional smart comparison const passed = requiresSmartComparison @@ -623,15 +846,36 @@ app.post('/execute', async (req, res) => { .map(([key, value]) => `${key}=${JSON.stringify(value)}`) .join('\n'); + // Format expected and actual outputs as compact JSON + let formattedExpected = testCase.expected; + let formattedActual = actualOutput; + + // Convert expected to compact format if it's an array/object + if (Array.isArray(formattedExpected) || (typeof formattedExpected === 'object' && formattedExpected !== null)) { + formattedExpected = JSON.stringify(formattedExpected); + } else if (typeof formattedExpected === 'string') { + try { + const parsed = JSON.parse(formattedExpected); + formattedExpected = JSON.stringify(parsed); + } catch (e) { + // Keep as string if can't parse + } + } + + // Convert actual to compact format if it's an array/object + if (Array.isArray(formattedActual) || (typeof formattedActual === 'object' && formattedActual !== null)) { + formattedActual = JSON.stringify(formattedActual); + } + return { input: inputDisplay, - expected: testCase.expected, - actual: actualOutput, + expected: formattedExpected, + actual: formattedActual || (friendlyError ? '' : formattedActual), passed, status: result.status.description, time: result.time, memory: result.memory, - stderr: stderr || null + stderr: (friendlyError || stderr) || null }; }); diff --git a/index.html b/index.html index d527fd2..9eea113 100644 --- a/index.html +++ b/index.html @@ -8,18 +8,19 @@ - - + + + - + - + diff --git a/package-lock.json b/package-lock.json index e5740fd..3139ceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vite_react_shadcn_ts", "version": "0.0.0", "dependencies": { + "@babel/standalone": "^7.28.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lint": "^6.8.5", "@hookform/resolvers": "^3.9.0", @@ -46,8 +47,10 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "framer-motion": "^12.23.12", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", + "mermaid": "^10.9.1", "monaco-vim": "^0.4.2", "next-themes": "^0.3.0", "react": "^18.3.1", @@ -60,6 +63,7 @@ "react-router-dom": "^6.26.2", "react-syntax-highlighter": "^15.6.1", "react-textarea-autosize": "^8.5.9", + "reactflow": "^11.10.0", "recharts": "^2.12.7", "safe-stable-stringify": "^2.5.0", "sonner": "^1.5.0", @@ -148,6 +152,14 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/standalone": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.28.2.tgz", + "integrity": "sha512-1kjA8XzBRN68HoDDYKP38bucHtxYWCIX8XdYwe1drRNUOjOVNt8EMy9jiE6UwaGFfU7NOHCG+C8KgBc9CR08nA==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", @@ -162,6 +174,11 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" + }, "node_modules/@codemirror/autocomplete": { "version": "6.18.6", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", @@ -2510,6 +2527,102 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@remix-run/router": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", @@ -3098,24 +3211,145 @@ "react": "^18 || ^19" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", "license": "MIT" }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -3131,6 +3365,21 @@ "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", "license": "MIT" }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, "node_modules/@types/d3-scale": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", @@ -3140,6 +3389,16 @@ "@types/d3-time": "*" } }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" + }, "node_modules/@types/d3-shape": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", @@ -3155,12 +3414,34 @@ "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", "license": "MIT" }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -3183,6 +3464,11 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -3894,6 +4180,11 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4324,6 +4615,14 @@ "dev": true, "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -4361,6 +4660,65 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -4373,6 +4731,40 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -4382,6 +4774,80 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -4391,6 +4857,30 @@ "node": ">=12" } }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -4400,6 +4890,25 @@ "node": ">=12" } }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -4421,6 +4930,65 @@ "node": ">=12" } }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -4437,6 +5005,26 @@ "node": ">=12" } }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -4482,6 +5070,48 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, "node_modules/date-fns": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", @@ -4492,6 +5122,11 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -4534,6 +5169,14 @@ "dev": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4566,6 +5209,14 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4582,6 +5233,11 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4595,6 +5251,11 @@ "dev": true, "license": "ISC" }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==" + }, "node_modules/embla-carousel": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz", @@ -5079,12 +5740,38 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", + "node_modules/framer-motion": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", + "dependencies": { + "motion-dom": "^12.23.12", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -5337,6 +6024,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5609,6 +6307,29 @@ "dev": true, "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5619,6 +6340,24 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5673,6 +6412,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.castarray": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", @@ -6354,6 +7098,514 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "10.9.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.3.tgz", + "integrity": "sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==", + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5 <3.1.7", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/mermaid/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mermaid/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/mermaid/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mermaid/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mermaid/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mermaid/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mermaid/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/mermaid/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mermaid/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mermaid/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -6824,6 +8076,27 @@ "monaco-editor": "*" } }, + "node_modules/motion-dom": { + "version": "12.23.12", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz", + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6883,6 +8156,11 @@ "dev": true, "license": "MIT" }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7593,6 +8871,23 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7826,6 +9121,11 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -7885,6 +9185,22 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -7893,6 +9209,11 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -8126,6 +9447,11 @@ "inline-style-parser": "0.2.4" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -8312,6 +9638,14 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -8588,12 +9922,49 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/vaul": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", @@ -8721,6 +10092,11 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -8913,6 +10289,33 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index b3e2062..2e59b5a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test:ci": "bun run lint && bun run build" }, "dependencies": { + "@babel/standalone": "^7.28.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lint": "^6.8.5", "@hookform/resolvers": "^3.9.0", @@ -52,8 +53,10 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "framer-motion": "^12.23.12", "input-otp": "^1.2.4", "lucide-react": "^0.462.0", + "mermaid": "^10.9.1", "monaco-vim": "^0.4.2", "next-themes": "^0.3.0", "react": "^18.3.1", @@ -66,6 +69,7 @@ "react-router-dom": "^6.26.2", "react-syntax-highlighter": "^15.6.1", "react-textarea-autosize": "^8.5.9", + "reactflow": "^11.10.0", "recharts": "^2.12.7", "safe-stable-stringify": "^2.5.0", "sonner": "^1.5.0", diff --git a/public/favicon.ico b/public/favicon.ico index dd5a126..15aa942 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/simplyalgo-logo.png b/public/simplyalgo-logo.png new file mode 100644 index 0000000..15aa942 Binary files /dev/null and b/public/simplyalgo-logo.png differ diff --git a/scripts/backfill-json-test-cases.js b/scripts/backfill-json-test-cases.js new file mode 100644 index 0000000..affeb25 --- /dev/null +++ b/scripts/backfill-json-test-cases.js @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +import { createClient } from '@supabase/supabase-js'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Load environment variables from the API server +dotenv.config({ path: path.resolve(__dirname, '../code-executor-api/.env') }); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); + +/** + * Parse legacy input string to JSON object + */ +function parseLegacyInput(inputString, functionSignature) { + console.log('Parsing:', inputString); + + // Extract parameter names from function signature + const paramMatch = functionSignature.match(/def\s+\w+\s*\(([^)]+)\)/); + if (!paramMatch) { + console.warn('Could not parse function signature:', functionSignature); + return {}; + } + + const params = paramMatch[1] + .split(',') + .map(p => p.split(':')[0].trim()) + .filter(p => p !== 'self'); + + console.log('Parameters:', params); + + const inputParams = {}; + + // Handle format: "list1 = [1,2,4], list2 = [1,3,4]" + if (inputString.includes(' = ')) { + // Split by comma but handle arrays properly + const parts = []; + let current = ''; + let bracketDepth = 0; + let insideQuotes = false; + + for (let i = 0; i < inputString.length; i++) { + const char = inputString[i]; + + if (char === '"' && inputString[i-1] !== '\\') { + insideQuotes = !insideQuotes; + } else if (!insideQuotes) { + if (char === '[' || char === '{') bracketDepth++; + else if (char === ']' || char === '}') bracketDepth--; + } + + if (char === ',' && !insideQuotes && bracketDepth === 0) { + parts.push(current.trim()); + current = ''; + } else { + current += char; + } + } + if (current.trim()) parts.push(current.trim()); + + console.log('Split into parts:', parts); + + for (const part of parts) { + if (part.includes(' = ')) { + const [paramName, paramValue] = part.split(' = ', 2); + const cleanParamName = paramName.trim(); + const cleanParamValue = paramValue.trim(); + + try { + inputParams[cleanParamName] = JSON.parse(cleanParamValue); + } catch { + // Remove quotes if it's a quoted string + inputParams[cleanParamName] = cleanParamValue.replace(/^"(.*)"$/, '$1'); + } + } + } + } else { + // Format: positional values on separate lines + const lines = inputString.split('\n').map(line => line.trim()).filter(line => line); + for (let i = 0; i < Math.min(params.length, lines.length); i++) { + try { + inputParams[params[i]] = JSON.parse(lines[i]); + } catch { + inputParams[params[i]] = lines[i].replace(/^"(.*)"$/, '$1'); + } + } + } + + console.log('Parsed to:', inputParams); + return inputParams; +} + +/** + * Parse legacy expected output to JSON + */ +function parseLegacyExpected(expectedString) { + try { + return JSON.parse(expectedString); + } catch { + return expectedString; + } +} + +/** + * Backfill JSON columns for all test cases + */ +async function backfillTestCases() { + console.log('🚀 Starting test case JSON backfill...'); + + // Get all test cases that don't have JSON data yet + const { data: testCases, error: fetchError } = await supabase + .from('test_cases') + .select(` + id, + input, + expected_output, + input_json, + expected_json, + problems!inner ( + id, + title, + function_signature + ) + `) + .is('input_json', null) + .is('expected_json', null); + + if (fetchError) { + console.error('❌ Error fetching test cases:', fetchError); + return; + } + + console.log(`📋 Found ${testCases.length} test cases to migrate`); + + let successCount = 0; + let errorCount = 0; + + for (const tc of testCases) { + try { + console.log(`\n🔄 Processing test case ${tc.id} for problem: ${tc.problems.title}`); + + // Parse legacy input and expected + const inputJson = parseLegacyInput(tc.input, tc.problems.function_signature); + const expectedJson = parseLegacyExpected(tc.expected_output); + + // Update the database + const { error: updateError } = await supabase + .from('test_cases') + .update({ + input_json: inputJson, + expected_json: expectedJson + }) + .eq('id', tc.id); + + if (updateError) { + console.error(`❌ Error updating test case ${tc.id}:`, updateError); + errorCount++; + } else { + console.log(`✅ Migrated test case ${tc.id}`); + console.log(` Input: ${JSON.stringify(inputJson)}`); + console.log(` Expected: ${JSON.stringify(expectedJson)}`); + successCount++; + } + } catch (error) { + console.error(`❌ Error processing test case ${tc.id}:`, error); + errorCount++; + } + } + + console.log(`\n📊 Migration complete:`); + console.log(` ✅ Success: ${successCount}`); + console.log(` ❌ Errors: ${errorCount}`); + console.log(` 📋 Total: ${testCases.length}`); +} + +// Run the migration +if (import.meta.url === `file://${process.argv[1]}`) { + backfillTestCases().catch(console.error); +} \ No newline at end of file diff --git a/scripts/check-secrets.sh b/scripts/check-secrets.sh new file mode 100755 index 0000000..056f5f8 --- /dev/null +++ b/scripts/check-secrets.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}🔒 Checking for hardcoded secrets and sensitive data...${NC}" + +SECRETS_FOUND=0 + +# Simple patterns that work reliably +echo -e "${YELLOW}Checking for API keys and secrets...${NC}" + +# Check for OpenAI keys +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" "sk_[a-zA-Z0-9]" . 2>/dev/null; then + echo -e "${RED}❌ Found potential OpenAI API key${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +# Check for Stripe keys +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" "pk_[a-zA-Z0-9]" . 2>/dev/null; then + echo -e "${RED}❌ Found potential Stripe public key${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +# Check for AWS keys +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" "AKIA[0-9A-Z]" . 2>/dev/null; then + echo -e "${RED}❌ Found potential AWS Access Key${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +# Check for GitHub tokens +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" "ghp_[0-9A-Za-z]" . 2>/dev/null; then + echo -e "${RED}❌ Found potential GitHub personal access token${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +# Check for database connection strings +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" "postgres://.*:.*@" . 2>/dev/null; then + echo -e "${RED}❌ Found potential PostgreSQL connection string with credentials${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --include="*.json" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" "mysql://.*:.*@" . 2>/dev/null; then + echo -e "${RED}❌ Found potential MySQL connection string with credentials${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +# Check if .env files are staged for commit (this is what matters) +echo -e "${YELLOW}Checking if .env files are staged for commit...${NC}" +if git diff --cached --name-only | grep -E "\.env$|^\.env\."; then + echo -e "${RED}❌ Found .env files staged for commit:${NC}" + git diff --cached --name-only | grep -E "\.env$|^\.env\." + echo -e "${RED}❌ .env files should never be committed!${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +# Check for common secret patterns in code +echo -e "${YELLOW}Checking for hardcoded secrets in code...${NC}" +if grep -r --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" --exclude-dir="node_modules" --exclude-dir="dist" --exclude-dir="build" -i "api.key.*=.*['\"][a-z0-9]" . 2>/dev/null | grep -v "your_api_key_here" | grep -v "your_key_here" | grep -v "api_key_placeholder"; then + echo -e "${RED}❌ Found potential hardcoded API key assignment${NC}" + SECRETS_FOUND=$((SECRETS_FOUND + 1)) +fi + +if [ $SECRETS_FOUND -gt 0 ]; then + echo -e "${RED}❌ Security check failed! Found $SECRETS_FOUND potential security issues.${NC}" + echo -e "${RED}Please remove any hardcoded secrets before committing.${NC}" + echo -e "${YELLOW}💡 Use environment variables or secure secret management instead.${NC}" + echo -e "${YELLOW}💡 Make sure .env files are in .gitignore and not committed.${NC}" + exit 1 +else + echo -e "${GREEN}✅ No obvious hardcoded secrets detected!${NC}" + exit 0 +fi \ No newline at end of file diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index 4c3e782..21bc0c3 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -1,100 +1,82 @@ -#!/bin/bash +#!/usr/bin/env bash -# Development startup script for SimplyAlgo platform -# Starts both the frontend (Bun) and API server (Node.js) in parallel - -set -e +set -euo pipefail echo "🚀 Starting SimplyAlgo Development Environment..." -# Colors for output -RED='\033[0;31m' +# Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color +RED='\033[0;31m' +NC='\033[0m' + +API_DIR="code-executor-api" +API_PORT="3001" +FRONTEND_PORT="8080" -# Function to cleanup on exit -# Ensure cleanup runs on SIGINT, SIGTERM, or normal exit +# Ensure cleanup on exit +API_PID="" +FRONTEND_PID="" cleanup() { - echo -e "${YELLOW}🧹 Cleaning up background processes...${NC}" - if [[ -n "${API_PID:-}" ]]; then - kill ${API_PID} 2>/dev/null || true - fi - if [[ -n "${FRONTEND_PID:-}" ]]; then - kill ${FRONTEND_PID} 2>/dev/null || true - fi + echo -e "${YELLOW}🧹 Cleaning up processes...${NC}" + if [[ -n "${API_PID}" ]]; then kill ${API_PID} 2>/dev/null || true; fi + if [[ -n "${FRONTEND_PID}" ]]; then kill ${FRONTEND_PID} 2>/dev/null || true; fi } -trap cleanup SIGINT SIGTERM EXIT - -# Check if bun is installed -if ! command -v bun &> /dev/null; then - echo -e "${RED}❌ Bun is not installed. Please install Bun first: https://bun.sh${NC}" - exit 1 -fi - -# Check if node is installed -if ! command -v node &> /dev/null; then - echo -e "${RED}❌ Node.js is not installed. Please install Node.js first${NC}" - exit 1 -fi +trap cleanup EXIT INT TERM -echo -e "${BLUE}📦 Installing frontend dependencies...${NC}" -bun install - -echo -e "${BLUE}📦 Installing API dependencies...${NC}" -cd code-executor-api -npm install - -# Check if .env exists in API directory -if [ ! -f .env ]; then - echo -e "${YELLOW}⚠️ Creating API .env file with default values...${NC}" - cat > .env << EOL -PORT=3001 +# Ensure API .env exists with safe defaults (no secrets) +if [[ ! -f "${API_DIR}/.env" ]]; then + echo -e "${YELLOW}⚠️ Creating ${API_DIR}/.env with defaults...${NC}" + cat > "${API_DIR}/.env" </dev/null +npm install --no-audit --no-fund --silent +echo -e "${YELLOW}🚀 Starting API dev server on :${API_PORT}...${NC}" npm run dev & API_PID=$! -cd .. - -# Wait for API to start -echo -e "${YELLOW}⏳ Waiting for API server to start...${NC}" -sleep 3 - -# Check if API is running -if curl -f http://localhost:3001/health &>/dev/null; then - echo -e "${GREEN}✅ API server is running at http://localhost:3001${NC}" -else - echo -e "${YELLOW}⚠️ API server may not be fully ready yet (this is normal)${NC}" -fi - -echo -e "${BLUE}🚀 Starting frontend server on port 5173...${NC}" +popd >/dev/null + +# Wait for API readiness (60s) +for i in {1..60}; do + if curl -fsS "http://localhost:${API_PORT}/health" >/dev/null; then + echo -e "${GREEN}✅ API ready at http://localhost:${API_PORT}${NC}" + break + fi + sleep 1 +done +curl -fsS "http://localhost:${API_PORT}/health" >/dev/null || { echo -e "${RED}❌ API failed to start${NC}"; exit 1; } + +echo -e "${YELLOW}📦 Installing frontend deps...${NC}" +bun install --no-audit --silent +echo -e "${YELLOW}🚀 Starting frontend dev server on :${FRONTEND_PORT}...${NC}" bun run dev & FRONTEND_PID=$! -# Wait for frontend to start -echo -e "${YELLOW}⏳ Waiting for frontend server to start...${NC}" -sleep 5 +# Wait for frontend readiness (60s) +for i in {1..60}; do + if curl -fsS "http://localhost:${FRONTEND_PORT}/" >/dev/null; then + echo -e "${GREEN}✅ Frontend ready at http://localhost:${FRONTEND_PORT}${NC}" + break + fi + sleep 1 +done +curl -fsS "http://localhost:${FRONTEND_PORT}/" >/dev/null || { echo -e "${RED}❌ Frontend failed to start${NC}"; exit 1; } -echo -e "${GREEN}🎉 Development environment is ready!${NC}" -echo -e "${GREEN}📱 Frontend: http://localhost:5173${NC}" -echo -e "${GREEN}🔧 API Server: http://localhost:3001${NC}" -echo -e "${GREEN}💊 API Health Check: http://localhost:3001/health${NC}" -echo -e "${GREEN}⚖️ Judge0 Status: http://localhost:3001/judge0-info${NC}" echo "" -echo -e "${BLUE}Press Ctrl+C to stop both servers${NC}" +echo -e "${GREEN}🎉 Dev environment is ready!${NC}" +echo -e "${GREEN}📱 Frontend:${NC} http://localhost:${FRONTEND_PORT}" +echo -e "${GREEN}🔧 API:${NC} http://localhost:${API_PORT} (health at /health)" +echo "" +echo -e "${YELLOW}Press Ctrl+C to stop both servers${NC}" -# Wait for user to stop -wait $API_PID $FRONTEND_PID \ No newline at end of file +# Wait on both +wait ${API_PID} ${FRONTEND_PID} \ No newline at end of file diff --git a/scripts/reset-test-cases.js b/scripts/reset-test-cases.js new file mode 100644 index 0000000..d51d71b --- /dev/null +++ b/scripts/reset-test-cases.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node + +import { createClient } from '@supabase/supabase-js'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Load environment variables from the API server +dotenv.config({ path: path.resolve(__dirname, '../code-executor-api/.env') }); + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); + +async function resetTestCases() { + console.log('🚀 Resetting test cases with clean JSON data...'); + + try { + // First, clear existing test cases + console.log('🧹 Clearing existing test cases...'); + const { error: deleteError } = await supabase + .from('test_cases') + .delete() + .neq('id', '00000000-0000-0000-0000-000000000000'); // Delete all + + if (deleteError) { + console.error('❌ Error clearing test cases:', deleteError); + return; + } + + console.log('✅ Existing test cases cleared'); + + // Add JSONB columns (ignore if already exist) + console.log('🔧 Adding JSONB columns...'); + // Note: We'll handle this through raw SQL if needed, but columns might already exist + + // Insert new test cases with JSON data + console.log('📝 Inserting new test cases...'); + + const testCasesToInsert = [ + // Merge Two Sorted Lists + { + problem_id: 'merge-two-sorted-lists', + input: 'list1 = [1,2,4], list2 = [1,3,4]', + expected_output: '[1,1,2,3,4,4]', + input_json: { list1: [1,2,4], list2: [1,3,4] }, + expected_json: [1,1,2,3,4,4], + is_example: true + }, + { + problem_id: 'merge-two-sorted-lists', + input: 'list1 = [], list2 = []', + expected_output: '[]', + input_json: { list1: [], list2: [] }, + expected_json: [], + is_example: false + }, + { + problem_id: 'merge-two-sorted-lists', + input: 'list1 = [], list2 = [0]', + expected_output: '[0]', + input_json: { list1: [], list2: [0] }, + expected_json: [0], + is_example: false + }, + // Two Sum + { + problem_id: 'two-sum', + input: 'nums = [2,7,11,15], target = 9', + expected_output: '[0,1]', + input_json: { nums: [2,7,11,15], target: 9 }, + expected_json: [0,1], + is_example: true + }, + { + problem_id: 'two-sum', + input: 'nums = [3,2,4], target = 6', + expected_output: '[1,2]', + input_json: { nums: [3,2,4], target: 6 }, + expected_json: [1,2], + is_example: false + }, + // Group Anagrams + { + problem_id: 'group-anagrams', + input: 'strs = ["eat","tea","tan","ate","nat","bat"]', + expected_output: '[["bat"],["nat","tan"],["ate","eat","tea"]]', + input_json: { strs: ["eat","tea","tan","ate","nat","bat"] }, + expected_json: [["bat"],["nat","tan"],["ate","eat","tea"]], + is_example: true + }, + // Valid Anagram + { + problem_id: 'valid-anagram', + input: 's = "anagram", t = "nagaram"', + expected_output: 'true', + input_json: { s: "anagram", t: "nagaram" }, + expected_json: true, + is_example: true + }, + // Valid Parentheses + { + problem_id: 'valid-parentheses', + input: 's = "()"', + expected_output: 'true', + input_json: { s: "()" }, + expected_json: true, + is_example: true + } + ]; + + // Insert all test cases + const { error: insertError } = await supabase + .from('test_cases') + .insert(testCasesToInsert); + + if (insertError) { + console.error('❌ Error inserting test cases:', insertError); + return; + } + + console.log(`✅ Successfully inserted ${testCasesToInsert.length} test cases with JSON data`); + + // Verify the data + console.log('🔍 Verifying inserted data...'); + const { data: verifyData, error: verifyError } = await supabase + .from('test_cases') + .select(` + problem_id, + input_json, + expected_json, + is_example, + problems!inner (title) + `) + .limit(5); + + if (verifyError) { + console.error('❌ Error verifying data:', verifyError); + return; + } + + console.log('📋 Sample of inserted data:'); + verifyData.forEach(tc => { + console.log(` ${tc.problems.title}:`); + console.log(` Input: ${JSON.stringify(tc.input_json)}`); + console.log(` Expected: ${JSON.stringify(tc.expected_json)}`); + console.log(` Example: ${tc.is_example}`); + }); + + console.log('\n🎉 Test cases successfully reset with clean JSON data!'); + console.log(' Server will now use structured JSON instead of text parsing'); + + } catch (error) { + console.error('❌ Unexpected error:', error); + } +} + +// Run the reset +if (import.meta.url === `file://${process.argv[1]}`) { + resetTestCases().catch(console.error); +} \ No newline at end of file diff --git a/scripts/reset-test-cases.sql b/scripts/reset-test-cases.sql new file mode 100644 index 0000000..60cd7d6 --- /dev/null +++ b/scripts/reset-test-cases.sql @@ -0,0 +1,58 @@ +-- Clean slate: Remove existing test cases and recreate with proper JSON structure + +-- Clear existing test cases +DELETE FROM public.test_cases; + +-- Add JSONB columns to test_cases table +ALTER TABLE public.test_cases +ADD COLUMN IF NOT EXISTS input_json jsonb, +ADD COLUMN IF NOT EXISTS expected_json jsonb; + +-- Add indexes for better performance +CREATE INDEX IF NOT EXISTS idx_test_cases_input_json ON public.test_cases USING gin (input_json); +CREATE INDEX IF NOT EXISTS idx_test_cases_expected_json ON public.test_cases USING gin (expected_json); + +-- Insert test cases with proper JSON structure + +-- Merge Two Sorted Lists test cases (the one we're currently testing) +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'merge-two-sorted-lists'), 'list1 = [1,2,4], list2 = [1,3,4]', '[1,1,2,3,4,4]', + '{"list1": [1,2,4], "list2": [1,3,4]}', '[1,1,2,3,4,4]', true), +((SELECT id FROM problems WHERE id = 'merge-two-sorted-lists'), 'list1 = [], list2 = []', '[]', + '{"list1": [], "list2": []}', '[]', false), +((SELECT id FROM problems WHERE id = 'merge-two-sorted-lists'), 'list1 = [], list2 = [0]', '[0]', + '{"list1": [], "list2": [0]}', '[0]', false); + +-- Two Sum test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'two-sum'), 'nums = [2,7,11,15], target = 9', '[0,1]', + '{"nums": [2,7,11,15], "target": 9}', '[0,1]', true), +((SELECT id FROM problems WHERE id = 'two-sum'), 'nums = [3,2,4], target = 6', '[1,2]', + '{"nums": [3,2,4], "target": 6}', '[1,2]', false), +((SELECT id FROM problems WHERE id = 'two-sum'), 'nums = [3,3], target = 6', '[0,1]', + '{"nums": [3,3], "target": 6}', '[0,1]', false); + +-- Group Anagrams test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'group-anagrams'), 'strs = ["eat","tea","tan","ate","nat","bat"]', '[["bat"],["nat","tan"],["ate","eat","tea"]]', + '{"strs": ["eat","tea","tan","ate","nat","bat"]}', '[["bat"],["nat","tan"],["ate","eat","tea"]]', true), +((SELECT id FROM problems WHERE id = 'group-anagrams'), 'strs = [""]', '[[""]]', + '{"strs": [""]}', '[[""]]', false), +((SELECT id FROM problems WHERE id = 'group-anagrams'), 'strs = ["a"]', '[["a"]]', + '{"strs": ["a"]}', '[["a"]]', false); + +-- Valid Anagram test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'valid-anagram'), 's = "anagram", t = "nagaram"', 'true', + '{"s": "anagram", "t": "nagaram"}', 'true', true), +((SELECT id FROM problems WHERE id = 'valid-anagram'), 's = "rat", t = "car"', 'false', + '{"s": "rat", "t": "car"}', 'false', false); + +-- Valid Parentheses test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "()"', 'true', + '{"s": "()"}', 'true', true), +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "()[]{}"', 'true', + '{"s": "()[]{}""}', 'true', false), +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "(]"', 'false', + '{"s": "(]"}', 'false', false); \ No newline at end of file diff --git a/src/components/AIChat.tsx b/src/components/AIChat.tsx index 0ee1831..ac31b80 100644 --- a/src/components/AIChat.tsx +++ b/src/components/AIChat.tsx @@ -1,16 +1,21 @@ import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Send, Bot, User, Trash2, Loader2, Mic, MicOff } from 'lucide-react'; +import { Send, Bot, User, Trash2, Loader2, Mic, MicOff, ChartNetwork as DiagramIcon, Maximize2, Sparkles } from 'lucide-react'; import { useState, useEffect, useRef } from 'react'; +// Using a lightweight custom fullscreen overlay instead of Radix Dialog to avoid MIME issues in some dev setups import { useChatSession } from '@/hooks/useChatSession'; import { useSpeechToText } from '@/hooks/useSpeechToText'; import TextareaAutosize from 'react-textarea-autosize'; import { CodeSnippet } from '@/types'; +import Mermaid from '@/components/diagram/Mermaid'; +import FlowCanvas from '@/components/diagram/FlowCanvas'; +import type { FlowGraph } from '@/types'; import CodeSnippetButton from '@/components/CodeSnippetButton'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { CanvasContainer } from '@/components/canvas'; interface AIChatProps { problemId: string; @@ -22,40 +27,23 @@ interface AIChatProps { const AIChat = ({ problemId, problemDescription, onInsertCodeSnippet, problemTestCases }: AIChatProps) => { const [input, setInput] = useState(''); const scrollAreaRef = useRef(null); - - // Function to clean mathematical notation in message content - const cleanMathNotation = (content: string): string => { - if (typeof content !== 'string') return String(content); - - // Guard: don't process content that contains backticks (code blocks/inline code) - if (/`/.test(content)) { - return content; - } - - return content - // Clean LaTeX notation - .replace(/\\cdot/g, '·') - .replace(/\\log/g, 'log') - .replace(/\\times/g, '×') - .replace(/\\le/g, '≤') - .replace(/\\ge/g, '≥') - .replace(/\\ne/g, '≠') - .replace(/\\infty/g, '∞') - // Clean up escaped parentheses - .replace(/\\\(/g, '(') - .replace(/\\\)/g, ')') - // Remove extra backslashes - .replace(/\s*\\\s*/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - }; + type ActiveDiagram = { engine: 'mermaid'; code: string } | { engine: 'reactflow'; graph: FlowGraph }; + const [isDiagramOpen, setIsDiagramOpen] = useState(false); + const [activeDiagram, setActiveDiagram] = useState(null); + const [hiddenVisualizeForIds, setHiddenVisualizeForIds] = useState>(new Set()); + + // Canvas state + const [isCanvasOpen, setIsCanvasOpen] = useState(false); + const [canvasCode, setCanvasCode] = useState(''); + const [canvasTitle, setCanvasTitle] = useState('Interactive Component'); const { session, messages, loading, isTyping, sendMessage, - clearConversation + clearConversation, + requestDiagram, } = useChatSession({ problemId, problemDescription, problemTestCases }); // Speech-to-text functionality @@ -93,6 +81,214 @@ const AIChat = ({ problemId, problemDescription, onInsertCodeSnippet, problemTes } }; + const handleVisualize = async (sourceMessageContent: string, messageId: string) => { + // Request a diagram separately without adding a user message bubble + setHiddenVisualizeForIds(prev => new Set(prev).add(messageId)); + await requestDiagram(sourceMessageContent); + }; + + const handleGenerateComponent = async (messageContent: string) => { + // For now, let's create a sample component - later we'll integrate with AI + const sampleCode = `function AlgorithmVisualizer() { + const [values, setValues] = useState([64, 34, 25, 12, 22, 11, 90]); + const [currentStep, setCurrentStep] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [speed, setSpeed] = useState(500); + + const sortSteps = useMemo(() => { + const arr = [...values]; + const steps = [{ array: [...arr], comparing: [], swapping: [] }]; + + // Bubble sort with step tracking + for (let i = 0; i < arr.length - 1; i++) { + for (let j = 0; j < arr.length - i - 1; j++) { + steps.push({ array: [...arr], comparing: [j, j + 1], swapping: [] }); + if (arr[j] > arr[j + 1]) { + [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; + steps.push({ array: [...arr], comparing: [], swapping: [j, j + 1] }); + } + } + } + return steps; + }, [values]); + + useEffect(() => { + let timer; + if (isPlaying && currentStep < sortSteps.length - 1) { + timer = setTimeout(() => setCurrentStep(s => s + 1), speed); + } else if (currentStep >= sortSteps.length - 1) { + setIsPlaying(false); + } + return () => clearTimeout(timer); + }, [isPlaying, currentStep, speed, sortSteps.length]); + + const reset = () => { + setCurrentStep(0); + setIsPlaying(false); + }; + + const randomize = () => { + const newValues = Array.from({ length: 7 }, () => Math.floor(Math.random() * 100) + 1); + setValues(newValues); + reset(); + }; + + const currentStepData = sortSteps[currentStep] || sortSteps[0]; + + return React.createElement('div', { + className: "w-full h-full bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 p-8" + }, + React.createElement('div', { + className: "max-w-4xl mx-auto space-y-6" + }, + React.createElement(Card, {}, + React.createElement(CardHeader, {}, + React.createElement(CardTitle, { + className: "flex items-center gap-2" + }, + React.createElement(CircleHelp, { className: "h-5 w-5" }), + "Bubble Sort Visualizer" + ) + ), + React.createElement(CardContent, { + className: "space-y-6" + }, + // Array Visualization + React.createElement('div', { + className: "flex items-end justify-center gap-2 h-64 p-4" + }, + React.createElement(AnimatePresence, {}, + currentStepData.array.map((value, index) => + React.createElement(motion.div, { + key: \`\${index}-\${value}\`, + layout: true, + initial: { scale: 0.8, opacity: 0 }, + animate: { + scale: 1, + opacity: 1, + backgroundColor: currentStepData.comparing.includes(index) + ? '#fbbf24' + : currentStepData.swapping.includes(index) + ? '#ef4444' + : '#3b82f6' + }, + exit: { scale: 0.8, opacity: 0 }, + className: "flex flex-col items-center" + }, + React.createElement('div', { + className: "text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300" + }, value), + React.createElement(motion.div, { + className: "w-12 rounded-t-lg", + style: { + height: \`\${(value / Math.max(...values)) * 200}px\`, + backgroundColor: currentStepData.comparing.includes(index) + ? '#fbbf24' + : currentStepData.swapping.includes(index) + ? '#ef4444' + : '#3b82f6' + }, + animate: { + scale: currentStepData.comparing.includes(index) || currentStepData.swapping.includes(index) ? 1.1 : 1 + } + }) + ) + ) + ) + ), + + // Controls + React.createElement('div', { + className: "flex items-center justify-between" + }, + React.createElement('div', { + className: "flex items-center gap-2" + }, + React.createElement(Button, { + onClick: () => setCurrentStep(Math.max(0, currentStep - 1)), + disabled: currentStep === 0 + }, "Previous"), + React.createElement(Button, { + onClick: () => setIsPlaying(!isPlaying), + disabled: currentStep >= sortSteps.length - 1 + }, + isPlaying ? React.createElement(Pause, { className: "h-4 w-4" }) : React.createElement(Play, { className: "h-4 w-4" }), + isPlaying ? 'Pause' : 'Play' + ), + React.createElement(Button, { + onClick: () => setCurrentStep(Math.min(sortSteps.length - 1, currentStep + 1)), + disabled: currentStep >= sortSteps.length - 1 + }, "Next") + ), + + React.createElement('div', { + className: "flex items-center gap-2" + }, + React.createElement(Button, { + onClick: reset, + variant: "outline" + }, + React.createElement(RotateCcw, { className: "h-4 w-4 mr-2" }), + "Reset" + ), + React.createElement(Button, { + onClick: randomize, + variant: "outline" + }, + React.createElement(Shuffle, { className: "h-4 w-4 mr-2" }), + "Randomize" + ) + ) + ), + + // Speed Control + React.createElement('div', { + className: "flex items-center gap-4" + }, + React.createElement(Label, {}, "Speed"), + React.createElement(Slider, { + value: [speed], + onValueChange: (value) => setSpeed(value[0]), + max: 1000, + min: 100, + step: 100, + className: "flex-1" + }), + React.createElement('span', { + className: "text-sm text-gray-600 dark:text-gray-400 w-16" + }, \`\${speed}ms\`) + ), + + // Step Info + React.createElement('div', { + className: "text-center text-sm text-gray-600 dark:text-gray-400" + }, + \`Step \${currentStep + 1} of \${sortSteps.length}\`, + currentStepData.comparing.length > 0 && React.createElement('span', { + className: "ml-2" + }, \`Comparing positions \${currentStepData.comparing.join(' and ')}\`), + currentStepData.swapping.length > 0 && React.createElement('span', { + className: "ml-2" + }, \`Swapping positions \${currentStepData.swapping.join(' and ')}\`) + ) + ) + ) + ) + ); +} + +return AlgorithmVisualizer;`; + + setCanvasCode(sampleCode); + setCanvasTitle('Algorithm Visualizer'); + setIsCanvasOpen(true); + }; + + const openDiagramDialog = (diagram: ActiveDiagram) => { + setActiveDiagram(diagram); + setIsDiagramOpen(true); + }; + const toggleMicrophone = async () => { if (!hasNativeSupport) return; @@ -186,22 +382,22 @@ const AIChat = ({ problemId, problemDescription, onInsertCodeSnippet, problemTes
{message.role === 'user' ? (

{message.content}

) : ( -
+
+ ); + } return !inline && match ? ( ); }, - p: ({children}) => ( -

- {children} -

- ), - ul: ({children}) => ( -
    - {children} -
- ), - ol: ({children}) => ( -
    - {children} -
- ), - li: ({children}) => ( -
  • - {children} -
  • - ), - strong: ({children}) => ( - {children} - ), - em: ({children}) => ( - {children} - ), - h1: ({children}) => ( -

    {children}

    - ), - h2: ({children}) => ( -

    {children}

    - ), - h3: ({children}) => ( -

    {children}

    - ), - blockquote: ({children}) => ( -
    - {children} -
    + p: ({children}) =>

    {children}

    , + ul: ({children}) =>
      {children}
    , + ol: ({children}) =>
      {children}
    , + li: ({children}: { children?: React.ReactNode }) => ( +
  • {children}
  • ), }} > - {cleanMathNotation(message.content)} + {message.content}
    )}
    + + {/* Mermaid diagram bubble if attached as structured payload */} + {message.role === 'assistant' && (message as unknown as { diagram?: { engine: 'mermaid'; code: string } }).diagram?.engine === 'mermaid' && ( +
    +
    +
    +
    Diagram Mermaid
    + +
    + +
    +
    + )} + + {/* React Flow diagram bubble if attached */} + {message.role === 'assistant' && (message as unknown as { diagram?: { engine: 'reactflow'; graph: FlowGraph } }).diagram?.engine === 'reactflow' && ( +
    +
    +
    +
    Diagram React Flow
    + +
    + +
    +
    + )} - {/* Code Snippet Buttons for AI messages */} - {message.role === 'assistant' && message.codeSnippets && message.codeSnippets.length > 0 && ( + {/* Actions: Visualize button and code snippets */} + {message.role === 'assistant' && (
    - {message.codeSnippets.map((snippet) => ( + {(() => { + const lastUserMsg = [...messages].reverse().find(m => m.role === 'user')?.content || ''; + const userAsked = /(visualize|diagram|draw|flowchart|mermaid)/i.test(lastUserMsg); + const hasDiagram = Boolean((message as unknown as { diagram?: unknown }).diagram); + const shouldShow = !hasDiagram && (userAsked || (message as unknown as { suggestDiagram?: boolean }).suggestDiagram === true) && !hiddenVisualizeForIds.has(message.id); + return shouldShow ? ( +
    + + +
    + ) : null; + })()} + {message.codeSnippets && message.codeSnippets.length > 0 && ( +
    + {message.codeSnippets.map((snippet) => (
    )}
    - ))} + ))} +
    + )}
    )} @@ -330,7 +570,7 @@ const AIChat = ({ problemId, problemDescription, onInsertCodeSnippet, problemTes
    -
    +
    @@ -407,6 +647,32 @@ const AIChat = ({ problemId, problemDescription, onInsertCodeSnippet, problemTes
    + {isDiagramOpen && ( +
    +
    setIsDiagramOpen(false)} /> +
    +
    +
    Diagram
    + +
    + {activeDiagram && ( + activeDiagram.engine === 'mermaid' ? ( + + ) : ( + + ) + )} +
    +
    + )} + + {/* Canvas Modal for Interactive Components */} + setIsCanvasOpen(false)} + initialCode={canvasCode} + title={canvasTitle} + /> ); }; diff --git a/src/components/canvas/CanvasContainer.tsx b/src/components/canvas/CanvasContainer.tsx new file mode 100644 index 0000000..13f3c12 --- /dev/null +++ b/src/components/canvas/CanvasContainer.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import CanvasModal from './CanvasModal'; +import ComponentCompiler from './ComponentCompiler'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Textarea } from '@/components/ui/textarea'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Code2, Eye, Download } from 'lucide-react'; + +interface CanvasContainerProps { + initialCode?: string; + title?: string; + isOpen: boolean; + onClose: () => void; +} + +export default function CanvasContainer({ + initialCode = '', + title = "Interactive Component", + isOpen, + onClose +}: CanvasContainerProps) { + const [code, setCode] = useState(initialCode); + const [compileError, setCompileError] = useState(null); + + // Update code when initialCode changes + React.useEffect(() => { + if (initialCode) { + setCode(initialCode); + } + }, [initialCode]); + + return ( + + {/* No tabs - just show the preview directly */} +
    + {compileError && ( +
    +

    + {compileError} +

    +
    + )} + +
    +
    + ); +} \ No newline at end of file diff --git a/src/components/canvas/CanvasModal.tsx b/src/components/canvas/CanvasModal.tsx new file mode 100644 index 0000000..0eb973d --- /dev/null +++ b/src/components/canvas/CanvasModal.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Minimize2, Download, Code2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface CanvasModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: React.ReactNode; + componentCode?: string; + onDownload?: () => void; + onViewCode?: () => void; +} + +export default function CanvasModal({ + isOpen, + onClose, + title = "Interactive Component", + children, + componentCode, + onDownload, + onViewCode +}: CanvasModalProps) { + if (!isOpen) return null; + + return ( + + {isOpen && ( + + e.stopPropagation()} + > + {/* Header */} +
    +
    + + + +
    + +

    + {title} +

    + +
    + {onViewCode && ( + + )} + {onDownload && ( + + )} + +
    +
    + + {/* Content */} +
    + + {children} + +
    +
    +
    + )} +
    + ); +} \ No newline at end of file diff --git a/src/components/canvas/ComponentCompiler.tsx b/src/components/canvas/ComponentCompiler.tsx new file mode 100644 index 0000000..e441949 --- /dev/null +++ b/src/components/canvas/ComponentCompiler.tsx @@ -0,0 +1,259 @@ +import React, { useState, useEffect, useRef, ErrorInfo } from 'react'; +import { transform } from '@babel/standalone'; +import { motion } from 'framer-motion'; +import { AlertTriangle, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; + +// Error Boundary Component +class ComponentErrorBoundary extends React.Component< + { children: React.ReactNode; onError: (error: Error, errorInfo: ErrorInfo) => void }, + { hasError: boolean; error?: Error } +> { + constructor(props: any) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.props.onError(error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
    + +

    + Component Error +

    +

    + {this.state.error?.message || 'Something went wrong while rendering the component'} +

    + +
    + ); + } + + return this.props.children; + } +} + +interface ComponentCompilerProps { + code: string; + onError?: (error: string) => void; + className?: string; +} + +export default function ComponentCompiler({ code, onError, className }: ComponentCompilerProps) { + const [CompiledComponent, setCompiledComponent] = useState(null); + const [compileError, setCompileError] = useState(null); + const [isCompiling, setIsCompiling] = useState(false); + const mountRef = useRef(null); + + useEffect(() => { + if (!code.trim()) { + setCompiledComponent(null); + setCompileError(null); + return; + } + + compileComponent(code); + }, [code]); + + const compileComponent = async (sourceCode: string) => { + setIsCompiling(true); + setCompileError(null); + + try { + // Transform the component code with Babel + const transformed = transform(sourceCode, { + presets: ['react', 'typescript'], + plugins: ['proposal-class-properties'], + }); + + if (!transformed.code) { + throw new Error('Failed to transform component code'); + } + + // Create a function that returns the component + const componentFunction = new Function( + 'React', + 'useState', + 'useEffect', + 'useRef', + 'useMemo', + 'useCallback', + 'motion', + 'AnimatePresence', + 'Button', + 'Card', + 'CardContent', + 'CardHeader', + 'CardTitle', + 'Input', + 'Label', + 'Slider', + 'Tabs', + 'TabsContent', + 'TabsList', + 'TabsTrigger', + 'AlertCircle', + 'Pause', + 'Play', + 'RotateCcw', + 'StepForward', + 'Shuffle', + 'CircleHelp', + ` + ${transformed.code} + + // Return the default export or the last declared component + if (typeof exports !== 'undefined' && exports.default) { + return exports.default; + } + + // Find the last function/class declaration that looks like a component + const componentMatch = \`${sourceCode}\`.match(/(?:export\\s+default\\s+)?(?:function|class|const)\\s+(\\w+)/g); + if (componentMatch) { + const lastComponent = componentMatch[componentMatch.length - 1]; + const componentName = lastComponent.replace(/^(?:export\\s+default\\s+)?(?:function|class|const)\\s+/, ''); + return eval(componentName); + } + + return null; + ` + ); + + // Import all the dependencies the component might need + const { useState, useEffect, useRef, useMemo, useCallback } = React; + const { motion, AnimatePresence } = await import('framer-motion'); + const { Button } = await import('@/components/ui/button'); + const { Card, CardContent, CardHeader, CardTitle } = await import('@/components/ui/card'); + const { Input } = await import('@/components/ui/input'); + const { Label } = await import('@/components/ui/label'); + const { Slider } = await import('@/components/ui/slider'); + const { Tabs, TabsContent, TabsList, TabsTrigger } = await import('@/components/ui/tabs'); + const { + AlertCircle, + Pause, + Play, + RotateCcw, + StepForward, + Shuffle, + CircleHelp + } = await import('lucide-react'); + + // Execute the function to get the component + const Component = componentFunction( + React, + useState, + useEffect, + useRef, + useMemo, + useCallback, + motion, + AnimatePresence, + Button, + Card, + CardContent, + CardHeader, + CardTitle, + Input, + Label, + Slider, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + AlertCircle, + Pause, + Play, + RotateCcw, + StepForward, + Shuffle, + CircleHelp + ); + + if (typeof Component !== 'function') { + throw new Error('Compiled code did not return a valid React component'); + } + + setCompiledComponent(() => Component); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown compilation error'; + setCompileError(errorMessage); + onError?.(errorMessage); + console.error('Component compilation error:', error); + } finally { + setIsCompiling(false); + } + }; + + const handleComponentError = (error: Error, errorInfo: ErrorInfo) => { + const errorMessage = `Runtime Error: ${error.message}`; + setCompileError(errorMessage); + onError?.(errorMessage); + console.error('Component runtime error:', error, errorInfo); + }; + + if (isCompiling) { + return ( +
    + + Compiling component... +
    + ); + } + + if (compileError) { + return ( + + +
    + +
    +

    + Compilation Error +

    +

    + {compileError} +

    +
    +
    +
    +
    + ); + } + + if (!CompiledComponent) { + return ( +
    + No component to display +
    + ); + } + + return ( +
    + + + +
    + ); +} \ No newline at end of file diff --git a/src/components/canvas/index.tsx b/src/components/canvas/index.tsx new file mode 100644 index 0000000..afc05ae --- /dev/null +++ b/src/components/canvas/index.tsx @@ -0,0 +1,3 @@ +export { default as CanvasModal } from './CanvasModal'; +export { default as ComponentCompiler } from './ComponentCompiler'; +export { default as CanvasContainer } from './CanvasContainer'; \ No newline at end of file diff --git a/src/components/diagram/FlowCanvas.tsx b/src/components/diagram/FlowCanvas.tsx new file mode 100644 index 0000000..3609846 --- /dev/null +++ b/src/components/diagram/FlowCanvas.tsx @@ -0,0 +1,34 @@ +import React, { useMemo } from 'react'; +import ReactFlow, { Background, Controls, MiniMap } from 'reactflow'; +import 'reactflow/dist/style.css'; + +import type { FlowGraph } from '@/types'; + +type Props = { + graph: FlowGraph; + className?: string; + caption?: string; + height?: number | string; +}; + +export default function FlowCanvas({ graph, className, caption, height = '20rem' }: Props) { + const nodes = useMemo(() => Array.isArray(graph?.nodes) ? graph.nodes : [], [graph]); + const edges = useMemo(() => Array.isArray(graph?.edges) ? graph.edges : [], [graph]); + + return ( +
    +
    + + + + + +
    + {caption ? ( +
    {caption}
    + ) : null} +
    + ); +} + + diff --git a/src/components/diagram/Mermaid.tsx b/src/components/diagram/Mermaid.tsx new file mode 100644 index 0000000..59c01eb --- /dev/null +++ b/src/components/diagram/Mermaid.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +// Dynamically import mermaid to avoid bundler MIME/type issues and reduce initial bundle size + +type MermaidProps = { + chart: string; + className?: string; + caption?: string; +}; + +// Minimal safe config: disable htmlLabels to reduce XSS surface +const baseConfig: any = { + startOnLoad: false, + securityLevel: 'strict', + theme: 'base', + themeVariables: { + primaryColor: '#FFB88A', + primaryTextColor: '#1f2937', + primaryBorderColor: '#ffa94d', + lineColor: '#ffa94d', + textColor: '#1f2937', + noteBkgColor: '#fff4e6', + noteTextColor: '#374151', + }, +}; + +export default function Mermaid({ chart, className, caption }: MermaidProps) { + const id = useMemo(() => `mermaid-${Math.random().toString(36).slice(2)}`, []); + const containerRef = useRef(null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + // Prefer ESM builds; fall back to package entry if needed + const mod: any = await import('mermaid/dist/mermaid.esm.min.mjs') + .catch(() => import('mermaid/dist/mermaid.esm.mjs')) + .catch(() => import('mermaid')); + const mm = mod?.default ?? mod; + if (!mm || cancelled) return; + mm.initialize(baseConfig); + // Sanitize: collapse newlines within bracketed labels to avoid parser errors + const sanitized = chart.replace(/\[(?:[^\]\n]|\\.|\n)*\]/g, (label) => label.replace(/\n+/g, ' ')); + const { svg } = await mm.render(id, sanitized); + if (!cancelled && containerRef.current) { + containerRef.current.innerHTML = svg; + } + } catch (e) { + if (!cancelled && containerRef.current) { + console.error('Mermaid rendering error:', e); + containerRef.current.innerHTML = '
    Failed to render diagram
    '; + } + } + })(); + return () => { cancelled = true; }; + }, [chart, id]); + + return ( +
    +
    + {caption && ( +
    {caption}
    + )} +
    + ); +} + + diff --git a/src/components/diagram/mermaid.d.ts b/src/components/diagram/mermaid.d.ts new file mode 100644 index 0000000..9b3b2cb --- /dev/null +++ b/src/components/diagram/mermaid.d.ts @@ -0,0 +1,15 @@ +// Minimal type declaration for mermaid to satisfy TypeScript +declare module 'mermaid' { + export type Config = { + startOnLoad?: boolean; + securityLevel?: 'loose' | 'strict'; + theme?: string; + themeVariables?: Record; + }; + export function initialize(config: Config): void; + export function render(id: string, definition: string): Promise<{ svg: string; bindFunctions?: (element: Element) => void }>; + const _default: any; + export default _default; +} + + diff --git a/src/hooks/useChatSession.ts b/src/hooks/useChatSession.ts index bacd3d7..880a7dd 100644 --- a/src/hooks/useChatSession.ts +++ b/src/hooks/useChatSession.ts @@ -1,9 +1,50 @@ import { useState, useEffect, useCallback } from 'react'; import { supabase } from '@/integrations/supabase/client'; -import { ChatMessage, ChatSession, CodeSnippet } from '@/types'; +import { ChatMessage, ChatSession, CodeSnippet, FlowGraph } from '@/types'; import { useAuth } from './useAuth'; import { useToast } from './use-toast'; +// --- Diagram payload helper & types --- +type DiagramPayload = + | { engine: 'mermaid'; code: string; title?: string } + | { engine: 'reactflow'; graph: FlowGraph; title?: string }; + +type MaybeMermaid = { engine?: unknown; code?: unknown; title?: unknown } | null | undefined; +type MaybeReactflow = { engine?: unknown; graph?: { nodes?: unknown; edges?: unknown } | unknown; title?: unknown } | null | undefined; + +const isMermaidDiagram = (d: unknown): d is { engine: 'mermaid'; code: string; title?: string } => { + const m = d as MaybeMermaid; + return !!m && m.engine === 'mermaid' && typeof m.code === 'string'; +}; + +const isReactflowDiagram = ( + d: unknown +): d is { engine: 'reactflow'; graph: { nodes: unknown[]; edges: unknown[] }; title?: string } => { + const r = d as MaybeReactflow; + const hasGraph = !!r && typeof r === 'object' && 'graph' in (r as object); + const graph = hasGraph ? (r as { graph: unknown }).graph as { nodes?: unknown; edges?: unknown } : undefined; + const hasEngine = !!r && typeof r === 'object' && 'engine' in (r as object); + const engine = hasEngine ? (r as { engine: unknown }).engine : undefined; + return ( + engine === 'reactflow' && + !!graph && + Array.isArray(graph.nodes) && + Array.isArray(graph.edges) + ); +}; + +const getDiagramPayload = (diagram: unknown): DiagramPayload | undefined => { + if (isMermaidDiagram(diagram)) { + return { engine: 'mermaid', code: diagram.code, title: diagram.title }; + } + if (isReactflowDiagram(diagram)) { + const g = diagram.graph as unknown as FlowGraph; + const t = diagram.title as string | undefined; + return { engine: 'reactflow', graph: g, title: t }; + } + return undefined; +}; + // --- Code snippet dedup helpers --- const normalizeSnippet = (s: CodeSnippet): string => { const type = s.insertionHint?.type || ''; @@ -122,7 +163,15 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases if (messagesError) throw messagesError; - const formattedMessages: ChatMessage[] = sessionMessages.map(msg => ({ + type DbMessage = { + id: string; + role: 'user' | 'assistant'; + content: string; + created_at: string; + session_id: string; + code_snippets?: CodeSnippet[] | null; + }; + const formattedMessages: ChatMessage[] = (sessionMessages as DbMessage[]).map((msg) => ({ id: msg.id, role: msg.role as 'user' | 'assistant', content: msg.content, @@ -177,7 +226,7 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases }, [session, toast]); // Send message to AI and save both user and AI messages - const sendMessage = useCallback(async (content: string) => { + const sendMessage = useCallback(async (content: string, options?: { action?: 'diagram' }) => { if (!content.trim() || !session || isTyping) return; const userMessage: ChatMessage = { @@ -208,7 +257,8 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases message: content, problemDescription, conversationHistory, - testCases: problemTestCases + testCases: problemTestCases, + diagram: options?.action === 'diagram' } }); @@ -244,7 +294,9 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases content: aiResponseContent, timestamp: new Date(), sessionId: session.id, - codeSnippets: dedupedSnippets + codeSnippets: dedupedSnippets, + diagram: getDiagramPayload(data?.diagram), + suggestDiagram: typeof data.suggestDiagram === 'boolean' ? data.suggestDiagram : undefined }; // Add AI response to UI @@ -270,15 +322,21 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases if (!session) return; try { - // Delete all messages for this session - const { error } = await supabase - .from('ai_chat_messages') - .delete() - .eq('session_id', session.id); + // Call edge function to clear chat (messages + session) + const { error, data } = await supabase.functions.invoke('ai-chat', { + body: { + action: 'clear_chat', + sessionId: session.id, + userId: user?.id, + }, + }); - if (error) throw error; + if (error || (data && data.ok === false)) { + throw error || new Error('Failed to clear chat'); + } setMessages([]); + setSession(null); toast({ title: "Success", @@ -293,19 +351,67 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases variant: "destructive" }); } - }, [session, toast]); + }, [session, user?.id, toast]); // Initialize session on mount useEffect(() => { initializeSession(); }, [initializeSession]); + // Generate diagram without posting a user message + const requestDiagram = useCallback(async (sourceText: string) => { + if (!session || isTyping) return; + setIsTyping(true); + try { + const conversationHistory = messages.map(msg => ({ role: msg.role, content: msg.content })); + const { data, error } = await supabase.functions.invoke('ai-chat', { + body: { + message: sourceText, + problemDescription, + conversationHistory, + diagram: true, + preferredEngines: ['reactflow', 'mermaid'] + } + }); + if (error) throw error; + + const diagramPayload = getDiagramPayload(data?.diagram ?? data); + + if (!diagramPayload) { + toast({ title: 'No diagram', description: 'The model did not return a diagram for this request.' }); + return; + } + + const aiResponse: ChatMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: '', + timestamp: new Date(), + sessionId: session.id, + diagram: diagramPayload + }; + + setMessages(prev => [...prev, aiResponse]); + await saveMessage(aiResponse); + } catch (error) { + console.error('Error generating diagram:', error); + toast({ + title: 'Diagram error', + description: 'Failed to generate diagram. Please try again.', + variant: 'destructive' + }); + } finally { + setIsTyping(false); + } + }, [session, isTyping, messages, problemDescription, saveMessage, toast]); + return { session, messages, loading, isTyping, sendMessage, - clearConversation + clearConversation, + requestDiagram }; }; \ No newline at end of file diff --git a/src/pages/ProblemSolver.tsx b/src/pages/ProblemSolver.tsx index 4fe8177..42bb9f3 100644 --- a/src/pages/ProblemSolver.tsx +++ b/src/pages/ProblemSolver.tsx @@ -5,26 +5,18 @@ import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/componen import CodeEditor from '@/components/CodeEditor'; import AIChat from '@/components/AIChat'; import Notes from '@/components/Notes'; -import { ArrowLeft, Star, StarOff, Copy, Check, X, Clock, Calendar } from 'lucide-react'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import safeStableStringify from 'safe-stable-stringify'; -import { useTheme } from '@/hooks/useTheme'; -import { pythonSolutions, Solution } from '@/data/pythonSolutions'; +import { ArrowLeft, Star, StarOff, Copy, Check, X, Clock } from 'lucide-react'; import { useParams, useNavigate } from 'react-router-dom'; -import { toast } from 'sonner'; import { useAuth } from '@/hooks/useAuth'; import { useProblems } from '@/hooks/useProblems'; import { useUserStats } from '@/hooks/useUserStats'; -import { useSubmissions } from '@/hooks/useSubmissions'; import { UserAttemptsService } from '@/services/userAttempts'; import { TestRunnerService } from '@/services/testRunner'; import { TestCase, TestResult, CodeSnippet } from '@/types'; -import { useState, useEffect, useRef, useMemo } from 'react'; -import { insertCodeSnippet } from '@/utils/codeInsertion'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { toast } from 'sonner'; import Timer from '@/components/Timer'; import { supabase } from '@/integrations/supabase/client'; -import { ScrollArea } from '@/components/ui/scroll-area'; const ProblemSolver = () => { const { problemId } = useParams<{ problemId: string }>(); @@ -32,21 +24,17 @@ const ProblemSolver = () => { const { user } = useAuth(); const { problems, toggleStar, loading, error, refetch } = useProblems(user?.id); const { updateStatsOnProblemSolved } = useUserStats(user?.id); - const { submissions, loading: submissionsLoading, refetch: refetchSubmissions } = useSubmissions(user?.id, problemId); - const { isDark } = useTheme(); const [activeTab, setActiveTab] = useState('question'); - - const [code, setCode] = useState(''); const [testResults, setTestResults] = useState([]); const [isRunning, setIsRunning] = useState(false); const codeEditorRef = useRef<{ getValue: () => string; setValue: (value: string) => void; - getPosition: () => any; - setPosition: (position: any) => void; + getPosition: () => { lineNumber: number; column: number } | null; + setPosition: (pos: { lineNumber: number; column: number }) => void; focus: () => void; - deltaDecorations: (oldDecorations: string[], newDecorations: any[]) => string[]; + deltaDecorations: (oldDecorations: string[], newDecorations: unknown[]) => string[]; } | null>(null); // Panel visibility state @@ -64,50 +52,23 @@ const ProblemSolver = () => { }); // Panel toggle functions - const toggleLeftPanel = () => { + const toggleLeftPanel = useCallback(() => { const newValue = !showLeftPanel; setShowLeftPanel(newValue); localStorage.setItem('showLeftPanel', JSON.stringify(newValue)); - }; - - // Compact JSON formatter for single-line array/object display with circular reference protection - const toCompactJson = (value: any): string => { - if (typeof value === 'string') { - const trimmed = value.trim(); - if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) { - try { - const parsed = JSON.parse(trimmed); - const result = safeStableStringify(parsed); - return result; - } catch { - return trimmed; - } - } - return JSON.stringify(value); - } - try { - const result = safeStableStringify(value); - return result; - } catch { - try { - return JSON.stringify(value); - } catch { - return String(value); - } - } - }; + }, [showLeftPanel]); - const toggleBottomPanel = () => { + const toggleBottomPanel = useCallback(() => { const newValue = !showBottomPanel; setShowBottomPanel(newValue); localStorage.setItem('showBottomPanel', JSON.stringify(newValue)); - }; + }, [showBottomPanel]); - const toggleRightPanel = () => { + const toggleRightPanel = useCallback(() => { const newValue = !showRightPanel; setShowRightPanel(newValue); localStorage.setItem('showRightPanel', JSON.stringify(newValue)); - }; + }, [showRightPanel]); // Keyboard shortcuts - moved to top with other hooks useEffect(() => { @@ -140,22 +101,9 @@ const ProblemSolver = () => { return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [toggleLeftPanel, toggleBottomPanel, toggleRightPanel]); + }, [showLeftPanel, showBottomPanel, showRightPanel, toggleLeftPanel, toggleBottomPanel, toggleRightPanel]); const problem = problems.find(p => p.id === problemId); - - // Deduplicate submissions by code content, keeping the most recent for each unique solution - const uniqueSubmissions = useMemo(() => { - const sorted = [...(submissions || [])].sort((a, b) => - new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); - const byCode = new Map[number]>(); - for (const s of sorted) { - const key = s.code.trim(); - if (!byCode.has(key)) byCode.set(key, s); - } - return Array.from(byCode.values()); - }, [submissions]); if (loading) { return ( @@ -214,7 +162,7 @@ const ProblemSolver = () => { snippetType: snippet.insertionHint?.type }); - // Try backend GPT-assisted insertion first + // Always prefer backend GPT-guided insertion for precise placement let newCodeFromBackend: string | null = null; let insertedAtLine: number | undefined; try { @@ -225,7 +173,6 @@ const ProblemSolver = () => { snippet, cursorPosition, problemDescription: problem.description, - // Minimal message/context to satisfy backend shape message: '[snippet insertion request]', conversationHistory: [] } @@ -240,18 +187,20 @@ const ProblemSolver = () => { console.warn('Backend insert_snippet failed, falling back to local:', e); } - // Fallback to local insertion if backend failed or did not change code - const result = newCodeFromBackend - ? { - newCode: newCodeFromBackend, - newCursorPosition: { - line: typeof insertedAtLine === 'number' && insertedAtLine >= 0 - ? insertedAtLine + (snippet.code.split('\n').length - 1) - : cursorPosition.line, - column: 0 - } - } - : insertCodeSnippet(currentCode, snippet, cursorPosition); + if (!newCodeFromBackend) { + toast.error('AI placement failed. Please try again.'); + return; + } + + const result: { newCode: string; newCursorPosition: { line: number; column: number } } = { + newCode: newCodeFromBackend, + newCursorPosition: { + line: typeof insertedAtLine === 'number' && insertedAtLine >= 0 + ? insertedAtLine + (snippet.code.split('\n').length - 1) + : cursorPosition.line, + column: 0 + } + }; console.log('✨ Insertion result:', result); // Use Monaco's setValue to update the editor directly @@ -271,17 +220,21 @@ const ProblemSolver = () => { editor.focus(); // Add temporary highlight - const decorations = editor.deltaDecorations([], [{ - range: new (window as any).monaco.Range( - Math.max(1, result.newCursorPosition.line - snippet.code.split('\n').length + 2), - 1, - result.newCursorPosition.line + 1, - result.newCursorPosition.column + 1 - ), + const monaco = (window as unknown as { monaco?: { Range: new (startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) => unknown } }).monaco; + const linesAdded = snippet.code.split('\n').length; + const startLine = Math.max(1, result.newCursorPosition.line - linesAdded + 2); + const endLine = startLine + linesAdded - 1; + const startColumn = 1; + const endColumn = result.newCursorPosition.column + 1; + const highlightRange = monaco?.Range + ? new monaco.Range(startLine, startColumn, endLine, endColumn) + : undefined; + const decorations = editor.deltaDecorations([], highlightRange ? [{ + range: highlightRange, options: { className: 'inserted-code-highlight' } - }]); + }] : []); // Remove highlight after 2 seconds setTimeout(() => { @@ -320,8 +273,6 @@ const ProblemSolver = () => { toast.success('All tests passed! 🎉'); await UserAttemptsService.markProblemSolved(user.id, problem.id, code, response.results); await handleProblemSolved(problem.difficulty as 'Easy' | 'Medium' | 'Hard'); - // Refetch submissions to show the new accepted solution - await refetchSubmissions(); } else { toast.error(`${passedCount}/${totalCount} test cases passed`); } @@ -361,104 +312,28 @@ const ProblemSolver = () => { }; - const renderValue = (value: any): string => { + const renderValue = (value: unknown): string => { if (value === null || value === undefined) return 'null'; if (typeof value === 'number' || typeof value === 'boolean') return String(value); - - // Handle arrays and objects directly for pretty printing - if (Array.isArray(value)) { - try { - return JSON.stringify(value, null, 2); - } catch { - return String(value); + if (typeof value === 'string') { + // If it looks like JSON, render compact one-line JSON + const trimmed = value.trim(); + if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))){ + try { return JSON.stringify(JSON.parse(trimmed)); } catch { return value; } } + return value; } - if (typeof value === 'object') { try { - return JSON.stringify(value, null, 2); + // Compact one-line JSON for arrays/objects to match Expected/Your Output style + return JSON.stringify(value as Record); } catch { return String(value); } } - - if (typeof value === 'string') { - // Try to pretty-print if it's JSON-like - const trimmed = value.trim(); - if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))){ - try { - const parsed = JSON.parse(trimmed); - return JSON.stringify(parsed, null, 2); - } catch { - return value; - } - } - return value; - } - return String(value); }; - // Human-friendly formatter (not strict JSON): - // - Strings are shown without quotes - // - Arrays render as [ a, b ] or multi-line for nested arrays - // - Objects render as { key: value } - const toHumanReadable = (value: any, indent = 0): string => { - const pad = (n: number) => ' '.repeat(n); - - // If value is a JSON-like string, parse then format recursively - if (typeof value === 'string') { - const trimmed = value.trim(); - const looksJson = - (trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']')) || - (trimmed.startsWith('"') && trimmed.endsWith('"')) || - (trimmed === 'null' || trimmed === 'true' || trimmed === 'false'); - if (looksJson) { - try { - const parsed = JSON.parse(trimmed); - return toHumanReadable(parsed, indent); - } catch { - // fall through to scalar formatting - } - } - } - - const needsQuotes = (s: string): boolean => { - return s === '' || /[\s,[\]{}:]/.test(s); - }; - - const formatScalar = (v: any): string => { - if (v === null || v === undefined) return 'null'; - const t = typeof v; - if (t === 'number' || t === 'boolean') return String(v); - if (t === 'string') return needsQuotes(v) ? `"${v}"` : v; - return String(v); - }; - - if (Array.isArray(value)) { - if (value.length === 0) return '[]'; - const complex = value.some((el) => Array.isArray(el) || (el && typeof el === 'object')); - if (complex) { - const inner = value - .map((el) => `${pad(indent + 2)}${toHumanReadable(el, indent + 2)}`) - .join(',\n'); - return `[\n${inner}\n${pad(indent)}]`; - } - const inner = value.map((el) => formatScalar(el)).join(', '); - return `[ ${inner} ]`; - } - - if (value && typeof value === 'object') { - const keys = Object.keys(value).sort(); - if (keys.length === 0) return '{}'; - const lines = keys.map((k) => `${pad(indent + 2)}${k}: ${toHumanReadable((value as any)[k], indent + 2)}`); - return `{\n${lines.join('\n')}\n${pad(indent)}}`; - } - - return formatScalar(value); - }; - return (
    {/* Header */} @@ -508,8 +383,8 @@ const ProblemSolver = () => { {showLeftPanel && ( <> -
    - +
    +
    @@ -527,222 +402,145 @@ const ProblemSolver = () => {
    -
    - - -
    +
    + +

    Problem Description

    {problem.description}

    -
    +
    - {problem.examples && problem.examples.length > 0 && ( -
    -

    Examples

    -
    - {problem.examples.map((example, index) => ( -
    -
    -
    - Input: {example.input} -
    + {problem.examples && problem.examples.length > 0 && ( +
    +

    Examples

    +
    + {problem.examples.map((example, index) => ( +
    +
    +
    + Input: +
    +{renderValue(example.input)}
    +                                    
    +
    +
    + Output: +
    +{renderValue(example.output)}
    +                                    
    +
    + {example.explanation && (
    - Output: {example.output} + Explanation: {example.explanation}
    - {example.explanation && ( -
    - Explanation: {example.explanation} -
    - )} -
    + )}
    - ))} -
    +
    + ))}
    - )} - +
    + )} - -
    - {problemId && pythonSolutions[problemId] ? ( -
    - {pythonSolutions[problemId].map((solution: Solution, index: number) => ( -
    -

    - {index + 1}. {solution.title} -

    -
    -
    -
    - -
    - -
    - -
    - - {solution.code} - -
    -
    - -
    -
    -

    Explanation

    -

    {solution.explanation}

    -
    -
    -

    Time & Space Complexity

    -
      -
    • • Time complexity: {solution.complexity.time}
    • -
    • • Space complexity: {solution.complexity.space}
    • -
    -
    -
    -
    - ))} + +
    +

    1. Brute Force

    +
    +
    +
    + + +
    - ) : ( -
    -
    No solutions available
    -
    - Solutions for this problem haven't been added yet. -
    + +
    +
    +                            {`def twoSum(self, nums: List[int], target: int) -> List[int]:
    +    for i in range(len(nums)):
    +        for j in range(i + 1, len(nums)):
    +            if nums[i] + nums[j] == target:
    +                return [i, j]
    +    return []`}
    +                          
    +
    + +
    +

    Time & Space Complexity

    +
      +
    • • Time complexity: O(n²)
    • +
    • • Space complexity: O(1)
    • +
    +
    +
    + +
    +

    2. Hash Map

    +
    +
    +
    + + +
    - )} + +
    +
    +                            {`def twoSum(self, nums: List[int], target: int) -> List[int]:
    +    hashmap = {}
    +    for i, num in enumerate(nums):
    +        complement = target - num
    +        if complement in hashmap:
    +            return [hashmap[complement], i]
    +        hashmap[num] = i
    +    return []`}
    +                          
    + +
    +

    Time & Space Complexity

    +
      +
    • • Time complexity: O(n)
    • +
    • • Space complexity: O(n)
    • +
    +
    +
    - - -
    + +

    Submissions

    - {submissionsLoading ? ( -
    -
    -
    - ) : uniqueSubmissions.length === 0 ? ( -
    -
    No accepted submissions yet
    -
    - Solve this problem to see your submissions here! +
    +
    +
    + Accepted +
    +
    + Python + 2 minutes ago
    - ) : ( -
    - {uniqueSubmissions.map((submission, index) => ( -
    -
    -
    - - - Accepted - - - Solution #{uniqueSubmissions.length - index} - -
    -
    -
    - - {new Date(submission.created_at).toLocaleDateString()} -
    - {new Date(submission.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} -
    -
    - - {/* Full code with syntax highlighting */} -
    -
    - - Python Solution - - -
    -
    - - {submission.code} - -
    -
    - - {/* Test results summary */} - {submission.test_results && Array.isArray(submission.test_results) && ( -
    -
    - - - {submission.test_results.filter(r => r.passed).length}/{submission.test_results.length} test cases passed - -
    - {submission.test_results.some(r => r.time) && ( -
    - - Runtime: {submission.test_results.find(r => r.time)?.time || 'N/A'} -
    - )} -
    - )} -
    - ))} +
    +
    + Accepted +
    +
    + Python + 5 minutes ago +
    - )}
    - +
    - - -
    - -
    -
    + +
    @@ -850,15 +648,13 @@ const ProblemSolver = () => {
    Input:
    -
    -{result.input}
    -                                    
    +
    {renderValue(result.input)}
    Expected Output:
    -
    {toCompactJson(result.expected)}
    +
    {renderValue(result.expected)}
    @@ -868,7 +664,7 @@ const ProblemSolver = () => { ? 'text-green-700 dark:text-green-300' : 'text-red-700 dark:text-red-300' }`}> - {toCompactJson(result.actual) || 'No output'} + {renderValue(result.actual) || 'No output'}
    diff --git a/src/types/index.ts b/src/types/index.ts index 6045908..3f956d3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -55,6 +55,13 @@ export interface CodeSnippet { }; } +// React Flow diagram types (declarative, safe to render on client) +export interface FlowNodePosition { x: number; y: number } +export interface FlowNodeData { label: string } +export interface FlowNode { id: string; type?: string; data: FlowNodeData; position: FlowNodePosition } +export interface FlowEdge { id: string; source: string; target: string; label?: string } +export interface FlowGraph { nodes: FlowNode[]; edges: FlowEdge[] } + export interface ChatMessage { id: string; role: 'user' | 'assistant'; @@ -62,6 +69,18 @@ export interface ChatMessage { timestamp: Date; sessionId?: string; codeSnippets?: CodeSnippet[]; + diagram?: + | { + engine: 'mermaid'; + code: string; // raw mermaid DSL + title?: string; + } + | { + engine: 'reactflow'; + graph: FlowGraph; + title?: string; + }; + suggestDiagram?: boolean; } export interface ChatSession { diff --git a/supabase/functions/ai-chat/index.ts b/supabase/functions/ai-chat/index.ts index e3e68f3..8e64b1d 100644 --- a/supabase/functions/ai-chat/index.ts +++ b/supabase/functions/ai-chat/index.ts @@ -26,27 +26,222 @@ interface ChatMessage { } interface RequestBody { - message: string; - problemDescription: string; - conversationHistory: ChatMessage[]; - // Optional action for smart insertion - action?: 'insert_snippet'; + message?: string; + problemDescription?: string; + conversationHistory?: ChatMessage[]; + // Optional action for smart insertion or clearing chat + action?: 'insert_snippet' | 'clear_chat'; + // Optional explicit diagram request + diagram?: boolean; + // Preferred engines order for diagram generation + preferredEngines?: Array<'reactflow' | 'mermaid'>; // Payload for insertion code?: string; snippet?: CodeSnippet; cursorPosition?: { line: number; column: number }; // Optional problem test cases to condition the tutor (will be executed on Judge0) testCases?: unknown[]; + // For clear_chat action + sessionId?: string; + userId?: string; } interface AIResponse { response: string; codeSnippets?: CodeSnippet[]; + diagram?: + | { engine: 'mermaid'; code: string; title?: string } + | { engine: 'reactflow'; graph: { nodes: Array<{ id: string; type?: string; data: { label: string }; position: { x: number; y: number } }>; edges: Array<{ id: string; source: string; target: string; label?: string }> }; title?: string }; + suggestDiagram?: boolean; + diagramDebug?: { + tried: Array<'reactflow' | 'mermaid'>; + reactflow?: { ok: boolean; reason?: string }; + mermaid?: { ok: boolean; reason?: string }; + }; } // Initialize OpenAI client (will be created with proper error handling in the handler) let openai: OpenAI; +// Model selection via env var; default to o3-mini if not set +const configuredModel = (Deno.env.get('OPENAI_MODEL') || 'o3-mini').trim(); +const modelSource = Deno.env.get('OPENAI_MODEL') ? 'OPENAI_MODEL env set' : 'defaulted to o3-mini (no OPENAI_MODEL)'; +const useResponsesApi = /^(gpt-5|o3)/i.test(configuredModel); + +// ---- Responses API helpers ---- +type ResponsesApiRequest = { + model: string; + input: string; + max_output_tokens?: number; + reasoning?: { effort: 'minimal' | 'medium' | 'high' }; + text?: { verbosity?: 'low' | 'medium' | 'high'; format?: { type: 'json_object' } }; +}; + +function buildResponsesRequest( + model: string, + prompt: string, + opts: { maxTokens?: number; responseFormat?: 'json_object' | undefined } +): ResponsesApiRequest { + const req: ResponsesApiRequest = { + model, + input: prompt, + max_output_tokens: typeof opts.maxTokens === 'number' ? opts.maxTokens : undefined, + }; + if (/^gpt-5/i.test(model)) { + req.reasoning = { effort: 'minimal' }; + req.text = { + verbosity: opts.responseFormat ? 'low' : 'medium', + ...(opts.responseFormat ? { format: { type: 'json_object' } } : {}), + }; + } else if (/^o3/i.test(model)) { + req.reasoning = { effort: 'medium' }; + // o3 may ignore text.verbosity; omit for safety + } + return req; +} + +type ResponsesApiResponse = { + output_text?: string; + output?: Array<{ content?: Array<{ type?: string; text?: { value?: string } | string }> }>; + choices?: Array<{ message?: { content?: string } }>; +}; + +function extractResponsesText(response: ResponsesApiResponse): string { + // 1) Direct output_text + const direct = typeof response?.output_text === 'string' ? response.output_text : ''; + if (direct) return direct; + // 2) Traverse output[].content[] for output_text/text + const output = Array.isArray(response?.output) ? response.output : []; + let text = ''; + for (const item of output) { + const content = Array.isArray(item?.content) ? item.content : []; + for (const c of content) { + const type = c?.type; + const textField = (c as { text?: { value?: string } | string })?.text as unknown; + const nestedValue = textField && typeof textField === 'object' && 'value' in (textField as Record) + ? (textField as { value?: string }).value + : undefined; + if (type === 'output_text' && typeof nestedValue === 'string') { + text += nestedValue; + } else if (type === 'text') { + if (typeof textField === 'string') text += textField; + else if (typeof nestedValue === 'string') text += nestedValue; + } + } + } + if (text) return text; + // 3) Fallback to chat-like choices + const choices = Array.isArray(response?.choices) ? response.choices : []; + return choices?.[0]?.message?.content || ''; +} + +// Unified LLM callers (supports Responses API for gpt-5/o3 and falls back to Chat Completions) +async function llmText( + prompt: string, + opts: { temperature?: number; maxTokens?: number; responseFormat?: 'json_object' | undefined } +): Promise { + const model = configuredModel; + if (useResponsesApi) { + // Try configured Responses model first, then o3-mini, then fall back to Chat API + const responseModels = [model, 'o3-mini'].filter((v, i, a) => a.indexOf(v) === i); + for (const respModel of responseModels) { + try { + console.log(`[ai-chat] Using Responses API with model=${respModel}`); + const req = buildResponsesRequest(respModel, prompt, { maxTokens: opts.maxTokens, responseFormat: opts.responseFormat }); + const response = await openai.responses.create(req as unknown as ResponsesApiResponse); + const finalText = extractResponsesText(response).toString(); + if (finalText.trim().length > 0) { + return finalText; + } + console.warn(`[ai-chat] Responses API returned empty text for model=${respModel}; trying next option...`); + continue; + } catch (e) { + const err = e as unknown as { name?: string; message?: string }; + console.warn(`[ai-chat] Responses API failed for model=${respModel}. ${err?.name || ''}: ${err?.message || ''}`); + continue; + } + } + console.warn(`[ai-chat] All Responses API attempts failed; falling back to Chat Completions.`); + } + const chatModel = useResponsesApi ? 'gpt-4o-mini' : model; + console.log(`[ai-chat] Using Chat Completions API with model=${chatModel} (fallback=${useResponsesApi ? 'yes' : 'no'})`); + const chat = await openai.chat.completions.create({ + model: chatModel, + messages: [{ role: 'user', content: prompt }], + temperature: opts.temperature ?? 0.7, + max_tokens: opts.maxTokens ?? 500, + response_format: opts.responseFormat ? ({ type: opts.responseFormat } as { type: 'json_object' }) : undefined, + } as unknown as { choices: Array<{ message?: { content?: string } }> }); + return chat.choices[0]?.message?.content || ''; +} + +async function llmJson( + prompt: string, + opts: { temperature?: number; maxTokens?: number } +): Promise { + return await llmText(prompt, { + temperature: opts.temperature, + maxTokens: opts.maxTokens, + responseFormat: 'json_object', + }); +} + +/** + * Fast JSON helper: force a lightweight, fast model for tool-style calls + */ +async function llmJsonFast( + prompt: string, + opts?: { maxTokens?: number } +): Promise { + // Prefer Responses API for gpt-5-mini, fall back to Chat Completions with gpt-4o-mini + try { + console.log('[ai-chat] llmJsonFast using Responses API with model=gpt-5-mini'); + const req: Record = { + model: 'gpt-5-mini', + input: prompt, + max_output_tokens: typeof opts?.maxTokens === 'number' ? opts.maxTokens : 600, + text: { verbosity: 'low', format: { type: 'json_object' } }, + reasoning: { effort: 'minimal' }, + }; + const response = await openai.responses.create(req as unknown as { + output_text?: string; + output?: Array<{ content?: Array<{ type?: string; text?: { value?: string } | string }> }>; + }); + let text: string = (response as unknown as { output_text?: string }).output_text || ''; + const output: Array<{ content?: Array<{ type?: string; text?: { value?: string } | string }> }> = + (response as unknown as { output?: Array<{ content?: Array<{ type?: string; text?: { value?: string } | string }> }> }).output || []; + if (!text && Array.isArray(output)) { + for (const item of output) { + const content = Array.isArray(item?.content) ? item.content : []; + for (const c of content) { + const type = (c as { type?: string })?.type; + const textField = (c as { text?: { value?: string } | string })?.text as unknown; + const nestedValue = (textField && typeof textField === 'object' && 'value' in (textField as Record) + ? (textField as { value?: string }).value + : undefined); + if (type === 'output_text' && typeof nestedValue === 'string') { + text += nestedValue; + } else if (type === 'text') { + if (typeof textField === 'string') text += textField; + else if (typeof nestedValue === 'string') text += nestedValue; + } + } + } + } + return text; + } catch (err) { + console.warn('[ai-chat] llmJsonFast gpt-5-mini Responses failed; falling back to gpt-4o-mini Chat. Error:', err); + const chat = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: prompt }], + temperature: 0.0, + max_tokens: opts?.maxTokens ?? 600, + response_format: { type: 'json_object' } as { type: 'json_object' }, + } as unknown as { choices: Array<{ message?: { content?: string } }> }); + return chat.choices[0]?.message?.content || ''; + } +} + // Initialize Supabase client const supabaseUrl = Deno.env.get('SUPABASE_URL'); const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY'); @@ -57,6 +252,10 @@ if (!supabaseUrl || !supabaseKey) { const supabase = createClient(supabaseUrl, supabaseKey); +// Admin client (if service role key is provided) for maintenance actions like clearing chat +const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'); +const supabaseAdmin = supabaseServiceKey ? createClient(supabaseUrl, supabaseServiceKey) : supabase; + /** * Main conversation handler - generates AI response for general chat */ @@ -106,23 +305,293 @@ Important constraints: Respond naturally and conversationally. Focus on teaching and guiding rather than just providing answers. `; - const response = await openai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [ - { - role: "system", - content: "You are a helpful coding tutor. Be encouraging and educational. IMPORTANT: Do not provide code (no code blocks, no pseudo-code) unless the student explicitly asks for code or has shared code to review. Prefer questions and high-level hints first. Testing is handled automatically by Judge0 with official test cases — never ask the student to run tests, write tests, or provide test cases. You may discuss potential edge cases conceptually. Only after a likely-correct solution, ask one follow-up on time/space complexity." - }, - { - role: "user", - content: conversationPrompt - } + const systemGuidance = "You are a helpful coding tutor. Be encouraging and educational. IMPORTANT: Do not provide code (no code blocks, no pseudo-code) unless the student explicitly asks for code or has shared code to review. Prefer questions and high-level hints first. The student's code is auto-run on Judge0 with official tests; avoid asking them to run tests or provide test cases. Only after a likely-correct solution, ask one follow-up on time/space complexity."; + const combined = `${systemGuidance}\n\n${conversationPrompt}`; + const text = await llmText(combined, { temperature: 0.7, maxTokens: 500 }); + return text || "I'm sorry, I couldn't generate a response. Please try again."; +} + +// React Flow types and validator +type FlowNode = { id: string; type?: string; data: { label: string }; position: { x: number; y: number } }; +type FlowEdge = { id: string; source: string; target: string; label?: string }; +type FlowGraph = { nodes: FlowNode[]; edges: FlowEdge[] }; + +function validateFlowGraph(graph: unknown): graph is FlowGraph { + console.log('[Validation] Starting validation for:', JSON.stringify(graph, null, 2)); + + if (!graph || typeof graph !== 'object') { + console.log('[Validation] Failed: graph is not an object'); + return false; + } + + const g = graph as { nodes?: unknown; edges?: unknown }; + if (!Array.isArray(g.nodes)) { + console.log('[Validation] Failed: nodes is not an array, got:', typeof g.nodes); + return false; + } + if (!Array.isArray(g.edges)) { + console.log('[Validation] Failed: edges is not an array, got:', typeof g.edges); + return false; + } + + console.log(`[Validation] Found ${g.nodes.length} nodes and ${g.edges.length} edges`); + + const idSet = new Set(); + for (let i = 0; i < g.nodes.length; i++) { + const n = g.nodes[i]; + const node = n as FlowNode; + console.log(`[Validation] Checking node ${i}:`, JSON.stringify(node, null, 2)); + + if (!node || typeof node.id !== 'string') { + console.log(`[Validation] Failed: node ${i} has invalid id:`, typeof node?.id); + return false; + } + if (!node.position || typeof node.position.x !== 'number' || typeof node.position.y !== 'number') { + console.log(`[Validation] Failed: node ${i} has invalid position:`, node.position); + return false; + } + if (!node.data || typeof node.data.label !== 'string') { + console.log(`[Validation] Failed: node ${i} has invalid data:`, node.data); + return false; + } + if (idSet.has(node.id)) { + console.log(`[Validation] Failed: duplicate node id: ${node.id}`); + return false; + } + idSet.add(node.id); + } + + for (let i = 0; i < g.edges.length; i++) { + const e = g.edges[i]; + const edge = e as FlowEdge; + console.log(`[Validation] Checking edge ${i}:`, JSON.stringify(edge, null, 2)); + + if (!edge || typeof edge.id !== 'string' || typeof edge.source !== 'string' || typeof edge.target !== 'string') { + console.log(`[Validation] Failed: edge ${i} has invalid properties:`, { + id: typeof edge?.id, + source: typeof edge?.source, + target: typeof edge?.target + }); + return false; + } + } + + console.log('[Validation] All checks passed!'); + return true; +} + +/** + * Try to generate a React Flow diagram first; fallback to Mermaid + */ +async function maybeGenerateDiagram( + message: string, + problemDescription: string, + conversationHistory: ChatMessage[], + force = false, + preferredEngines?: Array<'reactflow' | 'mermaid'> +): Promise< + | { engine: 'reactflow'; graph: FlowGraph; title?: string } + | { engine: 'mermaid'; code: string; title?: string } + | undefined +> { + const wantsDiagram = /\b(visualize|diagram|draw|show.*diagram|mermaid|flow|graph)\b/i.test(message); + if (!force && !wantsDiagram) return undefined; + const engineOrder: Array<'reactflow' | 'mermaid'> = Array.isArray(preferredEngines) && preferredEngines.length + ? Array.from(new Set(preferredEngines.concat(['mermaid', 'reactflow'] as const))) as Array<'reactflow' | 'mermaid'> + : ['mermaid', 'reactflow']; + + const rfPrompt = `Create a React Flow diagram that visualizes the algorithm step-by-step with meaningful progression. + +OUTPUT FORMAT (copy structure exactly): +{ + "reactflow": { + "nodes": [ + {"id": "n1", "type": "default", "data": {"label": "Initialize variables"}, "position": {"x": 100, "y": 50}}, + {"id": "n2", "type": "default", "data": {"label": "Check condition"}, "position": {"x": 100, "y": 150}}, + {"id": "n3", "type": "default", "data": {"label": "Process step"}, "position": {"x": 100, "y": 250}} ], - temperature: 0.5, - max_tokens: 500 - }); + "edges": [ + {"id": "e1", "source": "n1", "target": "n2"}, + {"id": "e2", "source": "n2", "target": "n3"} + ] + } +} - return response.choices[0]?.message?.content || "I'm sorry, I couldn't generate a response. Please try again."; +REQUIREMENTS: +- Create 6-10 nodes showing algorithm flow +- Include key algorithmic concepts: initialization, loops, conditions, updates +- For data structures: show operations like "compare", "merge", "split", "traverse" +- For search algorithms: show "check condition", "update bounds", "found/not found" +- For dynamic programming: show "base case", "recurrence", "memoize" +- For sorting: show "partition", "merge", "swap" +- Use decision branches for conditional logic (spread horizontally +200px) +- Show meaningful progression that teaches the approach +- Space vertically +100px, horizontally +200px for branches +- Node labels max 25 characters, clear and educational +- Simple IDs: n1, n2, n3, etc. + +Problem context: ${problemDescription} +User request: ${message} +Conversation context: ${conversationHistory.slice(-3).map(m => m.content).join(' ').substring(0, 300)} + +Create an educational algorithm visualization:`; + + // Attempt functions + const tryReactFlow = async (): Promise<{ ok: true; diagram: { engine: 'reactflow'; graph: FlowGraph } } | { ok: false; reason: string }> => { + let raw = ''; + try { + raw = (await llmJson(rfPrompt, { temperature: 0.2, maxTokens: 700 })).trim(); + console.log('[React Flow] Raw AI response:', raw.substring(0, 500) + (raw.length > 500 ? '...' : '')); + } catch (e) { + console.log('[React Flow] llmJson failed, trying llmText:', e); + try { + raw = (await llmText(rfPrompt, { temperature: 0.2, maxTokens: 700 })).trim(); + console.log('[React Flow] Raw AI response from llmText:', raw.substring(0, 500) + (raw.length > 500 ? '...' : '')); + } catch (e2) { + console.log('[React Flow] Both llmJson and llmText failed:', e2); + return { ok: false, reason: `Responses+fallback failed: ${(e2 as Error)?.message || 'unknown error'}` }; + } + } + if (!raw) { + console.log('[React Flow] Empty response'); + return { ok: false, reason: 'Empty response for reactflow JSON' }; + } + try { + // Clean the response to extract JSON + let cleanJson = raw.trim(); + + // Remove markdown code blocks if present + if (cleanJson.startsWith('```')) { + cleanJson = cleanJson.replace(/^```(?:json)?/m, '').replace(/```$/m, '').trim(); + } + + // Try to find JSON object boundaries + const jsonStart = cleanJson.indexOf('{'); + const jsonEnd = cleanJson.lastIndexOf('}'); + if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { + cleanJson = cleanJson.substring(jsonStart, jsonEnd + 1); + } + + console.log('[React Flow] Cleaned JSON:', cleanJson); + + const parsed = JSON.parse(cleanJson); + console.log('[React Flow] Parsed JSON:', JSON.stringify(parsed, null, 2)); + const rf = (parsed && parsed.reactflow) ? parsed.reactflow : undefined; + if (!rf) { + console.log('[React Flow] Missing reactflow key in parsed JSON'); + return { ok: false, reason: 'Missing reactflow key in JSON' }; + } + console.log('[React Flow] Found reactflow data:', JSON.stringify(rf, null, 2)); + if (validateFlowGraph(rf)) { + console.log('[React Flow] Validation passed, returning diagram'); + return { ok: true, diagram: { engine: 'reactflow', graph: rf } }; + } + console.log('[React Flow] Schema validation failed'); + return { ok: false, reason: 'Schema validation failed' }; + } catch (parseErr) { + console.log('[React Flow] JSON parse error:', parseErr, 'Raw:', raw); + return { ok: false, reason: `Invalid JSON: ${(parseErr as Error)?.message || 'parse error'}` }; + } + }; + + const mermaidPrompt = `Output ONLY a JSON with a simple Mermaid flowchart. Use this exact format: + +{ + "mermaid": "flowchart TD\\n A[Start] --> B[Compare]\\n B --> C[Choose smaller]\\n C --> D[End]" +} + +RULES: +- Use "flowchart TD" syntax only +- Maximum 8 nodes for simplicity +- Node labels must be simple text in brackets: [Text here] +- Use --> for arrows +- No special characters except spaces and hyphens in labels +- Each line must end with \\n +- Keep labels under 15 characters + +For: ${problemDescription.split('.')[0]} +User request: ${message} + +Output only the JSON:`; + + const tryMermaid = async (): Promise<{ ok: true; diagram: { engine: 'mermaid'; code: string } } | { ok: false; reason: string }> => { + let mermaidRaw = ''; + try { + mermaidRaw = (await llmJson(mermaidPrompt, { temperature: 0.2, maxTokens: 700 })).trim(); + } catch (e) { + try { + mermaidRaw = (await llmText(mermaidPrompt, { temperature: 0.2, maxTokens: 700 })).trim(); + } catch (e2) { + return { ok: false, reason: `Responses+fallback failed: ${(e2 as Error)?.message || 'unknown error'}` }; + } + } + if (!mermaidRaw) return { ok: false, reason: 'Empty response for mermaid JSON' }; + let mermaidCode = ''; + try { + const parsed = JSON.parse(mermaidRaw); + if (parsed && typeof parsed.mermaid === 'string') { + mermaidCode = String(parsed.mermaid); + } + } catch (parseErr) { + const fence = mermaidRaw.match(/```mermaid([\s\S]*?)```/i); + if (fence && fence[1]) { + mermaidCode = fence[1]; + } else if (/flowchart\s+LR|graph\s+LR/i.test(mermaidRaw)) { + mermaidCode = mermaidRaw; + } else { + return { ok: false, reason: `Invalid JSON and no fence: ${(parseErr as Error)?.message || 'parse error'}` }; + } + } + const sanitized = (mermaidCode || '') + .replace(/^```mermaid\n?/i, '') + .replace(/```$/i, '') + .trim(); + if (!sanitized) return { ok: false, reason: 'Mermaid content empty after sanitization' }; + return { ok: true, diagram: { engine: 'mermaid', code: sanitized } }; + }; + + const tried: Array<'reactflow' | 'mermaid'> = []; + const debug: { reactflow?: { ok: boolean; reason?: string }; mermaid?: { ok: boolean; reason?: string } } = {}; + + console.log('[Diagram Generation] Engine order:', engineOrder); + + for (const engine of engineOrder) { + console.log(`[Diagram Generation] Trying engine: ${engine}`); + if (engine === 'reactflow') { + // Temporarily disable React Flow - use Mermaid only for now + tried.push('reactflow'); + console.log('[Diagram Generation] React Flow disabled, skipping'); + debug.reactflow = { ok: false, reason: 'React Flow temporarily disabled' }; + } else if (engine === 'mermaid') { + tried.push('mermaid'); + const mm = await tryMermaid(); + if (mm.ok) { + console.log('[Diagram Generation] Mermaid succeeded'); + return mm.diagram; + } + console.log('[Diagram Generation] Mermaid failed:', mm.reason); + debug.mermaid = { ok: false, reason: mm.reason }; + } + } + + // No diagram; attach debug info to console + console.warn('[ai-chat] Diagram generation failed', { tried: engineOrder, ...debug }); + return undefined; +} + +// Generate a brief, friendly explanation of a given Mermaid diagram +async function explainMermaid( + mermaidCode: string, + problemDescription: string +): Promise { + const explainPrompt = `You are a helpful tutor. In 2-4 short bullet points, explain the following Mermaid diagram for a coding problem. Avoid code, be concise, no questions. + +Problem context (for reference): ${problemDescription} + +Diagram (Mermaid DSL):\n${mermaidCode}`; + + const text = await llmText(explainPrompt, { temperature: 0.5, maxTokens: 180 }); + return text?.trim() || 'Here is the diagram.'; } /** @@ -137,12 +606,12 @@ async function analyzeCodeSnippets( // Only analyze if message clearly indicates code intent const hasExplicitCode = /```[\s\S]*?```|`[^`]+`/m.test(message); const explicitAsk = /\b(write|show|give|provide|insert|add|implement|code|import|define|declare|create)\b/i.test(message); - const looksLikeCode = /^(\s*)(def|class)\s+\w+|^(\s*)\w+\s*=\s*.+|\b\w+\(.*\)|\bfrom\b\s+\w+\s+\bimport\b/m.test(message); + // Don't auto-trigger on vague code-like text; rely on explicit ask or explicit code + const looksLikeCode = false; const lastAssistant = (conversationHistory || []).slice().reverse().find(m => m.role === 'assistant')?.content?.trim() || ''; const assistantJustAskedQuestion = /\?\s*$/.test(lastAssistant); - // Gate strictly: only if the user pasted code, explicitly asked, or message looks like code - const allowAnalysis = hasExplicitCode || explicitAsk || looksLikeCode; + const allowAnalysis = hasExplicitCode || explicitAsk; if (!allowAnalysis || (assistantJustAskedQuestion && !hasExplicitCode)) { return []; } @@ -216,53 +685,45 @@ Student: "Maybe if char in seen:" Response: Provide complete conditional logic with proper indentation Student: "Two pointers approach?" -Respond with conceptual guidance only unless the student explicitly asks for code or pastes code.`; +Respond by extracting any concrete, safe-to-insert scaffolding (e.g., pointer initialization), but avoid full solutions unless explicitly requested.`; try { - const response = await openai.chat.completions.create({ - model: "gpt-4o-mini", - messages: [{ role: "user", content: analysisPrompt }], - response_format: { type: "json_object" }, - temperature: 0.1, // Low temperature for consistent analysis - max_tokens: 1000 - }); - + const raw = await llmJson(analysisPrompt, { temperature: 0.1, maxTokens: 1000 }); let analysisResult; try { - analysisResult = JSON.parse( - response.choices[0]?.message?.content || '{"codeSnippets": []}' - ); + analysisResult = JSON.parse(raw || '{"codeSnippets": []}'); } catch (parseError) { console.error('Failed to parse AI response:', parseError); return []; } // Add unique IDs and validate structure - const codeSnippets: CodeSnippet[] = (analysisResult.codeSnippets || []).map((snippet: Record, index: number) => ({ + const codeSnippets: CodeSnippet[] = (analysisResult.codeSnippets || []).map((snippet: { [k: string]: unknown; insertionHint?: { type?: string; scope?: string; description?: string } }, index: number) => ({ id: `snippet-${Date.now()}-${index}`, code: typeof snippet.code === 'string' ? snippet.code : '', language: typeof snippet.language === 'string' ? (snippet.language as string) : 'python', isValidated: typeof snippet.isValidated === 'boolean' ? (snippet.isValidated as boolean) : false, - insertionType: typeof snippet.insertionType === 'string' ? (snippet.insertionType as any) : 'smart', + insertionType: typeof snippet.insertionType === 'string' ? (snippet.insertionType as 'smart' | 'cursor' | 'append' | 'prepend' | 'replace') : 'smart', insertionHint: { - type: typeof (snippet as any).insertionHint?.type === 'string' ? (snippet as any).insertionHint.type : 'statement', - scope: typeof (snippet as any).insertionHint?.scope === 'string' ? (snippet as any).insertionHint.scope : 'function', - description: typeof (snippet as any).insertionHint?.description === 'string' ? (snippet as any).insertionHint.description : 'Code snippet' + type: typeof snippet.insertionHint?.type === 'string' ? snippet.insertionHint.type as 'import' | 'variable' | 'function' | 'statement' | 'class' : 'statement', + scope: typeof snippet.insertionHint?.scope === 'string' ? snippet.insertionHint.scope as 'global' | 'function' | 'class' : 'function', + description: typeof snippet.insertionHint?.description === 'string' ? snippet.insertionHint.description : 'Code snippet' } })); - // Filter validated and remove incomplete control-flow headers - const validated = codeSnippets.filter(snippet => - snippet.code && snippet.code.trim().length > 0 && snippet.isValidated - ).filter(snippet => { - const c = snippet.code.trim(); - const incompleteHeader = /^(for\s+\w+\s+in\s+\w+\s*:\s*$)|(if\s+.+:\s*$)|(while\s+.+:\s*$)/.test(c); - return !incompleteHeader; - }).filter(snippet => { - // Drop import suggestions unless the user explicitly asked about imports - const isImportSnippet = (snippet.insertionHint?.type === 'import') || /^\s*(from\s+\S+\s+import\s+\S+|import\s+\S+)/.test(snippet.code); - const explicitImportAsk = /\b(import|from\s+\w+\s+import|how\s+to\s+import)\b/i.test(message); - return !isImportSnippet || explicitImportAsk; + const normalizedMessage = message.replace(/\s+/g, ' ').toLowerCase(); + const validated = codeSnippets + .filter(snippet => snippet.code && snippet.code.trim().length > 0 && snippet.isValidated) + .filter(snippet => { + const code = snippet.code.trim(); + const lower = code.replace(/\s+/g, ' ').toLowerCase(); + const lineCount = code.split('\n').length; + const isControlFlow = /(^|\n)\s*(if|for|while)\b/.test(code) || /(^|\n)\s*(class|def)\b/.test(code); + const tooLong = code.length > 200 || lineCount > 3; + const appearsInUserText = normalizedMessage.includes(lower); + const isSimpleType = snippet.insertionHint?.type === 'import' || snippet.insertionHint?.type === 'variable' || snippet.insertionHint?.type === 'statement'; + const allowedByPolicy = (hasExplicitCode || explicitAsk) && (appearsInUserText || isSimpleType); + return allowedByPolicy && !isControlFlow && !tooLong; }); // Dedupe within the same response @@ -284,7 +745,7 @@ Respond with conceptual guidance only unless the student explicitly asks for cod } /** - * Use LLM to compute the best insertion point and return updated code + * Use LLM to compute the best insertion line and indentation, then deterministically insert ONLY the snippet */ async function insertSnippetSmart( code: string, @@ -292,7 +753,34 @@ async function insertSnippetSmart( problemDescription: string, cursorPosition?: { line: number; column: number } ): Promise<{ newCode: string; insertedAtLine?: number; rationale?: string }> { - const prompt = `You are assisting with inserting a small code snippet into a student's Python solution file. + // 1) Deterministic, fast path: anchor-based insertion if the first line exists + try { + const snippetLines = (snippet.code || '').split('\n'); + const firstLineTrim = (snippetLines[0] || '').trim(); + if (firstLineTrim.length > 0) { + const lines = code.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim() === firstLineTrim) { + // If next lines already match snippet continuation, skip (already inserted) + const secondLineTrim = (snippetLines[1] || '').trim(); + if (secondLineTrim && lines[i + 1]?.trim() === secondLineTrim) { + return { newCode: code, insertedAtLine: -1, rationale: 'Snippet already present after anchor' }; + } + const indent = lines[i].match(/^\s*/)?.[0] || ''; + const toInsert = snippetLines.slice(1).map((l, idx) => (idx === 0 ? indent + l.trim() : indent + l)); + const newLines = [...lines]; + newLines.splice(i + 1, 0, ...toInsert); + const newCode = newLines.join('\n'); + console.log('[ai-chat] insert_snippet anchor-based insertion at line', i + 1); + return { newCode, insertedAtLine: i + 1, rationale: 'Anchor-based placement after matching first line' }; + } + } + } + } catch (e) { + console.warn('[ai-chat] Anchor-based insertion failed, continuing to model placement:', e); + } + + const placementPrompt = `You will choose where to insert a SHORT code snippet into a Python file. PROBLEM CONTEXT: ${problemDescription} @@ -310,40 +798,55 @@ ${snippet.code} CURSOR POSITION (0-based line, column): ${cursorPosition ? `${cursorPosition.line},${cursorPosition.column}` : 'null'} Task: -- Determine the best insertion location according to the snippet type/scope and code structure. -- Maintain valid Python syntax and correct indentation. -- Avoid duplicating existing code. If the snippet (normalized whitespace) already exists in the file, return the original code. -- If insertion is ambiguous, prefer placing inside the active function near the cursor when provided; otherwise, at a logical spot following Python best practices. +- Determine the 0-based line index where the FIRST line of the snippet should be inserted. +- Provide an indentation string (spaces or tabs) appropriate for that location. Do NOT include any other code. +- If the exact snippet (normalized whitespace) already exists, set insertAtLine to -1. +- If insertion is ambiguous, prefer placing inside the active function near the cursor when provided. Output strictly as JSON (no markdown): { - "newCode": "", - "insertedAtLine": <0-based line index where first line of snippet was inserted or -1 if unchanged>, + "insertAtLine": , + "indentation": "", "rationale": "" }`; - const response = await openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [ - { role: 'system', content: 'You are a precise code editing assistant. Always return valid JSON with the full updated file content.' }, - { role: 'user', content: prompt } - ], - response_format: { type: 'json_object' }, - temperature: 0.1, - max_tokens: 2000 - }); - + console.log('[ai-chat] insert_snippet using model=gpt-4o-mini (placement-only)'); + const raw = await llmJsonFast(placementPrompt, { maxTokens: 500 }); + let insertAtLine: number | undefined; + let indent: string | undefined; + let rationale: string | undefined; try { - const content = response.choices[0]?.message?.content || '{"newCode":"","insertedAtLine":-1}'; - const parsed = JSON.parse(content); - return { - newCode: typeof parsed.newCode === 'string' ? parsed.newCode : code, - insertedAtLine: typeof parsed.insertedAtLine === 'number' ? parsed.insertedAtLine : undefined, - rationale: typeof parsed.rationale === 'string' ? parsed.rationale : undefined - }; + const parsed = JSON.parse(raw || '{}'); + if (typeof parsed.insertAtLine === 'number') insertAtLine = parsed.insertAtLine; + if (typeof parsed.indentation === 'string') indent = parsed.indentation; + if (typeof parsed.rationale === 'string') rationale = parsed.rationale; } catch { - return { newCode: code }; + // ignore + } + + // If snippet already exists or placement invalid, return original + if (insertAtLine === -1 || insertAtLine === undefined || insertAtLine < 0) { + return { newCode: code, insertedAtLine: -1, rationale }; } + + // Deterministic insertion of ONLY the provided snippet + const lines = code.split('\n'); + const safeInsertLine = Math.min(Math.max(0, insertAtLine), lines.length); + // Derive indentation if not provided + const contextIndent = indent !== undefined ? indent : (lines[safeInsertLine]?.match(/^\s*/)?.[0] || ''); + + const snippetLines = snippet.code.split('\n'); + const indentedSnippet: string[] = snippetLines.map((line, idx) => { + if (idx === 0) { + return contextIndent + line.trim(); + } + return contextIndent + line; + }); + + const newLines = [...lines]; + newLines.splice(safeInsertLine, 0, ...indentedSnippet); + const newCode = newLines.join('\n'); + return { newCode, insertedAtLine: safeInsertLine, rationale }; } /** @@ -386,11 +889,58 @@ serve(async (req) => { openai = new OpenAI({ apiKey: openaiKey, }); + console.log(`[ai-chat] Model selection: model=${configuredModel} | api=${useResponsesApi ? 'Responses' : 'Chat'} | source=${modelSource}`); // Parse request body const body: RequestBody = await req.json(); - const { message, problemDescription, conversationHistory, action, code, snippet, cursorPosition, testCases } = body; + const { message, problemDescription, conversationHistory, action, code, snippet, cursorPosition, testCases, diagram: diagramRequested, preferredEngines, sessionId, userId } = body; + + // Clear chat action + if (req.method === 'POST' && action === 'clear_chat') { + if (!sessionId || !userId) { + return new Response( + JSON.stringify({ error: 'Missing sessionId or userId for clear_chat action' }), + { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + try { + // Delete all messages for this session + const { error: messagesError } = await supabaseAdmin + .from('ai_chat_messages') + .delete() + .eq('session_id', sessionId); + + if (messagesError) { + console.error('Error deleting messages:', messagesError); + throw messagesError; + } + + // Delete the session itself + const { error: sessionError } = await supabaseAdmin + .from('ai_chat_sessions') + .delete() + .eq('id', sessionId) + .eq('user_id', userId); + + if (sessionError) { + console.error('Error deleting session:', sessionError); + throw sessionError; + } + + return new Response( + JSON.stringify({ ok: true, message: 'Chat cleared successfully' }), + { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } catch (error) { + console.error('Error clearing chat:', error); + return new Response( + JSON.stringify({ ok: false, error: 'Failed to clear chat' }), + { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + } - // Validate required fields + // Validate required fields for normal chat operations if (!message || !problemDescription) { return new Response( JSON.stringify({ error: 'Missing required fields: message, problemDescription' }), @@ -413,15 +963,35 @@ serve(async (req) => { return new Response(JSON.stringify(result), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } - // Default chat behavior: generate conversation + analyze snippets - const [conversationResponse, codeSnippets] = await Promise.all([ + // If diagram explicitly requested, prioritize diagram and skip snippet analysis + if (diagramRequested) { + const diagram = await maybeGenerateDiagram(message, problemDescription, conversationHistory || [], true, preferredEngines); + const responseText = diagram + ? (diagram.engine === 'mermaid' ? await explainMermaid(diagram.code, problemDescription) : 'Here is an interactive diagram of the approach.') + : 'Unable to create a diagram for this message.'; + const aiResponse: AIResponse = { response: responseText, diagram }; + return new Response(JSON.stringify(aiResponse), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); + } + + // Default chat behavior: generate conversation + analyze snippets + opportunistic diagram + const [conversationResponse, codeSnippets, diagram] = await Promise.all([ generateConversationResponse(message, problemDescription, conversationHistory || [], testCases), - analyzeCodeSnippets(message, conversationHistory || [], problemDescription, testCases) + analyzeCodeSnippets(message, conversationHistory || [], problemDescription, testCases), + maybeGenerateDiagram(message, problemDescription, conversationHistory || [], false, preferredEngines) ]); + // Heuristic: suggest diagram if user mentions visualization OR problem classes where visuals help + const userAskedForDiagram = /\b(visualize|diagram|flowchart|mermaid|draw)\b/i.test(message); + const contextHints = /\b(linked list|two pointers|tree|graph|dfs|bfs|heap|priority queue|sliding window|dp|dynamic programming)\b/i.test( + [message, ...conversationHistory.slice(-2).map(m => m.content)].join(' ') + ); + const suggestDiagram = !!diagram || userAskedForDiagram || contextHints; + const aiResponse: AIResponse = { response: conversationResponse, - codeSnippets: codeSnippets.length > 0 ? codeSnippets : undefined + codeSnippets: codeSnippets.length > 0 ? codeSnippets : undefined, + diagram: diagram, + suggestDiagram }; return new Response( diff --git a/supabase/migrations/20250111000000_add_json_columns_to_test_cases.sql b/supabase/migrations/20250111000000_add_json_columns_to_test_cases.sql new file mode 100644 index 0000000..de6775f --- /dev/null +++ b/supabase/migrations/20250111000000_add_json_columns_to_test_cases.sql @@ -0,0 +1,165 @@ +-- Add JSONB columns to test_cases table for structured data +-- This migration adds JSON-native columns while keeping legacy text columns for backward compatibility + +-- Add the new JSONB columns +ALTER TABLE public.test_cases +ADD COLUMN input_json jsonb, +ADD COLUMN expected_json jsonb; + +-- Add indexes for better performance on JSON queries +CREATE INDEX idx_test_cases_input_json ON public.test_cases USING gin (input_json); +CREATE INDEX idx_test_cases_expected_json ON public.test_cases USING gin (expected_json); + +-- Add comments to document the migration strategy +COMMENT ON COLUMN public.test_cases.input_json IS 'Structured JSON input parameters, e.g., {"list1": [1,2,4], "list2": [1,3,4]}'; +COMMENT ON COLUMN public.test_cases.expected_json IS 'Structured JSON expected output, e.g., [1,1,2,3,4,4] or primitive values'; +COMMENT ON COLUMN public.test_cases.input IS 'Legacy text input format - kept for backward compatibility'; +COMMENT ON COLUMN public.test_cases.expected_output IS 'Legacy text expected output - kept for backward compatibility'; + +-- Create a function to help migrate existing text data to JSON format +CREATE OR REPLACE FUNCTION migrate_test_case_to_json( + problem_signature text, + input_text text, + expected_text text +) RETURNS TABLE(input_json jsonb, expected_json jsonb) AS $$ +DECLARE + param_names text[]; + param_name text; + param_value text; + result_input jsonb := '{}'; + result_expected jsonb; +BEGIN + -- Extract parameter names from function signature + -- Example: "def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode])" + -- Should extract ["list1", "list2"] + + -- Simple regex to extract parameter names (excluding 'self') + SELECT array_agg(trim(split_part(param, ':', 1))) + INTO param_names + FROM ( + SELECT unnest(string_to_array( + regexp_replace( + regexp_replace(problem_signature, '.*\(([^)]+)\).*', '\1'), + '\s*(self\s*,?\s*)', '', 'g' + ), + ',' + )) as param + ) t + WHERE trim(param) != ''; + + -- Parse input_text in format "list1 = [1,2,4], list2 = [1,3,4]" + IF input_text ~ '=' THEN + -- Split by comma, but handle arrays properly + DECLARE + parts text[]; + part text; + eq_pos int; + param_name_clean text; + param_value_clean text; + BEGIN + -- Simple split for now - can be enhanced with proper bracket counting + parts := string_to_array(input_text, ','); + + -- Try to reconstruct split parameters that got broken by commas in arrays + DECLARE + reconstructed_parts text[] := '{}'; + current_part text := ''; + bracket_count int := 0; + i int; + BEGIN + FOR i IN 1..array_length(parts, 1) LOOP + current_part := current_part || CASE WHEN current_part = '' THEN '' ELSE ',' END || parts[i]; + + -- Count brackets to determine if we're inside an array + bracket_count := bracket_count + + (length(parts[i]) - length(replace(parts[i], '[', ''))) - + (length(parts[i]) - length(replace(parts[i], ']', ''))); + + -- If brackets are balanced and we have an equals sign, this is a complete parameter + IF bracket_count = 0 AND current_part ~ '=' THEN + reconstructed_parts := reconstructed_parts || current_part; + current_part := ''; + END IF; + END LOOP; + + -- Add any remaining part + IF current_part != '' THEN + reconstructed_parts := reconstructed_parts || current_part; + END IF; + + parts := reconstructed_parts; + END; + + -- Parse each parameter + FOREACH part IN ARRAY parts LOOP + eq_pos := position(' = ' in part); + IF eq_pos > 0 THEN + param_name_clean := trim(substring(part, 1, eq_pos - 1)); + param_value_clean := trim(substring(part, eq_pos + 3)); + + -- Try to parse as JSON, fallback to string + BEGIN + result_input := result_input || jsonb_build_object(param_name_clean, param_value_clean::jsonb); + EXCEPTION WHEN others THEN + -- Remove quotes if it's a simple string + param_value_clean := regexp_replace(param_value_clean, '^"(.*)"$', '\1'); + result_input := result_input || jsonb_build_object(param_name_clean, param_value_clean); + END; + END IF; + END LOOP; + END; + ELSE + -- Format 2: positional values, map to parameter names + DECLARE + lines text[]; + i int; + BEGIN + lines := string_to_array(input_text, E'\n'); + FOR i IN 1..LEAST(array_length(param_names, 1), array_length(lines, 1)) LOOP + BEGIN + result_input := result_input || jsonb_build_object(param_names[i], lines[i]::jsonb); + EXCEPTION WHEN others THEN + result_input := result_input || jsonb_build_object(param_names[i], lines[i]); + END; + END LOOP; + END; + END IF; + + -- Parse expected output + BEGIN + result_expected := expected_text::jsonb; + EXCEPTION WHEN others THEN + result_expected := to_jsonb(expected_text); + END; + + RETURN QUERY SELECT result_input, result_expected; +END; +$$ LANGUAGE plpgsql; + +-- Create a procedure to backfill existing data +CREATE OR REPLACE FUNCTION backfill_json_test_cases() RETURNS void AS $$ +DECLARE + rec record; + migrated record; +BEGIN + FOR rec IN + SELECT tc.id, tc.input, tc.expected_output, p.function_signature + FROM test_cases tc + JOIN problems p ON tc.problem_id = p.id + WHERE tc.input_json IS NULL AND tc.expected_json IS NULL + LOOP + -- Migrate this test case + SELECT * INTO migrated + FROM migrate_test_case_to_json(rec.function_signature, rec.input, rec.expected_output); + + -- Update the row with JSON data + UPDATE test_cases + SET input_json = migrated.input_json, + expected_json = migrated.expected_json + WHERE id = rec.id; + + RAISE NOTICE 'Migrated test case %: input=% expected=%', + rec.id, migrated.input_json, migrated.expected_json; + END LOOP; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/supabase/migrations/20250111000001_replace_test_cases_with_json.sql b/supabase/migrations/20250111000001_replace_test_cases_with_json.sql new file mode 100644 index 0000000..dd1efb4 --- /dev/null +++ b/supabase/migrations/20250111000001_replace_test_cases_with_json.sql @@ -0,0 +1,112 @@ +-- Clean slate: Remove existing test cases and recreate with proper JSON structure +-- This is simpler than backfilling and ensures clean, consistent data + +-- Clear existing test cases +DELETE FROM public.test_cases; + +-- Add JSONB columns to test_cases table +ALTER TABLE public.test_cases +ADD COLUMN IF NOT EXISTS input_json jsonb, +ADD COLUMN IF NOT EXISTS expected_json jsonb; + +-- Add indexes for better performance +CREATE INDEX IF NOT EXISTS idx_test_cases_input_json ON public.test_cases USING gin (input_json); +CREATE INDEX IF NOT EXISTS idx_test_cases_expected_json ON public.test_cases USING gin (expected_json); + +-- Add comments +COMMENT ON COLUMN public.test_cases.input_json IS 'Structured JSON input parameters, e.g., {"list1": [1,2,4], "list2": [1,3,4]}'; +COMMENT ON COLUMN public.test_cases.expected_json IS 'Structured JSON expected output, e.g., [1,1,2,3,4,4] or primitive values'; + +-- Insert test cases with proper JSON structure + +-- Two Sum test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'two-sum'), 'nums = [2,7,11,15], target = 9', '[0,1]', + '{"nums": [2,7,11,15], "target": 9}', '[0,1]', true), +((SELECT id FROM problems WHERE id = 'two-sum'), 'nums = [3,2,4], target = 6', '[1,2]', + '{"nums": [3,2,4], "target": 6}', '[1,2]', false), +((SELECT id FROM problems WHERE id = 'two-sum'), 'nums = [3,3], target = 6', '[0,1]', + '{"nums": [3,3], "target": 6}', '[0,1]', false); + +-- Valid Anagram test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'valid-anagram'), 's = "anagram", t = "nagaram"', 'true', + '{"s": "anagram", "t": "nagaram"}', 'true', true), +((SELECT id FROM problems WHERE id = 'valid-anagram'), 's = "rat", t = "car"', 'false', + '{"s": "rat", "t": "car"}', 'false', false), +((SELECT id FROM problems WHERE id = 'valid-anagram'), 's = "a", t = "ab"', 'false', + '{"s": "a", "t": "ab"}', 'false', false); + +-- Group Anagrams test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'group-anagrams'), 'strs = ["eat","tea","tan","ate","nat","bat"]', '[["bat"],["nat","tan"],["ate","eat","tea"]]', + '{"strs": ["eat","tea","tan","ate","nat","bat"]}', '[["bat"],["nat","tan"],["ate","eat","tea"]]', true), +((SELECT id FROM problems WHERE id = 'group-anagrams'), 'strs = [""]', '[[""]]', + '{"strs": [""]}', '[[""]]', false), +((SELECT id FROM problems WHERE id = 'group-anagrams'), 'strs = ["a"]', '[["a"]]', + '{"strs": ["a"]}', '[["a"]]', false); + +-- Valid Parentheses test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "()"', 'true', + '{"s": "()"}', 'true', true), +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "()[]{}"', 'true', + '{"s": "()[]{}""}', 'true', false), +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "(]"', 'false', + '{"s": "(]"}', 'false', false), +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "([)]"', 'false', + '{"s": "([)]"}', 'false', false), +((SELECT id FROM problems WHERE id = 'valid-parentheses'), 's = "{[]}"', 'true', + '{"s": "{[]}"}', 'true', false); + +-- Best Time to Buy and Sell Stock test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'best-time-to-buy-and-sell-stock'), 'prices = [7,1,5,3,6,4]', '5', + '{"prices": [7,1,5,3,6,4]}', '5', true), +((SELECT id FROM problems WHERE id = 'best-time-to-buy-and-sell-stock'), 'prices = [7,6,4,3,1]', '0', + '{"prices": [7,6,4,3,1]}', '0', false), +((SELECT id FROM problems WHERE id = 'best-time-to-buy-and-sell-stock'), 'prices = [1,2]', '1', + '{"prices": [1,2]}', '1', false); + +-- Merge Two Sorted Lists test cases (ListNode problems) +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'merge-two-sorted-lists'), 'list1 = [1,2,4], list2 = [1,3,4]', '[1,1,2,3,4,4]', + '{"list1": [1,2,4], "list2": [1,3,4]}', '[1,1,2,3,4,4]', true), +((SELECT id FROM problems WHERE id = 'merge-two-sorted-lists'), 'list1 = [], list2 = []', '[]', + '{"list1": [], "list2": []}', '[]', false), +((SELECT id FROM problems WHERE id = 'merge-two-sorted-lists'), 'list1 = [], list2 = [0]', '[0]', + '{"list1": [], "list2": [0]}', '[0]', false); + +-- Maximum Subarray test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'maximum-subarray'), 'nums = [-2,1,-3,4,-1,2,1,-5,4]', '6', + '{"nums": [-2,1,-3,4,-1,2,1,-5,4]}', '6', true), +((SELECT id FROM problems WHERE id = 'maximum-subarray'), 'nums = [1]', '1', + '{"nums": [1]}', '1', false), +((SELECT id FROM problems WHERE id = 'maximum-subarray'), 'nums = [5,4,-1,7,8]', '23', + '{"nums": [5,4,-1,7,8]}', '23', false); + +-- Contains Duplicate test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'contains-duplicate'), 'nums = [1,2,3,1]', 'true', + '{"nums": [1,2,3,1]}', 'true', true), +((SELECT id FROM problems WHERE id = 'contains-duplicate'), 'nums = [1,2,3,4]', 'false', + '{"nums": [1,2,3,4]}', 'false', false), +((SELECT id FROM problems WHERE id = 'contains-duplicate'), 'nums = [1,1,1,3,3,4,3,2,4,2]', 'true', + '{"nums": [1,1,1,3,3,4,3,2,4,2]}', 'true', false); + +-- Product of Array Except Self test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'product-of-array-except-self'), 'nums = [1,2,3,4]', '[24,12,8,6]', + '{"nums": [1,2,3,4]}', '[24,12,8,6]', true), +((SELECT id FROM problems WHERE id = 'product-of-array-except-self'), 'nums = [-1,1,0,-3,3]', '[0,0,9,0,0]', + '{"nums": [-1,1,0,-3,3]}', '[0,0,9,0,0]', false); + +-- Maximum Product Subarray test cases +INSERT INTO public.test_cases (problem_id, input, expected_output, input_json, expected_json, is_example) VALUES +((SELECT id FROM problems WHERE id = 'maximum-product-subarray'), 'nums = [2,3,-2,4]', '6', + '{"nums": [2,3,-2,4]}', '6', true), +((SELECT id FROM problems WHERE id = 'maximum-product-subarray'), 'nums = [-2,0,-1]', '0', + '{"nums": [-2,0,-1]}', '0', false), +((SELECT id FROM problems WHERE id = 'maximum-product-subarray'), 'nums = [-2,3,-4]', '24', + '{"nums": [-2,3,-4]}', '24', false); \ No newline at end of file