@darkrishabh/bench-ai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +333 -0
  2. package/dist/cli/app.d.ts +11 -0
  3. package/dist/cli/app.d.ts.map +1 -0
  4. package/dist/cli/app.js +48 -0
  5. package/dist/cli/app.js.map +1 -0
  6. package/dist/cli/components/DiffView.d.ts +5 -0
  7. package/dist/cli/components/DiffView.d.ts.map +1 -0
  8. package/dist/cli/components/DiffView.js +14 -0
  9. package/dist/cli/components/DiffView.js.map +1 -0
  10. package/dist/cli/components/EvalView.d.ts +6 -0
  11. package/dist/cli/components/EvalView.d.ts.map +1 -0
  12. package/dist/cli/components/EvalView.js +82 -0
  13. package/dist/cli/components/EvalView.js.map +1 -0
  14. package/dist/cli/components/Spinner.d.ts +4 -0
  15. package/dist/cli/components/Spinner.d.ts.map +1 -0
  16. package/dist/cli/components/Spinner.js +15 -0
  17. package/dist/cli/components/Spinner.js.map +1 -0
  18. package/dist/cli/index.d.ts +3 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +117 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/run-command.d.ts +11 -0
  23. package/dist/cli/run-command.d.ts.map +1 -0
  24. package/dist/cli/run-command.js +119 -0
  25. package/dist/cli/run-command.js.map +1 -0
  26. package/dist/engine/cost.d.ts +3 -0
  27. package/dist/engine/cost.d.ts.map +1 -0
  28. package/dist/engine/cost.js +52 -0
  29. package/dist/engine/cost.js.map +1 -0
  30. package/dist/engine/diff.d.ts +6 -0
  31. package/dist/engine/diff.d.ts.map +1 -0
  32. package/dist/engine/diff.js +43 -0
  33. package/dist/engine/diff.js.map +1 -0
  34. package/dist/engine/eval.d.ts +14 -0
  35. package/dist/engine/eval.d.ts.map +1 -0
  36. package/dist/engine/eval.js +194 -0
  37. package/dist/engine/eval.js.map +1 -0
  38. package/dist/engine/index.d.ts +15 -0
  39. package/dist/engine/index.d.ts.map +1 -0
  40. package/dist/engine/index.js +10 -0
  41. package/dist/engine/index.js.map +1 -0
  42. package/dist/engine/providers/base.d.ts +7 -0
  43. package/dist/engine/providers/base.d.ts.map +1 -0
  44. package/dist/engine/providers/base.js +2 -0
  45. package/dist/engine/providers/base.js.map +1 -0
  46. package/dist/engine/providers/claude.d.ts +15 -0
  47. package/dist/engine/providers/claude.d.ts.map +1 -0
  48. package/dist/engine/providers/claude.js +53 -0
  49. package/dist/engine/providers/claude.js.map +1 -0
  50. package/dist/engine/providers/minimax.d.ts +16 -0
  51. package/dist/engine/providers/minimax.d.ts.map +1 -0
  52. package/dist/engine/providers/minimax.js +67 -0
  53. package/dist/engine/providers/minimax.js.map +1 -0
  54. package/dist/engine/providers/ollama.d.ts +14 -0
  55. package/dist/engine/providers/ollama.d.ts.map +1 -0
  56. package/dist/engine/providers/ollama.js +60 -0
  57. package/dist/engine/providers/ollama.js.map +1 -0
  58. package/dist/engine/providers/openai-compatible.d.ts +19 -0
  59. package/dist/engine/providers/openai-compatible.d.ts.map +1 -0
  60. package/dist/engine/providers/openai-compatible.js +109 -0
  61. package/dist/engine/providers/openai-compatible.js.map +1 -0
  62. package/dist/engine/providers/subprocess.d.ts +55 -0
  63. package/dist/engine/providers/subprocess.d.ts.map +1 -0
  64. package/dist/engine/providers/subprocess.js +111 -0
  65. package/dist/engine/providers/subprocess.js.map +1 -0
  66. package/dist/engine/suite-loader.d.ts +11 -0
  67. package/dist/engine/suite-loader.d.ts.map +1 -0
  68. package/dist/engine/suite-loader.js +75 -0
  69. package/dist/engine/suite-loader.js.map +1 -0
  70. package/dist/engine/types.d.ts +104 -0
  71. package/dist/engine/types.d.ts.map +1 -0
  72. package/dist/engine/types.js +2 -0
  73. package/dist/engine/types.js.map +1 -0
  74. package/next-env.d.ts +6 -0
  75. package/next.config.ts +26 -0
  76. package/package.json +72 -0
  77. package/public/icon.svg +14 -0
  78. package/src/app/api/diff/route.ts +135 -0
  79. package/src/app/api/models/route.ts +96 -0
  80. package/src/app/api/suite/route.ts +314 -0
  81. package/src/app/globals.css +215 -0
  82. package/src/app/icon.svg +14 -0
  83. package/src/app/layout.tsx +44 -0
  84. package/src/app/opengraph-image.tsx +73 -0
  85. package/src/app/page.tsx +952 -0
  86. package/src/app/suite/layout.tsx +12 -0
  87. package/src/app/suite/page.tsx +206 -0
  88. package/src/app/twitter-image.tsx +1 -0
  89. package/src/components/BenchAiLogo.tsx +38 -0
  90. package/src/components/ComparePanel.tsx +643 -0
  91. package/src/components/ConfigPanel.tsx +809 -0
  92. package/src/components/MarkdownOutput.tsx +16 -0
  93. package/src/components/ModelResponseCard.tsx +313 -0
  94. package/src/components/QuickComparisonBar.tsx +184 -0
  95. package/src/components/ResponsesLineDiff.tsx +149 -0
  96. package/src/components/SettingsPanel.tsx +591 -0
  97. package/src/components/SuitePanel.tsx +875 -0
  98. package/src/lib/brand.ts +4 -0
  99. package/src/lib/config-yaml.ts +70 -0
  100. package/src/lib/consume-suite-sse.ts +70 -0
  101. package/src/lib/describe-judge.ts +23 -0
  102. package/src/lib/model-chip-palette.ts +9 -0
  103. package/src/lib/openai-model-list.ts +33 -0
  104. package/src/lib/provider-ui.ts +30 -0
  105. package/src/lib/resolve-credentials.ts +80 -0
  106. package/src/lib/run-history.ts +66 -0
  107. package/src/lib/simple-line-diff.ts +50 -0
  108. package/src/lib/storage.ts +100 -0
  109. package/src/lib/suite-judge-meta.ts +13 -0
  110. package/src/lib/suite-run-history.ts +81 -0
  111. package/src/types.ts +170 -0
  112. package/vercel.json +5 -0
