profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM

Commit

project setup and tooling

commit 7f04a77

13 changed files with +7613 and −0

Jump to a changed file
  1. .env.example +47 −0
  2. .eslintrc.json +21 −0
  3. .gitignore +39 −0
  4. .prettierignore +8 −0
  5. .prettierrc +8 −0
  6. components.json +21 −0
  7. next.config.mjs +19 −0
  8. package.json +84 −0
  9. pnpm-lock.yaml +7211 −0
  10. postcss.config.mjs +6 −0
  11. tailwind.config.ts +92 −0
  12. tsconfig.json +30 −0
  13. 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 +});