diff --git a/.eslintrc.overrides.js b/.eslintrc.overrides.js new file mode 100644 index 0000000..4380b11 --- /dev/null +++ b/.eslintrc.overrides.js @@ -0,0 +1,26 @@ +module.exports = { + overrides: [ + // UI components - less strict rules since they're often auto-generated + { + files: ["src/components/ui/**/*.tsx"], + rules: { + "react-refresh/only-export-components": "warn", + "@typescript-eslint/no-empty-object-type": "off" + } + }, + // Config files + { + files: ["*.config.ts", "*.config.js"], + rules: { + "@typescript-eslint/no-require-imports": "off" + } + }, + // Supabase generated files + { + files: ["src/integrations/**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "warn" + } + } + ] +}; \ No newline at end of file diff --git a/CI_CD_README.md b/CI_CD_README.md new file mode 100644 index 0000000..61b3a48 --- /dev/null +++ b/CI_CD_README.md @@ -0,0 +1,182 @@ +# CI/CD Setup for SimplyAlgo + +This document describes the GitHub Actions workflows set up for the SimplyAlgo LeetCode platform. + +## ๐Ÿš€ Workflows + +### 1. CI/CD Pipeline (`.github/workflows/ci.yml`) +**Comprehensive testing and deployment pipeline** + +**Triggers:** +- Push to `main` branch +- Pull requests to `main` branch + +**Jobs:** +1. **test-frontend** - Tests the React/Vite frontend + - Uses Bun for package management + - Runs linting with `bun run lint` + - Builds the app with `bun run build` + - Starts preview server for health check + +2. **test-api** - Tests the Node.js API server + - Uses Node.js 18 + - Installs dependencies in `code-executor-api/` + - Creates test environment file + - Tests API server startup and health endpoints + +3. **integration-test** - Tests both services together + - Starts both frontend and API servers + - Runs connectivity tests between services + - Validates end-to-end functionality + +4. **deploy-staging** - Deployment preparation (main branch only) + - Runs after all tests pass + - Builds production assets + - Ready for staging deployment + +5. **security-check** - Security audit + - Runs `bun audit` and `npm audit` + - Checks for vulnerable dependencies + +### 2. Development Tests (`.github/workflows/dev-test.yml`) +**Quick testing for development branches** + +**Triggers:** +- Push to `main` or `develop` branches +- Pull requests to `main` or `develop` branches + +**Features:** +- Faster execution with focused tests +- Tests both `bun run dev` and API server startup +- Validates that development environment works correctly + +## ๐Ÿ› ๏ธ Local Development + +### Quick Start +```bash +# Start both frontend and API server +bun run dev:all + +# Or individually: +bun run dev # Frontend only (port 5173) +bun run api # API server only (port 3001) +``` + +### Development Script Features +The `scripts/dev-start.sh` script provides: +- โœ… Dependency installation for both frontend and API +- โœ… Automatic .env file creation for API +- โœ… Health checks for both servers +- โœ… Graceful shutdown with Ctrl+C +- โœ… Colored output for better visibility + +### Environment Setup + +#### Frontend (.env in root) +```env +VITE_SUPABASE_URL=your-supabase-url +VITE_SUPABASE_ANON_KEY=your-supabase-anon-key +``` + +#### API (code-executor-api/.env) +```env +PORT=3001 +JUDGE0_API_URL=https://judge0-extra-ce.p.rapidapi.com +SUPABASE_URL=your-supabase-url +SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key +JUDGE0_API_KEY=your-judge0-api-key # Optional +``` + +## ๐Ÿ“‹ Available Scripts + +### Frontend (Root directory) +```bash +bun run dev # Start Vite dev server +bun run dev:all # Start both frontend and API +bun run build # Build for production +bun run lint # Run ESLint +bun run preview # Preview production build +bun run test:ci # Run CI tests (lint + build) +``` + +### API (code-executor-api/) +```bash +npm run start # Start production server +npm run dev # Start with file watching +``` + +## ๐Ÿ”ง CI/CD Configuration + +### Branch Protection +Recommended branch protection rules for `main`: +- โœ… Require status checks (all CI jobs must pass) +- โœ… Require up-to-date branches +- โœ… Require signed commits (optional) +- โœ… Include administrators + +### Environment Variables (GitHub Secrets) +For production deployment, add these secrets: +- `VITE_SUPABASE_URL` +- `VITE_SUPABASE_ANON_KEY` +- `SUPABASE_SERVICE_ROLE_KEY` +- `JUDGE0_API_KEY` (optional) + +## ๐Ÿ“Š Status Badges + +Add these to your main README.md: + +```markdown +![CI/CD Pipeline](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/ci.yml/badge.svg) +![Dev Tests](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/dev-test.yml/badge.svg) +``` + +## ๐Ÿšจ Troubleshooting + +### Common Issues + +1. **API Server fails to start in CI** + - Check environment variables + - Verify .env file creation in workflow + +2. **Frontend build fails** + - Check for TypeScript errors + - Verify all dependencies are installed + +3. **Integration tests timeout** + - Increase timeout in workflow + - Check server startup timing + +4. **Bun cache issues** + - Clear cache in GitHub Actions settings + - Update cache key in workflow + +### Debug Commands +```bash +# Test locally what CI does: +bun install +bun run lint +bun run build + +# Test API startup: +cd code-executor-api +npm install +npm start +curl http://localhost:3001/health +``` + +## ๐Ÿ”„ Workflow Updates + +To modify workflows: +1. Edit `.github/workflows/ci.yml` or `dev-test.yml` +2. Test changes on feature branch first +3. Monitor workflow runs in GitHub Actions tab +4. Update this documentation when adding new features + +## ๐Ÿ“ˆ Future Enhancements + +Planned improvements: +- [ ] Add automated testing with Jest/Vitest +- [ ] Docker containerization for consistent environments +- [ ] Automated deployment to staging/production +- [ ] Performance monitoring integration +- [ ] Slack/Discord notifications for build status \ No newline at end of file diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index 160304d..2ce621a 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 463bbd4..8a3bca5 100644 --- a/code-executor-api/server.js +++ b/code-executor-api/server.js @@ -81,12 +81,13 @@ function smartCompare(actual, expected) { const normalizedActual = normalizeArrayOfArrays(actual); const normalizedExpected = normalizeArrayOfArrays(expected); - return JSON.stringify(normalizedActual) === JSON.stringify(normalizedExpected); + const result = JSON.stringify(normalizedActual) === JSON.stringify(normalizedExpected); + return result; } // For simple arrays, just sort both - const sortedActual = [...actual].sort(); - const sortedExpected = [...expected].sort(); + const sortedActual = [...actual].sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)); + const sortedExpected = [...expected].sort((a, b) => (a > b ? 1 : a < b ? -1 : 0)); return JSON.stringify(sortedActual) === JSON.stringify(sortedExpected); } @@ -217,8 +218,9 @@ function parseTestCaseInput(inputString, functionSignature) { // Format 1a: "nums = [2,7,11,15]\ntarget = 9" (multi-line) // Format 1b: "s = \"anagram\", t = \"nagaram\"" (single line, comma-separated) - if (lines.length === 1 && lines[0].includes(',')) { + if (lines.length === 1 && lines[0].includes(',') && !lines[0].includes('[') && !lines[0].includes(']')) { // Single line with comma-separated parameters: "s = \"anagram\", t = \"nagaram\"" + // BUT NOT arrays like: "strs = [\"eat\",\"tea\"]" const line = lines[0]; console.log('Parsing single line with comma separation:', line); @@ -481,6 +483,7 @@ app.post('/execute', async (req, res) => { // Check if this problem requires smart comparison const requiresSmartComparison = problemId && SMART_COMPARISON_PROBLEMS.has(problemId); + console.log(`๐ŸŽฏ Problem: ${problemId}, Requires smart comparison: ${requiresSmartComparison}`); let { testCases } = req.body; // Validate request diff --git a/eslint.config.js b/eslint.config.js index e67846f..e85c991 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,6 +24,12 @@ export default tseslint.config( { allowConstantExport: true }, ], "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-empty-object-type": "warn", + "@typescript-eslint/no-require-imports": "warn", + "no-useless-escape": "warn", + "prefer-const": "warn", + "react-hooks/exhaustive-deps": "warn", }, } ); diff --git a/package.json b/package.json index 57e382f..b3e2062 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "type": "module", "scripts": { "dev": "vite", + "dev:all": "./scripts/dev-start.sh", "build": "vite build", "build:dev": "vite build --mode development", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "api": "cd code-executor-api && npm run dev", + "test:ci": "bun run lint && bun run build" }, "dependencies": { "@codemirror/lang-python": "^6.2.1", @@ -64,6 +67,7 @@ "react-syntax-highlighter": "^15.6.1", "react-textarea-autosize": "^8.5.9", "recharts": "^2.12.7", + "safe-stable-stringify": "^2.5.0", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh new file mode 100755 index 0000000..4c3e782 --- /dev/null +++ b/scripts/dev-start.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# Development startup script for SimplyAlgo platform +# Starts both the frontend (Bun) and API server (Node.js) in parallel + +set -e + +echo "๐Ÿš€ Starting SimplyAlgo Development Environment..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to cleanup on exit +# Ensure cleanup runs on SIGINT, SIGTERM, or normal exit +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 +} +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 + +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 +JUDGE0_API_URL=https://judge0-extra-ce.p.rapidapi.com +# Add your actual Supabase credentials below: +SUPABASE_URL=your-supabase-url +SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key +# Optional: Add your Judge0 API key for better rate limits +# JUDGE0_API_KEY=your-judge0-api-key +EOL + echo -e "${YELLOW}๐Ÿ“ Please update code-executor-api/.env with your actual credentials${NC}" +fi + +cd .. + +echo -e "${BLUE}๐Ÿš€ Starting API server on port 3001...${NC}" +cd code-executor-api +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}" +bun run dev & +FRONTEND_PID=$! + +# Wait for frontend to start +echo -e "${YELLOW}โณ Waiting for frontend server to start...${NC}" +sleep 5 + +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}" + +# Wait for user to stop +wait $API_PID $FRONTEND_PID \ No newline at end of file diff --git a/src/components/AIChat.tsx b/src/components/AIChat.tsx index 2fbc958..0ee1831 100644 --- a/src/components/AIChat.tsx +++ b/src/components/AIChat.tsx @@ -22,6 +22,33 @@ 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(); + }; const { session, messages, @@ -168,7 +195,12 @@ const AIChat = ({ problemId, problemDescription, onInsertCodeSnippet, problemTes
); }, - p: ({children}) =>

