A Val Town app for comparing LLM model outputs side-by-side. Users enter a prompt, select 1-4 models from the OpenCode Zen catalog, and see responses stream in parallel columns. Supports multi-turn conversations where each model maintains its own conversation history.
backend/index.ts — Hono app entry point, serves frontend, mounts route
modulesbackend/routes/auth.ts — Password auth via PROMPTCOMPARE_PASSWORD env var,
sets HttpOnly session cookiebackend/routes/api.ts — REST endpoints for conversations + streaming chat
proxy to OpenCode Zenbackend/database/migrations.ts — SQLite schema (val-scoped via
std/sqlite/main.ts)backend/database/queries.ts — Typed query functionsfrontend/index.html — Shell with CDN imports (React 18.2.0 via esm.sh,
Tailwind via twind)frontend/index.tsx — App entry, routing (login vs main)frontend/components/ — Chat UI components inspired by ai-elements patternsshared/types.ts — TypeScript interfaces shared between frontend and backendconversations (
id TEXT PRIMARY KEY,
title TEXT,
model_ids TEXT NOT NULL, -- JSON array of model IDs
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
messages (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'user'
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
)
responses (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL,
model_id TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (message_id) REFERENCES messages(id)
)
POST /api/auth — Login with password, returns session cookieGET /api/auth/check — Check if session is validPOST /api/auth/logout — Clear session cookieGET /api/conversations — List conversationsGET /api/conversations/:id — Get conversation with messages and responsesPOST /api/conversations — Create new conversation (title, model_ids)DELETE /api/conversations/:id — Delete conversationPOST /api/chat — Send a message, streams responses from selected models via
SSEGET /api/models — Return non-deprecated models from models.jsonWhen a user sends a message:
message row and a response row per modelSSE event format:
event: chunk
data: {"model_id": "claude-sonnet-4-6", "content": "Hello"}
event: done
data: {"model_id": "claude-sonnet-4-6"}
event: error
data: {"model_id": "claude-sonnet-4-6", "error": "..."}
PROMPTCOMPARE_PASSWORDPOST /api/auth with { password } body/api/* routes (except auth) check cookie via middleware+--sidebar--+------------------main-------------------+
| [New Chat] | Model A | Model B | Model C |
| | +-response--+ +-response-+ +-------+ |
| Conv 1 | | streaming | | streaming| | ... | |
| Conv 2 | | markdown | | markdown | | | |
| Conv 3 | +-----------+ +----------+ +-------+ |
| | |
| | +--prompt input bar------------------+ |
| | | Type a message... [Send] | |
| | +------------------------------------+ |
+------------+------------------------------------------+
models.json, filtered to exclude status: "deprecated"std/sqlite/main.ts) for persistence