@@ -0,0 +1,314 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ ClaudeProvider,
4
+ OllamaProvider,
5
+ MinimaxProvider,
6
+ OpenAICompatibleProvider,
7
+ createClaudeCLIProvider,
8
+ createCodexProvider,
9
+ parseSuiteConfig,
10
+ runSuite,
11
+ } from "@darkrishabh/bench-ai";
12
+ import type { SuiteConfig, SuiteResult, Provider } from "@darkrishabh/bench-ai";
13
+ import type { LLMInstance } from "@/types";
14
+ import { PRESET_BASE_URLS } from "@/types";
15
+ import type { SuiteJudgeMeta } from "@/lib/suite-judge-meta";
16
+
17
+ /** Long suite runs: raise on Vercel via plan limits (Pro supports up to 800s). */
18
+ export const maxDuration = 300;
19
+ export const dynamic = "force-dynamic";
20
+
21
+ function buildProvider(instance: LLMInstance): Provider | null {
22
+ try {
23
+ switch (instance.provider) {
24
+ case "claude": {
25
+ const apiKey = instance.apiKey?.trim() || process.env.ANTHROPIC_API_KEY;
26
+ if (!apiKey) return null;
27
+ return new ClaudeProvider(apiKey, instance.model, {
28
+ maxTokens: instance.maxTokens,
29
+ temperature: instance.temperature,
30
+ });
31
+ }
32
+ case "ollama":
33
+ return new OllamaProvider(
34
+ instance.baseUrl || process.env.OLLAMA_BASE_URL || "http://localhost:11434",
35
+ instance.model,
36
+ { temperature: instance.temperature }
37
+ );
38
+ case "minimax": {
39
+ const apiKey = instance.apiKey?.trim() || process.env.MINIMAX_API_KEY;
40
+ const groupId = instance.groupId?.trim() || process.env.MINIMAX_GROUP_ID;
41
+ if (!apiKey || !groupId) return null;
42
+ return new MinimaxProvider(apiKey, groupId, instance.model, {
43
+ maxTokens: instance.maxTokens,
44
+ temperature: instance.temperature,
45
+ });
46
+ }
47
+ case "claude-cli":
48
+ return createClaudeCLIProvider(instance.model, { timeoutMs: 120_000 });
49
+ case "codex":
50
+ return createCodexProvider(instance.model, { timeoutMs: 120_000 });
51
+ default: {
52
+ const apiKey = instance.apiKey?.trim() || process.env[`${instance.provider.toUpperCase().replace(/-/g, "_")}_API_KEY`] || "";
53
+ const baseUrl =
54
+ instance.baseUrl?.trim() ||
55
+ PRESET_BASE_URLS[instance.provider as keyof typeof PRESET_BASE_URLS] ||
56
+ "";
57
+ if (!baseUrl) return null;
58
+ return new OpenAICompatibleProvider(instance.provider, baseUrl, apiKey, instance.model, {
59
+ maxTokens: instance.maxTokens,
60
+ temperature: instance.temperature,
61
+ });
62
+ }
63
+ }
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function countLlmRubricAssertions(config: SuiteConfig): number {
70
+ let n = 0;
71
+ for (const t of config.tests) {
72
+ for (const a of t.assert ?? []) {
73
+ if (a.type === "llm-rubric") n++;
74
+ }
75
+ }
76
+ return n;
77
+ }
78
+
79
+ function buildSuiteJudgeMeta(
80
+ judgeBody: {
81
+ mode?: string;
82
+ ollamaBaseUrl?: string;
83
+ ollamaModel?: string;
84
+ } | undefined,
85
+ judgeProvider: Provider | undefined,
86
+ rubricCount: number
87
+ ): SuiteJudgeMeta {
88
+ const mode = judgeBody?.mode ?? "auto";
89
+
90
+ if (!judgeBody || mode === "none") {
91
+ return {
92
+ rubricAssertionCount: rubricCount,
93
+ willEvaluateRubrics: false,
94
+ judgeMode: mode,
95
+ judgeBackend: "off",
96
+ summary:
97
+ rubricCount > 0
98
+ ? `Judge is off (mode: none). ${rubricCount} llm-rubric assertion(s) did not call an LLM — enable a judge under Settings → Judge.`
99
+ : "No llm-rubric assertions in this suite; judge settings are unused for this run.",
100
+ };
101
+ }
102
+
103
+ if (mode === "ollama") {
104
+ const url = judgeBody.ollamaBaseUrl?.trim() || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
105
+ const model = judgeBody.ollamaModel?.trim() || "llama3.2";
106
+ const label = `ollama/${model}`;
107
+ const will = rubricCount > 0;
108
+ return {
109
+ rubricAssertionCount: rubricCount,
110
+ willEvaluateRubrics: will,
111
+ judgeMode: mode,
112
+ judgeBackend: "ollama",
113
+ judgeLabel: label,
114
+ summary: will
115
+ ? `Rubric judge is Ollama at ${url} (${model}). Each llm-rubric triggers a judge request — see run log lines "→ Judge LLM".`
116
+ : `Judge is set to Ollama (${model}) but this suite has no llm-rubric assertions.`,
117
+ };
118
+ }
119
+
120
+ if (judgeProvider) {
121
+ const label = `${judgeProvider.name}/${judgeProvider.model}`;
122
+ const will = rubricCount > 0;
123
+ return {
124
+ rubricAssertionCount: rubricCount,
125
+ willEvaluateRubrics: will,
126
+ judgeMode: mode,
127
+ judgeBackend: "claude",
128
+ judgeLabel: label,
129
+ summary: will
130
+ ? `Rubric judge is Claude (${label}). Each llm-rubric triggers a judge request — see run log lines "→ Judge LLM".`
131
+ : `Claude judge is configured (${label}) but this suite has no llm-rubric assertions.`,
132
+ };
133
+ }
134
+
135
+ return {
136
+ rubricAssertionCount: rubricCount,
137
+ willEvaluateRubrics: false,
138
+ judgeMode: mode,
139
+ judgeBackend: "off",
140
+ summary:
141
+ rubricCount > 0
142
+ ? `No Anthropic API key available for the judge (${mode} mode). Add your key under Settings → Secrets for the judge variable, or set ANTHROPIC_API_KEY on the server. llm-rubric did not call Claude.`
143
+ : "No llm-rubric assertions; Claude judge key is still missing if you add rubrics later.",
144
+ };
145
+ }
146
+
147
+ function buildJudgeProvider(judge?: {
148
+ mode?: string;
149
+ anthropicApiKey?: string;
150
+ claudeModel?: string;
151
+ ollamaBaseUrl?: string;
152
+ ollamaModel?: string;
153
+ }): Provider | undefined {
154
+ if (!judge || judge.mode === "none") return undefined;
155
+
156
+ if (judge.mode === "ollama") {
157
+ return new OllamaProvider(
158
+ judge.ollamaBaseUrl?.trim() || process.env.OLLAMA_BASE_URL || "http://localhost:11434",
159
+ (judge.ollamaModel || "llama3.2").trim(),
160
+ {}
161
+ );
162
+ }
163
+
164
+ const fromBody = judge.anthropicApiKey?.trim();
165
+ const fromEnv = process.env.ANTHROPIC_API_KEY?.trim();
166
+ const key = fromBody || (judge.mode === "auto" || judge.mode === "claude" ? fromEnv : undefined);
167
+ if (!key) return undefined;
168
+
169
+ const model = judge.claudeModel?.trim() || "claude-3-5-haiku-20241022";
170
+ return new ClaudeProvider(key, model);
171
+ }
172
+
173
+ type JudgeBody = {
174
+ mode?: string;
175
+ anthropicApiKey?: string;
176
+ claudeModel?: string;
177
+ ollamaBaseUrl?: string;
178
+ ollamaModel?: string;
179
+ };
180
+
181
+ type SuitePostBody = {
182
+ yaml?: string;
183
+ instances?: LLMInstance[];
184
+ judge?: JudgeBody;
185
+ stream?: boolean;
186
+ };
187
+
188
+ type PreparedSuite = {
189
+ config: SuiteConfig;
190
+ providers: Provider[];
191
+ judgeProvider: Provider | undefined;
192
+ judgeMeta: SuiteJudgeMeta;
193
+ rubricCount: number;
194
+ };
195
+
196
+ function prepareSuite(body: SuitePostBody): PreparedSuite | NextResponse {
197
+ if (!body.yaml?.trim()) {
198
+ return NextResponse.json({ error: "yaml is required" }, { status: 400 });
199
+ }
200
+
201
+ let config: SuiteConfig;
202
+ try {
203
+ config = parseSuiteConfig(body.yaml);
204
+ } catch (err) {
205
+ return NextResponse.json(
206
+ { error: `Invalid suite config: ${err instanceof Error ? err.message : String(err)}` },
207
+ { status: 400 }
208
+ );
209
+ }
210
+
211
+ const enabled = (body.instances ?? []).filter((i) => i.enabled);
212
+ if (enabled.length === 0) {
213
+ return NextResponse.json({ error: "no enabled instances" }, { status: 400 });
214
+ }
215
+
216
+ const providers = enabled.map(buildProvider).filter((p): p is Provider => p !== null);
217
+ if (providers.length === 0) {
218
+ return NextResponse.json({ error: "no providers could be constructed — check credentials" }, { status: 400 });
219
+ }
220
+
221
+ const rubricCount = countLlmRubricAssertions(config);
222
+ const judgeProvider = buildJudgeProvider(body.judge);
223
+ const judgeMeta = buildSuiteJudgeMeta(body.judge, judgeProvider, rubricCount);
224
+
225
+ return { config, providers, judgeProvider, judgeMeta, rubricCount };
226
+ }
227
+
228
+ async function executeSuite(
229
+ prepared: PreparedSuite,
230
+ emitLine?: (line: string) => void
231
+ ): Promise<{ result: SuiteResult; runLog: string[]; judgeMeta: SuiteJudgeMeta }> {
232
+ const { config, providers, judgeProvider, judgeMeta, rubricCount } = prepared;
233
+ const runLog: string[] = [];
234
+ const log = (message: string) => {
235
+ const line = `[${new Date().toISOString().slice(11, 23)}] ${message}`;
236
+ runLog.push(line);
237
+ emitLine?.(line);
238
+ };
239
+
240
+ if (judgeMeta.willEvaluateRubrics && judgeMeta.judgeLabel) {
241
+ log(`Rubric judge ACTIVE (${judgeMeta.judgeBackend}): ${judgeMeta.judgeLabel} · ${rubricCount} llm-rubric step(s)`);
242
+ } else if (rubricCount > 0) {
243
+ log(`Rubric judge INACTIVE — ${rubricCount} llm-rubric step(s) will not call an LLM (see summary below)`);
244
+ } else {
245
+ log(`No llm-rubric assertions in suite — judge not used`);
246
+ }
247
+
248
+ const result = await runSuite({
249
+ config,
250
+ providers,
251
+ judgeProvider,
252
+ onLog: log,
253
+ });
254
+
255
+ return { result, runLog, judgeMeta };
256
+ }
257
+
258
+ function sseChunk(obj: unknown): Uint8Array {
259
+ return new TextEncoder().encode(`data: ${JSON.stringify(obj)}\n\n`);
260
+ }
261
+
262
+ export async function POST(req: NextRequest) {
263
+ const body = (await req.json()) as SuitePostBody;
264
+ const prepared = prepareSuite(body);
265
+ if (prepared instanceof NextResponse) return prepared;
266
+
267
+ if (body.stream === true) {
268
+ const stream = new ReadableStream({
269
+ async start(controller) {
270
+ try {
271
+ const out = await executeSuite(prepared, (line) => {
272
+ controller.enqueue(sseChunk({ type: "log", line }));
273
+ });
274
+ controller.enqueue(
275
+ sseChunk({
276
+ type: "done",
277
+ result: out.result,
278
+ runLog: out.runLog,
279
+ judgeMeta: out.judgeMeta,
280
+ })
281
+ );
282
+ } catch (e) {
283
+ controller.enqueue(
284
+ sseChunk({
285
+ type: "error",
286
+ message: e instanceof Error ? e.message : String(e),
287
+ })
288
+ );
289
+ } finally {
290
+ controller.close();
291
+ }
292
+ },
293
+ });
294
+
295
+ return new Response(stream, {
296
+ headers: {
297
+ "Content-Type": "text/event-stream; charset=utf-8",
298
+ "Cache-Control": "no-cache, no-transform",
299
+ Connection: "keep-alive",
300
+ "X-Accel-Buffering": "no",
301
+ },
302
+ });
303
+ }
304
+
305
+ try {
306
+ const out = await executeSuite(prepared);
307
+ return NextResponse.json({ ...out.result, runLog: out.runLog, judgeMeta: out.judgeMeta });
308
+ } catch (e) {
309
+ return NextResponse.json(
310
+ { error: e instanceof Error ? e.message : String(e) },
311
+ { status: 500 }
312
+ );
313
+ }
314
+ }
@@ -0,0 +1,215 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ /* Canvas & surfaces — Bench AI light shell */
5
+ --bg: #f9f9f9;
6
+ --bg-gradient: #f9f9f9;
7
+ --surface: #ffffff;
8
+ --surface-subtle: #fafafa;
9
+ --surface-muted: #f4f4f5;
10
+ --surface-hover: #f0f0f1;
11
+
12
+ /* Borders */
13
+ --border: rgba(15, 23, 42, 0.07);
14
+ --border-strong: rgba(15, 23, 42, 0.11);
15
+ --border-focus: rgba(15, 23, 42, 0.18);
16
+
17
+ /* Text */
18
+ --text-1: #0a0a0a;
19
+ --text-2: #525252;
20
+ --text-3: #a3a3a3;
21
+
22
+ /* Brand / actions */
23
+ --accent: #2563eb;
24
+ --accent-hover: #1d4ed8;
25
+ --accent-muted: #3b82f6;
26
+ --accent-subtle: #eff6ff;
27
+ --accent-text: #1d4ed8;
28
+ --ring: #2563eb;
29
+ --ring-subtle: rgba(37, 99, 235, 0.2);
30
+
31
+ /* Quick comparison metric hues */
32
+ --chart-latency: #16a34a;
33
+ --chart-tokens: #7c3aed;
34
+ --chart-cost: #b45309;
35
+
36
+ /* Response code panels */
37
+ --code-bg: #faf6f0;
38
+ --code-border: rgba(120, 83, 50, 0.14);
39
+
40
+ /* Semantic */
41
+ --green: #047857;
42
+ --green-subtle: #ecfdf5;
43
+ --red: #b91c1c;
44
+ --red-subtle: #fef2f2;
45
+ --amber: #b45309;
46
+ --amber-subtle: #fffbeb;
47
+
48
+ /* Provider accents (muted for headers) */
49
+ --claude: #5b21b6;
50
+ --claude-subtle: #f5f3ff;
51
+ --claude-border: #e9d5ff;
52
+ --ollama: #047857;
53
+ --ollama-subtle: #ecfdf5;
54
+ --ollama-border: #a7f3d0;
55
+ --minimax: #c2410c;
56
+ --minimax-subtle: #fff7ed;
57
+ --minimax-border: #fed7aa;
58
+ --openai: #0d9488;
59
+ --openai-subtle: #f0fdfa;
60
+ --openai-border: #99f6e4;
61
+ --groq: #c2410c;
62
+ --groq-subtle: #fff7ed;
63
+ --groq-border: #fed7aa;
64
+ --openrouter: #4f46e5;
65
+ --openrouter-subtle: #eef2ff;
66
+ --openrouter-border: #c7d2fe;
67
+ --nvidia-nim: #4d7c0f;
68
+ --nvidia-nim-subtle: #f7fee7;
69
+ --nvidia-nim-border: #d9f99d;
70
+ --together: #1d4ed8;
71
+ --together-subtle: #eff6ff;
72
+ --together-border: #bfdbfe;
73
+ --perplexity: #0f766e;
74
+ --perplexity-subtle: #f0fdfa;
75
+ --perplexity-border: #99f6e4;
76
+ --custom: #64748b;
77
+ --custom-subtle: #f8fafc;
78
+ --custom-border: #e2e8f0;
79
+
80
+ /* Elevation */
81
+ --shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.035);
82
+ --shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.05);
83
+ --shadow-md: 0 4px 20px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
84
+ --shadow-lg: 0 24px 48px rgba(15, 23, 42, 0.1), 0 8px 16px rgba(15, 23, 42, 0.05);
85
+ --shadow-drawer: -8px 0 40px rgba(15, 23, 42, 0.08);
86
+
87
+ --r-sm: 6px;
88
+ --r-md: 10px;
89
+ --r-lg: 14px;
90
+ --r-xl: 18px;
91
+ --r-2xl: 22px;
92
+
93
+ --font: var(--font-inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
94
+ --font-mono: "SF Mono", "JetBrains Mono", "Fira Code", ui-monospace, monospace;
95
+ }
96
+
97
+ html { font-size: 15px; }
98
+
99
+ body {
100
+ font-family: var(--font);
101
+ background: var(--bg);
102
+ color: var(--text-1);
103
+ line-height: 1.55;
104
+ -webkit-font-smoothing: antialiased;
105
+ -moz-osx-font-smoothing: grayscale;
106
+ }
107
+
108
+ /* Focus — consistent, accessible */
109
+ button:focus-visible,
110
+ a:focus-visible {
111
+ outline: 2px solid var(--ring);
112
+ outline-offset: 2px;
113
+ }
114
+
115
+ input:focus-visible,
116
+ textarea:focus-visible,
117
+ select:focus-visible {
118
+ outline: none;
119
+ border-color: var(--ring) !important;
120
+ box-shadow: 0 0 0 3px var(--ring-subtle);
121
+ }
122
+
123
+ /* ── Markdown ─────────────────────────────────────────────────── */
124
+ .md {
125
+ font-size: 0.925rem;
126
+ line-height: 1.72;
127
+ color: var(--text-1);
128
+ word-break: break-word;
129
+ }
130
+ .md > *:last-child { margin-bottom: 0; }
131
+
132
+ .md p { margin-bottom: 0.85em; }
133
+
134
+ .md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
135
+ font-weight: 600;
136
+ line-height: 1.35;
137
+ margin: 1.35em 0 0.5em;
138
+ color: var(--text-1);
139
+ letter-spacing: -0.02em;
140
+ }
141
+ .md h1 { font-size: 1.2em; }
142
+ .md h2 { font-size: 1.08em; }
143
+ .md h3 { font-size: 1em; }
144
+ .md h4, .md h5, .md h6 { font-size: 0.92em; }
145
+ .md h1:first-child,
146
+ .md h2:first-child,
147
+ .md h3:first-child { margin-top: 0; }
148
+
149
+ .md ul, .md ol { padding-left: 1.45em; margin-bottom: 0.85em; }
150
+ .md li { margin-bottom: 0.35em; line-height: 1.65; }
151
+ .md li:last-child { margin-bottom: 0; }
152
+ .md li > ul, .md li > ol { margin-top: 0.28em; margin-bottom: 0; }
153
+
154
+ .md code {
155
+ font-family: var(--font-mono);
156
+ font-size: 0.84em;
157
+ background: color-mix(in srgb, var(--code-bg) 75%, var(--surface));
158
+ border: 1px solid var(--code-border);
159
+ border-radius: 5px;
160
+ padding: 0.12em 0.38em;
161
+ color: var(--text-1);
162
+ }
163
+
164
+ .md pre {
165
+ background: var(--code-bg);
166
+ border: 1px solid var(--code-border);
167
+ border-radius: var(--r-md);
168
+ padding: 1rem 1.1rem;
169
+ overflow-x: auto;
170
+ margin-bottom: 0.85em;
171
+ }
172
+ .md pre code {
173
+ background: none;
174
+ border: none;
175
+ padding: 0;
176
+ font-size: 0.82em;
177
+ line-height: 1.65;
178
+ color: var(--text-1);
179
+ }
180
+
181
+ .md blockquote {
182
+ border-left: 3px solid var(--border-strong);
183
+ padding: 0.15em 0 0.15em 1em;
184
+ color: var(--text-2);
185
+ margin: 0.85em 0;
186
+ font-style: italic;
187
+ }
188
+
189
+ .md table {
190
+ width: 100%;
191
+ border-collapse: collapse;
192
+ margin-bottom: 0.85em;
193
+ font-size: 0.88em;
194
+ }
195
+ .md thead tr { background: var(--surface-subtle); }
196
+ .md th {
197
+ padding: 0.55rem 0.8rem;
198
+ text-align: left;
199
+ font-weight: 600;
200
+ border-bottom: 1px solid var(--border-strong);
201
+ white-space: nowrap;
202
+ color: var(--text-1);
203
+ }
204
+ .md td {
205
+ padding: 0.5rem 0.8rem;
206
+ border-bottom: 1px solid var(--border);
207
+ }
208
+ .md tr:last-child td { border-bottom: none; }
209
+ .md tbody tr:hover { background: var(--surface-hover); }
210
+
211
+ .md a { color: var(--accent); text-decoration: none; font-weight: 500; }
212
+ .md a:hover { text-decoration: underline; }
213
+ .md strong { font-weight: 600; }
214
+ .md em { font-style: italic; color: var(--text-2); }
215
+ .md hr { border: none; border-top: 1px solid var(--border); margin: 1.25em 0; }
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Bench AI">
2
+ <defs>
3
+ <linearGradient id="bg" x1="4" y1="2" x2="28" y2="30" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#1e40af"/>
5
+ <stop offset="1" stop-color="#172554"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="32" height="32" rx="8" fill="url(#bg)"/>
9
+ <!-- Side-by-side panels = compare -->
10
+ <rect x="7" y="9" width="7" height="14" rx="2" fill="#ffffff" fill-opacity="0.95"/>
11
+ <rect x="18" y="9" width="7" height="14" rx="2" fill="#ffffff" fill-opacity="0.78"/>
12
+ <!-- Subtle divider -->
13
+ <rect x="15" y="8" width="2" height="16" rx="1" fill="#ffffff" fill-opacity="0.35"/>
14
+ </svg>
@@ -0,0 +1,44 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "./globals.css";
4
+ import { BRAND_NAME, BRAND_TAGLINE } from "../lib/brand";
5
+
6
+ const inter = Inter({
7
+ subsets: ["latin"],
8
+ variable: "--font-inter",
9
+ display: "swap",
10
+ });
11
+
12
+ const siteUrl = "https://bench-ai-web.vercel.app";
13
+
14
+ export const metadata: Metadata = {
15
+ metadataBase: new URL(siteUrl),
16
+ title: BRAND_NAME,
17
+ description: BRAND_TAGLINE,
18
+ openGraph: {
19
+ title: BRAND_NAME,
20
+ description: BRAND_TAGLINE,
21
+ url: "/",
22
+ siteName: BRAND_NAME,
23
+ type: "website",
24
+ },
25
+ twitter: {
26
+ card: "summary_large_image",
27
+ title: BRAND_NAME,
28
+ description: BRAND_TAGLINE,
29
+ },
30
+ };
31
+
32
+ export default function RootLayout({
33
+ children,
34
+ }: {
35
+ children: React.ReactNode;
36
+ }) {
37
+ return (
38
+ <html lang="en" className={inter.variable}>
39
+ <body style={{ fontFamily: "var(--font-inter, var(--font))" }}>
40
+ {children}
41
+ </body>
42
+ </html>
43
+ );
44
+ }
@@ -0,0 +1,73 @@
1
+ import { ImageResponse } from "next/og";
2
+ import { BRAND_NAME, BRAND_TAGLINE } from "../lib/brand";
3
+
4
+ export const alt = `${BRAND_NAME} — ${BRAND_TAGLINE}`;
5
+ export const size = { width: 1200, height: 630 };
6
+ export const contentType = "image/png";
7
+
8
+ /** Social preview card; copy matches `../lib/brand.ts` (not static PNG). */
9
+ export default function OpenGraphImage() {
10
+ return new ImageResponse(
11
+ (
12
+ <div
13
+ style={{
14
+ height: "100%",
15
+ width: "100%",
16
+ display: "flex",
17
+ flexDirection: "column",
18
+ alignItems: "flex-start",
19
+ justifyContent: "center",
20
+ backgroundColor: "#f9f9f9",
21
+ backgroundImage:
22
+ "linear-gradient(135deg, #f9f9f9 0%, #ffffff 45%, #eff6ff 100%)",
23
+ padding: 72,
24
+ }}
25
+ >
26
+ <div
27
+ style={{
28
+ width: 120,
29
+ height: 6,
30
+ borderRadius: 3,
31
+ backgroundColor: "#2563eb",
32
+ marginBottom: 36,
33
+ }}
34
+ />
35
+ <div
36
+ style={{
37
+ display: "flex",
38
+ flexDirection: "column",
39
+ gap: 20,
40
+ }}
41
+ >
42
+ <div
43
+ style={{
44
+ fontSize: 76,
45
+ fontWeight: 700,
46
+ color: "#0a0a0a",
47
+ letterSpacing: "-0.03em",
48
+ lineHeight: 1.05,
49
+ fontFamily:
50
+ 'ui-sans-serif, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
51
+ }}
52
+ >
53
+ {BRAND_NAME}
54
+ </div>
55
+ <div
56
+ style={{
57
+ fontSize: 30,
58
+ fontWeight: 500,
59
+ color: "#525252",
60
+ maxWidth: 920,
61
+ lineHeight: 1.35,
62
+ fontFamily:
63
+ 'ui-sans-serif, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
64
+ }}
65
+ >
66
+ {BRAND_TAGLINE}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ ),
71
+ { ...size }
72
+ );
73
+ }