{children}

, - ul: ({children}) =>
    {children}
, - ol: ({children}) =>
    {children}
, - li: ({children}) =>
  • {children}
  • , + p: ({children}) => ( +

    + {children} +

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

    {children}

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

    {children}

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

    {children}

    + ), + blockquote: ({children}) => ( +
    + {children} +
    + ), }} > - {message.content} + {cleanMathNotation(message.content)}
    )} diff --git a/src/components/Notes.tsx b/src/components/Notes.tsx index d29c93f..47c8d3d 100644 --- a/src/components/Notes.tsx +++ b/src/components/Notes.tsx @@ -131,6 +131,7 @@ const Notes = ({ problemId }: NotesProps) => { // Cleanup debounced function useEffect(() => { return () => { + debouncedSave.flush(); debouncedSave.cancel(); }; }, [debouncedSave]); diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 56a0979..69d63a4 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )) Command.displayName = CommandPrimitive.displayName -interface CommandDialogProps extends DialogProps {} +type CommandDialogProps = DialogProps const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 9f9a6dc..12c9136 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,8 +2,7 @@ import * as React from "react" import { cn } from "@/lib/utils" -export interface TextareaProps - extends React.TextareaHTMLAttributes {} +export type TextareaProps = React.TextareaHTMLAttributes const Textarea = React.forwardRef( ({ className, ...props }, ref) => { diff --git a/src/data/pythonSolutions.ts b/src/data/pythonSolutions.ts new file mode 100644 index 0000000..695ef61 --- /dev/null +++ b/src/data/pythonSolutions.ts @@ -0,0 +1,161 @@ +// Python solutions for LeetCode problems +export interface Solution { + title: string; + code: string; + complexity: { + time: string; + space: string; + }; + explanation: string; +} + +export const pythonSolutions: Record = { + 'two-sum': [ + { + title: "Brute Force", + code: `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 []`, + complexity: { time: "O(nยฒ)", space: "O(1)" }, + explanation: "Check every pair of numbers to see if they sum to target." + }, + { + title: "Hash Map (Optimal)", + code: `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 []`, + complexity: { time: "O(n)", space: "O(n)" }, + explanation: "Use hash map to store numbers and their indices, check for complement in O(1) time." + } + ], + + 'group-anagrams': [ + { + title: "Sort and Group", + code: `def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + from collections import defaultdict + groups = defaultdict(list) + + for s in strs: + # Sort the string to get the key + key = ''.join(sorted(s)) + groups[key].append(s) + + return list(groups.values())`, + complexity: { time: "O(n ร— m log m)", space: "O(n ร— m)" }, + explanation: "Sort each string to create a key, then group strings with same sorted key. Where n is number of strings and m is maximum length of a string." + }, + { + title: "Character Count (Alternative)", + code: `def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + from collections import defaultdict + groups = defaultdict(list) + + for s in strs: + # Count characters as key + count = [0] * 26 + for char in s: + count[ord(char) - ord('a')] += 1 + # Convert to tuple to use as dictionary key + key = tuple(count) + groups[key].append(s) + + return list(groups.values())`, + complexity: { time: "O(n ร— m)", space: "O(n ร— m)" }, + explanation: "Use character count array as key instead of sorting. More efficient for longer strings." + } + ], + + 'valid-anagram': [ + { + title: "Sorting", + code: `def isAnagram(self, s: str, t: str) -> bool: + return sorted(s) == sorted(t)`, + complexity: { time: "O(n log n)", space: "O(n)" }, + explanation: "Sort both strings and compare if they're equal." + }, + { + title: "Character Count (Optimal)", + code: `def isAnagram(self, s: str, t: str) -> bool: + if len(s) != len(t): + return False + + from collections import Counter + return Counter(s) == Counter(t)`, + complexity: { time: "O(n)", space: "O(1)" }, + explanation: "Count characters in both strings and compare the counts. Space is O(1) since we only have 26 possible lowercase letters." + } + ], + + 'valid-parentheses': [ + { + title: "Stack", + code: `def isValid(self, s: str) -> bool: + stack = [] + mapping = {')': '(', '}': '{', ']': '['} + + for char in s: + if char in mapping: + # Closing bracket + if not stack or stack.pop() != mapping[char]: + return False + else: + # Opening bracket + stack.append(char) + + return not stack`, + complexity: { time: "O(n)", space: "O(n)" }, + explanation: "Use stack to track opening brackets and match with closing brackets. Return true only if all brackets are properly matched." + } + ], + + 'top-k-frequent-elements': [ + { + title: "Counter + Heap", + code: `def topKFrequent(self, nums: List[int], k: int) -> List[int]: + from collections import Counter + import heapq + + # Count frequencies + counter = Counter(nums) + + # Use heap to get top k frequent + return heapq.nlargest(k, counter.keys(), key=counter.get)`, + complexity: { time: "O(n log k)", space: "O(n)" }, + explanation: "Count frequencies, then use heap to efficiently get top k elements." + }, + { + title: "Bucket Sort (Optimal)", + code: `def topKFrequent(self, nums: List[int], k: int) -> List[int]: + from collections import Counter + + counter = Counter(nums) + # Create buckets for each possible frequency + buckets = [[] for _ in range(len(nums) + 1)] + + # Place numbers in buckets by frequency + for num, freq in counter.items(): + buckets[freq].append(num) + + # Collect top k from highest frequency buckets + result = [] + for i in range(len(buckets) - 1, -1, -1): + for num in buckets[i]: + result.append(num) + if len(result) == k: + return result + + return result`, + complexity: { time: "O(n)", space: "O(n)" }, + explanation: "Use bucket sort approach with frequency as bucket index. Optimal O(n) time complexity." + } + ] +}; \ No newline at end of file diff --git a/src/hooks/useChatSession.ts b/src/hooks/useChatSession.ts index 5813733..bacd3d7 100644 --- a/src/hooks/useChatSession.ts +++ b/src/hooks/useChatSession.ts @@ -9,7 +9,9 @@ const normalizeSnippet = (s: CodeSnippet): string => { const type = s.insertionHint?.type || ''; const scope = s.insertionHint?.scope || ''; const code = (s.code || '').replace(/\s+/g, ' ').trim(); - return `${type}|${scope}|${code}`; + const language = (s.language || '').toLowerCase(); + const insertionType = (s.insertionType || '').toString().toLowerCase(); + return `${language}|${insertionType}|${type}|${scope}|${code}`; }; const getSeenSnippetKeys = (existingMessages: ChatMessage[]): Set => { @@ -216,8 +218,25 @@ export const useChatSession = ({ problemId, problemDescription, problemTestCases const aiResponseContent = data.response; const rawSnippets: CodeSnippet[] | undefined = data.codeSnippets && Array.isArray(data.codeSnippets) ? data.codeSnippets : undefined; + + // Gate snippet visibility: only when user explicitly asks for code or pasted code + const lastUserMsg = content; + const hasExplicitCode = /```[\s\S]*?```|`[^`]+`/m.test(lastUserMsg); + const explicitAsk = /\b(write|show|give|provide|insert|add|implement|code|import|define|declare|create)\b/i.test(lastUserMsg); + const allowSnippets = hasExplicitCode || explicitAsk; + // Dedupe snippets against entire session and within this response - const dedupedSnippets = dedupeSnippets(rawSnippets, messages); + let dedupedSnippets = allowSnippets ? dedupeSnippets(rawSnippets, messages) : undefined; + + // Additional guard: drop import suggestions unless the user explicitly asked about imports + const explicitImportAsk = /\b(import|from\s+\w+\s+import|how\s+to\s+import)\b/i.test(lastUserMsg); + if (dedupedSnippets && !explicitImportAsk) { + dedupedSnippets = dedupedSnippets.filter(s => { + const isImport = (s.insertionHint?.type === 'import') || /^(\s*)(from\s+\S+\s+import\s+\S+|import\s+\S+)/.test(s.code || ''); + return !isImport; + }); + if (dedupedSnippets.length === 0) dedupedSnippets = undefined; + } const aiResponse: ChatMessage = { id: (Date.now() + 1).toString(), diff --git a/src/hooks/useProblems.ts b/src/hooks/useProblems.ts index d2c50c8..754319d 100644 --- a/src/hooks/useProblems.ts +++ b/src/hooks/useProblems.ts @@ -45,7 +45,7 @@ export const useProblems = (userId?: string) => { const fetchProblems = async () => { try { // Fetch problems with category names and user attempt data - let query = supabase + const query = supabase .from('problems') .select(` *, diff --git a/src/hooks/useSubmissions.ts b/src/hooks/useSubmissions.ts new file mode 100644 index 0000000..1139b35 --- /dev/null +++ b/src/hooks/useSubmissions.ts @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import { UserAttemptsService, UserAttempt } from '@/services/userAttempts'; + +export const useSubmissions = (userId: string | undefined, problemId: string | undefined) => { + const [submissions, setSubmissions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!userId || !problemId) { + setSubmissions([]); + setLoading(false); + return; + } + + const fetchSubmissions = async () => { + try { + setLoading(true); + setError(null); + const data = await UserAttemptsService.getAcceptedSubmissions(userId, problemId); + setSubmissions(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch submissions'); + setSubmissions([]); + } finally { + setLoading(false); + } + }; + + fetchSubmissions(); + }, [userId, problemId]); + + const refetch = async () => { + if (!userId || !problemId) return; + + try { + setLoading(true); + setError(null); + const data = await UserAttemptsService.getAcceptedSubmissions(userId, problemId); + setSubmissions(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch submissions'); + } finally { + setLoading(false); + } + }; + + return { submissions, loading, error, refetch }; +}; \ No newline at end of file diff --git a/src/pages/ProblemSolver.tsx b/src/pages/ProblemSolver.tsx index 12c9f31..4fe8177 100644 --- a/src/pages/ProblemSolver.tsx +++ b/src/pages/ProblemSolver.tsx @@ -5,19 +5,26 @@ 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 } from 'lucide-react'; +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 { 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 } from 'react'; -import { toast } from 'sonner'; +import { useState, useEffect, useRef, useMemo } from 'react'; import { insertCodeSnippet } from '@/utils/codeInsertion'; 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 }>(); @@ -25,11 +32,22 @@ 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(null); + const codeEditorRef = useRef<{ + getValue: () => string; + setValue: (value: string) => void; + getPosition: () => any; + setPosition: (position: any) => void; + focus: () => void; + deltaDecorations: (oldDecorations: string[], newDecorations: any[]) => string[]; + } | null>(null); // Panel visibility state const [showLeftPanel, setShowLeftPanel] = useState(() => { @@ -52,6 +70,33 @@ const ProblemSolver = () => { 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); + } + } + }; + const toggleBottomPanel = () => { const newValue = !showBottomPanel; setShowBottomPanel(newValue); @@ -95,9 +140,22 @@ const ProblemSolver = () => { return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [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 ( @@ -262,6 +320,8 @@ 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`); } @@ -304,14 +364,16 @@ const ProblemSolver = () => { const renderValue = (value: any): string => { if (value === null || value === undefined) return 'null'; if (typeof value === 'number' || typeof value === 'boolean') 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 { return JSON.stringify(JSON.parse(trimmed), null, 2); } catch { return value; } + + // Handle arrays and objects directly for pretty printing + if (Array.isArray(value)) { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); } - return value; } + if (typeof value === 'object') { try { return JSON.stringify(value, null, 2); @@ -319,9 +381,84 @@ const ProblemSolver = () => { 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 */} @@ -371,8 +508,8 @@ const ProblemSolver = () => { {showLeftPanel && ( <> -
    - +
    +
    @@ -390,139 +527,222 @@ const ProblemSolver = () => {
    -
    - -
    +
    + + +

    Problem Description

    {problem.description}

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

    Examples

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

    Examples

    +
    + {problem.examples.map((example, index) => ( +
    +
    - Explanation: {example.explanation} + Input: {example.input}
    - )} +
    + Output: {example.output} +
    + {example.explanation && ( +
    + Explanation: {example.explanation} +
    + )} +
    -
    - ))} + ))} +
    -
    - )} + )} + - -
    -

    1. Brute Force

    -
    -
    -
    - - - + +
    + {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}
    • +
    +
    +
    +
    + ))}
    - -
    -
    -                            {`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

    -
    -
    -
    - - - + ) : ( +
    +
    No solutions available
    +
    + Solutions for this problem haven't been added yet. +
    - -
    -
    -                            {`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

    -
    -
    -
    - Accepted -
    -
    - Python - 2 minutes ago -
    + {submissionsLoading ? ( +
    +
    -
    -
    - Accepted -
    -
    - Python - 5 minutes ago + ) : uniqueSubmissions.length === 0 ? ( +
    +
    No accepted submissions yet
    +
    + Solve this problem to see your submissions here!
    + ) : ( +
    + {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'} +
    + )} +
    + )} +
    + ))} +
    + )}
    -
    + - - + + +
    + +
    +
    @@ -638,7 +858,7 @@ const ProblemSolver = () => {
    Expected Output:
    -
    {renderValue(result.expected)}
    +
    {toCompactJson(result.expected)}
    @@ -648,7 +868,7 @@ const ProblemSolver = () => { ? 'text-green-700 dark:text-green-300' : 'text-red-700 dark:text-red-300' }`}> - {renderValue(result.actual) || 'No output'} + {toCompactJson(result.actual) || 'No output'}
    diff --git a/src/services/userAttempts.ts b/src/services/userAttempts.ts index 742042e..e7c5e24 100644 --- a/src/services/userAttempts.ts +++ b/src/services/userAttempts.ts @@ -8,6 +8,10 @@ export interface UserAttempt { status: 'pending' | 'passed' | 'failed' | 'error'; created_at: string; updated_at: string; + test_results?: any; + execution_time?: number; + memory_usage?: number; + language?: string; } export class UserAttemptsService { @@ -197,4 +201,22 @@ export class UserAttemptsService { return true; } + + // Get all accepted submissions for a specific problem + static async getAcceptedSubmissions(userId: string, problemId: string): Promise { + const { data, error } = await supabase + .from('user_problem_attempts') + .select('*') + .eq('user_id', userId) + .eq('problem_id', problemId) + .eq('status', 'passed') + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching accepted submissions:', error); + return []; + } + + return data || []; + } } \ No newline at end of file diff --git a/supabase/functions/ai-chat/index.ts b/supabase/functions/ai-chat/index.ts index 95511ae..e3e68f3 100644 --- a/supabase/functions/ai-chat/index.ts +++ b/supabase/functions/ai-chat/index.ts @@ -111,14 +111,14 @@ Respond naturally and conversationally. Focus on teaching and guiding rather tha 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. 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." + 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 } ], - temperature: 0.7, + temperature: 0.5, max_tokens: 500 }); @@ -137,10 +137,11 @@ 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); + const looksLikeCode = /^(\s*)(def|class)\s+\w+|^(\s*)\w+\s*=\s*.+|\b\w+\(.*\)|\bfrom\b\s+\w+\s+\bimport\b/m.test(message); 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; if (!allowAnalysis || (assistantJustAskedQuestion && !hasExplicitCode)) { return []; @@ -215,7 +216,7 @@ Student: "Maybe if char in seen:" Response: Provide complete conditional logic with proper indentation Student: "Two pointers approach?" -Respond by extracting any concrete, safe-to-insert scaffolding (e.g., pointer initialization), but avoid full solutions unless explicitly requested.`; +Respond with conceptual guidance only unless the student explicitly asks for code or pastes code.`; try { const response = await openai.chat.completions.create({ @@ -257,6 +258,11 @@ Respond by extracting any concrete, safe-to-insert scaffolding (e.g., pointer in 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; }); // Dedupe within the same response diff --git a/tailwind.config.ts b/tailwind.config.ts index 06d1e13..c50edb6 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -100,5 +100,6 @@ export default { } } }, + // eslint-disable-next-line @typescript-eslint/no-require-imports plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], } satisfies Config; diff --git a/test-connection.js b/test-connection.js deleted file mode 100644 index f3ace9c..0000000 --- a/test-connection.js +++ /dev/null @@ -1,80 +0,0 @@ -// Test the connection between frontend and backend API - -async function testConnection() { - console.log('๐Ÿ”— Testing Frontend โ†” Backend Connection'); - console.log('========================================='); - - const API_URL = 'http://localhost:3001'; - - try { - // Test 1: Health check - console.log('\n1๏ธโƒฃ Testing health endpoint...'); - const healthResponse = await fetch(`${API_URL}/health`); - - if (healthResponse.ok) { - const health = await healthResponse.json(); - console.log('โœ… Health check passed:', health.status); - } else { - console.log('โŒ Health check failed'); - return; - } - - // Test 2: Judge0 info - console.log('\n2๏ธโƒฃ Testing Judge0 connection...'); - const judge0Response = await fetch(`${API_URL}/judge0-info`); - - if (judge0Response.ok) { - const judge0Info = await judge0Response.json(); - console.log('โœ… Judge0 connection:', judge0Info.judge0Available ? 'Connected' : 'Failed'); - console.log(' Supported languages:', judge0Info.supportedLanguages?.join(', ')); - } else { - console.log('โŒ Judge0 connection failed'); - } - - // Test 3: Code execution with problemId (the new dynamic way) - console.log('\n3๏ธโƒฃ Testing dynamic code execution...'); - const testPayload = { - language: 'python', - problemId: 'two-sum', // Should fetch test cases from Supabase - code: `def twoSum(nums: List[int], target: int) -> List[int]: - d = {} - for i in range(len(nums)): - if target - nums[i] in d: - return [d[target-nums[i]], i] - else: - d[nums[i]] = i` - }; - - const executeResponse = await fetch(`${API_URL}/execute`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(testPayload) - }); - - if (executeResponse.ok) { - const result = await executeResponse.json(); - console.log(`โœ… Code execution successful!`); - console.log(` Test cases: ${result.results.length}`); - console.log(` Passed: ${result.results.filter(r => r.passed).length}`); - } else { - const error = await executeResponse.json(); - console.log('โŒ Code execution failed:', error.error); - } - - console.log('\n๐ŸŽ‰ Connection test completed!'); - console.log('\n๐Ÿ“ How to use in your frontend:'); - console.log('1. Make sure API server is running: npm start (in code-executor-api/)'); - console.log('2. Frontend calls TestRunnerService.runCode() with problemId'); - console.log('3. API fetches test cases from Supabase automatically'); - console.log('4. User only writes the function - no boilerplate needed!'); - - } catch (error) { - console.error('โŒ Connection failed:', error.message); - console.log('\n๐Ÿ’ก Make sure:'); - console.log('โ€ข API server is running on port 3001'); - console.log('โ€ข Supabase credentials are in code-executor-api/.env'); - console.log('โ€ข Judge0 API key is configured'); - } -} - -testConnection(); \ No newline at end of file diff --git a/test-dynamic-problems.js b/test-dynamic-problems.js deleted file mode 100644 index d03598c..0000000 --- a/test-dynamic-problems.js +++ /dev/null @@ -1,104 +0,0 @@ -// Test the new dynamic problem system - -// Test 1: Two Sum problem -const twoSumTest = { - language: 'python', - problemId: 'two-sum', // API will fetch test cases from DB - code: `def twoSum(nums: List[int], target: int) -> List[int]: - d = {} - for i in range(len(nums)): - if target - nums[i] in d: - return [d[target-nums[i]], i] - else: - d[nums[i]] = i` -}; - -// Test 2: Valid Parentheses problem -const validParenthesesTest = { - language: 'python', - problemId: 'valid-parentheses', // Different problem, different test cases - code: `def isValid(s: str) -> bool: - stack = [] - mapping = {")": "(", "}": "{", "]": "["} - - for char in s: - if char in mapping: - top_element = stack.pop() if stack else '#' - if mapping[char] != top_element: - return False - else: - stack.append(char) - - return not stack` -}; - -// Test 3: Reverse Integer problem -const reverseIntegerTest = { - language: 'python', - problemId: 'reverse-integer', - code: `def reverse(x: int) -> int: - sign = -1 if x < 0 else 1 - x = abs(x) - result = 0 - - while x: - result = result * 10 + x % 10 - x //= 10 - - result *= sign - return result if -2**31 <= result <= 2**31 - 1 else 0` -}; - -async function testProblem(testData, problemName) { - try { - console.log(`\n๐Ÿš€ Testing ${problemName}...`); - console.log(`๐Ÿ“‹ Problem ID: ${testData.problemId}`); - console.log(`๐Ÿ“ User Code: Function only (no boilerplate)`); - - const response = await fetch('http://localhost:3001/execute', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(testData) - }); - - const result = await response.json(); - - if (response.ok) { - console.log(`โœ… ${problemName} execution successful!`); - console.log(`๐Ÿ“Š Executed ${result.results.length} test cases\n`); - - result.results.forEach((test, i) => { - const status = test.passed ? 'โœ…' : 'โŒ'; - console.log(` ${status} Test ${i + 1}: Expected ${JSON.stringify(test.expected)}, Got ${test.actual}`); - }); - - const passedCount = result.results.filter(r => r.passed).length; - console.log(`\n๐ŸŽฏ ${problemName}: ${passedCount}/${result.results.length} passed`); - - } else { - console.error(`โŒ ${problemName} failed:`, result); - } - } catch (error) { - console.error(`โŒ ${problemName} error:`, error.message); - } -} - -async function runAllTests() { - console.log('๐ŸŽฏ Testing Dynamic Problem System'); - console.log('====================================='); - - // Test different problems with their own test cases - await testProblem(twoSumTest, 'Two Sum'); - await testProblem(validParenthesesTest, 'Valid Parentheses'); - await testProblem(reverseIntegerTest, 'Reverse Integer'); - - console.log('\nโœจ All dynamic tests completed!'); - console.log('\n๐Ÿ’ก Key Features Demonstrated:'); - console.log('โ€ข Each problem has its own test cases from "database"'); - console.log('โ€ข User submits clean function code only'); - console.log('โ€ข API automatically wraps with correct test cases'); - console.log('โ€ข Multiple problems work with same system'); - console.log('โ€ข Truly dynamic and extensible!'); -} - -runAllTests(); \ No newline at end of file diff --git a/test-judge0.js b/test-judge0.js deleted file mode 100644 index 485088f..0000000 --- a/test-judge0.js +++ /dev/null @@ -1,51 +0,0 @@ -// Test Judge0 API with batched submissions -const testData = { - language: 'python', - code: `# Simple test function -def add_numbers(a, b): - return a + b - -# Read input and execute -import sys -lines = sys.stdin.read().strip().split('\\n') -a, b = map(int, lines[0].split()) -result = add_numbers(a, b) -print(result)`, - testCases: [ - { input: ['2 3'], expected: '5' }, - { input: ['10 15'], expected: '25' }, - { input: ['0 0'], expected: '0' }, - { input: ['-1 1'], expected: '0' }, - { input: ['100 200'], expected: '300' } - ] -}; - -async function testAPI() { - try { - console.log('Testing Judge0 API with batch submission...'); - - const response = await fetch('http://localhost:3001/execute', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(testData) - }); - - const result = await response.json(); - - if (response.ok) { - console.log('โœ… API Test Successful!'); - console.log(`Executed ${result.results.length} test cases`); - - result.results.forEach((test, i) => { - const status = test.passed ? 'โœ…' : 'โŒ'; - console.log(`${status} Test ${i + 1}: Expected "${test.expected}", Got "${test.actual}" (${test.status})`); - }); - } else { - console.error('โŒ API Test Failed:', result); - } - } catch (error) { - console.error('โŒ Test Error:', error.message); - } -} - -testAPI(); \ No newline at end of file diff --git a/test-leetcode-style.js b/test-leetcode-style.js deleted file mode 100644 index 5c99232..0000000 --- a/test-leetcode-style.js +++ /dev/null @@ -1,60 +0,0 @@ -// Test the new LeetCode-style system -const testData = { - language: 'python', - // User submits ONLY the function - no boilerplate! - code: `def twoSum(nums: List[int], target: int) -> List[int]: - d = {} - for i in range(len(nums)): - if target - nums[i] in d: - return [d[target-nums[i]], i] - else: - d[nums[i]] = i`, - // Test cases are handled by the API - testCases: [ - { input: 'test_case_0', expected: '[0, 1]' }, // nums=[2,7,11,15], target=9 - { input: 'test_case_1', expected: '[1, 2]' }, // nums=[3,2,4], target=6 - { input: 'test_case_2', expected: '[0, 1]' } // nums=[3,3], target=6 - ] -}; - -async function testLeetCodeStyle() { - try { - console.log('๐Ÿš€ Testing LeetCode-style execution...'); - console.log('๐Ÿ“ User submitted code (clean function only):'); - console.log(testData.code); - console.log('\nโšก Sending to API...'); - - const response = await fetch('http://localhost:3001/execute', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(testData) - }); - - const result = await response.json(); - - if (response.ok) { - console.log('\nโœ… LeetCode-style execution successful!'); - console.log(`๐Ÿ“Š Executed ${result.results.length} test cases\n`); - - result.results.forEach((test, i) => { - const status = test.passed ? 'โœ…' : 'โŒ'; - console.log(`${status} Test ${i + 1}:`); - console.log(` Expected: ${test.expected}`); - console.log(` Got: ${test.actual}`); - console.log(` Status: ${test.status}`); - console.log(` Time: ${test.time}s`); - console.log(` Memory: ${test.memory} KB\n`); - }); - - const passedCount = result.results.filter(r => r.passed).length; - console.log(`๐ŸŽฏ Summary: ${passedCount}/${result.results.length} test cases passed`); - - } else { - console.error('โŒ API Test Failed:', result); - } - } catch (error) { - console.error('โŒ Connection Error:', error.message); - } -} - -testLeetCodeStyle(); \ No newline at end of file diff --git a/test-supabase-integration.js b/test-supabase-integration.js deleted file mode 100644 index b853743..0000000 --- a/test-supabase-integration.js +++ /dev/null @@ -1,110 +0,0 @@ -// Test real Supabase integration with dynamic problem fetching - -async function testSupabaseIntegration() { - console.log('๐Ÿ” Testing Real Supabase Integration'); - console.log('====================================='); - - // Test with a real problem from your Supabase database - const testData = { - language: 'python', - problemId: 'two-sum', // This should exist in your Supabase problems table - code: `def twoSum(nums: List[int], target: int) -> List[int]: - d = {} - for i in range(len(nums)): - if target - nums[i] in d: - return [d[target-nums[i]], i] - else: - d[nums[i]] = i` - }; - - try { - console.log(`\n๐Ÿš€ Testing problem: ${testData.problemId}`); - console.log('๐Ÿ“‹ API will fetch test cases from Supabase automatically'); - console.log('๐Ÿ“ User submitted clean function code only\n'); - - const response = await fetch('http://localhost:3001/execute', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(testData) - }); - - const result = await response.json(); - - if (response.ok) { - console.log('โœ… Supabase integration successful!'); - console.log(`๐Ÿ“Š Executed ${result.results.length} test cases from database\n`); - - result.results.forEach((test, i) => { - const status = test.passed ? 'โœ…' : 'โŒ'; - console.log(` ${status} Test ${i + 1}:`); - console.log(` Expected: ${JSON.stringify(test.expected)}`); - console.log(` Got: ${test.actual}`); - console.log(` Status: ${test.status}`); - if (test.time) console.log(` Time: ${test.time}s`); - if (test.memory) console.log(` Memory: ${test.memory} KB`); - console.log(''); - }); - - const passedCount = result.results.filter(r => r.passed).length; - console.log(`๐ŸŽฏ Result: ${passedCount}/${result.results.length} test cases passed`); - - if (passedCount === result.results.length) { - console.log('๐ŸŽ‰ All tests passed! Your solution is correct!'); - } else { - console.log('๐Ÿค” Some tests failed. Check your solution logic.'); - } - - } else { - console.error('โŒ Test failed:', result); - - if (result.error && result.error.includes('not found')) { - console.log('\n๐Ÿ’ก Make sure:'); - console.log(' โ€ข Your Supabase is running and accessible'); - console.log(' โ€ข The problem "two-sum" exists in your problems table'); - console.log(' โ€ข Test cases exist for this problem in test_cases table'); - } - } - } catch (error) { - console.error('โŒ Connection error:', error.message); - console.log('\n๐Ÿ’ก Make sure:'); - console.log(' โ€ข Your API server is running on port 3001'); - console.log(' โ€ข Your .env file has correct Supabase credentials'); - } - - console.log('\nโœจ Integration test completed!'); - console.log('\n๐Ÿ”ง What this test demonstrates:'); - console.log('โ€ข Real Supabase database queries'); - console.log('โ€ข Dynamic problem and test case fetching'); - console.log('โ€ข Clean user code (function only)'); - console.log('โ€ข Automatic test case parsing and execution'); - console.log('โ€ข True LeetCode-style experience!'); -} - -// Also test checking available problems -async function testAvailableProblems() { - console.log('\n๐Ÿ“š Testing Available Problems Query...'); - - try { - // This would be a new endpoint to list available problems - const response = await fetch('http://localhost:3001/problems'); - - if (response.ok) { - const problems = await response.json(); - console.log(`โœ… Found ${problems.length} problems in database`); - - if (problems.length > 0) { - console.log('\n๐Ÿ“‹ Sample problems:'); - problems.slice(0, 3).forEach(p => { - console.log(` โ€ข ${p.id}: ${p.title} (${p.difficulty})`); - }); - } - } else { - console.log('โš ๏ธ Problems endpoint not implemented yet'); - } - } catch (error) { - console.log('โš ๏ธ Problems endpoint not available yet'); - } -} - -// Run the tests -testSupabaseIntegration().then(() => testAvailableProblems()); \ No newline at end of file