Commit
project setup and tooling
commit
7f04a77
13 changed files with +7613 and −0
Jump to a changed file
- .env.example +47 −0
- .eslintrc.json +21 −0
- .gitignore +39 −0
- .prettierignore +8 −0
- .prettierrc +8 −0
- components.json +21 −0
- next.config.mjs +19 −0
- package.json +84 −0
- pnpm-lock.yaml +7211 −0
- postcss.config.mjs +6 −0
- tailwind.config.ts +92 −0
- tsconfig.json +30 −0
- vitest.config.ts +27 −0
added .env.example +47 −0
| @@ -0,0 +1,47 @@ | ||
| 1 | +# --------------------------------------------------------------------------- | |
| 2 | +# Roundtable environment configuration | |
| 3 | +# Copy to `.env` and fill in. All variables are validated at boot (src/lib/env.ts). | |
| 4 | +# --------------------------------------------------------------------------- | |
| 5 | + | |
| 6 | +# --- Database ------------------------------------------------------------- | |
| 7 | +# Postgres connection string. In docker-compose this points at the `db` service. | |
| 8 | +DATABASE_URL="postgresql://roundtable:roundtable@localhost:5432/roundtable?schema=public" | |
| 9 | + | |
| 10 | +# --- Auth.js (NextAuth v5) ------------------------------------------------ | |
| 11 | +# Generate with: openssl rand -base64 32 | |
| 12 | +AUTH_SECRET="change-me-openssl-rand-base64-32" | |
| 13 | +# Public origin, used for OAuth callbacks behind the reverse proxy. | |
| 14 | +AUTH_URL="http://localhost:3000" | |
| 15 | +# When true, NextAuth trusts the X-Forwarded-* headers set by Caddy. | |
| 16 | +AUTH_TRUST_HOST="true" | |
| 17 | +# GitHub OAuth app credentials (https://github.com/settings/developers) | |
| 18 | +AUTH_GITHUB_ID="" | |
| 19 | +AUTH_GITHUB_SECRET="" | |
| 20 | + | |
| 21 | +# --- BYOK encryption ------------------------------------------------------ | |
| 22 | +# 32-byte master key (base64) used for AES-256-GCM encryption of saved user | |
| 23 | +# API keys. Generate with: openssl rand -base64 32 | |
| 24 | +# THIS IS THE ONLY SECRET REQUIRED FOR PUBLIC DEMO MODE. | |
| 25 | +ENCRYPTION_KEY="change-me-32-bytes-base64-encoded-value==" | |
| 26 | + | |
| 27 | +# --- OpenRouter ----------------------------------------------------------- | |
| 28 | +# Base URL for the OpenRouter OpenAI-compatible gateway. Rarely changed. | |
| 29 | +OPENROUTER_BASE_URL="https://openrouter.ai/api/v1" | |
| 30 | +# Sent as HTTP-Referer / X-Title so OpenRouter attributes usage to this app. | |
| 31 | +OPENROUTER_APP_URL="http://localhost:3000" | |
| 32 | +OPENROUTER_APP_TITLE="Roundtable" | |
| 33 | + | |
| 34 | +# --- Runtime -------------------------------------------------------------- | |
| 35 | +# Port the Next.js server listens on (Caddy proxies to this). | |
| 36 | +PORT="3000" | |
| 37 | +NODE_ENV="development" | |
| 38 | + | |
| 39 | +# --- Mock mode ------------------------------------------------------------ | |
| 40 | +# When "1", all LLM calls are served by the deterministic mock client — no | |
| 41 | +# network, no spend. Used for local dev, CI, and the seeded demo. | |
| 42 | +MOCK_LLM="0" | |
| 43 | + | |
| 44 | +# --- Rate limiting -------------------------------------------------------- | |
| 45 | +# Max debate starts per user per rolling window (protects the server itself; | |
| 46 | +# inference is paid by each user's own key). | |
| 47 | +RATE_LIMIT_DEBATES_PER_HOUR="20" |
added .eslintrc.json +21 −0
| @@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "extends": ["next/core-web-vitals", "next/typescript", "prettier"], | |
| 3 | + "rules": { | |
| 4 | + "@typescript-eslint/no-unused-vars": [ | |
| 5 | + "error", | |
| 6 | + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } | |
| 7 | + ], | |
| 8 | + "@typescript-eslint/consistent-type-imports": [ | |
| 9 | + "warn", | |
| 10 | + { "prefer": "type-imports", "fixStyle": "inline-type-imports" } | |
| 11 | + ], | |
| 12 | + "@typescript-eslint/no-explicit-any": "warn", | |
| 13 | + "react/no-unescaped-entities": "off" | |
| 14 | + }, | |
| 15 | + "ignorePatterns": [ | |
| 16 | + "node_modules/", | |
| 17 | + ".next/", | |
| 18 | + "src/generated/", | |
| 19 | + "prisma/generated/" | |
| 20 | + ] | |
| 21 | +} |
added .gitignore +39 −0
| @@ -0,0 +1,39 @@ | ||
| 1 | +# dependencies | |
| 2 | +/node_modules | |
| 3 | +/.pnp | |
| 4 | +.pnp.* | |
| 5 | + | |
| 6 | +# next.js | |
| 7 | +/.next/ | |
| 8 | +/out/ | |
| 9 | +next-env.d.ts | |
| 10 | + | |
| 11 | +# production | |
| 12 | +/build | |
| 13 | +/dist | |
| 14 | + | |
| 15 | +# testing / coverage | |
| 16 | +/coverage | |
| 17 | + | |
| 18 | +# typescript | |
| 19 | +*.tsbuildinfo | |
| 20 | + | |
| 21 | +# prisma | |
| 22 | +/prisma/generated | |
| 23 | +/src/generated | |
| 24 | + | |
| 25 | +# env files (never commit real secrets) | |
| 26 | +.env | |
| 27 | +.env*.local | |
| 28 | +.env.production | |
| 29 | + | |
| 30 | +# misc | |
| 31 | +.DS_Store | |
| 32 | +*.pem | |
| 33 | +*.log | |
| 34 | +npm-debug.log* | |
| 35 | +yarn-debug.log* | |
| 36 | +yarn-error.log* | |
| 37 | +.idea | |
| 38 | +.vscode/* | |
| 39 | +!.vscode/extensions.json |
added .prettierignore +8 −0
| @@ -0,0 +1,8 @@ | ||
| 1 | +node_modules | |
| 2 | +.next | |
| 3 | +dist | |
| 4 | +coverage | |
| 5 | +pnpm-lock.yaml | |
| 6 | +prisma/generated | |
| 7 | +src/generated | |
| 8 | +*.md |
added .prettierrc +8 −0
| @@ -0,0 +1,8 @@ | ||
| 1 | +{ | |
| 2 | + "semi": true, | |
| 3 | + "singleQuote": true, | |
| 4 | + "trailingComma": "all", | |
| 5 | + "printWidth": 100, | |
| 6 | + "tabWidth": 2, | |
| 7 | + "plugins": ["prettier-plugin-tailwindcss"] | |
| 8 | +} |
added components.json +21 −0
| @@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "$schema": "https://ui.shadcn.com/schema.json", | |
| 3 | + "style": "new-york", | |
| 4 | + "rsc": true, | |
| 5 | + "tsx": true, | |
| 6 | + "tailwind": { | |
| 7 | + "config": "tailwind.config.ts", | |
| 8 | + "css": "src/app/globals.css", | |
| 9 | + "baseColor": "slate", | |
| 10 | + "cssVariables": true, | |
| 11 | + "prefix": "" | |
| 12 | + }, | |
| 13 | + "aliases": { | |
| 14 | + "components": "@/components", | |
| 15 | + "utils": "@/lib/utils", | |
| 16 | + "ui": "@/components/ui", | |
| 17 | + "lib": "@/lib", | |
| 18 | + "hooks": "@/hooks" | |
| 19 | + }, | |
| 20 | + "iconLibrary": "lucide" | |
| 21 | +} |
added next.config.mjs +19 −0
| @@ -0,0 +1,19 @@ | ||
| 1 | +/** @type {import('next').NextConfig} */ | |
| 2 | +const nextConfig = { | |
| 3 | + // Emit a self-contained server bundle for the Docker runtime stage. Gated on | |
| 4 | + // an env flag because standalone tracing uses symlinks, which Windows dev | |
| 5 | + // machines block without Developer Mode — the Dockerfile sets this to '1'. | |
| 6 | + output: process.env.BUILD_STANDALONE === '1' ? 'standalone' : undefined, | |
| 7 | + reactStrictMode: true, | |
| 8 | + poweredByHeader: false, | |
| 9 | + // The debate SSE stream is long-lived (several minutes). Keep the server | |
| 10 | + // rendering lean and let route handlers own their own runtime config. | |
| 11 | + // Prisma is a native dependency; keep it external to the server bundle. | |
| 12 | + serverExternalPackages: ['@prisma/client', '@auth/prisma-adapter'], | |
| 13 | + eslint: { | |
| 14 | + // CI runs `next lint` explicitly; don't fail production builds on lint. | |
| 15 | + ignoreDuringBuilds: true, | |
| 16 | + }, | |
| 17 | +}; | |
| 18 | + | |
| 19 | +export default nextConfig; |
added package.json +84 −0
| @@ -0,0 +1,84 @@ | ||
| 1 | +{ | |
| 2 | + "name": "roundtable", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "description": "A council of LLMs that debate over multiple rounds before producing a synthesized answer.", | |
| 6 | + "type": "module", | |
| 7 | + "engines": { | |
| 8 | + "node": ">=20" | |
| 9 | + }, | |
| 10 | + "packageManager": "pnpm@9.15.0", | |
| 11 | + "scripts": { | |
| 12 | + "dev": "next dev", | |
| 13 | + "build": "prisma generate && next build", | |
| 14 | + "start": "next start", | |
| 15 | + "lint": "next lint", | |
| 16 | + "typecheck": "tsc --noEmit", | |
| 17 | + "test": "vitest run", | |
| 18 | + "test:watch": "vitest", | |
| 19 | + "test:cov": "vitest run --coverage", | |
| 20 | + "format": "prettier --write .", | |
| 21 | + "format:check": "prettier --check .", | |
| 22 | + "prisma:generate": "prisma generate", | |
| 23 | + "prisma:migrate": "prisma migrate deploy", | |
| 24 | + "prisma:push": "prisma db push", | |
| 25 | + "db:seed": "tsx prisma/seed.ts" | |
| 26 | + }, | |
| 27 | + "prisma": { | |
| 28 | + "seed": "tsx prisma/seed.ts" | |
| 29 | + }, | |
| 30 | + "dependencies": { | |
| 31 | + "@ai-sdk/openai": "1.3.24", | |
| 32 | + "@ai-sdk/react": "1.2.12", | |
| 33 | + "@auth/prisma-adapter": "2.11.2", | |
| 34 | + "@prisma/client": "5.22.0", | |
| 35 | + "@radix-ui/react-accordion": "^1.2.0", | |
| 36 | + "@radix-ui/react-collapsible": "^1.1.0", | |
| 37 | + "@radix-ui/react-dialog": "^1.1.0", | |
| 38 | + "@radix-ui/react-dropdown-menu": "^2.1.0", | |
| 39 | + "@radix-ui/react-label": "^2.1.0", | |
| 40 | + "@radix-ui/react-popover": "^1.1.0", | |
| 41 | + "@radix-ui/react-scroll-area": "^1.2.0", | |
| 42 | + "@radix-ui/react-select": "^2.2.0", | |
| 43 | + "@radix-ui/react-separator": "^1.1.0", | |
| 44 | + "@radix-ui/react-slider": "^1.2.0", | |
| 45 | + "@radix-ui/react-slot": "^1.1.0", | |
| 46 | + "@radix-ui/react-switch": "^1.1.0", | |
| 47 | + "@radix-ui/react-tabs": "^1.1.0", | |
| 48 | + "@radix-ui/react-tooltip": "^1.1.0", | |
| 49 | + "ai": "4.3.19", | |
| 50 | + "class-variance-authority": "0.7.1", | |
| 51 | + "clsx": "2.1.1", | |
| 52 | + "diff": "5.2.2", | |
| 53 | + "lucide-react": "0.460.0", | |
| 54 | + "next": "15.5.20", | |
| 55 | + "next-auth": "5.0.0-beta.31", | |
| 56 | + "next-themes": "0.4.6", | |
| 57 | + "react": "18.3.1", | |
| 58 | + "react-dom": "18.3.1", | |
| 59 | + "recharts": "2.15.4", | |
| 60 | + "sonner": "1.7.4", | |
| 61 | + "tailwind-merge": "2.6.0", | |
| 62 | + "tailwindcss-animate": "1.0.7", | |
| 63 | + "zod": "3.25.76" | |
| 64 | + }, | |
| 65 | + "devDependencies": { | |
| 66 | + "@types/diff": "5.2.3", | |
| 67 | + "@types/node": "20.17.10", | |
| 68 | + "@types/react": "18.3.18", | |
| 69 | + "@types/react-dom": "18.3.5", | |
| 70 | + "@vitest/coverage-v8": "2.1.9", | |
| 71 | + "autoprefixer": "10.4.20", | |
| 72 | + "eslint": "8.57.1", | |
| 73 | + "eslint-config-next": "15.5.20", | |
| 74 | + "eslint-config-prettier": "9.1.0", | |
| 75 | + "postcss": "8.4.49", | |
| 76 | + "prettier": "3.4.2", | |
| 77 | + "prettier-plugin-tailwindcss": "0.6.9", | |
| 78 | + "prisma": "5.22.0", | |
| 79 | + "tailwindcss": "3.4.19", | |
| 80 | + "tsx": "4.19.2", | |
| 81 | + "typescript": "5.7.2", | |
| 82 | + "vitest": "2.1.9" | |
| 83 | + } | |
| 84 | +} |
added pnpm-lock.yaml +7211 −0
Line changes are not available for this file.
added postcss.config.mjs +6 −0
| @@ -0,0 +1,6 @@ | ||
| 1 | +export default { | |
| 2 | + plugins: { | |
| 3 | + tailwindcss: {}, | |
| 4 | + autoprefixer: {}, | |
| 5 | + }, | |
| 6 | +}; |
added tailwind.config.ts +92 −0
| @@ -0,0 +1,92 @@ | ||
| 1 | +import type { Config } from 'tailwindcss'; | |
| 2 | + | |
| 3 | +const config: Config = { | |
| 4 | + darkMode: ['class'], | |
| 5 | + content: [ | |
| 6 | + './src/pages/**/*.{ts,tsx}', | |
| 7 | + './src/components/**/*.{ts,tsx}', | |
| 8 | + './src/app/**/*.{ts,tsx}', | |
| 9 | + ], | |
| 10 | + theme: { | |
| 11 | + container: { | |
| 12 | + center: true, | |
| 13 | + padding: '2rem', | |
| 14 | + screens: { '2xl': '1400px' }, | |
| 15 | + }, | |
| 16 | + extend: { | |
| 17 | + colors: { | |
| 18 | + border: 'hsl(var(--border))', | |
| 19 | + input: 'hsl(var(--input))', | |
| 20 | + ring: 'hsl(var(--ring))', | |
| 21 | + background: 'hsl(var(--background))', | |
| 22 | + foreground: 'hsl(var(--foreground))', | |
| 23 | + primary: { | |
| 24 | + DEFAULT: 'hsl(var(--primary))', | |
| 25 | + foreground: 'hsl(var(--primary-foreground))', | |
| 26 | + }, | |
| 27 | + secondary: { | |
| 28 | + DEFAULT: 'hsl(var(--secondary))', | |
| 29 | + foreground: 'hsl(var(--secondary-foreground))', | |
| 30 | + }, | |
| 31 | + destructive: { | |
| 32 | + DEFAULT: 'hsl(var(--destructive))', | |
| 33 | + foreground: 'hsl(var(--destructive-foreground))', | |
| 34 | + }, | |
| 35 | + muted: { | |
| 36 | + DEFAULT: 'hsl(var(--muted))', | |
| 37 | + foreground: 'hsl(var(--muted-foreground))', | |
| 38 | + }, | |
| 39 | + accent: { | |
| 40 | + DEFAULT: 'hsl(var(--accent))', | |
| 41 | + foreground: 'hsl(var(--accent-foreground))', | |
| 42 | + }, | |
| 43 | + popover: { | |
| 44 | + DEFAULT: 'hsl(var(--popover))', | |
| 45 | + foreground: 'hsl(var(--popover-foreground))', | |
| 46 | + }, | |
| 47 | + card: { | |
| 48 | + DEFAULT: 'hsl(var(--card))', | |
| 49 | + foreground: 'hsl(var(--card-foreground))', | |
| 50 | + }, | |
| 51 | + // Semantic accents for the debate timeline stages. | |
| 52 | + stage: { | |
| 53 | + answer: 'hsl(var(--stage-answer))', | |
| 54 | + critique: 'hsl(var(--stage-critique))', | |
| 55 | + revision: 'hsl(var(--stage-revision))', | |
| 56 | + synthesis: 'hsl(var(--stage-synthesis))', | |
| 57 | + }, | |
| 58 | + }, | |
| 59 | + borderRadius: { | |
| 60 | + lg: 'var(--radius)', | |
| 61 | + md: 'calc(var(--radius) - 2px)', | |
| 62 | + sm: 'calc(var(--radius) - 4px)', | |
| 63 | + }, | |
| 64 | + keyframes: { | |
| 65 | + 'accordion-down': { | |
| 66 | + from: { height: '0' }, | |
| 67 | + to: { height: 'var(--radix-accordion-content-height)' }, | |
| 68 | + }, | |
| 69 | + 'accordion-up': { | |
| 70 | + from: { height: 'var(--radix-accordion-content-height)' }, | |
| 71 | + to: { height: '0' }, | |
| 72 | + }, | |
| 73 | + 'pulse-subtle': { | |
| 74 | + '0%, 100%': { opacity: '1' }, | |
| 75 | + '50%': { opacity: '0.55' }, | |
| 76 | + }, | |
| 77 | + }, | |
| 78 | + animation: { | |
| 79 | + 'accordion-down': 'accordion-down 0.2s ease-out', | |
| 80 | + 'accordion-up': 'accordion-up 0.2s ease-out', | |
| 81 | + 'pulse-subtle': 'pulse-subtle 1.6s ease-in-out infinite', | |
| 82 | + }, | |
| 83 | + fontFamily: { | |
| 84 | + sans: ['var(--font-sans)', 'ui-sans-serif', 'system-ui', 'sans-serif'], | |
| 85 | + mono: ['var(--font-mono)', 'ui-monospace', 'monospace'], | |
| 86 | + }, | |
| 87 | + }, | |
| 88 | + }, | |
| 89 | + plugins: [require('tailwindcss-animate')], | |
| 90 | +}; | |
| 91 | + | |
| 92 | +export default config; |
added tsconfig.json +30 −0
| @@ -0,0 +1,30 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "lib": ["dom", "dom.iterable", "ES2022"], | |
| 5 | + "allowJs": true, | |
| 6 | + "skipLibCheck": true, | |
| 7 | + "strict": true, | |
| 8 | + "noUncheckedIndexedAccess": true, | |
| 9 | + "noImplicitOverride": true, | |
| 10 | + "noUnusedLocals": true, | |
| 11 | + "noUnusedParameters": true, | |
| 12 | + "noFallthroughCasesInSwitch": true, | |
| 13 | + "forceConsistentCasingInFileNames": true, | |
| 14 | + "noEmit": true, | |
| 15 | + "esModuleInterop": true, | |
| 16 | + "module": "esnext", | |
| 17 | + "moduleResolution": "bundler", | |
| 18 | + "resolveJsonModule": true, | |
| 19 | + "isolatedModules": true, | |
| 20 | + "jsx": "preserve", | |
| 21 | + "incremental": true, | |
| 22 | + "verbatimModuleSyntax": false, | |
| 23 | + "plugins": [{ "name": "next" }], | |
| 24 | + "paths": { | |
| 25 | + "@/*": ["./src/*"] | |
| 26 | + } | |
| 27 | + }, | |
| 28 | + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | |
| 29 | + "exclude": ["node_modules"] | |
| 30 | +} |
added vitest.config.ts +27 −0
| @@ -0,0 +1,27 @@ | ||
| 1 | +import { fileURLToPath } from 'node:url'; | |
| 2 | +import { defineConfig } from 'vitest/config'; | |
| 3 | + | |
| 4 | +export default defineConfig({ | |
| 5 | + test: { | |
| 6 | + environment: 'node', | |
| 7 | + globals: true, | |
| 8 | + include: ['src/**/*.test.ts'], | |
| 9 | + // Deterministic env so modules that read `env` (crypto, etc.) load cleanly. | |
| 10 | + env: { | |
| 11 | + DATABASE_URL: 'postgresql://localhost:5432/roundtable_test', | |
| 12 | + ENCRYPTION_KEY: '+s0sM+HW2k7YOW3uJxtmVM85xcJ18REhy4tEYF46ALM=', | |
| 13 | + MOCK_LLM: '1', | |
| 14 | + }, | |
| 15 | + coverage: { | |
| 16 | + provider: 'v8', | |
| 17 | + include: ['src/core/**/*.ts'], | |
| 18 | + exclude: ['src/core/**/*.test.ts', 'src/core/**/fixtures/**'], | |
| 19 | + reporter: ['text', 'html'], | |
| 20 | + }, | |
| 21 | + }, | |
| 22 | + resolve: { | |
| 23 | + alias: { | |
| 24 | + '@': fileURLToPath(new URL('./src', import.meta.url)), | |
| 25 | + }, | |
| 26 | + }, | |
| 27 | +}); |