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}) => (
-
- ),
- 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}) => ,
+ 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 && (
+
+ )}
+
+
+
+ );
+}
\ 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.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