diff --git a/README.md b/README.md index 38e5b1a..f00e5da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,15 @@ > _Code together. Think faster._ -A real-time collaborative code editor with AI-powered assistance. Multiple users can simultaneously write, edit, and debug code in the same room — with live cursor tracking, instant code synchronization, and an AI assistant powered by Google Gemini that streams responses in real-time. +- **Real-Time Collaboration:** A real-time collaborative code editor with AI-powered assistance. Multiple users can simultaneously write, edit, and debug code in the same room — with live cursor tracking, instant code synchronization, and an AI assistant powered by Google Gemini that streams responses in real-time. + +- **Scalability:** Designed a stateless backend architecture by replacing in-memory state with Redis, enabling **horizontal scaling across multiple server** instances. + +- **AI Integration:** Implemented an **AI assistant with streaming responses (SSE)**, delivering real-time incremental outputs similar to ChatGPT. + +- **Distributed Architecture:** Developed a Redis-based data model **(room:{id}:users)** as a single source of truth for user presence, ensuring consistency in distributed environments. + +- **Persistence & Reliability:** Built a snapshot persistence system with authenticated fetch keepalive, ensuring code is saved reliably even during page unload events. [![CI Pipeline](https://github.com/rah7202/smart-code-lab/actions/workflows/ci.yml/badge.svg)](https://github.com/rah7202/smart-code-lab/actions) @@ -37,38 +45,86 @@ A real-time collaborative code editor with AI-powered assistance. Multiple users --- -## Architecture +## System Architecture -``` -┌─────────────────────────────────────┐ -│ Frontend (React) │ -│ ┌──────────┐ ┌──────┐ ┌────────┐ │ -│ │ Monaco │ │ AI │ │Version │ │ -│ │ Editor │ │Panel │ │History │ │ -│ └────┬─────┘ └──┬───┘ └───┬────┘ │ -│ │ │ │ │ -│ ┌────┴──────────┴──────────┴────┐ │ -│ │ Hooks Layer │ │ -│ │ useCollaboration │ │ -│ │ useAI (SSE streaming) │ │ -│ │ useEditorPersistence │ │ -│ └────┬──────────┬──────────┬────┘ │ -└───────┼──────────┼──────────┼───────┘ - │ WebSocket│ REST/SSE │ REST - │ │ │ -┌───────┼──────────┼──────────┼───────┐ -│ │ Backend (Express) │ │ -│ ┌────┴────┐ ┌───┴───┐ ┌───┴────┐ │ -│ │Socket.IO│ │ AI │ │ Room │ │ -│ │ Server │ │Service│ │Service │ │ -│ └────┬────┘ └───┬───┘ └───┬────┘ │ -│ │ │ │ │ -│ ┌────┴──────────┴──────────┴────┐ │ -│ │ Data Layer │ │ -│ │ PostgreSQL Redis │ │ -│ │ (Prisma ORM) (state/adapter)│ │ -│ └───────────────────────────────┘ │ -└─────────────────────────────────────┘ +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart TB + + subgraph Client["🖥️ Frontend · React + Vite"] + direction LR + Monaco["Monaco Editor"] + AIP["AI Panel"] + VH["Version History"] + end + + subgraph HooksLayer["🪝 Hooks Layer"] + direction LR + HC["useCollaboration"] + HA["useAI"] + HP["useEditorPersistence"] + end + + Client --> HooksLayer + + HC <-->|"WebSocket"| SocketIO + HA <-->|"SSE Stream"| AIRoute + HP <-->|"REST + JWT"| RoomRoute + + subgraph Server["⚙️ Backend · Express"] + + MW["Middleware\nHelmet · CORS · Rate Limit · JWT · Zod"] + + subgraph Core["Services"] + direction LR + SocketIO["Socket.IO Server\n+ Redis Adapter"] + AIRoute["AI Service\nGemini 2.5 Flash"] + RoomRoute["Room Service"] + CompileSvc["Compile Service"] + end + + MW --> Core + end + + CompileSvc -->|"HTTP"| Judge0["Judge0 API"] + AIRoute -->|"API"| Gemini["Google Gemini"] + + subgraph DataStores["💾 Data Layer"] + direction LR + PG["PostgreSQL · Prisma ORM\nUser · Room · Snapshot · AIMessage"] + RD["Redis\nRoom State · Presence · Pub/Sub"] + end + + Core --> DataStores + SocketIO -.->|"horizontal scaling"| RD + + %% 🎨 COLOR DEFINITIONS + classDef client fill:#3b82f6,stroke:#1e40af,color:#fff + classDef core fill:#10b981,stroke:#065f46,color:#fff + classDef service fill:#f59e0b,stroke:#92400e,color:#fff + classDef realtime fill:#8b5cf6,stroke:#5b21b6,color:#fff + classDef data fill:#ef4444,stroke:#7f1d1d,color:#fff + classDef external fill:#f3f4f6,stroke:#9ca3af,color:#111 + + %% 🎯 APPLY COLORS + + %% Frontend + class Monaco,AIP,VH client + + %% Hooks + Middleware + class HC,HA,HP,MW core + + %% Services + class AIRoute,RoomRoute,CompileSvc service + + %% Realtime + class SocketIO realtime + + %% Data + class PG,RD data + + %% External APIs + class Judge0,Gemini external ``` --- @@ -134,7 +190,7 @@ smart-code-lab/ - **Node.js** ≥ 20 - **npm** ≥ 9 -- **PostgreSQL** (local or hosted, e.g., Neon) +- **PostgreSQL** (local or hosted, e.g., Neon DB) - **Redis** (local or hosted, e.g., Render Redis) - **Gemini API Key** — [Get one here](https://aistudio.google.com/apikey) @@ -277,7 +333,6 @@ frontend job (runs after backend passes): | **Yjs / CRDT Integration** | 🔵 Next Major | Replace keystroke broadcasting with binary delta sync for better concurrency | | **Docker Compose** | 🟡 Medium | Redis + PostgreSQL + App in one-command setup | | **More Languages** | 🟡 Medium | Java, Go, Rust, etc. | -| **Room Sharing Link** | 🟢 Easy | Copy-to-clipboard shareable URL | | **AI Context Awareness** | 🔵 Future | Pass execution output to Gemini for smarter debugging | | **JWT Blacklist on Logout** | 🟢 Easy | `redis.set(blacklist:${token})` with TTL | diff --git a/backend/README.md b/backend/README.md index 1bea843..813a8dc 100644 --- a/backend/README.md +++ b/backend/README.md @@ -67,6 +67,138 @@ backend/ ## Architecture +### System Architecture + +```mermaid + +%%{init: {'theme': 'base'}}%% +flowchart LR + + Client["🌐 Client\n(Browser)"] + + subgraph MiddlewarePipeline["🛡️ Middleware Pipeline"] + direction TB + M1["Helmet\nSecurity Headers"] + M2["CORS\nOrigin Allowlist"] + M3["Body Parser\nJSON · 50KB limit"] + M4["Rate Limiter\n100 req/min"] + M1 --> M2 --> M3 --> M4 + end + + Client -->|"HTTP / REST"| MiddlewarePipeline + + subgraph AuthGate["🔐 Auth Gate"] + direction TB + JWT["JWT Verify\nExtract userId"] + ZOD["Zod Validate\nSchema check"] + JWT --> ZOD + end + + MiddlewarePipeline --> AuthGate + + subgraph API["📋 API Routes"] + direction TB + + subgraph R1["Auth"] + A1["POST /signup"] + A2["POST /signin"] + end + + subgraph R2["Room"] + B1["GET /:roomId"] + B2["POST /create"] + B3["POST /:roomId/save"] + end + + subgraph R3["Compile"] + C1["POST /compile"] + end + + subgraph R4["AI"] + D1["POST /generate"] + D2["POST /stream ← SSE"] + D3["GET /history/:roomId"] + D4["DELETE /history/:roomId"] + end + + subgraph R5["Snapshot"] + E1["POST /:roomId"] + E2["GET /:roomId"] + end + end + + AuthGate --> API + + subgraph Services["⚙️ Service Layer"] + direction TB + AuthSvc["Auth Service\nbcrypt + JWT sign"] + RoomSvc["Room Service\nFind or Create"] + CompileSvc["Judge0 Service\nCode Execution"] + AISvc["AI Service\nGemini 2.5 Flash\nBatch + Stream"] + SnapSvc["Snapshot Service\nDeduplicated Save"] + end + + R1 --> AuthSvc + R2 --> RoomSvc + R3 --> CompileSvc + R4 --> AISvc + R5 --> SnapSvc + + subgraph DataLayer["💾 Data Stores"] + direction TB + PG["PostgreSQL\n(Prisma ORM)\n───\nUser · Room\nCodeSnapshot\nAIMessage"] + RD["Redis\n───\nRoom state\nUser presence\nSocket mapping"] + end + + AuthSvc & RoomSvc & SnapSvc --> PG + RoomSvc --> RD + + subgraph External["🌍 External APIs"] + direction TB + Judge0["Judge0 CE\nCode Runner"] + Gemini["Google Gemini\nAI Generation"] + end + + CompileSvc --> Judge0 + AISvc --> Gemini + + WSClient["🔌 Socket.IO\nClient"] + + subgraph SocketServer["🔌 Socket.IO Server"] + direction TB + SAuth["JWT Handshake Auth"] + SEvents["Events\n───\njoin · content-edited\ncursor-move · disconnect"] + SAdapter["Redis Adapter\nMulti-server pub/sub"] + SAuth --> SEvents --> SAdapter + end + + WSClient -->|"WebSocket"| SocketServer + SAdapter --> RD + SEvents --> PG + + %% 🎨 COLOR DEFINITIONS + classDef client fill:#3b82f6,stroke:#1e40af,color:#fff + classDef core fill:#10b981,stroke:#065f46,color:#fff + classDef service fill:#f59e0b,stroke:#92400e,color:#fff + classDef socket fill:#8b5cf6,stroke:#5b21b6,color:#fff + classDef data fill:#ef4444,stroke:#7f1d1d,color:#fff + classDef external fill:#f3f4f6,stroke:#9ca3af,color:#111 + + %% 🎯 APPLY COLORS + class Client client + + class M1,M2,M3,M4,JWT,ZOD,A1,A2,B1,B2,B3,C1,D1,D2,D3,D4,E1,E2 core + + class AuthSvc,RoomSvc,CompileSvc,AISvc,SnapSvc service + + class WSClient,SAuth,SEvents,SAdapter socket + + class PG,RD data + + class Judge0,Gemini external + +``` + ### Request Flow ``` @@ -79,10 +211,10 @@ Client Request CORS (origin allowlist) │ ▼ - Rate Limiter (global: 100/min, AI: 10/min) + Body Parser (50KB limit) │ ▼ - Body Parser (50KB limit) + Rate Limiter (global: 100/min, AI: 10/min) │ ▼ authenticate middleware (JWT verify) @@ -545,7 +677,7 @@ Room (1) ──→ (N) AIMessage # Run all tests with coverage npm test -# Coverage thresholds: 70% lines, 70% functions +# Coverage thresholds: 70% lines, 60% functions ``` Test environment variables are set in `src/__tests__/envSetup.ts` — `JWT_SECRET`, `NODE_ENV=test`, and `DATABASE_URL` are all provided so no real database is needed (Prisma is mocked). diff --git a/frontend/README.md b/frontend/README.md index 23d32c8..ff17f6f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -59,7 +59,96 @@ frontend/ ## Architecture -### EditorPage — The Orchestration Hub +### System Architecture + +```mermaid +%%{init: {'theme': 'base'}}%% +flowchart TB + + subgraph Pages["🖥️ Pages (React Router)"] + Login["Login"] + Signup["Signup"] + Home["Home"] + Editor["Editor Page"] + end + + Login & Signup -->|"JWT token"| Home + Home -->|"/editor/:roomId"| Editor + + subgraph EditorLayout["📐 Editor Page Layout"] + direction LR + + subgraph Left["Editor Panel (2/3)"] + Monaco["Monaco Editor\n(Uncontrolled)"] + Toolbar["Selection Toolbar\nExplain · Review · Fix · Optimize"] + end + + subgraph Right["Right Panel (1/3)"] + Input["Code Input + Run"] + AI["AI Chat Panel\n(Resizable)"] + Versions["Version History"] + end + end + + Editor --> EditorLayout + + subgraph Hooks["🪝 Custom Hooks (Separation of Concerns)"] + direction TB + + spacer1[" "]:::invisible + + usePersist["useEditorPersistence\n───\nDB load/save\nCtrl+S snapshots\nDebounced auto-save\nbeforeunload backup"] + + useCollab["useCollab\n(Realtime Sync)\n───\nSocket.IO lifecycle\nRoom join/leave\nCursor decorations\nCode broadcast"] + + useAI["useAI\n───\nSSE stream reader\nRate limit (5/min)\n3 modes: code · selection · question\nChat history state"] + end + + Monaco -->|"onChange\n(user keystrokes only)"| usePersist + Monaco -->|"emitCodeChange"| useCollab + Toolbar -->|"onAsk(prompt, code)"| useAI + Input -->|"analyzeCode()"| useAI + + useCollab -->|"applyCode()\ncursor CSS"| Monaco + usePersist -->|"applyCode()\napplyLang()"| Monaco + + subgraph Transport["📡 Communication Layer"] + direction LR + + spacer2[" "]:::invisible + + Axios["Axios\n(REST + JWT)"] + Socket["Socket.IO\n(WebSocket)"] + SSE["fetch\n(SSE Stream)"] + end + + usePersist --> Axios + useCollab --> Socket + useAI --> SSE + + Backend["⚙️ Backend API\nhttp://localhost:8000"] + + Axios & Socket & SSE --> Backend + + %% 🎨 CLASS DEFINITIONS + classDef pages fill:#3b82f6,stroke:#1e40af,color:#fff + classDef layout fill:#10b981,stroke:#065f46,color:#fff + classDef hooks fill:#f59e0b,stroke:#92400e,color:#fff + classDef transport fill:#8b5cf6,stroke:#5b21b6,color:#fff + classDef backend fill:#ef4444,stroke:#7f1d1d,color:#fff + classDef invisible fill:transparent,stroke:none,color:transparent + + + %% 🎯 APPLY CLASSES + class Login,Signup,Home,Editor pages + class Monaco,Toolbar,Input,AI,Versions layout + class usePersist,useCollab,useAI hooks + class Axios,Socket,SSE transport + class Backend backend + class spacer1,spacer2 invisible +``` + +### Editor Page — The Orchestration Hub `EditorPage.tsx` is the main page component. It owns Monaco, refs, and imperative helpers — but delegates all business logic to three custom hooks: diff --git a/frontend/src/components/Home.tsx b/frontend/src/components/Home.tsx index ee8b4d8..5e22559 100644 --- a/frontend/src/components/Home.tsx +++ b/frontend/src/components/Home.tsx @@ -5,12 +5,15 @@ import toast from "react-hot-toast"; import { socket } from "../socket"; import { getUserFromToken } from "../utils/auth"; import { AxiosError } from "axios"; +import { FiCopy } from "react-icons/fi"; +import { FaCheck } from "react-icons/fa" import Footer from "./Footer"; export default function Home() { const navigate = useNavigate(); const [roomId, setRoomId] = useState(""); + const [copied, setCopied] = useState(false); const user = getUserFromToken(); const name = user?.username?.charAt(0).toUpperCase() + user?.username?.slice(1); @@ -47,6 +50,22 @@ export default function Home() { toast.success("Joined room successfully"); }; + const handleCopy = async () => { + if (!roomId) return + + try { + await navigator.clipboard.writeText(roomId); + setCopied(true); + toast.success("Room ID Copied"); + + setTimeout(() => setCopied(false), 1500); + + + } catch { + toast.error("Copy Failed"); + } + }; + const handleLogout = () => { try { @@ -71,10 +90,20 @@ export default function Home() { )}
- { setRoomId(e.target.value) }} /> - +
+ { setRoomId(e.target.value) }} /> + {roomId.trim() && ( + + )} +
+ +