diff --git a/README.md b/README.md deleted file mode 100644 index 891ae1f..0000000 --- a/README.md +++ /dev/null @@ -1,295 +0,0 @@ -# Smart Code Lab πŸ§ͺ - -A real-time collaborative code editor with AI-powered assistance, multi-language support, and version β€” built for teams who want to write, run, and review code together. - -![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white) -![React](https://img.shields.io/badge/React-20232A?style=flat&logo=react&logoColor=61DAFB) -![Node.js](https://img.shields.io/badge/Node.js-339933?style=flat&logo=node.js&logoColor=white) -![Prisma](https://img.shields.io/badge/Prisma-2D3748?style=flat&logo=prisma&logoColor=white) -![Socket.io](https://img.shields.io/badge/Socket.io-010101?style=flat&logo=socket.io&logoColor=white) - ---- - -## ✨ Features - -- **Real-time collaboration** β€” Live cursors, user presence, and instant code sync across all users in a room -- **Multi-language support** β€” Python, JavaScript, C++, C with per-language syntax themes and starter templates -- **AI assistance (Gemini Flash 2.5)** β€” Full code analysis, freeform Q&A, and selection-aware inline toolbar -- **Code execution** β€” Run code via Judge0 with stdin support and live output -- **Version history** β€” Auto-saved snapshots on every run and Ctrl+S, with one-click restore -- **Persistent rooms** β€” Code is saved to the database and reloaded on re-join -- **Download code** β€” Auto-named file with username + timestamp -- **Rate limiting** β€” Client + server side (5 req/min per user) - ---- - -## πŸ—οΈ Architecture - -```mermaid -graph TB - subgraph Client ["Frontend (React + TypeScript + Vite)"] - direction TB - EP["EditorPage"] - NB["Navbar"] - UP["UserPresenceBar"] - ME["Monaco Editor"] - ST["SelectionToolbar"] - - subgraph RightSide ["Right Panel"] - CI["CodeInputPanel"] - AI["AIPanel"] - VP["VersionPanel"] - end - - subgraph Hooks ["Custom Hooks"] - UC["useCollaboration"] - UAI["useAI"] - UEP["useEditorPersistence"] - end - - EP --> NB - EP --> UP - EP --> ME - EP --> ST - EP --> RightSide - EP --> Hooks - end - - subgraph Server ["Backend (Node.js + Express + TypeScript)"] - direction TB - APP["app.ts"] - - subgraph Routes ["Routes"] - AR["ai.route.ts"] - CR["compile.route.ts"] - RR["room.route.ts"] - SR["codeSnapshot.routes.ts"] - end - - subgraph Controllers ["Controllers"] - AC["ai.controller.ts"] - CC["compile.controller.ts"] - RC["room.controller.ts"] - SC["codeSnapshot.controller.ts"] - end - - subgraph Services ["Services"] - AS["ai.service.ts"] - J0["judge0.service.ts"] - RS["room.service.ts"] - CSS["codeSnapshot.service.ts"] - end - - SOC["socket.ts (Socket.IO)"] - DB["prisma.ts (Prisma ORM)"] - - APP --> Routes - APP --> SOC - Routes --> Controllers - Controllers --> Services - Services --> DB - end - - subgraph External ["External Services"] - GEM["Gemini Flash 2.5 API"] - J0EXT["Judge0 API"] - PG[("PostgreSQL / SQLite")] - end - - ME -- "onChange / onMount" --> UC - UC -- "socket events" --> SOC - SOC -- "cursor-move\ncontent-edited\nusers\ncode-sync" --> UC - - UAI -- "POST /ai/generate" --> AC - AC --> AS - AS --> GEM - - CI -- "POST /compile" --> CC - CC --> J0 - J0 --> J0EXT - - UEP -- "GET /room/:id\nPOST /room/:id/save\nPOST /snapshot/:id" --> RC - RC --> RS - RS --> DB - DB --> PG - - VP -- "GET /snapshots/:id" --> SC - SC --> CSS - CSS --> DB -``` - ---- - -## πŸ“ Project Structure - -``` -Smart Code Lab/ -β”œβ”€β”€ backend/ -β”‚ β”œβ”€β”€ prisma/ -β”‚ β”‚ β”œβ”€β”€ migrations/ -β”‚ β”‚ └── schema.prisma -β”‚ └── src/ -β”‚ β”œβ”€β”€ controllers/ -β”‚ β”‚ β”œβ”€β”€ ai.controller.ts -β”‚ β”‚ β”œβ”€β”€ codeSnapshot.controller.ts -β”‚ β”‚ β”œβ”€β”€ compile.controller.ts -β”‚ β”‚ └── room.controller.ts -β”‚ β”œβ”€β”€ db/ -β”‚ β”‚ └── prisma.ts -β”‚ β”œβ”€β”€ routes/ -β”‚ β”‚ β”œβ”€β”€ ai.route.ts -β”‚ β”‚ β”œβ”€β”€ codeSnapshot.routes.ts -β”‚ β”‚ β”œβ”€β”€ compile.route.ts -β”‚ β”‚ └── room.route.ts -β”‚ β”œβ”€β”€ services/ -β”‚ β”‚ β”œβ”€β”€ ai.service.ts -β”‚ β”‚ β”œβ”€β”€ codeSnapshot.service.ts -β”‚ β”‚ β”œβ”€β”€ judge0.service.ts -β”‚ β”‚ └── room.service.ts -β”‚ β”œβ”€β”€ sockets/ -β”‚ β”‚ └── socket.ts -β”‚ β”œβ”€β”€ types/ -β”‚ β”‚ └── index.ts -β”‚ β”œβ”€β”€ app.ts -β”‚ └── index.ts -β”‚ -└── frontend/ - └── src/ - β”œβ”€β”€ assets/ - β”œβ”€β”€ components/ - β”‚ β”œβ”€β”€ AIPanel.tsx - β”‚ β”œβ”€β”€ CodeInputPanel.tsx - β”‚ β”œβ”€β”€ EditorPage.tsx - β”‚ β”œβ”€β”€ Footer.tsx - β”‚ β”œβ”€β”€ Home.tsx - β”‚ β”œβ”€β”€ LanguageBadge.tsx - β”‚ β”œβ”€β”€ Navbar.tsx - β”‚ β”œβ”€β”€ RightPanel.tsx - β”‚ β”œβ”€β”€ SelectionToolbar.tsx - β”‚ β”œβ”€β”€ UserPresenceBar.tsx - β”‚ β”œβ”€β”€ VersionHistory.tsx - β”‚ └── VersionPanel.tsx - β”œβ”€β”€ hooks/ - β”‚ β”œβ”€β”€ useAI.ts - β”‚ β”œβ”€β”€ useCollaboration.ts - β”‚ └── useEditorPersistence.ts - β”œβ”€β”€ languageOptions.ts - β”œβ”€β”€ socket.ts - └── App.tsx -``` - ---- - -## πŸš€ Getting Started - -### Prerequisites - -- Node.js β‰₯ 18 -- npm or pnpm -- A running PostgreSQL instance (or update `schema.prisma` for SQLite) -- Judge0 API key (self-hosted or RapidAPI) -- Google Gemini API key - -### 1. Clone the repo - -```bash -git clone https://github.com/rah7202/smart-code-lab.git -cd smart-code-lab -``` - -### 2. Backend setup - -```bash -cd backend -npm install -``` - -Create a `.env` file: - -```env -DATABASE_URL="postgresql://user:password@localhost:5432/smartcodelab" -GEMINI_API_KEY="your_gemini_api_key" -JUDGE0_API_KEY="your_judge0_api_key" -JUDGE0_BASE_URL="https://judge0-ce.p.rapidapi.com" -PORT=8000 -``` - -Run migrations and start: - -```bash -npx prisma migrate dev -npm run dev -``` - -### 3. Frontend setup - -```bash -cd frontend -npm install -npm run dev -``` - -App runs at `http://localhost:5173` - ---- - -## πŸ”Œ API Reference - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `POST` | `/compile` | Execute code via Judge0 | -| `POST` | `/ai/generate` | Ask Gemini about code | -| `DELETE` | `/ai/history/:roomId` | Clear AI chat history | -| `GET` | `/room/:roomId` | Load room code + language | -| `POST` | `/room/:roomId/save` | Save current code to room | -| `POST` | `/snapshot/:roomId` | Save a named snapshot | -| `GET` | `/snapshots/:roomId` | Get all snapshots for a room | - -### Socket Events - -| Event | Direction | Payload | Description | -|-------|-----------|---------|-------------| -| `join` | Client β†’ Server | `{ RoomId, username }` | Join a room | -| `content-edited` | Client ↔ Server | `{ code, language }` | Broadcast code change | -| `cursor-move` | Client ↔ Server | `{ line, column, username, color, socketId }` | Broadcast cursor position | -| `code-sync` | Server β†’ Client | `string` | Send existing code to new joiner | -| `users` | Server β†’ Client | `User[]` | Updated user list with colors | - ---- - -## πŸ› οΈ Tech Stack - -| Layer | Technology | -|-------|-----------| -| Frontend framework | React 18 + TypeScript + Vite | -| Editor | Monaco Editor (`@monaco-editor/react`) | -| Styling | Tailwind CSS v4 | -| Real-time | Socket.IO | -| Backend | Node.js + Express + TypeScript | -| ORM | Prisma | -| Database | PostgreSQL | -| Code execution | Judge0 API | -| AI | Google Gemini Flash 2.5 | -| State management | React hooks (no external lib) | - ---- - -## ⌨️ Keyboard Shortcuts - -| Shortcut | Action | -|----------|--------| -| `Ctrl + S` | Save snapshot | -| `Enter` (in AI input) | Send question to Gemini | -| `Shift + Enter` | New line in AI input | - ---- - -## 🀝 Contributing - -Pull requests are welcome. For major changes, please open an issue first. - ---- - -## πŸ“„ License - -MIT Β© [rah7202](https://github.com/rah7202) diff --git a/ROOT_README.md b/ROOT_README.md new file mode 100644 index 0000000..38e5b1a --- /dev/null +++ b/ROOT_README.md @@ -0,0 +1,290 @@ +# Smart Code Lab + +> _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. + +[![CI Pipeline](https://github.com/rah7202/smart-code-lab/actions/workflows/ci.yml/badge.svg)](https://github.com/rah7202/smart-code-lab/actions) + +--- + +## Features + +| Feature | Description | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------- | +| πŸ–₯️ **Monaco Editor** | VS Code-quality editor with syntax highlighting, IntelliSense, and custom themes per language | +| πŸ‘₯ **Real-Time Collaboration** | Multiple users edit simultaneously with live cursor positions and colored name badges | +| πŸ€– **AI Assistant (Gemini)** | Ask Gemini to explain, review, fix, or optimize your code β€” responses stream in real-time via SSE | +| βœ‚οΈ **Selection Toolbar** | Select any code snippet β†’ inline floating toolbar with Explain, Review, Fix, Optimize, and custom Ask actions | +| ▢️ **Code Execution** | Run code directly in the browser (Python, JavaScript, C++, C) via Judge0 API | +| πŸ“Έ **Version History** | Automatic snapshots on every run + manual save with one-click restore | +| πŸ” **JWT Authentication** | Secure signup/signin with bcrypt password hashing and JWT tokens | +| ⚑ **Redis-Backed State** | Room state, user presence, and code content stored in Redis for horizontal scaling | +| πŸ–±οΈ **Resizable AI Panel** | Drag to resize the AI output section for a customized workspace | +| πŸ“₯ **Code Download** | Download your code as a properly named file (e.g., `Rahul-2026-04-04.py`) | +| 🎨 **Custom Themes** | Each language has its own color-tuned Monaco theme (blue for Python, amber for JS, purple for C++, teal for C) | + +--- + +## Supported Languages + +| Language | Badge | Judge0 ID | Monaco Theme | +| ---------- | ----- | --------- | ------------ | +| JavaScript | `JS` | 63 | Warm amber | +| Python | `PY` | 71 | Deep blue | +| C++ | `C++` | 54 | Purple | +| C | `C` | 50 | Teal | + +--- + +## 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)β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Project Structure + +``` +smart-code-lab/ +β”œβ”€β”€ .github/ +β”‚ └── workflows/ +β”‚ └── ci.yml # CI pipeline (GitHub Actions) +β”œβ”€β”€ backend/ # Express API + Socket.IO server +β”œβ”€β”€ frontend/ # React + Vite SPA +β”œβ”€β”€ .gitignore +└── README.md +``` + +--- + +## Tech Stack + +### Frontend + +| Technology | Version | Purpose | +| ---------------- | ------- | --------------------------------- | +| React | 19.2 | UI framework | +| Vite | 8.0 | Build tool & dev server | +| TypeScript | 5.9 | Type safety | +| TailwindCSS | 4.2 | Utility-first styling | +| Monaco Editor | 4.7 | Code editor (VS Code engine) | +| Socket.IO Client | 4.8 | Real-time WebSocket communication | +| Axios | 1.13 | HTTP client with interceptors | +| React Router DOM | 7.13 | Client-side routing | +| React Markdown | 10.1 | AI response rendering | +| Vitest | 4.1 | Testing framework | +| Testing Library | 16.3 | Component testing | + +### Backend + +| Technology | Version | Purpose | +| ------------------------ | ------- | --------------------------------- | +| Express | 5.2 | Web framework | +| TypeScript | 5.9 | Type safety | +| Prisma | 5.22 | ORM for PostgreSQL | +| PostgreSQL | β€” | Primary database | +| Redis | 5.11 | Session state + Socket.IO adapter | +| Socket.IO | 4.8 | Real-time WebSocket server | +| @socket.io/redis-adapter | 8.3 | Multi-server pub/sub | +| Google Generative AI | 0.24 | Gemini AI integration | +| JSON Web Token | 9.0 | Authentication | +| Bcrypt | 6.0 | Password hashing | +| Zod | 4.3 | Request validation | +| Helmet | 8.1 | Security headers | +| Express Rate Limit | 8.3 | Rate limiting | +| Jest | 30.3 | Testing framework | +| Supertest | 7.2 | HTTP assertion library | + +--- + +## Getting Started + +### Prerequisites + +- **Node.js** β‰₯ 20 +- **npm** β‰₯ 9 +- **PostgreSQL** (local or hosted, e.g., Neon) +- **Redis** (local or hosted, e.g., Render Redis) +- **Gemini API Key** β€” [Get one here](https://aistudio.google.com/apikey) + +### 1. Clone the Repository + +```bash +git clone https://github.com/rah7202/smart-code-lab.git +cd smart-code-lab +``` + +### 2. Backend Setup + +```bash +cd backend +npm install +``` + +Create `backend/.env.local`: + +```env +DATABASE_URL="postgresql://user:password@localhost:5432/smartcodelab" +GEMINI_API_KEY=your_gemini_api_key +JWT_SECRET=your_jwt_secret_here_minimum_64_chars +ALLOWED_ORIGINS=http://localhost:5173 +REDIS_URL=redis://localhost:6379 +LOG_LEVEL=debug +``` + +Generate Prisma client and run migrations: + +```bash +npx prisma generate +npx prisma migrate dev +``` + +Start the backend dev server: + +```bash +npm run dev +# β†’ Server starts on http://localhost:8000 +``` + +### 3. Frontend Setup + +```bash +cd ../frontend +npm install +``` + +Create `frontend/.env.local`: + +```env +VITE_BACKEND_URL=http://localhost:8000 +``` + +Start the frontend dev server: + +```bash +npm run dev +# β†’ App opens on http://localhost:5173 +``` + +### 4. Open the App + +1. Navigate to `http://localhost:5173` +2. Sign up for an account +3. Create a room or enter an existing Room ID +4. Start coding! + +--- + +## Environment Variables + +### Backend (`backend/.env.local`) + +| Variable | Required | Description | +| ----------------- | -------- | -------------------------------------------- | +| `DATABASE_URL` | βœ… | PostgreSQL connection string | +| `GEMINI_API_KEY` | βœ… | Google Gemini API key | +| `JWT_SECRET` | βœ… | Secret for signing JWT tokens (min 64 chars) | +| `ALLOWED_ORIGINS` | βœ… | Comma-separated allowed CORS origins | +| `REDIS_URL` | βœ… | Redis connection string | +| `LOG_LEVEL` | ❌ | Log level (`debug`, `info`, `warn`, `error`) | +| `NODE_ENV` | ❌ | Environment mode | + +### Frontend (`frontend/.env.local`) + +| Variable | Required | Description | +| ------------------ | -------- | ------------------ | +| `VITE_BACKEND_URL` | βœ… | Backend server URL | + +--- + +## Scripts Reference + +### Backend + +| Script | Description | +| --------------- | -------------------------------- | +| `npm run dev` | Start dev server with hot reload | +| `npm run build` | Compile TypeScript to `dist/` | +| `npm start` | Start production server | +| `npm test` | Run tests with coverage report | + +### Frontend + +| Script | Description | +| ----------------------- | --------------------------------- | +| `npm run dev` | Start Vite dev server | +| `npm run build` | Type-check + build for production | +| `npm test` | Run tests once | +| `npm run test:coverage` | Run tests with coverage report | +| `npm run lint` | Lint all files | + +--- + +## CI/CD Pipeline + +GitHub Actions runs on every push/PR to `main`. Both jobs enforce **70% coverage thresholds** for lines and functions. + +``` +backend job: + 1. Checkout β†’ Setup Node 20 β†’ npm ci (cached) + 2. npx prisma generate + 3. npm test -- --coverage --ci + 4. npm run build + +frontend job (runs after backend passes): + 1. Checkout β†’ Setup Node 20 β†’ npm ci (cached) + 2. npm run test:coverage + 3. npm run build +``` + +--- + +## Future Roadmap + +| Feature | Priority | Notes | +| --------------------------- | ------------- | ---------------------------------------------------------------------------- | +| **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 | + +--- + +## Author + +**Rahul** β€” [github.com/rah7202](https://github.com/rah7202) + +**Repository** β€” [github.com/rah7202/smart-code-lab](https://github.com/rah7202/smart-code-lab) diff --git a/backend/BACKEND_README.md b/backend/BACKEND_README.md new file mode 100644 index 0000000..1bea843 --- /dev/null +++ b/backend/BACKEND_README.md @@ -0,0 +1,562 @@ +# Smart Code Lab β€” Backend + +Express + TypeScript API server with Socket.IO real-time collaboration, Gemini AI integration, PostgreSQL via Prisma, and Redis for scalable state management. + +--- + +## File Structure + +``` +backend/ +β”œβ”€β”€ prisma/ +β”‚ β”œβ”€β”€ schema.prisma # Database models (User, Room, CodeSnapshot, AIMessage) +β”‚ └── migrations/ # Auto-generated migration files +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ index.ts # Entry point β€” Redis connect β†’ HTTP server β†’ Socket.IO +β”‚ β”œβ”€β”€ app.ts # Express config β€” CORS, Helmet, rate limiters, routes +β”‚ β”œβ”€β”€ controllers/ +β”‚ β”‚ β”œβ”€β”€ ai.controller.ts # AI generate (batch + SSE stream) + history CRUD +β”‚ β”‚ β”œβ”€β”€ auth.controller.ts # Signup + Signin +β”‚ β”‚ β”œβ”€β”€ codeSnapshot.controller.ts # Save + list snapshots +β”‚ β”‚ β”œβ”€β”€ compile.controller.ts # Judge0 code compilation proxy +β”‚ β”‚ └── room.controller.ts # Room CRUD + save code +β”‚ β”œβ”€β”€ services/ +β”‚ β”‚ β”œβ”€β”€ ai.service.ts # Gemini batch + streaming (AsyncGenerator) +β”‚ β”‚ β”œβ”€β”€ auth.service.ts # Bcrypt hash + JWT sign +β”‚ β”‚ β”œβ”€β”€ codeSnapshot.service.ts # Deduplicated snapshot saves +β”‚ β”‚ β”œβ”€β”€ judge0.service.ts # Judge0 API proxy +β”‚ β”‚ └── room.service.ts # Room creation + lookup +β”‚ β”œβ”€β”€ middleware/ +β”‚ β”‚ β”œβ”€β”€ auth.middleware.ts # JWT verification middleware +β”‚ β”‚ └── validate.ts # Zod schemas + validation middleware +β”‚ β”œβ”€β”€ routes/ +β”‚ β”‚ β”œβ”€β”€ ai.route.ts # /api/ai/* +β”‚ β”‚ β”œβ”€β”€ auth.route.ts # /api/auth/* +β”‚ β”‚ β”œβ”€β”€ codeSnapshot.routes.ts # /api/snapshot/* +β”‚ β”‚ β”œβ”€β”€ compile.route.ts # /api/compile +β”‚ β”‚ └── room.route.ts # /api/room/* +β”‚ β”œβ”€β”€ sockets/ +β”‚ β”‚ └── socket.ts # Socket.IO β€” Redis adapter, JWT auth, room events +β”‚ β”œβ”€β”€ db/ +β”‚ β”‚ β”œβ”€β”€ prisma.ts # PrismaClient singleton +β”‚ β”‚ └── redis.ts # Redis client + subscriber pair +β”‚ β”œβ”€β”€ types/ +β”‚ β”‚ └── index.ts # Shared TypeScript types +β”‚ β”œβ”€β”€ utils/ +β”‚ β”‚ └── logger.ts # Structured JSON logger +β”‚ └── __tests__/ +β”‚ β”œβ”€β”€ app.test.ts # Integration: health, CORS, rate-limit, helmet +β”‚ β”œβ”€β”€ envSetup.ts # Test environment variables +β”‚ β”œβ”€β”€ controllers/ +β”‚ β”‚ β”œβ”€β”€ ai.controller.test.ts +β”‚ β”‚ └── compile.controller.test.ts +β”‚ β”œβ”€β”€ middleware/ +β”‚ β”‚ └── validate.middleware.test.ts +β”‚ └── services/ +β”‚ β”œβ”€β”€ ai.service.test.ts +β”‚ β”œβ”€β”€ codeSnapshot.service.test.ts +β”‚ β”œβ”€β”€ judge0.service.test.ts +β”‚ └── room.service.test.ts +β”œβ”€β”€ .env.local +β”œβ”€β”€ jest.config.ts +β”œβ”€β”€ package.json +└── tsconfig.json +``` + +--- + +## Architecture + +### Request Flow + +``` +Client Request + β”‚ + β–Ό + Helmet (security headers) + β”‚ + β–Ό + CORS (origin allowlist) + β”‚ + β–Ό + Rate Limiter (global: 100/min, AI: 10/min) + β”‚ + β–Ό + Body Parser (50KB limit) + β”‚ + β–Ό + authenticate middleware (JWT verify) + β”‚ + β–Ό + validate middleware (Zod schema) + β”‚ + β–Ό + Controller β†’ Service β†’ Prisma/Redis + β”‚ + β–Ό + JSON Response +``` + +### Socket.IO Flow + +``` +WebSocket Handshake + β”‚ + β–Ό + JWT Middleware (socket.handshake.auth.token) + β”‚ + β–Ό + io.on("connection") + β”‚ + β”œβ”€β”€ "join" β†’ validate room exists β†’ add user to Redis β†’ broadcast users list + β”œβ”€β”€ "content-edited" β†’ store in Redis β†’ broadcast to room + β”œβ”€β”€ "cursor-move" β†’ enrich with username/color from Redis β†’ broadcast to room + └── "disconnect" β†’ remove from Redis β†’ broadcast updated users list +``` + +### Redis Architecture + +Room state is stored entirely in Redis, enabling horizontal scaling via the `@socket.io/redis-adapter`: + +``` +Redis Keys: + socket:{socketId}:room β†’ String (maps socket to room) + room:{roomId}:users β†’ Hash (socketId β†’ user JSON) + room:{roomId}:content β†’ String (serialized {code, language}) +``` + +When multiple backend instances run, Socket.IO events are pub/sub'd across all instances via the Redis adapter β€” any user in any room receives updates regardless of which server they're connected to. + +--- + +## Setup + +### Prerequisites + +- Node.js β‰₯ 20 +- PostgreSQL (local or Neon) +- Redis (local or Render) +- Gemini API Key from [Google AI Studio](https://aistudio.google.com/apikey) + +### Installation + +```bash +npm install +``` + +### Environment Variables + +Create `.env.local`: + +```env +DATABASE_URL="postgresql://user:password@localhost:5432/smartcodelab" +GEMINI_API_KEY=your_gemini_api_key +JWT_SECRET=your_jwt_secret_minimum_64_chars +ALLOWED_ORIGINS=http://localhost:5173 +REDIS_URL=redis://localhost:6379 +LOG_LEVEL=debug +NODE_ENV=development +``` + +| Variable | Required | Description | +| ----------------- | -------- | -------------------------------------------------------- | +| `DATABASE_URL` | βœ… | PostgreSQL connection string | +| `GEMINI_API_KEY` | βœ… | Google Gemini API key | +| `JWT_SECRET` | βœ… | Secret for signing JWT tokens (min 64 chars recommended) | +| `ALLOWED_ORIGINS` | βœ… | Comma-separated allowed CORS origins | +| `REDIS_URL` | βœ… | Redis connection string | +| `LOG_LEVEL` | ❌ | `debug` / `info` / `warn` / `error` | +| `NODE_ENV` | ❌ | `development` / `production` / `test` | + +### Database Setup + +```bash +# Generate Prisma client +npx prisma generate + +# Run migrations +npx prisma migrate dev + +# (Optional) Open Prisma Studio +npx prisma studio +``` + +### Scripts + +| Script | Command | Description | +| ------------------------- | ----------------------------------------------------- | -------------------------------- | +| `npm run dev` | `ts-node-dev --respawn --transpile-only src/index.ts` | Start dev server with hot reload | +| `npm run build` | `tsc` | Compile TypeScript to `dist/` | +| `npm start` | `node dist/index.js` | Start production server | +| `npm test` | `jest --coverage` | Run tests with coverage | +| `npm run prisma:generate` | `prisma generate` | Regenerate Prisma client | + +--- + +## API Reference + +All protected routes require the header: + +``` +Authorization: Bearer +``` + +--- + +### Authentication + +#### `POST /api/auth/signup` + +Create a new user account. + +**Request body:** + +```json +{ "username": "rahul", "password": "mypassword" } +``` + +**Validation:** username 3–30 chars, password min 6 chars + +**Response `201`:** + +```json +{ "token": "eyJhbGciOiJIUzI1NiIs..." } +``` + +--- + +#### `POST /api/auth/signin` + +Sign in with existing credentials. + +**Request body:** + +```json +{ "username": "rahul", "password": "mypassword" } +``` + +**Response `200`:** + +```json +{ "token": "eyJhbGciOiJIUzI1NiIs..." } +``` + +--- + +### Rooms πŸ”’ + +#### `GET /api/room/:roomId` + +Load room data. + +**Response `200`:** + +```json +{ + "roomId": "abc-123", + "language": "javascript", + "code": "console.log('hello')" +} +``` + +**Response `404`:** Room not found. + +--- + +#### `POST /api/room/create` + +Create a new room (owned by the authenticated user). + +**Response `200`:** + +```json +{ "roomId": "generated-uuid" } +``` + +--- + +#### `POST /api/room/:roomId/save` + +Save the current code to the room. Only the room owner can save. + +**Request body:** + +```json +{ "code": "print('hello')", "language": "python" } +``` + +**Validation:** `language` must be one of `javascript | python | cpp | c` + +**Response `200`:** + +```json +{ "success": true } +``` + +**Response `403`:** Not the room owner. + +--- + +### Code Execution πŸ”’ + +#### `POST /api/compile` + +Execute code via the Judge0 API. + +**Request body:** + +```json +{ + "code": "print('hello')", + "userLangId": 71, + "input": "" +} +``` + +| Field | Type | Description | +| ------------ | ------ | --------------------------------------------------- | +| `code` | string | 1–50,000 chars | +| `userLangId` | number | Judge0 language ID (63=JS, 71=Python, 54=C++, 50=C) | +| `input` | string | Optional stdin, max 10,000 chars | + +**Response `200`:** + +```json +{ + "output": "hello\n", + "status": { "id": 3, "description": "Accepted" }, + "time": "0.01" +} +``` + +--- + +### AI πŸ”’ (Rate limited: 10 req/min per IP) + +#### `POST /api/ai/generate` + +Generate AI response as a complete batch response. + +**Request body:** + +```json +{ "prompt": "Explain this code...", "roomId": "abc-123" } +``` + +**Response `200`:** + +```json +{ "success": true, "data": "## πŸ” Explanation\nThis code..." } +``` + +Saves both the user prompt and AI response to `AIMessage` table. + +--- + +#### `POST /api/ai/stream` + +Stream AI response via Server-Sent Events (SSE). + +Same request body as `/generate`. Response is a chunked stream: + +``` +data: {"chunk":"## πŸ” "} + +data: {"chunk":"Explanation\n"} + +data: {"chunk":"This code does..."} + +data: {"done":true} +``` + +Each chunk is a JSON-encoded object on a `data:` line, separated by `\n\n`. + +--- + +#### `GET /api/ai/history/:roomId` + +Get all AI chat messages for a room, ordered by `createdAt asc`. + +**Response `200`:** + +```json +[ + { + "id": "...", + "role": "user", + "content": "Explain this", + "createdAt": "..." + }, + { "id": "...", "role": "ai", "content": "## πŸ” ...", "createdAt": "..." } +] +``` + +--- + +#### `DELETE /api/ai/history/:roomId` + +Clear all AI chat history for a room. + +**Response `200`:** + +```json +{ "success": true } +``` + +--- + +### Snapshots πŸ”’ + +#### `POST /api/snapshot/:roomId` + +Save a code snapshot (version checkpoint). Skips save if code and language are identical to the last snapshot. + +**Request body:** + +```json +{ "code": "print('hi')", "language": "python" } +``` + +**Response `201`:** + +```json +{ "id": "snapshot-uuid", "createdAt": "2026-04-04T10:00:00Z" } +``` + +--- + +#### `GET /api/snapshot/:roomId` + +Get all snapshots for a room, ordered newest first. + +**Response `200`:** + +```json +[{ "id": "...", "code": "...", "language": "python", "createdAt": "..." }] +``` + +--- + +## Socket.IO Events + +Connection requires a JWT token in the socket handshake: + +```typescript +const socket = io("http://localhost:8000", { + auth: { token: localStorage.getItem("token") }, + transports: ["websocket"], +}); +``` + +### Client β†’ Server + +| Event | Payload | Description | +| ---------------- | ------------------------------------ | ------------------------------------------------------ | +| `join` | `{ RoomId: string }` | Join a room β€” triggers user list broadcast + code sync | +| `content-edited` | `{ code: string, language: string }` | Broadcast code changes to room on every keystroke | +| `cursor-move` | `{ line: number, column: number }` | Broadcast cursor position to room | + +### Server β†’ Client + +| Event | Payload | Description | +| ---------------- | --------------------------------------------- | ------------------------------------------------- | +| `users` | `User[]` | Updated user list `{ username, color, socketId }` | +| `code-sync` | `string` | Initial code sync when a user joins a room | +| `content-edited` | `{ code, language }` | Code update from a collaborator | +| `cursor-move` | `{ line, column, username, color, socketId }` | Cursor position from a collaborator | +| `error` | `string` | Error message (e.g., `"Room does not exist"`) | + +--- + +## Database Schema + +```prisma +model User { + id String @id @default(uuid()) + username String @unique + password String + createdAt DateTime @default(now()) + rooms Room[] +} + +model Room { + id String @id @default(uuid()) + language String + code String @db.Text + createdAt DateTime @default(now()) + userId String + user User @relation(fields: [userId], references: [id]) + snapshots CodeSnapshot[] + aiMessages AIMessage[] + @@index([userId]) +} + +model CodeSnapshot { + id String @id @default(uuid()) + roomId String + room Room @relation(fields: [roomId], references: [id]) + code String @db.Text + language String + createdAt DateTime @default(now()) + @@index([roomId]) +} + +model AIMessage { + id String @id @default(uuid()) + roomId String + room Room @relation(fields: [roomId], references: [id]) + role String // "user" | "ai" + content String @db.Text + createdAt DateTime @default(now()) + @@index([roomId]) +} +``` + +**Relationships:** + +``` +User (1) ──→ (N) Room +Room (1) ──→ (N) CodeSnapshot +Room (1) ──→ (N) AIMessage +``` + +--- + +## Security + +| Layer | Implementation | +| -------------------- | ------------------------------------------------------------------------------------------------ | +| **Authentication** | JWT tokens (7-day expiry) β€” `jsonwebtoken` + `bcrypt` password hashing | +| **Authorization** | `authenticate` middleware on all protected routes + room owner checks | +| **Input Validation** | Zod schemas on all mutation endpoints β€” invalid requests return `400` before hitting controllers | +| **Rate Limiting** | Global: 100 req/min per IP β€” AI routes: additional 10 req/min per IP | +| **CORS** | Origin allowlist from `ALLOWED_ORIGINS` β€” rejects all unlisted origins | +| **Security Headers** | Helmet (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.) | +| **Body Limit** | 50KB JSON body limit | +| **Socket Auth** | JWT verification on WebSocket handshake before any room events are processed | + +--- + +## Testing + +**Framework:** Jest + Supertest + ts-jest + +```bash +# Run all tests with coverage +npm test + +# Coverage thresholds: 70% lines, 70% 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). + +| Test File | What it Tests | +| ------------------------------ | --------------------------------------------------------------------- | +| `app.test.ts` | Health check, CORS allowlist/rejection, rate limiting, Helmet headers | +| `ai.controller.test.ts` | Generate, stream, history CRUD β€” Prisma and AI service mocked | +| `compile.controller.test.ts` | Judge0 proxy β€” accepted/error/timeout cases | +| `validate.middleware.test.ts` | All Zod schemas β€” valid inputs, edge cases, enum rejection | +| `ai.service.test.ts` | Gemini SDK mock β€” prompt validation, error propagation | +| `codeSnapshot.service.test.ts` | Deduplication logic, DB create/find | +| `judge0.service.test.ts` | Judge0 API proxy β€” response parsing | +| `room.service.test.ts` | Room CRUD β€” create, find, authorization | diff --git a/frontend/FRONTEND_README.md b/frontend/FRONTEND_README.md new file mode 100644 index 0000000..23d32c8 --- /dev/null +++ b/frontend/FRONTEND_README.md @@ -0,0 +1,356 @@ +# Smart Code Lab β€” Frontend + +React + Vite + TypeScript SPA. Monaco editor with real-time collaboration via Socket.IO, AI-powered code assistance with SSE streaming, and full version history. + +--- + +## File Structure + +``` +frontend/ +β”œβ”€β”€ public/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ App.tsx # Routes: /, /login, /signup, /editor/:roomId +β”‚ β”œβ”€β”€ App.css # Global styles +β”‚ β”œβ”€β”€ main.tsx # React entry point +β”‚ β”œβ”€β”€ socket.ts # Socket.IO client (autoConnect: false) +β”‚ β”œβ”€β”€ languageOptions.ts # 4 languages + custom Monaco themes +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ EditorPage.tsx # Main editor page β€” Monaco + all hooks +β”‚ β”‚ β”œβ”€β”€ AIPanel.tsx # AI chat with markdown + SSE rendering + resize +β”‚ β”‚ β”œβ”€β”€ CodeInputPanel.tsx # stdin input + Run + Ask Gemini buttons +β”‚ β”‚ β”œβ”€β”€ RightPanel.tsx # Composes CodeInputPanel + AIPanel + VersionPanel +β”‚ β”‚ β”œβ”€β”€ SelectionToolbar.tsx # Floating toolbar on text selection +β”‚ β”‚ β”œβ”€β”€ Navbar.tsx # Language/theme selectors, save/download/clear +β”‚ β”‚ β”œβ”€β”€ UserPresenceBar.tsx # Colored user badges for room members +β”‚ β”‚ β”œβ”€β”€ VersionPanel.tsx # Toggle for version history +β”‚ β”‚ β”œβ”€β”€ VersionHistory.tsx # Snapshot list with restore +β”‚ β”‚ β”œβ”€β”€ LanguageBadge.tsx # Language indicator badge (PY/JS/C++/C) +β”‚ β”‚ β”œβ”€β”€ Home.tsx # Room create/join + logout +β”‚ β”‚ β”œβ”€β”€ LoginPage.tsx # Sign in form +β”‚ β”‚ β”œβ”€β”€ Signup.tsx # Sign up form +β”‚ β”‚ └── Footer.tsx # GitHub link +β”‚ β”œβ”€β”€ hooks/ +β”‚ β”‚ β”œβ”€β”€ useAI.ts # AI SSE streaming + rate limiting + 3 modes +β”‚ β”‚ β”œβ”€β”€ useCollaboration.ts # Socket.IO collab + cursor decorations +β”‚ β”‚ └── useEditorPersistence.ts # DB sync, Ctrl+S, beforeunload snapshots +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ └── authAxios.ts # Axios instance with JWT interceptor + 401 redirect +β”‚ β”œβ”€β”€ utils/ +β”‚ β”‚ β”œβ”€β”€ auth.ts # Token validation + decode helpers +β”‚ β”‚ └── ProtectedRoute.tsx # Route guard component +β”‚ β”œβ”€β”€ assets/ +β”‚ β”‚ └── geminiLogo.png +β”‚ └── test/ +β”‚ β”œβ”€β”€ setup.ts # jsdom + @testing-library/jest-dom +β”‚ β”œβ”€β”€ socket.test.ts +β”‚ β”œβ”€β”€ languageOptions.test.ts +β”‚ β”œβ”€β”€ __mocks__/ +β”‚ β”‚ └── socket.ts # Socket.IO mock +β”‚ β”œβ”€β”€ components/ # 12 component test files +β”‚ └── hooks/ # 3 hook test files +β”œβ”€β”€ .env.local +β”œβ”€β”€ vite.config.ts # Vite + Vitest config +β”œβ”€β”€ package.json +└── tsconfig.json +``` + +--- + +## Architecture + +### EditorPage β€” 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: + +``` +EditorPage +β”‚ +β”œβ”€β”€ useEditorPersistence DB load/save, Ctrl+S, auto-save, beforeunload, download, restore +β”œβ”€β”€ useCollaboration Socket connect, code-sync, cursor decorations, emit helpers +└── useAI SSE streaming, rate limiting, chat history, 3 AI modes +``` + +### Monaco β€” Uncontrolled Pattern + +Monaco is rendered **without a `value` prop** (uncontrolled). This prevents React from fighting Monaco over content during collaboration. + +All updates go through two imperative helpers defined in `EditorPage`: + +```typescript +applyCode(code: string) +// Sets editor content via editor.setValue() +// Sets isRemoteUpdate = true first to block onChange re-emit + +applyLang(lang: string) +// Sets Monaco model language + theme without re-mounting +// Also updates React state (userLang, userLangId) as a mirror +``` + +The `onChange` handler only fires for **real user keystrokes** β€” all programmatic updates are gated by the `isRemoteUpdate` ref. + +### Data Flow + +``` +User types in Monaco + β”‚ + β–Ό +onChange fires (isRemoteUpdate = false) + β”‚ + β”œβ”€β”€ persistence.handleEditorChange(code, lang) β†’ debounced DB save + └── emitCodeChange({ code, language }) β†’ Socket.IO broadcast + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–Ό + Other users receive "content-edited" + β”‚ + β–Ό + onCodeChange(code) callback + β”‚ + β–Ό + applyCode(code) β†’ editor.setValue() with isRemoteUpdate = true + persistence.setUserCode(code) β†’ keeps AI / DB saves in sync +``` + +### Auth Flow + +```typescript +// authAxios.ts β€” shared axios instance +const api = axios.create({ baseURL: import.meta.env.VITE_BACKEND_URL }); + +api.interceptors.request.use((config) => { + config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`; + return config; +}); + +api.interceptors.response.use(null, (error) => { + if (error.response?.status === 401) { + localStorage.removeItem("token"); + window.location.href = "/login"; + } + return Promise.reject(error); +}); +``` + +Login/Signup pages use plain `axios` (not `api`) to avoid the 401 interceptor triggering a redirect loop during authentication failures. + +--- + +## Component Breakdown + +| Component | Responsibility | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **EditorPage** | Wires Monaco, all three hooks, and child components. Owns `editorRef`, `monacoRef`, `isRemoteUpdate`, `applyCode`, `applyLang` | +| **Navbar** | Language dropdown (with `LanguageBadge`), theme dropdown, font size slider, save/clear/download buttons, leave room | +| **UserPresenceBar** | Renders a colored pill badge for each connected user. Hides when room is empty | +| **RightPanel** | Wrapper that stacks `CodeInputPanel`, `AIPanel`, and `VersionPanel` | +| **CodeInputPanel** | stdin textarea, Run button with loading spinner, Ask Gemini button, rate limit countdown | +| **AIPanel** | Question input, execution output display, scrollable AI chat with markdown rendering, copy buttons on code blocks, clear chat, resizable via drag handle | +| **SelectionToolbar** | Appears above selected Monaco text. Quick actions: Explain / Review / Fix / Optimize. Custom "Ask…" input. Shows line count of selection | +| **VersionPanel** | Toggle button to open/close `VersionHistory` | +| **VersionHistory** | Lists snapshots with relative timestamps. Click any to restore. Fetches from `/api/snapshot/:roomId` | +| **LanguageBadge** | Colored dot + short label (PY / JS / C++ / C) β€” uses Tailwind classes from `languageOptions.ts` | +| **Home** | Room ID input, Join button, Create Room button (calls `/api/room/create`), logout | +| **LoginPage** | Username + password form, POSTs to `/api/auth/signin`, stores token in `localStorage` | +| **Signup** | Username + password form, POSTs to `/api/auth/signup`, stores token in `localStorage` | +| **Footer** | GitHub icon link | + +--- + +## Custom Hook Details + +### `useAI` + +Manages all AI interactions with three distinct modes. + +**SSE Streaming:** + +```typescript +const res = await fetch(`${URL}/api/ai/stream`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify({ prompt, roomId }), +}); + +const reader = res.body!.getReader(); +// Reads chunks, parses "data: {...}\n\n" SSE format +// Updates last history entry on each chunk +``` + +**AI Modes:** + +| Mode | Trigger | What Gets Sent | +| ----------- | ------------------------ | ------------------------------------------- | +| `code` | "Ask Gemini" button | Full editor code + structured review prompt | +| `selection` | Selection Toolbar action | Selected snippet + specific action prompt | +| `question` | Text input | Freeform question β€” no code injected | + +**Rate Limiting:** + +- Client-side 5 requests/60 seconds +- Timestamps tracked in a ref (`aiTimestamps`) +- Countdown timer displayed in `CodeInputPanel` +- Error toast when limit is hit + +**History:** + +- Fetches existing history from `/api/ai/history/:roomId` on mount +- Single insertion point: user message + empty AI placeholder added together before streaming starts +- Streaming updates the last entry in-place β€” no duplicate insertions + +--- + +### `useCollaboration` + +Manages the full Socket.IO lifecycle and cursor decorations. + +**Socket Lifecycle:** + +```typescript +useEffect(() => { + socket.auth = { token: localStorage.getItem("token") }; + socket.connect(); + + socket.on("connect", () => { + socket.emit("join", { RoomId: roomId }); // only after confirmed connected + }); + + // ... attach all listeners + + return () => { + socket.off("connect"); + // ... remove all listeners + socket.disconnect(); + }; +}, [roomId]); +``` + +**Cursor Decorations:** + +- Injects `