From 8f0a74b2e727e36895350e2ccd6593f8c2c8253d Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 17 Apr 2026 15:28:02 +0200 Subject: [PATCH] =?UTF-8?q?feat(research-lab):=20tier=20gate=20(beta+),=20?= =?UTF-8?q?1=E2=80=935=20star=20ratings,=20run=20detail=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Branding: research-lab registered in @mana/shared-branding with requiredTier: 'beta' + a custom flask-on-purple icon, so guest/public users are filtered out of the workbench picker. - Backend: compare routes now return resultId alongside each CompareEntry so the frontend can wire ratings to the eval_results rows in research.*. - Frontend: click-to-rate stars in CompareColumn (persists via POST /v1/runs/:runId/results/:resultId/rate), recent-run list rows are now buttons that navigate to /research-lab/runs/[id], and the detail route reconstructs CompareEntry shapes from eval_results + reuses CompareColumn for a full read-only view of any past run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/modules/research-lab/ListView.svelte | 46 +++- .../components/CompareColumn.svelte | 80 ++++++- .../web/src/lib/modules/research-lab/types.ts | 2 + .../(app)/research-lab/runs/[id]/+page.svelte | 207 ++++++++++++++++++ packages/shared-branding/src/app-icons.ts | 3 + packages/shared-branding/src/mana-apps.ts | 17 ++ packages/shared-research/src/types.ts | 1 + services/mana-research/src/routes/extract.ts | 5 +- services/mana-research/src/routes/research.ts | 5 +- services/mana-research/src/routes/search.ts | 8 +- 10 files changed, 358 insertions(+), 16 deletions(-) create mode 100644 apps/mana/apps/web/src/routes/(app)/research-lab/runs/[id]/+page.svelte diff --git a/apps/mana/apps/web/src/lib/modules/research-lab/ListView.svelte b/apps/mana/apps/web/src/lib/modules/research-lab/ListView.svelte index 0fbe6a9f2..e978fa4af 100644 --- a/apps/mana/apps/web/src/lib/modules/research-lab/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/research-lab/ListView.svelte @@ -6,6 +6,7 @@ is a thin orchestrator over the mana-research service. --> + + + Research Run · Mana + + +
+
+ + {#if run} +
+ {run.category} + {run.mode} + {#if run.totalCostCredits > 0} + {run.totalCostCredits}¢ + {/if} + {formatDate(run.createdAt)} +
+ {/if} +
+ + {#if loading} +

Lade Run …

+ {:else if error} +
{error}
+ {:else if run} +

{run.query}

+

+ {run.providersRequested.length} Anbieter · Run {run.id.slice(0, 8)} +

+
+ {#each entries as entry (entry.resultId)} + + {/each} +
+ {/if} +
+ + diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index 0ae3c6141..b65bb7fb3 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -116,6 +116,9 @@ export const APP_ICONS = { 'news-research': svgToDataUrl( `` ), + 'research-lab': svgToDataUrl( + `` + ), guides: svgToDataUrl( `` ), diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index 907e04d9e..ed26b23a3 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -496,6 +496,23 @@ export const MANA_APPS: ManaApp[] = [ status: 'development', requiredTier: 'guest', }, + { + id: 'research-lab', + name: 'Research Lab', + description: { + de: 'Web-Research Anbieter Seite-an-Seite vergleichen', + en: 'Compare web-research providers side-by-side', + }, + longDescription: { + de: 'Schick dieselbe Anfrage parallel an bis zu fünf Anbieter (Brave, Tavily, Exa, Perplexity, Claude, Gemini, OpenAI …) und vergleich Antworten, Latenzen und Kosten in einer Ansicht. Alle Runs werden serverseitig persistiert für spätere Auswertung.', + en: 'Send the same query to up to five providers in parallel (Brave, Tavily, Exa, Perplexity, Claude, Gemini, OpenAI …) and compare answers, latency, and cost side-by-side. All runs are persisted server-side for later review.', + }, + icon: APP_ICONS['research-lab'], + color: '#8b5cf6', + comingSoon: false, + status: 'beta', + requiredTier: 'beta', + }, { id: 'calc', name: 'Calc', diff --git a/packages/shared-research/src/types.ts b/packages/shared-research/src/types.ts index 4ead32756..1c451d278 100644 --- a/packages/shared-research/src/types.ts +++ b/packages/shared-research/src/types.ts @@ -72,6 +72,7 @@ export interface CompareResponse { success: boolean; data?: T; meta: ProviderMeta; + resultId?: string; }>; } diff --git a/services/mana-research/src/routes/extract.ts b/services/mana-research/src/routes/extract.ts index 40fcb9a21..5574d0153 100644 --- a/services/mana-research/src/routes/extract.ts +++ b/services/mana-research/src/routes/extract.ts @@ -126,10 +126,11 @@ export function createExtractRoutes( ); let totalCost = 0; + const resultIds: string[] = []; for (let i = 0; i < providers.length; i++) { const out = settled[i]; totalCost += out.meta.costCredits; - await storage.addResult({ + const row = await storage.addResult({ runId: run.id, providerId: providers[i].id, success: out.success, @@ -140,6 +141,7 @@ export function createExtractRoutes( normalizedResult: out.data ?? null, errorCode: out.meta.errorCode ?? null, }); + resultIds.push(row.id); } if (totalCost > 0) await storage.finalizeRunCost(run.id, totalCost); @@ -151,6 +153,7 @@ export function createExtractRoutes( success: settled[i].success, data: settled[i].data as { content: ExtractedContent } | undefined, meta: settled[i].meta, + resultId: resultIds[i], })), }); }); diff --git a/services/mana-research/src/routes/research.ts b/services/mana-research/src/routes/research.ts index 69c47fb44..d8aa411cf 100644 --- a/services/mana-research/src/routes/research.ts +++ b/services/mana-research/src/routes/research.ts @@ -130,10 +130,11 @@ export function createResearchRoutes( ); let totalCost = 0; + const resultIds: string[] = []; for (let i = 0; i < providers.length; i++) { const out = settled[i]; totalCost += out.meta.costCredits; - await storage.addResult({ + const row = await storage.addResult({ runId: run.id, providerId: providers[i].id, success: out.success, @@ -144,6 +145,7 @@ export function createResearchRoutes( normalizedResult: out.data ?? null, errorCode: out.meta.errorCode ?? null, }); + resultIds.push(row.id); } if (totalCost > 0) await storage.finalizeRunCost(run.id, totalCost); @@ -155,6 +157,7 @@ export function createResearchRoutes( success: settled[i].success, data: settled[i].data as { answer: AgentAnswer } | undefined, meta: settled[i].meta, + resultId: resultIds[i], })), }); }); diff --git a/services/mana-research/src/routes/search.ts b/services/mana-research/src/routes/search.ts index e8d4f682a..8db5e199c 100644 --- a/services/mana-research/src/routes/search.ts +++ b/services/mana-research/src/routes/search.ts @@ -86,7 +86,7 @@ export function createSearchRoutes( deps ); - await storage.addResult({ + const resultRow = await storage.addResult({ runId: run.id, providerId, success: out.success, @@ -110,6 +110,7 @@ export function createSearchRoutes( success: out.success, data: out.data, meta: out.meta, + resultId: resultRow.id, }); }) .post('/compare', async (c) => { @@ -147,10 +148,11 @@ export function createSearchRoutes( ); let totalCost = 0; + const resultIds: string[] = []; for (let i = 0; i < providers.length; i++) { const out = settled[i]; totalCost += out.meta.costCredits; - await storage.addResult({ + const row = await storage.addResult({ runId: run.id, providerId: providers[i].id, success: out.success, @@ -161,6 +163,7 @@ export function createSearchRoutes( normalizedResult: out.data ?? null, errorCode: out.meta.errorCode ?? null, }); + resultIds.push(row.id); } if (totalCost > 0) await storage.finalizeRunCost(run.id, totalCost); @@ -172,6 +175,7 @@ export function createSearchRoutes( success: settled[i].success, data: settled[i].data as { results: SearchHit[] } | undefined, meta: settled[i].meta, + resultId: resultIds[i], })), }); });