From bedd4c18c3548ff60cfea123ad6ef5914676d574 Mon Sep 17 00:00:00 2001 From: Irakli Grigolia Date: Fri, 8 Aug 2025 14:11:18 -0400 Subject: [PATCH 1/2] feat: implement submissions history with syntax highlighting and test results feat: add dev environment setup with CI/CD workflows and code solutions chore: clean up debug logs, fix code analysis logic, and pin bun version to 1.1.3 fix: improve TypeScript types and use GitHub secrets for CI - Add proper TypeScript interfaces for ReactMarkdown components - Fix React hook dependencies in ProblemSolver component - Add type-safe Monaco editor interface - Use GitHub secrets instead of hardcoded credentials in CI workflow chore: update CI workflow and improve dev-start script - Upgrade actions/cache from v3 to v4 in CI workflow - Enhance health check logic for frontend and API services with retries - Update Node.js version from 18 to 20 in CI workflow - Refactor environment file creation to use printf for better readability - Improve cleanup handling in dev-start script for background processes - Ensure proper signal handling for cleanup on exit fix: remove gituh actions --- .eslintrc.overrides.js | 26 ++ CI_CD_README.md | 182 +++++++++++ bun.lockb | Bin 198351 -> 258698 bytes code-executor-api/server.js | 11 +- eslint.config.js | 6 + package.json | 6 +- scripts/dev-start.sh | 100 ++++++ src/components/AIChat.tsx | 80 ++++- src/components/Notes.tsx | 1 + src/components/ui/command.tsx | 2 +- src/components/ui/textarea.tsx | 3 +- src/data/pythonSolutions.ts | 161 ++++++++++ src/hooks/useChatSession.ts | 23 +- src/hooks/useProblems.ts | 2 +- src/hooks/useSubmissions.ts | 49 +++ src/pages/ProblemSolver.tsx | 478 ++++++++++++++++++++-------- src/services/userAttempts.ts | 22 ++ supabase/functions/ai-chat/index.ts | 14 +- tailwind.config.ts | 1 + test-connection.js | 80 ----- test-dynamic-problems.js | 104 ------ test-judge0.js | 51 --- test-leetcode-style.js | 60 ---- test-supabase-integration.js | 110 ------- 24 files changed, 1022 insertions(+), 550 deletions(-) create mode 100644 .eslintrc.overrides.js create mode 100644 CI_CD_README.md mode change 100644 => 100755 bun.lockb create mode 100755 scripts/dev-start.sh create mode 100644 src/data/pythonSolutions.ts create mode 100644 src/hooks/useSubmissions.ts delete mode 100644 test-connection.js delete mode 100644 test-dynamic-problems.js delete mode 100644 test-judge0.js delete mode 100644 test-leetcode-style.js delete mode 100644 test-supabase-integration.js 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 160304d398161f97165233dfe6636caa631bfdfb..2ce621a80e4fd9b79109b148577d97f3ea9248e6 GIT binary patch delta 74914 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 Ae*gdg delta 37095 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 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..765bab1 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 { 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,30 @@ 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'); + + // Debug scroll area + useEffect(() => { + if (activeTab === 'solution') { + setTimeout(() => { + const scrollArea = document.querySelector('[data-radix-scroll-area-viewport]'); + const contentDiv = scrollArea?.querySelector('div[style*="blue"]'); + }, 100); + } + }, [activeTab]); 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 +78,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.replace(/": /g, '":').replace(/, "/g, ',"'); + } catch { + return trimmed; + } + } + return JSON.stringify(value); + } + try { + const result = safeStableStringify(value); + return result.replace(/": /g, '":').replace(/, "/g, ',"'); + } catch { + try { + return JSON.stringify(value); + } catch { + return String(value); + } + } + }; + const toggleBottomPanel = () => { const newValue = !showBottomPanel; setShowBottomPanel(newValue); @@ -95,9 +148,24 @@ 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 = (() => { + // Sort newest first by created_at + const sorted = [...(submissions || [])].sort((a, b) => + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + // Keep only the first (newest) for each unique trimmed code + const byCode = new Map(); + for (const s of sorted) { + const key = s.code.trim(); + if (!byCode.has(key)) byCode.set(key, s); + } + return Array.from(byCode.values()); + })(); if (loading) { return ( @@ -262,6 +330,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 +374,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 +391,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 +518,8 @@ const ProblemSolver = () => { {showLeftPanel && ( <> -
    - +
    +
    @@ -390,139 +537,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 +868,7 @@ const ProblemSolver = () => {
    Expected Output:
    -
    {renderValue(result.expected)}
    +
    {toCompactJson(result.expected)}
    @@ -648,7 +878,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 From a1abe6256d0463f53a568de1b530de44b6d0c905 Mon Sep 17 00:00:00 2001 From: Irakli Grigolia Date: Sun, 10 Aug 2025 11:02:54 -0400 Subject: [PATCH 2/2] refactor: optimize unique submissions logic and clean up unused debug code in ProblemSolver component - Replace inline function with useMemo for deduplicating submissions - Remove unnecessary debug scroll area effect - Simplify JSON stringification logic --- src/pages/ProblemSolver.tsx | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/pages/ProblemSolver.tsx b/src/pages/ProblemSolver.tsx index 765bab1..4fe8177 100644 --- a/src/pages/ProblemSolver.tsx +++ b/src/pages/ProblemSolver.tsx @@ -20,7 +20,7 @@ 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 { useState, useEffect, useRef, useMemo } from 'react'; import { insertCodeSnippet } from '@/utils/codeInsertion'; import Timer from '@/components/Timer'; import { supabase } from '@/integrations/supabase/client'; @@ -36,15 +36,7 @@ const ProblemSolver = () => { const { isDark } = useTheme(); const [activeTab, setActiveTab] = useState('question'); - // Debug scroll area - useEffect(() => { - if (activeTab === 'solution') { - setTimeout(() => { - const scrollArea = document.querySelector('[data-radix-scroll-area-viewport]'); - const contentDiv = scrollArea?.querySelector('div[style*="blue"]'); - }, 100); - } - }, [activeTab]); + const [code, setCode] = useState(''); const [testResults, setTestResults] = useState([]); const [isRunning, setIsRunning] = useState(false); @@ -86,7 +78,7 @@ const ProblemSolver = () => { try { const parsed = JSON.parse(trimmed); const result = safeStableStringify(parsed); - return result.replace(/": /g, '":').replace(/, "/g, ',"'); + return result; } catch { return trimmed; } @@ -95,7 +87,7 @@ const ProblemSolver = () => { } try { const result = safeStableStringify(value); - return result.replace(/": /g, '":').replace(/, "/g, ',"'); + return result; } catch { try { return JSON.stringify(value); @@ -153,19 +145,17 @@ const ProblemSolver = () => { const problem = problems.find(p => p.id === problemId); // Deduplicate submissions by code content, keeping the most recent for each unique solution - const uniqueSubmissions = (() => { - // Sort newest first by created_at + const uniqueSubmissions = useMemo(() => { const sorted = [...(submissions || [])].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); - // Keep only the first (newest) for each unique trimmed code - const byCode = new Map(); + 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 (