feat(research-lab): tier gate (beta+), 1–5 star ratings, run detail route

- 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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-17 15:28:02 +02:00
parent bd1e273f60
commit 8f0a74b2e7
10 changed files with 358 additions and 16 deletions

View file

@ -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],
})),
});
});

View file

@ -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],
})),
});
});

View file

@ -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],
})),
});
});