diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..76e225e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "PowerShell(pnpm exec *)", + "PowerShell(pnpm db:generate *)", + "PowerShell(Get-Process *)", + "PowerShell(Stop-Process *)", + "PowerShell(pnpm db:push *)", + "PowerShell(pnpm lint *)", + "PowerShell(pnpm build *)", + "PowerShell(npx tsc --noEmit)", + "mcp__ide__getDiagnostics", + "PowerShell(pnpm run *)" + ] + } +} diff --git a/.env.example b/.env.example index 9325c3d..f8e1eaf 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,39 @@ -APP_DOMAIN="app.example.com" DATABASE_URL="postgresql://user:password@localhost:5432/mydatabase" # Replace with your actual database URL BETTER_AUTH_SECRET="supersecretkey" # Replace with a strong secret key -BETTER_AUTH_URL="https://app.example.com" # Base URL of your app +# Base URL of your app. MUST be a public HTTPS URL — GitHub delivers webhooks +# here to auto-register hooks and post reviews. For local dev, expose your +# server with a tunnel (e.g. cloudflared / ngrok) and use that URL; GitHub +# cannot reach http://localhost. +BETTER_AUTH_URL="https://app.example.com" NEXT_PUBLIC_APP_URL="https://app.example.com" +# Optional override for the GitHub webhook endpoint origin. If set, webhooks are +# registered against this URL instead of the app URL above. Use it in local dev +# to point GitHub at a public HTTPS tunnel (e.g. ngrok) while BETTER_AUTH_URL / +# NEXT_PUBLIC_APP_URL stay on http://localhost:3000. Accepts the bare origin +# (https://xxxx.ngrok-free.app) or the full /api/webhooks/github URL. Leave unset +# in production — the app URL is used automatically. +WEBHOOK_PUBLIC_URL="" + GH_CLIENT_ID="" GH_CLIENT_SECRET="" GROQ_API_KEY="" # Free key from https://console.groq.com/keys OPENAI_API_KEY="" # Optional — only needed when using the "openai" provider OPENAI_BASE_URL="" # Optional — override for OpenAI-compatible endpoints (e.g. OpenRouter) +ANTHROPIC_API_KEY="" # Optional — app-level default for the "anthropic" (Claude) provider. If unset, Claude requires each user to bring their own key in Settings. +GEMINI_API_KEY="" # Optional — app-level default for the "gemini" (Google) provider. If unset, Gemini requires each user to bring their own key in Settings. Get one at https://aistudio.google.com/apikey + +# 32-byte key used to encrypt user-supplied AI API keys at rest (AES-256-GCM). +# REQUIRED to save a user API key in Settings; reviews on the default Groq path +# keep working without it. Generate one with: +# node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" +SETTINGS_ENCRYPTION_KEY="" +# Legacy/global webhook secret. Each repo now gets its own per-repo secret +# generated automatically when its webhook is registered. This value is only a +# fallback for hooks configured manually before per-repo secrets existed. GH_WEBHOOK_SECRET="supersecretwebhooksecret" # Replace with a strong secret key INNGEST_EVENT_KEY="" diff --git a/README.md b/README.md index 4a13c3e..3c215cb 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,448 @@ -# AI Code Reviewer +# CodeReviewAI -An intelligent AI-powered code review platform that automatically analyzes GitHub pull requests and provides comprehensive feedback, suggestions, and risk assessments. Built with Next.js, this application integrates with GitHub to streamline the code review process using advanced AI models. +> **Connect a GitHub repo once — every pull request is reviewed by AI automatically.** + +CodeReviewAI is an AI-powered code review platform. It connects to your GitHub repositories, automatically registers a webhook, and reviews every pull request the moment it's opened or updated. Reviews include a plain-language summary, a 0–100 risk score, and severity-tagged inline comments (with concrete code fixes) that can be posted straight back to the PR — or kept in the dashboard for you to review first. + +It also ships a full analytics dashboard: review trends, risk distribution, an activity heatmap, a repository leaderboard, and a breakdown of the most common issue categories across all your reviews. + +--- + +## Table of Contents + +- [Features](#features) +- [Tech Stack](#tech-stack) +- [How It Works](#how-it-works) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Environment Variables](#environment-variables) +- [Getting Started (Local Setup)](#getting-started-local-setup) +- [Local Webhooks (ngrok / tunnel)](#local-webhooks-ngrok--tunnel) +- [Available Scripts](#available-scripts) +- [Database Schema](#database-schema) +- [API Surface](#api-surface) +- [AI Providers](#ai-providers) +- [Project Structure](#project-structure) +- [Deployment (Vercel)](#deployment-vercel) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) + +--- ## Features -- 🤖 **Automated Code Reviews**: Leverage AI (Google Gemini & OpenAI) to automatically review pull requests -- 🔐 **GitHub Integration**: Seamless authentication and repository synchronization -- 📊 **Dashboard Analytics**: Visualize review statistics, activity heatmaps, and risk scores -- 🎯 **Risk Assessment**: Intelligent scoring system to identify potential issues -- 💬 **Inline Comments**: AI-generated comments posted directly to GitHub PRs -- 🔄 **Background Processing**: Asynchronous review processing using Inngest -- 🌙 **Dark Mode**: Built-in theme support for comfortable viewing -- 📱 **Responsive Design**: Works seamlessly across desktop and mobile devices +| | Feature | Description | +|---|---|---| +| 🤖 | **Automated AI reviews** | Pull requests are analyzed by an LLM that returns a summary, a risk score, and detailed inline comments. | +| ⚡ | **Auto-review on push** | Connect a repo once. Every PR opened — and every new commit pushed to an open PR — is reviewed automatically, zero manual steps. | +| 🪝 | **Automatic webhook setup** | Connecting a repo auto-registers a GitHub `pull_request` webhook with a unique per-repo HMAC secret. Disconnecting tears it down. Self-healing on reconnect. | +| 💬 | **Inline GitHub comments** | AI comments are posted directly onto the PR diff at the exact line, with severity emoji, category, and a suggested fix. | +| 🎛️ | **Per-repo automation controls** | Toggle auto-review and auto-post independently, and choose whether posts are a `COMMENT` or a `REQUEST_CHANGES` review. | +| 🎯 | **Risk scoring** | Each review gets a 0–100 risk score; the dashboard rolls these up into pass rates and distributions. | +| 🔍 | **Code comparison view** | Side-by-side old vs. suggested code for each finding, with diff highlighting. | +| 📊 | **Analytics dashboard** | Stats cards, review trend chart, risk distribution donut, top issue categories, 52-week activity heatmap, and a repo leaderboard. | +| 🔄 | **Reliable background jobs** | Reviews run asynchronously via Inngest with automatic retries, model fallback, and cancellation support. | +| 🔐 | **GitHub OAuth + email/password** | Sign in with GitHub (for repo access) or email/password. 7-day persistent sessions. | +| 🌙 | **Dark-first, responsive UI** | Built with Tailwind v4, Radix UI, and Framer Motion. Works across desktop and mobile. | + +--- + +## Screenshot + +![Add a screenshot of the app here](./docs/screenshot.png) ## Tech Stack -- **Framework**: [Next.js 16](https://nextjs.org) with App Router -- **Language**: TypeScript -- **Database**: PostgreSQL with [Prisma ORM](https://www.prisma.io/) -- **Authentication**: [Better Auth](https://www.better-auth.com/) with GitHub OAuth -- **AI Models**: Google Gemini API & OpenAI API -- **Background Jobs**: [Inngest](https://www.inngest.com/) -- **API Layer**: [tRPC](https://trpc.io/) for type-safe APIs -- **Styling**: [Tailwind CSS](https://tailwindcss.com/) with Radix UI components -- **State Management**: TanStack Query (React Query) -- **Animations**: Framer Motion & React Spring +| Layer | Technology | +|---|---| +| **Framework** | [Next.js 16](https://nextjs.org) (App Router, Turbopack) | +| **Language** | TypeScript 5 | +| **UI runtime** | React 19 | +| **Styling** | [Tailwind CSS v4](https://tailwindcss.com), [Radix UI](https://www.radix-ui.com) / ShadCN (New York), `tw-animate-css` | +| **Animation** | [Framer Motion v12](https://www.framer.com/motion/), [React Spring](https://www.react-spring.dev/) | +| **Auth** | [Better Auth v1](https://www.better-auth.com) — GitHub OAuth + email/password | +| **Database** | PostgreSQL + [Prisma ORM v6](https://www.prisma.io) | +| **API layer** | [tRPC v11](https://trpc.io) + [TanStack Query v5](https://tanstack.com/query) + superjson | +| **Background jobs** | [Inngest v3](https://www.inngest.com) | +| **AI** | [Groq](https://groq.com) (default) & [OpenAI](https://openai.com) / OpenAI-compatible endpoints, via a pluggable provider registry | +| **Validation** | [Zod v4](https://zod.dev) | +| **Charts / viz** | [Recharts](https://recharts.org), `react-calendar-heatmap`, `react-countup` | +| **Notifications** | [Sonner](https://sonner.emilkowal.ski) toasts | +| **Icons** | [Lucide](https://lucide.dev) | +| **Tooling** | pnpm, ESLint, Husky (pre-commit) | + +--- + +## How It Works + +``` + ┌─────────────┐ connect repo ┌──────────────────┐ + You ───────► │ Dashboard │ ───────────────────► │ GitHub REST API │ + └─────────────┘ register webhook └──────────────────┘ + (per-repo secret) │ + │ PR opened / new commit + ▼ + ┌────────────────────────────────────────────────────────────────────┐ + │ POST /api/webhooks/github │ + │ 1. Verify HMAC signature (per-repo secret, w/ legacy fallback) │ + │ 2. Filter: only opened|synchronize|reopened, skip drafts & bots │ + │ 3. Create Review (PENDING) │ + │ 4. inngest.send("review/pr.requested") │ + └────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────────────────────────────────────────┐ + │ Inngest function review-pr (retries: 2, cancellable) │ + │ • fetch PR details + changed files from GitHub │ + │ • reviewCode(): chunk large diffs, call AI (primary→fallback model)│ + │ • save summary + riskScore + comments → Review (COMPLETED) │ + │ • if autoPost → post inline review to the PR (COMMENT / REQ_CHANGES)│ + └────────────────────────────────────────────────────────────────────┘ +``` + +1. **Authenticate** — sign in with GitHub (grants `repo` + `admin:repo_hook` scopes) or email/password. +2. **Sync & connect** — the app lists your GitHub repos; connecting one upserts a `Repository` row and registers a `pull_request` webhook with a freshly generated per-repo HMAC secret. `webhookStatus` becomes `ACTIVE`. +3. **Push triggers a review** — GitHub delivers a `pull_request` event on **opened**, **synchronize** (new commits), or **reopened**. Draft PRs, ignored actions, and bot senders are skipped. The endpoint verifies the signature, creates a `Review`, and enqueues a `review/pr.requested` Inngest event. +4. **AI analysis** — the Inngest job fetches the diff, chunks large PRs to stay under token limits, and asks the AI provider for a structured JSON review (tries the primary model, falls back to a cheaper one, retries on 413/429/503). +5. **Results saved** — the review is stored with `status = COMPLETED`, a `summary`, a `riskScore` (0–100), and an array of `comments`. +6. **Auto-post (optional)** — when `autoPostEnabled` is on, comments are posted as an inline GitHub review. A posting failure is a *soft* error: the review still shows `COMPLETED`, with the reason recorded. +7. **Dashboard** — browse every review, drill into a PR, compare old vs. suggested code, and view aggregate analytics. + +> A plain `git push` to a branch does **not** trigger a review. The change must arrive as a Pull Request — either a newly opened PR, or new commits on an already-open PR. + +--- + +## Architecture + +- **Server Components + tRPC** — pages are React Server Components; data access goes through type-safe tRPC routers (`src/server/api/routers`). Auth context is resolved per request via Better Auth. +- **Pluggable AI providers** — `src/server/services/ai` defines a `AiProvider` strategy interface, a registry, and per-provider configs. Adding a provider is: (1) add a config in `src/constant/ai.ts`, (2) implement the provider in `providers/`, (3) register it in `registry.ts`. Retry, chunking, prompt-building, and JSON validation live in the orchestrator (`review.ts`), not the providers. +- **Webhook automation is idempotent & self-healing** — `registerRepoWebhook` PATCHes an existing hook (re-pointing a drifted URL, re-syncing the secret), creates one if missing, and handles GitHub's 422 "hook already exists" by locating and patching it. So re-connecting always converges to a correct hook. +- **Background processing via Inngest** — the `review-pr` function is durable (each step is checkpointed), retries twice, and can be cancelled mid-flight via a `review/pr.cancelled` event keyed on `reviewId`. + +--- ## Prerequisites -Before you begin, ensure you have the following installed: +- **Node.js 20+** +- **pnpm** (recommended) — `npm i -g pnpm` +- A **PostgreSQL** database (local, or hosted e.g. [Neon](https://neon.tech), Supabase, Railway) +- A **GitHub OAuth App** (Client ID + Secret) +- An AI API key — a **free [Groq](https://console.groq.com/keys) key** is the default; an OpenAI (or OpenAI-compatible, e.g. OpenRouter) key is optional +- For local webhook testing: a tunneling tool such as **ngrok** or **cloudflared** (GitHub cannot reach `localhost`) + +### Creating the GitHub OAuth App + +1. Go to **GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**. +2. **Homepage URL**: your app URL (e.g. `http://localhost:3000`). +3. **Authorization callback URL**: `http://localhost:3000/api/auth/callback/github`. +4. Copy the **Client ID** and generate a **Client Secret** into `GH_CLIENT_ID` / `GH_CLIENT_SECRET`. + +The app requests these OAuth scopes: `read:user`, `user:email`, `repo`, `admin:repo_hook`. The `admin:repo_hook` scope is required to create the per-repo webhook used for auto-review — the connected account must be an **admin** of the repository. -- Node.js 20.x or higher -- pnpm (recommended) or npm -- PostgreSQL database -- GitHub OAuth App credentials -- Google Gemini API key or OpenAI API key +--- ## Environment Variables -Create a `.env` file in the root directory with the following variables: +Copy `.env.example` to `.env` and fill in the values: + +```bash +cp .env.example .env +``` + +| Variable | Required | Description | +|---|:---:|---| +| `DATABASE_URL` | ✅ | PostgreSQL connection string. | +| `BETTER_AUTH_SECRET` | ✅ | Random secret used to sign sessions. Generate with `openssl rand -hex 32`. | +| `BETTER_AUTH_URL` | ✅ | Canonical base URL of the app (e.g. `http://localhost:3000`). Used for OAuth callbacks. | +| `NEXT_PUBLIC_APP_URL` | ✅ | Public app URL exposed to the browser. Usually the same as `BETTER_AUTH_URL`. | +| `GH_CLIENT_ID` | ✅ | GitHub OAuth App client ID. | +| `GH_CLIENT_SECRET` | ✅ | GitHub OAuth App client secret. | +| `GROQ_API_KEY` | ✅* | Groq API key (the **default** AI provider). Free key at . | +| `INNGEST_EVENT_KEY` | ✅** | Inngest event key (for sending events). Required in production; the local Inngest dev server doesn't need it. | +| `INNGEST_SIGNING_KEY` | ✅** | Inngest signing key (for verifying requests). Required in production. | +| `WEBHOOK_PUBLIC_URL` | ⛔ optional | Public HTTPS origin GitHub delivers webhooks to. Use in local dev to point GitHub at a tunnel (e.g. ngrok) while auth stays on `localhost`. Accepts a bare origin or the full `/api/webhooks/github` URL. Leave unset in production. | +| `OPENAI_API_KEY` | ⛔ optional | Only needed when using the `openai` provider. | +| `OPENAI_BASE_URL` | ⛔ optional | Override for OpenAI-compatible endpoints (e.g. OpenRouter). | +| `GH_WEBHOOK_SECRET` | ⛔ optional | Legacy/global webhook secret. Each repo now gets its own per-repo secret automatically; this is only a fallback for hooks configured manually before per-repo secrets existed. | + +> \* At least one AI provider key is required. `GROQ_API_KEY` is needed for the default provider; `OPENAI_API_KEY` only if you switch to OpenAI. +> \*\* For **local development** you typically run `pnpm inngest:dev` and don't need the Inngest keys. They are required when deploying. + +### Example `.env` ```env -# Database -DATABASE_URL="postgresql://user:password@localhost:5432/aicodereviewer" +DATABASE_URL="postgresql://user:password@localhost:5432/codereviewai" -# Authentication -BETTER_AUTH_SECRET="your-secret-key-here" +BETTER_AUTH_SECRET="replace-with-openssl-rand-hex-32" BETTER_AUTH_URL="http://localhost:3000" +NEXT_PUBLIC_APP_URL="http://localhost:3000" + +# Local dev: tunnel GitHub webhooks to your machine while auth stays on localhost +WEBHOOK_PUBLIC_URL="https://your-subdomain.ngrok-free.app" -# GitHub OAuth -GITHUB_CLIENT_ID="your-github-client-id" -GITHUB_CLIENT_SECRET="your-github-client-secret" +GH_CLIENT_ID="your-github-oauth-client-id" +GH_CLIENT_SECRET="your-github-oauth-client-secret" -# AI API Keys (use at least one) -GOOGLE_GEMINI_API_KEY="your-gemini-api-key" -OPENAI_API_KEY="your-openai-api-key" +# Default AI provider (free tier) +GROQ_API_KEY="gsk_..." -# Inngest -INNGEST_EVENT_KEY="your-inngest-event-key" -INNGEST_SIGNING_KEY="your-inngest-signing-key" +# Optional: OpenAI or an OpenAI-compatible endpoint (e.g. OpenRouter) +OPENAI_API_KEY="" +OPENAI_BASE_URL="" + +# Legacy fallback only — per-repo secrets are generated automatically +GH_WEBHOOK_SECRET="replace-with-a-strong-secret" + +# Required in production; not needed with the local Inngest dev server +INNGEST_EVENT_KEY="" +INNGEST_SIGNING_KEY="" ``` -## Installation +> ⚠️ Never commit real secrets. `.env` is gitignored; only `.env.example` (with placeholders) is tracked. -1. **Clone the repository**: - ```bash - git clone https://github.com/yourusername/aicodereviewer.git - cd aicodereviewer - ``` +--- -2. **Install dependencies**: - ```bash - pnpm install - ``` +## Getting Started (Local Setup) -3. **Set up the database**: - ```bash - pnpm db:generate - pnpm db:push - ``` +```bash +# 1. Clone +git clone https://github.com/Lathiya50/aicodereviewer.git +cd aicodereviewer -4. **Run the development server**: - ```bash - pnpm dev - ``` +# 2. Install dependencies +pnpm install + +# 3. Configure environment +cp .env.example .env +# → fill in DATABASE_URL, GH_CLIENT_ID/SECRET, GROQ_API_KEY, BETTER_AUTH_SECRET, etc. + +# 4. Generate the Prisma client and push the schema to your DB +pnpm db:generate +pnpm db:push + +# 5. Start the dev server +pnpm dev + +# 6. In a SECOND terminal, start the Inngest dev server (runs background review jobs) +pnpm inngest:dev +``` + +Open . + +- App: +- Inngest dev dashboard: (started by `pnpm inngest:dev`) + +> **Windows note:** stop the `next dev` and `inngest dev` processes before running `pnpm db:generate` / `pnpm db:push` — a running process can lock the Prisma client DLL and cause an `EPERM` error. -5. **In a separate terminal, run Inngest dev server**: +--- + +## Local Webhooks (ngrok / tunnel) + +GitHub must reach the webhook endpoint over **public HTTPS** — it cannot deliver to `localhost`. To test auto-review locally: + +1. Start a tunnel to your dev server: ```bash - pnpm inngest:dev + ngrok http 3000 ``` +2. Set `WEBHOOK_PUBLIC_URL` to the tunnel URL (the app appends `/api/webhooks/github` automatically): + ```env + WEBHOOK_PUBLIC_URL="https://your-subdomain.ngrok-free.app" + ``` + Auth (`BETTER_AUTH_URL` / `NEXT_PUBLIC_APP_URL`) can stay on `http://localhost:3000`. +3. Restart `pnpm dev`, then **connect a repo** in the dashboard — the webhook is registered against the tunnel URL. +4. Open a PR (or push a commit to an open PR) in that repo to trigger a review. -6. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. +If your tunnel URL changes, just re-connect (or use **Reconnect** on the repo) — webhook registration is idempotent and re-points the existing hook. -## Available Scripts +--- -- `pnpm dev` - Start the Next.js development server -- `pnpm inngest:dev` - Start the Inngest development server -- `pnpm build` - Build the application for production -- `pnpm build:full` - Generate Prisma client and build the application -- `pnpm start` - Start the production server -- `pnpm lint` - Run ESLint for code quality checks -- `pnpm db:push` - Push database schema changes to the database -- `pnpm db:generate` - Generate Prisma Client +## Available Scripts -## How It Works +| Script | What it does | +|---|---| +| `pnpm dev` | Start the Next.js dev server (Turbopack). | +| `pnpm inngest:dev` | Start the local Inngest dev server (processes background review jobs). | +| `pnpm build` | Build for production. | +| `pnpm build:full` | `prisma generate && next build` — use this when the Prisma client isn't generated yet (e.g. CI/Vercel). | +| `pnpm start` | Start the production server. | +| `pnpm lint` | Run ESLint. | +| `pnpm db:generate` | Generate the Prisma client. | +| `pnpm db:push` | Push the Prisma schema to the database (no migration files). | -1. **Authentication**: Users sign in with their GitHub account -2. **Repository Sync**: The app syncs accessible GitHub repositories -3. **Webhook Integration**: GitHub webhooks trigger reviews on new pull requests -4. **AI Analysis**: Pull requests are analyzed using AI models (Gemini/OpenAI) -5. **Review Generation**: AI generates comprehensive feedback with risk scores -6. **Comment Posting**: Reviews are automatically posted back to GitHub -7. **Dashboard**: Users can view all reviews, statistics, and activity +--- ## Database Schema -The application uses PostgreSQL with the following main models: +PostgreSQL via Prisma. Schema lives in [`prisma/schema.prisma`](prisma/schema.prisma). + +### Auth models (Better Auth) + +| Model | Purpose | +|---|---| +| **User** | Accounts (name, email, image). Owns repositories and reviews. | +| **Session** | Active sessions (7-day lifetime, token, IP, user agent). | +| **Account** | Linked OAuth/credential accounts — stores the GitHub access token used for API calls. | +| **Verification** | Email/identifier verification tokens. | + +### Domain models + +**Repository** — a connected GitHub repo and its automation settings. + +| Field | Type | Notes | +|---|---|---| +| `githubId` | `Int` (unique) | GitHub repository id. | +| `fullName` | `String` | `owner/repo`. | +| `private`, `htmlUrl`, `name` | | Repo metadata. | +| `webhookId` | `BigInt?` | GitHub hook id (for update/delete). | +| `webhookSecret` | `String?` | Per-repo HMAC secret (never sent to the client). | +| `webhookStatus` | `WebhookStatus` | `NONE` \| `ACTIVE` \| `FAILED`. | +| `autoReviewEnabled` | `Boolean` (default `true`) | Run a review on push? | +| `autoPostEnabled` | `Boolean` (default `true`) | Post results back to the PR? | +| `postEvent` | `PostEvent` | `COMMENT` \| `REQUEST_CHANGES`. | + +**Review** — a single AI review of a PR. + +| Field | Type | Notes | +|---|---|---| +| `prNumber`, `prTitle`, `prUrl` | | PR identity. | +| `status` | `ReviewStatus` | `PENDING` \| `PROCESSING` \| `COMPLETED` \| `FAILED` \| `CANCELLED`. | +| `summary` | `Text?` | AI summary of the changes. | +| `riskScore` | `Int?` | 0–100. | +| `comments` | `Json?` | Array of inline findings (file, line, severity, category, message, suggestion, old/new code). | +| `error` | `Text?` | Failure / soft-error detail. | +| `postedToGithub` | `Boolean` | Whether comments were posted to the PR. | +| `githubReviewId` | `BigInt?` | The GitHub review id once posted. | + +**Enums** + +```prisma +enum WebhookStatus { NONE ACTIVE FAILED } +enum PostEvent { COMMENT REQUEST_CHANGES } +enum ReviewStatus { PENDING PROCESSING COMPLETED FAILED CANCELLED } +``` + +--- -- **User**: User accounts with GitHub authentication -- **Repository**: Synced GitHub repositories -- **Review**: AI-generated code reviews with status tracking -- **Session**: User session management -- **Account**: OAuth account information +## API Surface + +### HTTP route handlers (`src/app/api`) + +| Route | Purpose | +|---|---| +| `POST /api/webhooks/github` | Receives GitHub `pull_request` events; verifies HMAC, creates a review, enqueues the Inngest job. | +| `/api/auth/[...all]` | Better Auth handler (sign-in/up, OAuth callback, session). | +| `/api/trpc/[trpc]` | tRPC endpoint for all app data. | +| `/api/inngest` | Inngest serve endpoint (`GET`/`POST`/`PUT`) that hosts the `review-pr` function. | + +### tRPC routers (`src/server/api/routers`) + +| Router | Procedures | +|---|---| +| `health` | `query` — health check. | +| `repository` | `list`, `fetchFromGithub`, `connect`, `disconnect`, `setAutomation`, `reconnectWebhook`. | +| `pullRequest` | `list`, `get`, `files`. | +| `review` | `trigger`, `get`, `list`, `getLatestForPR`, `listForPR`, `cancel`, `postToGithub`. | +| `analytics` | `stats`, `reviewTrend`, `riskDistribution`, `topIssues`, `activityHeatmap`, `repoLeaderboard`. | + +### Inngest events + +| Event | Triggered by | Effect | +|---|---|---| +| `review/pr.requested` | Webhook (auto) or `review.trigger` (manual) | Runs the `review-pr` function. Webhook-originated events carry `autoPost` + `postEvent`. | +| `review/pr.cancelled` | `review.cancel` | Cancels an in-flight review (matched on `reviewId`). | + +--- + +## AI Providers + +The review engine is provider-agnostic. Configs live in [`src/constant/ai.ts`](src/constant/ai.ts). + +| Provider | Primary model | Fallback model | API key | Notes | +|---|---|---|---|---| +| **groq** (default) | `llama-3.3-70b-versatile` | `llama-3.1-8b-instant` | `GROQ_API_KEY` | Generous free tier. | +| **openai** | `gpt-4o-mini` | `gpt-3.5-turbo` | `OPENAI_API_KEY` | Supports OpenAI-compatible endpoints (OpenRouter, Azure) via `OPENAI_BASE_URL`. | + +The orchestrator (`review.ts`): +- **Chunks** large PRs so each request stays under the token budget, then merges results (max risk score wins, comments concatenated). +- **Falls back** from the primary to the cheaper model on rate/size limits, and **retries** transient errors (413/429/503) with exponential backoff. +- **Validates** the model's JSON output against a Zod schema, normalizing loose severity/category strings into the canonical set. + +**Review output schema** — each comment carries: `file`, `line`, `severity` (`critical`/`high`/`medium`/`low`), `category` (`bug`/`security`/`performance`/`style`/`suggestion`), `message`, an optional `suggestion`, and optional `oldCode`/`newCode` for the code-comparison view. + +**Adding a provider:** add a config to `PROVIDER_CONFIGS`, implement the `AiProvider` interface in `src/server/services/ai/providers/`, and register it in `registry.ts`. + +--- ## Project Structure ``` aicodereviewer/ -├── prisma/ # Database schema and migrations +├── prisma/ +│ └── schema.prisma # DB models + enums ├── src/ -│ ├── app/ # Next.js app router pages -│ │ ├── (auth)/ # Authentication pages -│ │ ├── (dashboard)/# Dashboard and repository pages -│ │ └── api/ # API routes and webhooks -│ ├── components/ # React components -│ ├── constant/ # Constants and configuration -│ ├── hooks/ # Custom React hooks -│ ├── lib/ # Utility libraries -│ └── server/ # Server-side code (tRPC routers, Inngest functions) +│ ├── app/ +│ │ ├── (auth)/ # sign-in, sign-up (split-screen layouts) +│ │ ├── (dashboard)/ # analytics, repos, repos/[id], PR pages, reviews +│ │ ├── api/ +│ │ │ ├── auth/[...all]/ # Better Auth handler +│ │ │ ├── trpc/[trpc]/ # tRPC endpoint +│ │ │ ├── inngest/ # Inngest serve endpoint +│ │ │ └── webhooks/github/ # GitHub webhook receiver +│ │ ├── layout.tsx # root layout (dark-first theme) +│ │ └── page.tsx # landing page (metadata) + landing-page.tsx (animations) +│ ├── components/ +│ │ ├── analytics/ # stats cards, charts, heatmap, leaderboard +│ │ ├── ui/ # ShadCN/Radix primitives +│ │ └── *.tsx # diff/code viewers, review result, header, etc. +│ ├── constant/ +│ │ └── ai.ts # AI provider configs + default provider +│ ├── lib/ # trpc client/provider, auth client, utils, motion +│ └── server/ +│ ├── api/ # tRPC root + routers + context +│ ├── auth/ # Better Auth config (GitHub OAuth, sessions) +│ ├── db/ # Prisma client +│ ├── inngest/ # client + functions (review-pr) +│ └── services/ +│ ├── ai/ # provider registry, providers, review orchestrator, types +│ └── github.ts # GitHub REST helpers + webhook + PR review posting +├── .env.example +├── next.config.ts # serverExternalPackages for Prisma/Auth/Inngest └── package.json ``` -## Key Features Explained - -### Automated Reviews -When a pull request is opened or updated, the system: -- Fetches the diff and file changes from GitHub -- Analyzes code patterns, potential bugs, and best practices -- Generates inline comments with suggestions -- Calculates a risk score based on complexity and potential issues +--- -### Dashboard -The dashboard provides: -- Overview of all reviewed pull requests -- Activity heatmap showing review frequency -- Statistics including total reviews, risk scores, and success rates -- Quick access to repository management +## Deployment (Vercel) -### Background Processing -Uses Inngest for reliable background job processing: -- Asynchronous PR analysis to avoid blocking -- Retry logic for failed reviews -- Status tracking (PENDING → PROCESSING → COMPLETED/FAILED) +1. Provision a **PostgreSQL** database (e.g. Neon) and an **Inngest** app (for `INNGEST_EVENT_KEY` / `INNGEST_SIGNING_KEY`). +2. In your Vercel project, set **all** required environment variables from the table above. In production: + - `BETTER_AUTH_URL` / `NEXT_PUBLIC_APP_URL` → your production HTTPS URL. + - Leave `WEBHOOK_PUBLIC_URL` **unset** — the app uses the app URL automatically. + - Set `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`. +3. Update the **GitHub OAuth App** callback URL to `https:///api/auth/callback/github`. +4. Set the build command to **`pnpm build:full`** (it runs `prisma generate` before `next build`). +5. Register the Inngest serve endpoint (`https:///api/inngest`) in the Inngest dashboard. -## Contributing +> `next.config.ts` marks `@prisma/client`, `prisma`, `better-auth`, and `inngest` as `serverExternalPackages` so Vercel's build-time page-data collection doesn't crash on these modules. -Contributions are welcome! Please follow these steps: +--- -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/your-feature` -3. Commit your changes: `git commit -m 'Add your feature'` -4. Push to the branch: `git push origin feature/your-feature` -5. Open a pull request +## Troubleshooting -## License +| Symptom | Likely cause / fix | +|---|---| +| Connecting a repo shows `FAILED` webhook status | The GitHub account isn't an admin of the repo, or the token lacks `admin:repo_hook`. Reconnect GitHub to grant the scope, then use **Reconnect** on the repo. | +| PRs aren't being reviewed | (1) Webhook can't reach `localhost` — set up a tunnel + `WEBHOOK_PUBLIC_URL`. (2) The change wasn't a PR event (a plain branch push doesn't trigger). (3) `autoReviewEnabled` is off. (4) The PR is a draft. | +| Reviews stay `PENDING` | The Inngest dev server isn't running locally — start `pnpm inngest:dev`. In prod, check the Inngest serve endpoint + keys. | +| `EPERM` during `pnpm db:generate` (Windows) | Stop `next dev` and `inngest dev` first — they lock the Prisma client DLL. | +| Auto-post recorded a soft error but review is `COMPLETED` | Expected behavior. The AI result is saved regardless; the post failed (e.g. comments fell outside the diff range, or `REQUEST_CHANGES` on your own PR — which auto-downgrades to `COMMENT`). The reason is stored on the review's `error` field. | +| No inline comments posted | All findings referenced lines outside the PR diff range. GitHub only accepts comments on changed lines. | -This project is private and proprietary. +--- -## Support +## Contributing -For issues or questions, please open an issue in the GitHub repository. +1. Fork the repository. +2. Create a feature branch: `git checkout -b feat/your-feature`. +3. Commit your changes (Husky runs lint on commit). +4. Push and open a pull request. diff --git a/docs/ui_redesign_epic.md b/docs/ui_redesign_epic.md deleted file mode 100644 index 71613cd..0000000 --- a/docs/ui_redesign_epic.md +++ /dev/null @@ -1,623 +0,0 @@ -# EPIC: CodeReviewAI — Complete UI/UX Redesign - -> **Status:** Draft — Awaiting Approval -> **Date:** 2026-03-14 -> **Scope:** End-to-end frontend redesign of every page, component, and interaction - ---- - -## 1. Executive Summary - -A ground-up redesign of the CodeReviewAI frontend to deliver a **modern, premium, and highly animated** user experience. The current UI is functional but visually flat, lacks motion, and feels utilitarian. The redesign will transform it into a polished product that feels on par with tools like Linear, Vercel, and Raycast — clean, fast, and delightful to use. - ---- - -## 2. Design Principles - -| Principle | Description | -|-----------|-------------| -| **Dark-first premium** | Rich dark theme as the hero experience; light theme equally polished | -| **Motion with purpose** | Every animation serves a function — entrance, feedback, or continuity | -| **Information density done right** | Show more without feeling crowded; use progressive disclosure | -| **Spatial consistency** | Consistent spacing scale, alignment grid, and visual rhythm | -| **Instant feedback** | Every click, hover, and state change has a visible response | - ---- - -## 3. New Libraries & Packages - -| Package | Purpose | Why | -|---------|---------|-----| -| **`@react-spring/web`** | Physics-based animations | Natural spring animations for counters, gauges, and number transitions — more natural feel than keyframes | -| **`react-syntax-highlighter`** | Code syntax highlighting | Professional code rendering in diff viewer and review comments with proper language tokenization | -| **`react-hot-toast`** (or **`sonner`**) | Toast notifications | Elegant, animated toast system for success/error/info feedback across the app | -| **`@radix-ui/react-tooltip`** | Tooltips | Accessible, animated tooltips for icons, truncated text, and contextual help | -| **`react-countup`** | Animated counters | Smooth number animations for analytics stats cards | - -> **Note:** Framer Motion is already installed but unused. It will become the primary animation engine. - -### Packages to Remove - -| Package | Reason | -|---------|--------| -| `react-icons` | Consolidate on Lucide React exclusively for icon consistency | - ---- - -## 4. Color System Overhaul - -### 4.1 Brand Accent Color - -Introduce a **brand accent** (vibrant indigo/violet) to replace the neutral-only palette: - -``` ---brand: oklch(0.65 0.25 270) /* Vibrant indigo */ ---brand-foreground: oklch(0.98 0 0) /* White on brand */ ---brand-muted: oklch(0.65 0.25 270 / 15%) -``` - -### 4.2 Enhanced Semantic Colors - -- **Success:** Emerald with gradient variants -- **Warning:** Amber with gradient variants -- **Danger:** Rose (shift from pure red for a more refined feel) -- **Info:** Sky blue - -### 4.3 Surface Hierarchy (Dark Theme) - -``` ---surface-0: oklch(0.13 0.005 270) /* App background — slight blue-ish tint */ ---surface-1: oklch(0.17 0.005 270) /* Cards */ ---surface-2: oklch(0.21 0.005 270) /* Elevated cards, modals */ ---surface-3: oklch(0.25 0.005 270) /* Hover states, active surfaces */ -``` - -### 4.4 Gradient System - -```css -.gradient-brand { background: linear-gradient(135deg, var(--brand), oklch(0.55 0.28 310)); } -.gradient-success { background: linear-gradient(135deg, emerald-500, teal-400); } -.gradient-danger { background: linear-gradient(135deg, rose-500, red-400); } -.gradient-surface { background: linear-gradient(180deg, var(--surface-1), var(--surface-0)); } -``` - ---- - -## 5. Typography Refinements - -- **Keep Geist Sans / Geist Mono** — they are excellent -- Add utility classes for the typographic scale: - -| Token | Size | Weight | Use | -|-------|------|--------|-----| -| `display` | 3.5rem / 56px | 700 | Landing hero | -| `h1` | 2.25rem / 36px | 600 | Page titles | -| `h2` | 1.5rem / 24px | 600 | Section headings | -| `h3` | 1.125rem / 18px | 500 | Card titles | -| `body` | 0.875rem / 14px | 400 | Default text | -| `caption` | 0.75rem / 12px | 400 | Secondary text, timestamps | -| `code` | 0.8125rem / 13px | 400 (mono) | Code, file paths | - ---- - -## 6. Animation Strategy - -### 6.1 Framer Motion — Global Defaults - -```ts -const defaultTransition = { type: "spring", stiffness: 400, damping: 30 }; -const fadeInUp = { initial: { opacity: 0, y: 12 }, animate: { opacity: 1, y: 0 } }; -const staggerChildren = { staggerChildren: 0.05 }; -``` - -### 6.2 Animation Catalog - -| Interaction | Animation | Library | -|-------------|-----------|---------| -| Page enter | Fade-in + slide up (staggered) | Framer Motion | -| Card hover | Subtle scale (1.01) + shadow lift + border glow | Framer Motion | -| Button press | Scale down (0.97) + release spring | Framer Motion | -| Tab switch | Underline slide with `layoutId` | Framer Motion | -| Modal open/close | Scale from 0.95 + fade + backdrop blur | Framer Motion | -| Number counting | Spring-based counting animation | react-spring / react-countup | -| Risk score gauge | Animated arc fill on mount | Framer Motion SVG | -| Loading states | Skeleton shimmer + pulse + breathing glow | CSS + Framer Motion | -| Toast appear | Slide in from top-right + fade | sonner | -| List items enter | Staggered fade-in | Framer Motion | -| Navigation | Active indicator slides to selected tab | Framer Motion `layoutId` | -| Diff viewer | Accordion expand with spring | Framer Motion | -| Status changes | Color morph with crossfade | Framer Motion | -| Scroll-reveal | Elements fade in as they enter viewport | Framer Motion `whileInView` | - -### 6.3 Micro-Interactions - -- **Checkboxes:** Bounce-in checkmark SVG animation -- **Buttons:** Ripple effect on click (CSS) -- **Input focus:** Border color transition + subtle glow -- **Hover cards:** Gradient border shimmer on hover -- **Copy button:** Check icon morph on success -- **Badge pulse:** Subtle pulse on "Processing" status - ---- - -## 7. Page-by-Page Redesign Plan - -### 7.1 Landing Page (`/`) - -**Current:** Simple hero + feature grid + how-it-works + CTA. Flat, no motion. - -**Redesigned:** - -- **Hero Section:** - - Animated gradient mesh background (CSS animated gradients, not heavy canvas) - - Large headline with gradient text and word-by-word stagger animation - - Animated terminal/code mockup showing a live review happening - - Floating badge: "Powered by AI" with subtle floating animation - - CTA buttons with hover glow effect and arrow icon animation - -- **Social Proof Bar:** - - Animated counter: "X reviews completed" with CountUp - - Logos or trust indicators with fade-in on scroll - -- **Features Section:** - - Bento grid layout (mixed card sizes) instead of uniform grid - - Each card has an icon with gradient background - - Cards animate in on scroll (`whileInView`) - - Hover: subtle tilt/lift with shadow - -- **How It Works:** - - Horizontal stepper with connecting animated line - - Each step animates in sequence - - Animated icons per step - -- **CTA Section:** - - Gradient background section - - Floating particles or subtle animated grid pattern - -- **Footer:** - - Expanded footer with links, brand, and social links - - Subtle top border gradient - -### 7.2 Auth Pages (`/sign-in`, `/sign-up`) - -**Current:** Centered card with basic form fields. No animation, no visual flair. - -**Redesigned:** - -- **Split layout** on desktop: left panel = brand/illustration, right panel = form -- Left panel features an animated abstract illustration or gradient mesh with the brand logo -- Form card enters with spring animation -- Input fields have animated floating labels -- Focus states with glowing brand-colored border -- GitHub button with icon animation on hover -- Password strength indicator (sign-up) with animated progress bar -- Error messages slide in with red accent -- Loading state: button text morphs to spinner -- Mobile: full-width form with gradient header strip - -### 7.3 Dashboard Layout - -**Current:** Simple header with nav links + theme toggle. No sidebar, mobile nav missing. - -**Redesigned:** - -- **Top navigation bar** — keep but enhance: - - Brand logo/wordmark on the left with subtle hover animation - - Navigation tabs with animated `layoutId` active indicator (sliding pill) - - Right side: notification bell (future-ready), theme toggle with sun/moon morph animation, user avatar dropdown - - Glass morphism effect: `backdrop-blur-xl` + border + subtle gradient - - On scroll: header shrinks slightly with smooth transition - -- **Mobile navigation:** - - Bottom tab bar (iOS-style) for primary nav items - - Smooth icon + label layout with active state animation - - No hamburger menu — direct access to all sections - -- **Breadcrumbs:** - - Add breadcrumb navigation for nested pages (repo > PR > review) - - Animated in/out of individual crumb items - -### 7.4 Repositories Page (`/repos`) - -**Current:** Grid of basic cards + import panel. Functional but plain. - -**Redesigned:** - -- **Page header:** Title with description, "Add Repository" button with plus icon animation -- **Connected repos grid:** - - Cards with gradient left border accent (based on language or custom color) - - Card shows: repo name, description excerpt, last review time, review count badge - - Hover: card lifts with shadow + border glow - - Staggered entrance animation - - Empty state: illustrated placeholder with CTA - -- **Import panel (modal/sheet):** - - Slide-in sheet from the right (Framer Motion `AnimatePresence`) - - Search input with debounced filtering and animated results - - Repo items animate in as a staggered list - - Select-all with animated checkbox - - "Connect" button with loading spinner and success checkmark animation - -- **Disconnect confirmation:** - - Custom modal with backdrop blur - - Warning icon with bounce animation - - Danger button with confirm interaction - -### 7.5 Repository Detail / PR List (`/repos/[id]`) - -**Current:** Tabs for Open/Closed/All, cards for each PR. Basic layout. - -**Redesigned:** - -- **Repository header card:** - - Full-width card with repo name, description, GitHub link, last synced - - Stats row: total PRs, reviewed, pending (animated counters) - - Gradient accent line at top - -- **PR list:** - - Custom tab bar with animated active indicator - - PR cards as a clean list view (not grid) for better scanability - - Each card: PR title, author avatar, branch badge, file change stats bar (visual additions/deletions bar), review status badge with color-coded dot - - Hover: background highlight with smooth transition - - Staggered entrance for list items - - Empty state per tab with contextual illustration - -### 7.6 PR Detail Page (`/repos/[id]/pr/[prNumber]`) - -**Current:** PR metadata + tabbed view (Reviews / Changed Files). Dense and utilitarian. - -**Redesigned:** - -- **PR header section:** - - PR title (large), PR number badge, state badge (open/closed/merged) with color - - Author info: avatar + name + relative time - - Branch flow: `base` ← `head` with animated arrow - - Stats: additions (green), deletions (red), files changed — animated counters - -- **Action bar:** - - "Run AI Review" button — prominent, brand gradient, with sparkle/wand icon animation - - Status: animated processing indicator (pulsing dot + text) - - Cancel button appears conditionally with slide-in - -- **Review Results tab (redesigned ReviewResult component):** - - **Risk Score:** Animated circular gauge (SVG arc) with gradient fill (green→yellow→red), centered score number with CountUp, severity label underneath - - **Severity Distribution:** Horizontal stacked bar with animated segment fills + legend - - **AI Summary:** Styled blockquote card with AI icon - - **Comments list:** - - Cards grouped by severity (critical first) - - Each card: colored left border, severity badge, category icon, file:line monospace link, message text, expandable suggestion with code block - - Hover: lift + glow matching severity color - - Staggered entrance - - **Post to GitHub floating bar:** - - Fixed bottom bar slides up when comments selected - - Selected count with animated badge - - Review type selector (radio pills) - - Post button with loading state - -- **Changed Files tab (redesigned DiffViewer):** - - File list as collapsible accordion - - Each file: name, change stats, expand/collapse icon with rotation animation - - Diff rendering: proper syntax highlighting via `react-syntax-highlighter` - - Line-by-line diff with green/red backgrounds and line numbers - - Smooth accordion expand animation - -### 7.7 Review Comparison Page (`/repos/[id]/pr/[prNumber]/compare`) - -**Current:** Dropdown selectors + summary cards + comment list. Functional but dry. - -**Redesigned:** - -- **Comparison header:** - - Two review selectors side-by-side (styled dropdowns) - - Visual "vs" divider with animated swap icon option - - Risk score delta: large number with animated trend arrow (up = red, down = green) - -- **Summary cards row:** - - Three cards: Fixed (green), New (amber), Unchanged (gray) - - Animated count with CountUp - - Icon per card with matching color - -- **Comparison comments list:** - - Cards with variant backgrounds (green tint = fixed, amber tint = new, neutral = unchanged) - - Status badge: "Fixed" / "New Issue" / "Unchanged" with icon - - Staggered list entrance - - Filter tabs to show only Fixed / New / Unchanged - -### 7.8 Reviews List (`/reviews`) - -**Current:** Filterable list of all reviews. Basic tab filtering. - -**Redesigned:** - -- **Page header:** Title, subtitle with total review count -- **Filter tabs:** Animated pill-style tabs with count badges -- **Review cards:** - - Clean list view with hover highlight - - Left: colored status dot (animated pulse if processing) - - Content: repo name, PR title, relative time - - Right: risk score badge (color-coded), comment count - - Retry button (failed reviews) with refresh icon animation -- **Auto-polling indicator:** Subtle animated dot in header when live-polling - -### 7.9 Analytics Dashboard (`/analytics`) - -**Current:** Grid of chart cards with Recharts. Static, loads all at once. - -**Redesigned:** - -- **KPI Stats Cards row:** - - Glassmorphism card style - - Large metric with CountUp animation - - Trend indicator arrow (up/down) with percentage - - Subtle gradient icon background - - Staggered entrance animation - -- **Review Trend Chart:** - - Area chart with gradient fill under the line - - Animated line drawing on mount - - Time range selector as pill toggle (7d / 30d / 90d) - - Hover tooltip redesigned with custom component - - Smooth data transition when changing ranges - -- **Risk Distribution (Donut):** - - Animated arc segments drawing in on mount - - Center: animated total count - - Custom legend with colored dots - -- **Top Issues (Bar chart):** - - Horizontal bars animate from left to right - - Bars have rounded ends and gradient fills - - Labels on the left, values on the right - -- **Activity Heatmap:** - - Custom redesigned heatmap with brand-colored gradient (instead of default green) - - Tooltip on hover showing date and count - - Smooth fade-in on mount - -- **Repo Leaderboard:** - - Ranked list with medal icons (gold/silver/bronze) for top 3 - - Progress bar with animated fill - - Risk score badge per repo - -- **Overall Health and Review Status cards:** - - Side-by-side fullwidth cards at bottom - - Circular progress indicator for pass rate - - Status breakdown with animated bars - -### 7.10 Error Page (`/error.tsx`) and 404 Page (`/not-found.tsx`) - -**Current:** Basic error/404 with simple icon and text. - -**Redesigned:** - -- Centered layout with animated illustration (CSS-only animated icon) -- Error code large and prominent with gradient text -- Friendly copy with personality -- Action buttons: "Go Back" / "Go Home" with animated icons -- Subtle animated background pattern - ---- - -## 8. Shared Component Library Updates - -### 8.1 New Components to Create - -| Component | Description | -|-----------|-------------| -| `AnimatedPage` | Wrapper for page-level enter/exit animations | -| `AnimatedList` | Staggered children animation wrapper | -| `AnimatedCounter` | Number counting component (wraps CountUp) | -| `GlassCard` | Glassmorphism card variant | -| `GradientButton` | Brand gradient CTA button | -| `AnimatedTabs` | Tab bar with sliding `layoutId` indicator | -| `BottomNav` | Mobile bottom navigation bar | -| `Breadcrumbs` | Animated breadcrumb navigation | -| `CircularGauge` | SVG circular gauge for risk scores | -| `StatusDot` | Animated status indicator dot | -| `EmptyState` | Reusable empty state with illustration | -| `PageHeader` | Consistent page title + description + actions layout | -| `Tooltip` | Radix tooltip with animation | -| `Toast` | Toast notification system (via sonner) | - -### 8.2 Existing Components to Update - -| Component | Changes | -|-----------|---------| -| `Button` | Add gradient variant, press animation, loading state with spinner | -| `Card` | Add hover lift animation, gradient border variant, glass variant | -| `Badge` | Add pulse variant for processing states, gradient variant | -| `Input` | Animated focus glow, floating label option | -| `Skeleton` | Improved shimmer with gradient sweep | -| `Header` | Complete redesign (see 7.3) | -| `UserMenu` | Enhanced dropdown with animations | -| `ThemeToggle` | Sun/moon morph animation | -| `DiffViewer` | Syntax highlighting, accordion animation | -| `ReviewResult` | Complete redesign (see 7.6) | -| `ShimmerSkeleton` | Enhanced shimmer animation | - ---- - -## 9. Responsive Breakpoints - -| Breakpoint | Width | Layout Adaptations | -|------------|-------|--------------------| -| `sm` | 640px+ | Single → two column grids | -| `md` | 768px+ | Show desktop nav, hide bottom nav | -| `lg` | 1024px+ | Full feature layouts, side-by-side panels | -| `xl` | 1280px+ | Max content width containers | - -### Mobile-First Approach - -- Bottom tab bar navigation (< 768px) -- Cards stack vertically on mobile -- Auth pages: single column (no split layout on mobile) -- Touch-friendly: minimum 44px tap targets -- Swipe gestures: swipeable tabs on mobile -- Pull-to-refresh indicator for live data pages - ---- - -## 10. Performance Considerations - -| Concern | Mitigation | -|---------|------------| -| Animation performance | Use `transform` and `opacity` only (GPU-accelerated). Avoid animating layout properties | -| Bundle size | Tree-shake Framer Motion imports. Use dynamic imports for heavy components (charts, diff viewer) | -| Font loading | Keep Geist fonts with `font-display: swap` to avoid FOIT | -| Image/illustration | Use CSS-only illustrations and SVG animations (no heavy image files) | -| Reduced motion | Respect `prefers-reduced-motion` — disable all non-essential animations | -| Code splitting | Dynamic import analytics charts and diff viewer (not needed on initial load) | - ---- - -## 11. Implementation Phases - -### Phase 1: Foundation (Estimated: Infrastructure & Global Styles) - -1. Install new dependencies (`sonner`, `react-syntax-highlighter`, `react-countup`, `@react-spring/web`) -2. Remove `react-icons` (replace `FaGithub` with Lucide's `Github` icon) -3. Overhaul `globals.css` — new color system, surface tokens, gradients, typography scale -4. Create animation utilities and shared motion variants -5. Create core shared components: `AnimatedPage`, `AnimatedList`, `GlassCard`, `GradientButton`, `AnimatedTabs`, `PageHeader`, `EmptyState`, `StatusDot`, `CircularGauge`, `AnimatedCounter`, `Breadcrumbs` -6. Set up `sonner` toast provider in root layout -7. Add `prefers-reduced-motion` media query handling - -### Phase 2: Navigation & Layout - -8. Redesign dashboard `Header` component (sliding nav indicator, glass effect, responsive) -9. Create `BottomNav` for mobile -10. Add `Breadcrumbs` to nested dashboard pages -11. Enhance `ThemeToggle` with sun/moon morph animation -12. Enhance `UserMenu` dropdown with animations - -### Phase 3: Auth Pages - -13. Redesign `/sign-in` page (split layout, animations, enhanced form) -14. Redesign `/sign-up` page (matching design, password strength indicator) - -### Phase 4: Landing Page - -15. Complete landing page redesign (hero, features bento grid, how-it-works, CTA, footer) - -### Phase 5: Core Dashboard Pages - -16. Redesign `/repos` page (enhanced cards, import sheet, empty state) -17. Redesign `/repos/[id]` page (repo header, PR list, tabs) -18. Redesign `/repos/[id]/pr/[prNumber]` page (review results, diff viewer, action bar) -19. Redesign `/repos/[id]/pr/[prNumber]/compare` page (comparison UI) -20. Redesign `/reviews` page (filter tabs, review cards, polling indicator) - -### Phase 6: Analytics & Misc - -21. Redesign `/analytics` page (all chart components, KPI cards, heatmap) -22. Redesign error page and 404 page -23. Responsive QA pass across all pages -24. Final polish: micro-interactions, hover states, edge cases - ---- - -## 12. Files to Modify / Create - -### New Files - -``` -src/components/ui/animated-page.tsx -src/components/ui/animated-list.tsx -src/components/ui/animated-counter.tsx -src/components/ui/animated-tabs.tsx -src/components/ui/glass-card.tsx -src/components/ui/gradient-button.tsx -src/components/ui/circular-gauge.tsx -src/components/ui/status-dot.tsx -src/components/ui/empty-state.tsx -src/components/ui/page-header.tsx -src/components/ui/tooltip.tsx -src/components/bottom-nav.tsx -src/components/breadcrumbs.tsx -src/lib/motion.ts (shared animation variants & config) -``` - -### Files to Modify - -``` -package.json (add/remove dependencies) -src/app/globals.css (complete color/token overhaul) -src/app/layout.tsx (add toast provider) -src/app/page.tsx (complete landing page redesign) -src/app/error.tsx (redesign) -src/app/not-found.tsx (redesign) -src/app/(auth)/layout.tsx (add split layout wrapper) -src/app/(auth)/sign-in/page.tsx (complete redesign) -src/app/(auth)/sign-up/page.tsx (complete redesign) -src/app/(dashboard)/layout.tsx (add bottom nav, breadcrumbs) -src/app/(dashboard)/repos/page.tsx (complete redesign) -src/app/(dashboard)/repos/[id]/page.tsx (redesign) -src/app/(dashboard)/repos/[id]/pr/[prNumber]/page.tsx (redesign) -src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx (redesign) -src/app/(dashboard)/reviews/page.tsx (redesign) -src/app/(dashboard)/analytics/page.tsx (redesign) -src/components/header.tsx (complete redesign) -src/components/user-menu.tsx (enhance with animations) -src/components/theme-toggle.tsx (sun/moon morph animation) -src/components/diff-viewer.tsx (syntax highlighting + accordion) -src/components/review-result.tsx (complete redesign with gauge) -src/components/review-comparison-card.tsx (redesign) -src/components/shimmer-skeleton.tsx (enhanced shimmer) -src/components/connect-github.tsx (redesign import panel) -src/components/analytics/stats-cards.tsx (glassmorphism + counters) -src/components/analytics/review-chart.tsx (gradient area chart) -src/components/analytics/risk-distribution.tsx (animated donut) -src/components/analytics/top-issues.tsx (animated bars) -src/components/analytics/activity-heatmap.tsx (brand color restyle) -src/components/analytics/repo-leaderboard.tsx (redesign) -src/components/ui/button.tsx (add variants, press animation) -src/components/ui/card.tsx (add hover/glass variants) -src/components/ui/badge.tsx (add pulse/gradient variants) -src/components/ui/input.tsx (focus glow animation) -src/components/ui/skeleton.tsx (improved shimmer) -``` - ---- - -## 13. Acceptance Criteria - -- [ ] All pages are visually redesigned with a premium, modern look -- [ ] Smooth animations on all page transitions, card hovers, button presses, and list renders -- [ ] Dark and light themes both look polished -- [ ] Fully responsive: mobile (320px+), tablet (768px+), desktop (1024px+) -- [ ] Mobile bottom navigation works on all dashboard pages -- [ ] Risk score displayed as animated circular gauge -- [ ] Analytics charts have animated entrances -- [ ] Diff viewer uses syntax highlighting -- [ ] Toast notifications replace any raw alert/error text -- [ ] `prefers-reduced-motion` respected -- [ ] No regressions in functionality — all features work as before -- [ ] Lighthouse performance score remains above 90 -- [ ] All existing tRPC API integrations continue to work unchanged -- [ ] No changes to backend/server code - ---- - -## 14. Out of Scope - -- Backend API changes -- Database schema changes -- New features / new pages -- Authentication flow logic changes -- AI review logic changes -- Third-party integrations beyond what exists - ---- - -## 15. Risks & Mitigations - -| Risk | Mitigation | -|------|------------| -| Bundle size increase from new libraries | Tree-shaking, dynamic imports, monitor with `next build` | -| Animation jank on low-end devices | GPU-only animations, `prefers-reduced-motion` support | -| Breaking existing functionality | No backend changes; test all flows after redesign | -| Inconsistent design between pages | Build shared component library first (Phase 1), use everywhere | - ---- - -**Ready for review.** Once approved, implementation will proceed phase by phase. diff --git a/docs/user_ai_settings_epic.md b/docs/user_ai_settings_epic.md new file mode 100644 index 0000000..654f212 --- /dev/null +++ b/docs/user_ai_settings_epic.md @@ -0,0 +1,295 @@ +# ⚙️ CodeReviewAI — User AI Settings (Model + API Key + Custom Instructions) Epic + +> **Goal:** Give every user a **Settings** page (reachable from the user menu, just above *Sign out*) where they can **choose the AI provider/model**, **bring their own API key**, and **add custom instructions** that get injected into the review prompt. When the user provides nothing, the app keeps working on its **default Groq key/model** exactly as today. + +--- + +## 📌 Status + +> **Status (2026-06-09): 📝 Proposed — awaiting approval.** No code written yet. Open Questions below are resolved per the product decisions captured in this epic header; implementation order is at the end. + +| Field | Value | +| ----- | ----- | +| Type | Feature (user-configurable AI) | +| Priority | High (unlocks BYO-key + tailored reviews; removes shared-key cost ceiling) | +| Risk | Medium (handles **secret API keys**; touches the core review pipeline) | +| Depends on | Existing AI provider/registry layer + Inngest review job (already built) | +| DB migration | **Yes** — new `UserAiSettings` model (1:1 with `User`) | +| New env var | **Yes** — `SETTINGS_ENCRYPTION_KEY` (32-byte key for at-rest encryption); optionally `ANTHROPIC_API_KEY` as an app-level default | +| New dependency | Anthropic provider (via `@anthropic-ai/sdk` or native `fetch`) | + +--- + +## 🎯 Problem Statement + +Today the AI review is **completely fixed**: + +1. **Provider/model is hardcoded to the default.** `reviewCode()` accepts an optional `providerName`, but the Inngest job (`review-pr.ts`) **never passes one** → every review runs on `groq` with `llama-3.3-70b-versatile` (primary) / `llama-3.1-8b-instant` (fallback). Users cannot pick OpenAI or Claude, and cannot pick a specific model. +2. **API keys come only from env.** Providers are built by factories that read `GROQ_API_KEY` / `OPENAI_API_KEY` from `process.env`. Every user shares the app owner's keys and rate limits — there's no way to bring your own key, and the owner pays for everyone. +3. **The system prompt is hardcoded.** `SYSTEM_PROMPT` in `review.ts` is a single string. There's no way for a user to say "we use 2-space indent / focus on security / ignore generated files / our team's conventions are X." + +The user expectation: *open Settings once → pick a model, paste my key (optional), write house rules → every review I run uses them.* + +--- + +## 🔍 Current State (what already exists — do NOT rebuild) + +| Capability | File | Status | +| ---------- | ---- | ------ | +| Provider config (groq, openai) + default | `src/constant/ai.ts` | ✅ `PROVIDER_CONFIGS` with `primaryModel`/`fallbackModel`, `apiKeyEnvVar`, signup URL. `DEFAULT_PROVIDER = "groq"`. | +| Provider registry + cache | `src/server/services/ai/registry.ts` | ✅ `getProvider(name)` returns a **singleton cached by name**; factories read keys from env. | +| Groq provider | `src/server/services/ai/providers/groq.ts` | ✅ Class **already takes `apiKey` + `baseUrl` in its constructor** (factory just wires env). | +| OpenAI provider | `src/server/services/ai/providers/openai.ts` | ✅ Class **already takes `apiKey` + `baseURL`** (SDK-based). | +| Review orchestrator | `src/server/services/ai/review.ts` | ✅ `reviewCode(prTitle, files, providerName?)`. Hardcoded `SYSTEM_PROMPT`; primary→fallback model loop with retry/backoff. | +| Inngest review job | `src/server/inngest/functions/review-pr.ts` | ⚠️ Calls `reviewCode(pr.title, files)` with **no provider/model/instructions**. | +| User menu (Sign out) | `src/components/user-menu.tsx` | ✅ Dropdown with avatar + Sign out. **No Settings entry.** | +| tRPC root | `src/server/api/root.ts` | ✅ `repository` / `pullRequest` / `review` / `analytics` routers. **No `settings` router.** | +| `User` model | `prisma/schema.prisma` | ⚠️ Auth fields only. **No AI settings.** | + +**Net:** the provider classes already accept an injected key, and `reviewCode` already accepts a provider name. The missing pieces are: **(a) a place to store the user's choices + key (encrypted)**, **(b) a Claude provider**, **(c) plumbing that resolves a user's settings and threads provider+model+key+instructions into the review**, and **(d) the Settings UI**. + +--- + +## ✅ Proposed Solution (high level) + +### 1. Store per-user AI settings (new `UserAiSettings` table, 1:1 with `User`) +Provider (`GROQ | OPENAI | ANTHROPIC`), selected model, an **encrypted API key** (+ masked `last4` for display), and free-text **custom instructions**. All fields optional → an empty row means "use app defaults." + +### 2. Add a Claude/Anthropic provider +Mirror the existing provider pattern: a config entry in `PROVIDER_CONFIGS`, a provider class taking an injected `apiKey`, and a registry registration. Default models e.g. `claude-opus-4-8` (primary) / `claude-haiku-4-5` (fallback). *(Confirm exact model ids against the Claude API reference at build time.)* + +### 3. Resolve settings → thread them through the review pipeline +- Extend `PROVIDER_CONFIGS` with a **selectable model list** per provider (the UI needs choices beyond just primary/fallback). +- Change `reviewCode()` to take a **resolved config object** `{ provider, model, apiKey?, customInstructions? }` instead of a bare `providerName`. +- Add a **non-cached** provider instantiation path: when a user key is present, build a **fresh** provider instance with that key (never cache user-keyed instances — that would leak keys across users). Keep the env-based singleton cache only for the app-default path. +- Inject `customInstructions` into the system prompt inside a clearly delimited block. + +### 4. Wire the Inngest job to load + apply settings +`review-pr.ts` loads the PR author/owner's `UserAiSettings`, resolves the effective config (user key → app env key → error), and passes it to `reviewCode`. **The decrypted key is read and used *inside* the `generate-review` step — it is never returned from a step** (Inngest persists step return values; a decrypted secret must not land in that store). + +### 5. Settings UI under the user menu +A `/settings` page (dashboard route group) with: provider select → dependent model select → API key input (masked, shows `••••last4`, with a "Remove key" action) → custom-instructions textarea (with char counter + cap). Add a **Settings** item to `UserMenu` directly above *Sign out*. + +--- + +## 🗄️ Data Model Changes + +### New model: `UserAiSettings` (1:1 with `User`) + +```prisma +model UserAiSettings { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + provider AiProvider @default(GROQ) // GROQ | OPENAI | ANTHROPIC + model String? // selected model id; null => provider default + + // ── Encrypted API key (AES-256-GCM). Plaintext NEVER stored or returned. ── + apiKeyCiphertext String? @db.Text // base64(iv):base64(authTag):base64(ciphertext) + apiKeyLast4 String? // last 4 chars, for masked display only + apiKeyProvider AiProvider? // which provider the saved key belongs to + + customInstructions String? @db.Text // appended to the system prompt (length-capped in app) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum AiProvider { + GROQ + OPENAI + ANTHROPIC +} +``` + +Add the back-relation on `User`: + +```prisma +model User { + // ... existing fields ... + aiSettings UserAiSettings? +} +``` + +> **Why a separate 1:1 table** (vs. columns on `User`): keeps the auth/`User` table clean, scopes the sensitive key column away from the row read on every auth check, and makes the encrypted blob easy to find/rotate. + +> **Single saved key for the active provider.** The MVP stores **one** encrypted key, tagged with `apiKeyProvider`. If the user switches provider, the UI prompts for that provider's key. *(Storing a key per provider — a `UserProviderKey` child table — is a clean follow-up; see Out of Scope.)* + +> **Encryption format:** `apiKeyCiphertext` is `iv:authTag:ciphertext` (each base64). Encryption key comes from `SETTINGS_ENCRYPTION_KEY` (32 raw bytes, provided as base64/hex in env). Decryption happens only at review time. + +**Migration:** purely additive (new table + back-relation + enum). No backfill — users with no row get app defaults. + +--- + +## 🔧 Change Set (file by file) + +| # | File | Change | +| - | ---- | ------ | +| 1 | `prisma/schema.prisma` | Add `UserAiSettings` model + `AiProvider` enum + `User.aiSettings` back-relation. Run `prisma migrate dev` (stop `next dev`/`inngest dev` first on Windows — see Prisma Windows lock note). | +| 2 | `src/server/services/crypto.ts` **(new)** | `encryptSecret(plaintext): string` / `decryptSecret(blob): string` using AES-256-GCM keyed by `SETTINGS_ENCRYPTION_KEY`. Throw a clear error if the env key is missing/wrong length. Never log plaintext. | +| 3 | `src/constant/ai.ts` | Add an `anthropic` entry to `PROVIDER_CONFIGS` (`apiKeyEnvVar: "ANTHROPIC_API_KEY"`, signup URL, primary/fallback models). Add a **`availableModels: readonly string[]`** field to every provider config (the selectable list for the UI). | +| 4 | `src/server/services/ai/providers/anthropic.ts` **(new)** | `AnthropicProvider` implementing `AiProvider` (constructor takes `apiKey`), `chat()` returning JSON content. `createAnthropicProvider()` factory reading env. Mirror groq/openai error+status shape so the orchestrator's retry logic still works. | +| 5 | `src/server/services/ai/registry.ts` | Register `anthropic`. Add `instantiateProvider(name, apiKey?)`: if `apiKey` given, return a **fresh, uncached** provider built with that key; else fall back to the cached env factory. Keep `getProvider` (cached) for the default path. | +| 6 | `src/server/services/ai/types.ts` | Add `availableModels` to `AiProviderConfig`. Add a `ResolvedAiConfig` type `{ provider: ProviderName; model?: string; apiKey?: string; customInstructions?: string }`. | +| 7 | `src/server/services/ai/review.ts` | Change `reviewCode(prTitle, files, config?: ResolvedAiConfig)`. Build the provider via `instantiateProvider(config.provider, config.apiKey)`. Use `config.model` as primary (fallback stays the provider's configured fallback). Inject `config.customInstructions` into the system prompt inside a delimited `## Additional reviewer instructions` block. Keep chunking/retry/fallback unchanged. | +| 8 | `src/server/services/ai/settings.ts` **(new)** | `resolveUserAiConfig(userId): Promise`: load `UserAiSettings`; pick provider+model; decrypt the key **only if** it belongs to the selected provider; if no user key, leave `apiKey` undefined so the env-default path is used. Validate `model` ∈ provider `availableModels` (ignore/repair invalid). | +| 9 | `src/server/inngest/functions/review-pr.ts` | In the `generate-review` step, call `resolveUserAiConfig(userId)` and pass the result to `reviewCode`. **Decrypt inside this step; do not return the key from any step.** | +| 10 | `src/server/api/routers/settings.ts` **(new)** | `protectedProcedure` router: `get` → returns `{ provider, model, customInstructions, hasApiKey, apiKeyLast4 }` (**never** the raw key); `update` → `{ provider, model?, customInstructions?, apiKey? }` (encrypt+store key, set `last4`+`apiKeyProvider`, validate model ∈ provider list, cap instructions length); `removeApiKey` → clears the key fields. Optionally `providers` → returns provider/model catalog for the UI. | +| 11 | `src/server/api/root.ts` | Mount `settings: settingsRouter`. | +| 12 | `src/app/(dashboard)/settings/page.tsx` **(new)** | Settings page: provider select → dependent model select → masked API-key input (+ Remove) → custom-instructions textarea (counter + cap) → Save. Uses the `settings` tRPC router; shows `••••last4` when a key is saved. | +| 13 | `src/components/user-menu.tsx` | Add a **Settings** `DropdownMenuItem` (gear icon, `Link href="/settings"`) **above** the separator/Sign out. | +| 14 | `.env` / docs | Document `SETTINGS_ENCRYPTION_KEY` (how to generate 32 bytes) and optional `ANTHROPIC_API_KEY` app default. Note default behavior when unset. | + +--- + +## 🔁 Effective-Config Resolution (the core rule) + +For a given review, the effective config is resolved per **selected provider**: + +``` +load UserAiSettings(userId) (may be null → all defaults) + │ + ▼ +provider = settings.provider ?? GROQ +model = settings.model (if ∈ availableModels[provider]) ?? provider.primaryModel + │ + ▼ +apiKey resolution: + ├─ settings has a saved key AND settings.apiKeyProvider == provider + │ → decrypt it, use the USER key ✅ + ├─ else app env key for that provider exists (process.env[apiKeyEnvVar]) + │ → use the APP key + └─ else → error: "Add your API key in Settings" + (note: GROQ always works because the app ships a default GROQ_API_KEY) + │ + ▼ +customInstructions = settings.customInstructions (capped) — injected into system prompt +``` + +This satisfies the product rule: **user key/model if provided, otherwise the default Groq key/model.** + +--- + +## 🔐 Security & Correctness Considerations + +1. **Encrypt at rest.** API keys are stored only as AES-256-GCM ciphertext (`SETTINGS_ENCRYPTION_KEY`). The plaintext key is **never** persisted, logged, or returned to the client. +2. **Never echo the key back.** `settings.get` returns only `hasApiKey` + `apiKeyLast4`. The full key cannot be read back via any API. +3. **Don't persist the key in Inngest step state.** Decrypt and use the key **inside** the `generate-review` step; never return it from a `step.run` (Inngest persists step outputs durably). +4. **Don't cache user-keyed providers.** `instantiateProvider(name, apiKey)` returns a fresh instance — only the env-default providers stay in the singleton cache, so one user's key can never be served to another. +5. **Model allowlist.** `update` and `resolveUserAiConfig` validate `model ∈ availableModels[provider]`. Prevents arbitrary/expensive model ids and provider/model mismatches. +6. **Custom-instruction hygiene.** Cap length (e.g. ≤ 4,000 chars) so a huge blob can't blow the prompt budget; inject inside a clearly delimited block so it can't silently override the structured-JSON contract. Prompt-injection risk is self-scoped (it only affects the user's own review), but still keep the JSON-output instruction last/authoritative. +7. **Encryption-key rotation.** If `SETTINGS_ENCRYPTION_KEY` changes, existing ciphertext won't decrypt → treat decryption failure as "no usable key" and fall back to the app key (don't crash the review). Document rotation as ops-only. +8. **Missing env key on startup.** If `SETTINGS_ENCRYPTION_KEY` is absent, the **Settings save (key path)** must fail with a clear message; reviews on the default Groq path keep working. + +--- + +## ✅ Acceptance Criteria + +1. A **Settings** entry appears in the user menu directly above *Sign out* and routes to `/settings`. +2. The settings page lets a user pick a **provider** (Groq / OpenAI / Claude) and a **model** from that provider's list; the model list updates when the provider changes. +3. A user can **save an API key**; it is stored **encrypted** (verified: DB column is ciphertext, not the raw key) and shown afterward only as `••••last4`. The raw key is never returned by any tRPC call. +4. A user can **remove** their saved key, reverting to the app default. +5. A user can save **custom instructions**; a subsequent review's prompt includes them (verifiable via the AI-review log / behavior). +6. With a user key + model set, a review runs on **that provider/model using the user's key**. +7. With **no** settings (or no key), reviews run on the **default Groq key/model** exactly as before (no regression). +8. Selecting OpenAI/Claude **without** a key and **without** a configured app env key for that provider produces a clear "add your key" error — it does **not** silently fall back or crash. +9. The decrypted key never appears in Inngest step output, logs, or any API response. +10. The new **Anthropic/Claude** provider produces valid structured-JSON reviews through the same orchestrator (chunking + retry + fallback). +11. `pnpm lint` + `pnpm build` pass; the Prisma migration applies cleanly. + +--- + +## 🛡️ No-Regression / "Don't Break the Flow" Checklist + +- [ ] Existing reviews with no `UserAiSettings` row behave identically to today (default Groq). +- [ ] `reviewCode` signature change is updated at **all** call sites (Inngest job + any manual trigger). +- [ ] The env-default provider cache (`getProvider`) is untouched for the default path; only the new user-keyed path is uncached. +- [ ] Auto-review-on-push (webhook → Inngest) still works and now honors the owner's settings. +- [ ] Manual `review.trigger` / `review.postToGithub` flow unaffected. +- [ ] Existing groq/openai providers' behavior unchanged when called via env factory. +- [ ] No secret (API key) is written to logs, Inngest step state, or any client response. + +--- + +## 🚀 Rollout / Ops + +- Generate and set `SETTINGS_ENCRYPTION_KEY` (e.g. `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`) **before** deploy. +- (Optional) set `ANTHROPIC_API_KEY` if you want Claude usable without each user bringing a key; otherwise Claude requires a user key. +- Migration is additive; no backfill. Ship UI + pipeline together so a saved setting is actually honored. + +--- + +## 🧭 Manual QA Plan + +| Step | Action | Expected | +| ---- | ------ | -------- | +| 1 | Open user menu | "Settings" shows above "Sign out"; opens `/settings` | +| 2 | Pick OpenAI, change model | Model list switches to OpenAI models | +| 3 | Save a key + instructions, reload | Key shows as `••••last4`; instructions persist; DB stores ciphertext | +| 4 | Run a review | Uses chosen provider/model + user key; prompt includes custom instructions | +| 5 | Remove key, run review | Falls back to app default (Groq if OpenAI/Claude key absent) | +| 6 | Pick Claude with a valid key, run review | Valid structured review via Anthropic | +| 7 | Pick OpenAI, no user key, no app `OPENAI_API_KEY`, run review | Clear "add your key" error; no crash | +| 8 | New user, no settings, run review | Default Groq path, identical to pre-feature behavior | +| 9 | Inspect Inngest run + logs | No plaintext key anywhere | + +--- + +## 📂 Files Touched (summary) + +``` +prisma/schema.prisma # UserAiSettings model + AiProvider enum + User relation + migration +src/server/services/crypto.ts (new) # AES-256-GCM encrypt/decrypt +src/constant/ai.ts # anthropic config + availableModels per provider +src/server/services/ai/providers/anthropic.ts (new) # Claude provider +src/server/services/ai/registry.ts # register anthropic + instantiateProvider(name, apiKey?) +src/server/services/ai/types.ts # availableModels + ResolvedAiConfig +src/server/services/ai/review.ts # reviewCode(config) + custom-instruction injection +src/server/services/ai/settings.ts (new) # resolveUserAiConfig(userId) +src/server/inngest/functions/review-pr.ts # load+apply settings inside generate-review step +src/server/api/routers/settings.ts (new) # get / update / removeApiKey (+ providers catalog) +src/server/api/root.ts # mount settings router +src/app/(dashboard)/settings/page.tsx (new) # Settings UI +src/components/user-menu.tsx # Settings menu item above Sign out +.env / docs # SETTINGS_ENCRYPTION_KEY + ANTHROPIC_API_KEY notes +``` + +--- + +## ❓ Open Questions (resolved — kept for the record) + +1. **BYO key behavior** → ✅ Optional; use the user's key/model if set, else default **Groq** key/model. +2. **Providers** → ✅ Groq, OpenAI, **Claude/Anthropic** (new provider). +3. **Key storage** → ✅ **Encrypt at rest** (AES-256-GCM, `SETTINGS_ENCRYPTION_KEY`). +4. **Scope** → ✅ **Global per-user** (one settings page under the user menu). + +Remaining nit to confirm at build time: +- **Exact Claude model ids** for primary/fallback and the selectable list (verify against the current Claude API reference). + +--- + +## 🚫 Out of Scope (this epic) + +- **Per-repo overrides** of model/instructions (global per-user only). +- **Storing a key per provider** (a `UserProviderKey` child table) — MVP stores one key for the active provider; switching providers re-prompts. +- A **"test my key / test connection"** button (nice follow-up; resolution + error messaging cover the basics). +- **Temperature / max-tokens / other tuning** knobs in the UI. +- **Org/team-shared** settings or billing/usage metering per user. +- **Encryption-key rotation tooling** (manual ops only for now). +- **Free-form arbitrary model ids** beyond each provider's curated `availableModels` list. + +--- + +## ⏭️ Next Step + +Approve, then implement in this order: + +1. Prisma `UserAiSettings` + `AiProvider` enum + migration. +2. `crypto.ts` (encrypt/decrypt) + `SETTINGS_ENCRYPTION_KEY`. +3. Anthropic provider + `availableModels` in `PROVIDER_CONFIGS` + registry `instantiateProvider`. +4. `reviewCode(config)` refactor + custom-instruction injection. +5. `resolveUserAiConfig` + wire into the Inngest `generate-review` step. +6. `settings` tRPC router + mount in root. +7. Settings page UI + user-menu entry. +8. QA per the plan above; set env vars; deploy. diff --git a/package.json b/package.json index 76a560a..9b01218 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "eslint", "db:push": "prisma db push", "db:generate": "prisma generate", + "postinstall": "prisma generate", "prepare": "husky" }, "dependencies": { @@ -25,7 +26,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.34.3", - "inngest": "^3.52.1", + "inngest": "^3.54.0", "lucide-react": "^0.564.0", "next": "16.1.6", "next-themes": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b46454..8cc8084 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^12.34.3 version: 12.34.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) inngest: - specifier: ^3.52.1 - version: 3.52.1(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(express@5.2.1)(hono@4.11.9)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(zod@4.3.6) + specifier: ^3.54.0 + version: 3.54.2(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(express@5.2.1)(hono@4.11.9)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(zod@4.3.6) lucide-react: specifier: ^0.564.0 version: 0.564.0(react@19.2.3) @@ -3556,8 +3556,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inngest@3.52.1: - resolution: {integrity: sha512-QrCIVOnAVtFAOLuqWIpK+/bFQyn0nvD6p0s3+2iFlsmaOB/fKcjbMC/5WXz8yLh0c7cBJLGeJRnCKbhI9cEW7Q==} + inngest@3.54.2: + resolution: {integrity: sha512-SAAc52c7n34E9dBEapy99g0vO5MPJP2yttoZpCu3LcYy1d8tz6CrkdoEQ4FBBgzMNnQxNpeAH5lvNeFCGNXbtA==} engines: {node: '>=20'} peerDependencies: '@sveltejs/kit': '>=1.27.3' @@ -9011,7 +9011,7 @@ snapshots: inherits@2.0.4: {} - inngest@3.52.1(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(express@5.2.1)(hono@4.11.9)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(zod@4.3.6): + inngest@3.54.2(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(express@5.2.1)(hono@4.11.9)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(zod@4.3.6): dependencies: '@bufbuild/protobuf': 2.11.0 '@inngest/ai': 0.1.7 diff --git a/prisma/backfill-automation-mode.sql b/prisma/backfill-automation-mode.sql new file mode 100644 index 0000000..5b54241 --- /dev/null +++ b/prisma/backfill-automation-mode.sql @@ -0,0 +1,15 @@ +-- Phase A backfill: derive each repo's single automationMode from the legacy +-- flag combination. Safe + idempotent — re-running maps to the same result. +-- autoReviewEnabled = false -> OFF +-- autoReview = true, autoPost = false -> REVIEW_ONLY +-- autoReview = true, autoPost = true, COMMENT -> COMMENT +-- autoReview = true, autoPost = true, REQUEST_CHANGES-> REQUEST_CHANGES +UPDATE "Repository" +SET "automationMode" = ( + CASE + WHEN "autoReviewEnabled" = false THEN 'OFF' + WHEN "autoPostEnabled" = false THEN 'REVIEW_ONLY' + WHEN "postEvent" = 'REQUEST_CHANGES' THEN 'REQUEST_CHANGES' + ELSE 'COMMENT' + END +)::"AutomationMode"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2da17e0..6a37d42 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,11 +26,40 @@ model User { repositories Repository[] reviews Review[] + aiSettings UserAiSettings? @@unique([email]) @@map("user") } +model UserAiSettings { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + provider AiProvider @default(GROQ) // GROQ | OPENAI | ANTHROPIC + model String? // selected model id; null => provider default + + // ── Encrypted API key (AES-256-GCM). Plaintext NEVER stored or returned. ── + apiKeyCiphertext String? @db.Text // base64(iv):base64(authTag):base64(ciphertext) + apiKeyLast4 String? // last 4 chars, for masked display only + apiKeyProvider AiProvider? // which provider the saved key belongs to + + customInstructions String? @db.Text // appended to the system prompt (length-capped in app) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("user_ai_settings") +} + +enum AiProvider { + GROQ + OPENAI + ANTHROPIC + GEMINI +} + model Session { id String @id expiresAt DateTime @@ -90,10 +119,44 @@ model Repository { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + // ── Webhook + automation ── + webhookId BigInt? // GitHub hook id, for update/delete + webhookSecret String? // per-repo HMAC secret (generated on register) + webhookStatus WebhookStatus @default(NONE) // NONE | ACTIVE | FAILED + + // Single source of truth for automation behavior. New connects default to the + // safe REVIEW_ONLY mode — review in the dashboard, never write to GitHub until + // the user opts in. Derived into autoReview/autoPost/postEvent at the edges. + automationMode AutomationMode @default(REVIEW_ONLY) + + // Legacy flags — kept during Phase A for backfill + DB-side consistency. + // Phase B (follow-up) drops these + the PostEvent enum once nothing reads them. + autoReviewEnabled Boolean @default(true) // run review on push? + autoPostEnabled Boolean @default(true) // post result back to PR? + postEvent PostEvent @default(COMMENT) // COMMENT | REQUEST_CHANGES + user User @relation(fields: [userId], references: [id], onDelete: Cascade) reviews Review[] } +enum WebhookStatus { + NONE + ACTIVE + FAILED +} + +enum PostEvent { + COMMENT + REQUEST_CHANGES +} + +enum AutomationMode { + OFF // do not auto-review on push + REVIEW_ONLY // review + save to dashboard; never write to GitHub + COMMENT // review + post non-blocking inline comments + REQUEST_CHANGES // review + post a blocking "request changes" review +} + model Review { id String @id @default(cuid()) repositoryId String diff --git a/src/app/(auth)/sign-in/page.tsx b/src/app/(auth)/sign-in/page.tsx index 840b7a4..bf796e1 100644 --- a/src/app/(auth)/sign-in/page.tsx +++ b/src/app/(auth)/sign-in/page.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { signIn, signOut } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; @@ -34,7 +33,6 @@ function SignInForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); - const [rememberMe, setRememberMe] = useState(false); const [loading, setLoading] = useState(false); const handleEmailSignIn = async (e: { preventDefault(): void }) => { @@ -42,7 +40,7 @@ function SignInForm() { setError(""); setLoading(true); - const result = await signIn.email({ email, password, rememberMe }); + const result = await signIn.email({ email, password, rememberMe: true }); if (result.error) { setError(result.error.message || "Invalid credentials. Please try again."); @@ -56,7 +54,15 @@ function SignInForm() { setError(""); setLoading(true); await signOut(); - await signIn.social({ provider: "github", callbackURL: callbackUrl }); + await signIn.social({ + provider: "github", + callbackURL: callbackUrl, + fetchOptions: { + body: { + rememberMe: true, + }, + }, + }); }; return ( @@ -217,25 +223,11 @@ function SignInForm() { /> -
- - 7-day session -
- {error && ( {error} @@ -263,7 +255,7 @@ function SignInForm() {

- + You can connect repositories and run your first review in under 2 minutes.

diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx index 5b9cf99..43dfea0 100644 --- a/src/app/(auth)/sign-up/page.tsx +++ b/src/app/(auth)/sign-up/page.tsx @@ -51,6 +51,11 @@ export default function SignUpPage() { name, email, password, + fetchOptions: { + body: { + rememberMe: true, + }, + }, }); if (result.error) { @@ -67,6 +72,11 @@ export default function SignUpPage() { await signIn.social({ provider: "github", callbackURL: "/repos", + fetchOptions: { + body: { + rememberMe: true, + }, + }, }); }; @@ -249,7 +259,7 @@ export default function SignUpPage() { {error} @@ -277,7 +287,7 @@ export default function SignUpPage() {

- + No credit card required. You can switch to GitHub auth anytime.

diff --git a/src/app/(dashboard)/analytics/page.tsx b/src/app/(dashboard)/analytics/page.tsx index dfc9eee..2ba56d0 100644 --- a/src/app/(dashboard)/analytics/page.tsx +++ b/src/app/(dashboard)/analytics/page.tsx @@ -6,6 +6,7 @@ import { trpc } from "@/lib/trpc"; import { AnimatedPage } from "@/components/ui/animated-page"; import { PageHeader } from "@/components/ui/page-header"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { StatsCards } from "@/components/analytics/stats-cards"; import { ReviewChart } from "@/components/analytics/review-chart"; import { RiskDistribution } from "@/components/analytics/risk-distribution"; @@ -88,7 +89,7 @@ export default function AnalyticsPage() {
{/* Overall Health */} -
+

Overall Health @@ -102,7 +103,7 @@ export default function AnalyticsPage() {

)} -
+ {/* Review Status Breakdown */} -
+

Review Status @@ -162,7 +163,7 @@ export default function AnalyticsPage() { ))}

)} -
+ diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 2530224..bd34666 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -23,9 +23,9 @@ export default async function DashboardLayout({ } return ( -
+
{/* Background effects */} -
+
@@ -33,8 +33,14 @@ export default async function DashboardLayout({
-
- {children} + {/* The single app-level scroll region */} +
+
+ {children} +
); diff --git a/src/app/(dashboard)/repos/[id]/page.tsx b/src/app/(dashboard)/repos/[id]/page.tsx index 8d6b10f..7bb7a97 100644 --- a/src/app/(dashboard)/repos/[id]/page.tsx +++ b/src/app/(dashboard)/repos/[id]/page.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { AnimatedPage } from "@/components/ui/animated-page"; import { AnimatedList, AnimatedListItem } from "@/components/ui/animated-list"; import { AnimatedTabs } from "@/components/ui/animated-tabs"; @@ -28,10 +29,10 @@ function ReviewStatusBadge({ status }: { status: string | null }) { if (!status) return null; const configs: Record = { - COMPLETED: { dot: "success", label: "Reviewed", className: "bg-emerald-500/10 text-emerald-500 ring-emerald-500/20" }, - PROCESSING: { dot: "processing", label: "Processing", className: "bg-blue-500/10 text-blue-500 ring-blue-500/20" }, - PENDING: { dot: "warning", label: "Pending", className: "bg-amber-500/10 text-amber-500 ring-amber-500/20" }, - FAILED: { dot: "error", label: "Failed", className: "bg-red-500/10 text-red-500 ring-red-500/20" }, + COMPLETED: { dot: "success", label: "Reviewed", className: "bg-success/10 text-success ring-success/20" }, + PROCESSING: { dot: "processing", label: "Processing", className: "bg-info/10 text-info ring-info/20" }, + PENDING: { dot: "warning", label: "Pending", className: "bg-warning/10 text-warning ring-warning/20" }, + FAILED: { dot: "error", label: "Failed", className: "bg-danger/10 text-danger ring-danger/20" }, CANCELLED: { dot: "info", label: "Cancelled", className: "bg-muted text-muted-foreground ring-border" }, }; @@ -47,9 +48,9 @@ function ReviewStatusBadge({ status }: { status: string | null }) { } function PRIcon({ state, mergedAt }: { state: string; mergedAt: string | null }) { - if (mergedAt !== null) return ; - if (state === "open") return ; - return ; + if (mergedAt !== null) return ; + if (state === "open") return ; + return ; } function PullRequestCard({ @@ -111,8 +112,8 @@ function PullRequestCard({ {pr.headRef} - +{pr.additions} - -{pr.deletions} + +{pr.additions} + -{pr.deletions} {pr.changedFiles} @@ -203,7 +204,7 @@ export default function RepositoryDetailPage({
{/* Repo header */} -
+
@@ -225,7 +226,7 @@ export default function RepositoryDetailPage({
-
+ {/* Tabs + PR List */}
diff --git a/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx b/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx index 2886b27..a1f940f 100644 --- a/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx +++ b/src/app/(dashboard)/repos/[id]/pr/[prNumber]/compare/page.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { AnimatedPage } from "@/components/ui/animated-page"; import { AnimatedList, AnimatedListItem } from "@/components/ui/animated-list"; import { AnimatedTabs } from "@/components/ui/animated-tabs"; @@ -143,8 +144,8 @@ export default function ReviewComparePage({ const tabs = [ { id: "all" as const, label: "All", count: comparison ? comparison.fixed.count + comparison.new.count + comparison.unchanged.count : 0 }, - { id: "fixed" as const, label: "Fixed", icon: , count: comparison?.fixed.count }, - { id: "new" as const, label: "New", icon: , count: comparison?.new.count }, + { id: "fixed" as const, label: "Fixed", icon: , count: comparison?.fixed.count }, + { id: "new" as const, label: "New", icon: , count: comparison?.new.count }, { id: "unchanged" as const, label: "Unchanged", icon: , count: comparison?.unchanged.count }, ]; @@ -162,7 +163,7 @@ export default function ReviewComparePage({
{/* Header */} -
+

Review Comparison #{pr.number} @@ -205,10 +206,10 @@ export default function ReviewComparePage({

-
+ {effectiveBaselineId === effectiveCurrentId && effectiveBaselineId && ( -
+
Please select two different reviews to compare.
)} @@ -225,13 +226,13 @@ export default function ReviewComparePage({ >
{scoreDelta < 0 ? ( - + ) : scoreDelta > 0 ? ( - + ) : ( )} - 0 ? "text-red-500" : "text-muted-foreground"}`}> + 0 ? "text-danger" : "text-muted-foreground"}`}> {scoreDelta > 0 ? "+" : ""}{scoreDelta}
@@ -242,9 +243,9 @@ export default function ReviewComparePage({ initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.15 }} - className="rounded-xl border border-emerald-500/20 bg-emerald-500/5 p-4 text-center" + className="rounded-xl border border-success/20 bg-success/5 p-4 text-center" > - {comparison.fixed.count} + {comparison.fixed.count}

Fixed

@@ -252,9 +253,9 @@ export default function ReviewComparePage({ initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} - className="rounded-xl border border-amber-500/20 bg-amber-500/5 p-4 text-center" + className="rounded-xl border border-warning/20 bg-warning/5 p-4 text-center" > - {comparison.new.count} + {comparison.new.count}

New Issues

diff --git a/src/app/(dashboard)/repos/[id]/pr/[prNumber]/page.tsx b/src/app/(dashboard)/repos/[id]/pr/[prNumber]/page.tsx index 3cc3599..dd70951 100644 --- a/src/app/(dashboard)/repos/[id]/pr/[prNumber]/page.tsx +++ b/src/app/(dashboard)/repos/[id]/pr/[prNumber]/page.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; import { AnimatedPage } from "@/components/ui/animated-page"; import { AnimatedTabs } from "@/components/ui/animated-tabs"; import { StatusDot } from "@/components/ui/status-dot"; @@ -33,7 +34,7 @@ import { function PRStateBadge({ state, merged }: { state: string; merged: boolean }) { if (merged) { return ( - + Merged @@ -41,14 +42,14 @@ function PRStateBadge({ state, merged }: { state: string; merged: boolean }) { } if (state === "open") { return ( - + Open ); } return ( - + Closed @@ -172,7 +173,7 @@ export default function PullRequestDetailPage({
{/* PR Header */} -
+
@@ -224,12 +225,12 @@ export default function PullRequestDetailPage({ {/* Stats row */}
- - {pr.additions} + + {pr.additions}
- - {pr.deletions} + + {pr.deletions}
@@ -287,7 +288,7 @@ export default function PullRequestDetailPage({ )}
-
+ {/* Tabs */} + triggerMutation.mutate({ repositoryId: id, prNumber: prNumberInt }) + } + isRetrying={triggerMutation.isPending} /> ) : ( {repo.language} )} {repo.private ? ( - + Private @@ -96,15 +102,108 @@ function RepoSelectItem({ ); } +type AutomationMode = "OFF" | "REVIEW_ONLY" | "COMMENT" | "REQUEST_CHANGES"; + +type ConnectedRepo = { + id: string; + name: string; + fullName: string; + private: boolean; + webhookStatus: "NONE" | "ACTIVE" | "FAILED"; + automationMode: AutomationMode; +}; + +// Single, explicit choice per repo. The first two never write to GitHub. +const AUTOMATION_OPTIONS: { + value: AutomationMode; + label: string; + description: string; +}[] = [ + { + value: "OFF", + label: "Off", + description: "No auto-review on push.", + }, + { + value: "REVIEW_ONLY", + label: "Review only", + description: "Review in the dashboard — nothing posted to GitHub.", + }, + { + value: "COMMENT", + label: "Comment on PR", + description: "Review + non-blocking inline comments on the PR.", + }, + { + value: "REQUEST_CHANGES", + label: "Request changes", + description: "Review + a blocking “request changes” review.", + }, +]; + +function WebhookStatusBadge({ status }: { status: ConnectedRepo["webhookStatus"] }) { + if (status === "ACTIVE") { + return ( + + + Webhook active + + ); + } + if (status === "FAILED") { + return ( + + + Webhook failed + + ); + } + return ( + + + Webhook not set + + ); +} + function ConnectedRepoCard({ repo, onDisconnect, isDisconnecting, }: { - repo: { id: string; name: string; fullName: string; private: boolean }; + repo: ConnectedRepo; onDisconnect: () => void; isDisconnecting: boolean; }) { + const utils = trpc.useUtils(); + + // Local mirror of automation state so the selection feels instant; the server + // is the source of truth and a refetch reconciles after each mutation. + const [mode, setMode] = useState(repo.automationMode); + + const setAutomation = trpc.repository.setAutomation.useMutation({ + onSuccess: () => utils.repository.list.invalidate(), + onError: (error) => { + toast.error(error.message); + // Revert optimistic state to whatever the server last told us. + setMode(repo.automationMode); + }, + }); + + const selectMode = (next: AutomationMode) => { + if (next === mode) return; + setMode(next); + setAutomation.mutate({ id: repo.id, automationMode: next }); + }; + + const reconnect = trpc.repository.reconnectWebhook.useMutation({ + onSuccess: () => { + utils.repository.list.invalidate(); + toast.success("Webhook connected"); + }, + onError: (error) => toast.error(error.message), + }); + return ( Disconnect repository? - This will remove {repo.fullName} and all its - review data. This action cannot be undone. + This will remove {repo.fullName}, its webhook on + GitHub, and all its review data. This action cannot be undone. @@ -158,18 +257,96 @@ function ConnectedRepoCard({
-
+
{repo.private ? ( - + Private ) : ( - + Public )} + +
+ + {/* Automation controls */} +
+ {repo.webhookStatus !== "ACTIVE" && ( +
+

+ {repo.webhookStatus === "FAILED" + ? "Webhook setup failed. Reviews won't run on push." + : "No webhook yet. Set one up to auto-review pushes."} +

+ +
+ )} + +
+

+ Automation +

+ {AUTOMATION_OPTIONS.map((option) => { + const active = mode === option.value; + return ( + + ); + })} +
@@ -202,12 +379,19 @@ export default function ReposPage() { enabled: showImport, }); const connectMutation = trpc.repository.connect.useMutation({ - onSuccess: () => { + onSuccess: (data) => { reposQuery.refetch(); githubQuery.refetch(); setSelected(new Set()); setShowImport(false); - toast.success("Repositories connected successfully"); + const failed = data.results.filter((r) => r.webhookStatus === "FAILED"); + if (failed.length > 0) { + toast.warning( + `Connected, but webhook setup failed for ${failed.length} repo(s). Use "Reconnect" on the repo card.`, + ); + } else { + toast.success("Repositories connected successfully"); + } }, onError: (error) => { toast.error(error.message); @@ -307,7 +491,7 @@ export default function ReposPage() { transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }} className="overflow-hidden" > -
+ {isGithubNotLinked ? ( ) : ( @@ -387,7 +571,7 @@ export default function ReposPage() { )} )} -
+ )} diff --git a/src/app/(dashboard)/reviews/page.tsx b/src/app/(dashboard)/reviews/page.tsx index 5a3817d..3218d6c 100644 --- a/src/app/(dashboard)/reviews/page.tsx +++ b/src/app/(dashboard)/reviews/page.tsx @@ -42,17 +42,17 @@ function formatRelativeTime(date: string | Date): string { } function getRiskConfig(score: number) { - if (score <= 30) return { label: "Low", className: "bg-emerald-500/10 text-emerald-500 ring-emerald-500/20" }; - if (score <= 60) return { label: "Medium", className: "bg-amber-500/10 text-amber-500 ring-amber-500/20" }; - return { label: "High", className: "bg-red-500/10 text-red-500 ring-red-500/20" }; + if (score <= 30) return { label: "Low", className: "bg-success/10 text-success ring-success/20" }; + if (score <= 60) return { label: "Medium", className: "bg-warning/10 text-warning ring-warning/20" }; + return { label: "High", className: "bg-danger/10 text-danger ring-danger/20" }; } function getStatusConfig(status: string): { icon: React.ReactNode; dot: "success" | "processing" | "warning" | "error" | "info"; label: string } { const configs: Record = { - COMPLETED: { icon: , dot: "success", label: "Completed" }, - PROCESSING: { icon: , dot: "processing", label: "Processing" }, - PENDING: { icon: , dot: "warning", label: "Pending" }, - FAILED: { icon: , dot: "error", label: "Failed" }, + COMPLETED: { icon: , dot: "success", label: "Completed" }, + PROCESSING: { icon: , dot: "processing", label: "Processing" }, + PENDING: { icon: , dot: "warning", label: "Pending" }, + FAILED: { icon: , dot: "error", label: "Failed" }, CANCELLED: { icon: , dot: "info", label: "Cancelled" }, }; return configs[status] || configs.PENDING; @@ -129,7 +129,7 @@ function ReviewCard({ +
+ )} + +
+ + setApiKey(e.target.value)} + /> + {activeProvider && ( + + Get a {activeProvider.label} key + + + )} +
+ + + + + {/* Custom instructions */} + + + + + + Custom Instructions + + + House rules injected into every review prompt (e.g. "we use 2-space indent, focus on security"). + + + +