Skip to content

Commit c464ba5

Browse files
fix(bulk-eval): Fix duplicate row results and detail drawer errors
- Fixed bulk evaluator creating duplicate database records - Changed from createRowResult (creates new) to updateRowResult (updates existing) - Now properly updates status from 'pending' to 'completed'/'failed' - Prevents "pending" status showing when data exists - Created new API endpoint /api/bulk/row-details - Returns row results and original data for detail drawer - Accepts run_id and row_index query parameters - Added database helper function getRowResultsByIndex() - Fixed "Failed to fetch row details" error in detail drawer - Updated fetchRowDetails() to use new endpoint - Fixed esbuild syntax error in ResultsTable.astro - Converted JSDoc comments to single-line comments in inline script - JSDoc comments cause parsing errors with is:inline directive All quality gates passing (typecheck, lint, format)
1 parent 32eddeb commit c464ba5

5 files changed

Lines changed: 123 additions & 43 deletions

File tree

src/components/bulk/ResultsTable.astro

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -530,10 +530,8 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
530530
</div>
531531

532532
<script is:inline define:vars={{ pageSize, csvData }}>
533-
/**
534-
* Client-side functionality for results table.
535-
* Handles pagination, row selection, bulk actions, and per-row actions.
536-
*/
533+
// Client-side functionality for results table.
534+
// Handles pagination, row selection, bulk actions, and per-row actions.
537535

538536
let currentPage = 1;
539537
const totalRows = csvData.length;
@@ -542,9 +540,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
542540
const selectAllCheckbox = document.getElementById('select-all-rows');
543541
const paginationInfo = document.getElementById('pagination-info');
544542

545-
/**
546-
* Update checkboxes based on visible rows
547-
*/
543+
// Update checkboxes based on visible rows
548544
function updateCheckboxes() {
549545
const rowCheckboxes = document.querySelectorAll('.row-checkbox');
550546

@@ -578,9 +574,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
578574
});
579575
}
580576

581-
/**
582-
* Update bulk action button states based on selection
583-
*/
577+
// Update bulk action button states based on selection
584578
function updateBulkActionButtons() {
585579
const selectedCount = getSelectedRows().length;
586580
const regenerateBtn = document.querySelector('[data-action="regenerate-selected"]');
@@ -594,9 +588,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
594588
}
595589
}
596590

597-
/**
598-
* Update pagination display and visible rows
599-
*/
591+
// Update pagination display and visible rows
600592
function updatePagination() {
601593
const start = (currentPage - 1) * pageSize + 1;
602594
const end = Math.min(currentPage * pageSize, totalRows);
@@ -635,9 +627,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
635627
}
636628
}
637629

638-
/**
639-
* Handle page button clicks
640-
*/
630+
// Handle page button clicks
641631
document.querySelectorAll('[data-page]').forEach((btn) => {
642632
btn.addEventListener('click', () => {
643633
const page = parseInt(btn.dataset.page || '1');
@@ -659,9 +649,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
659649
});
660650
});
661651

662-
/**
663-
* Handle previous page button
664-
*/
652+
// Handle previous page button
665653
document.querySelector('[data-action="prev-page"]')?.addEventListener('click', () => {
666654
if (currentPage > 1) {
667655
currentPage--;
@@ -676,9 +664,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
676664
}
677665
});
678666

679-
/**
680-
* Handle next page button
681-
*/
667+
// Handle next page button
682668
document.querySelector('[data-action="next-page"]')?.addEventListener('click', () => {
683669
if (currentPage < totalPages) {
684670
currentPage++;
@@ -693,9 +679,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
693679
}
694680
});
695681

696-
/**
697-
* Handle per-row action clicks
698-
*/
682+
// Handle per-row action clicks
699683
document.querySelectorAll('[data-action]').forEach((element) => {
700684
element.addEventListener('click', (e) => {
701685
e.preventDefault();
@@ -784,9 +768,7 @@ const getActionsLeftOffset = (headerCount: number) => `calc(6rem + ${headerCount
784768
});
785769
});
786770

787-
/**
788-
* Get selected row indices
789-
*/
771+
// Get selected row indices
790772
function getSelectedRows() {
791773
const rowCheckboxes = document.querySelectorAll('.row-checkbox');
792774
return Array.from(rowCheckboxes)

src/lib/bulk-evaluation/bulk-evaluator.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
getBulkDataset,
2020
updateRunStatus,
2121
createRowResult,
22+
updateRowResult,
2223
getModelById,
2324
decryptApiKey,
2425
} from '@lib/db';
@@ -203,9 +204,9 @@ export class BulkEvaluator {
203204
systemPrompt: string,
204205
temperature: number
205206
): Promise<RowEvaluationResult> {
206-
// Create initial pending row result
207+
// Create initial pending row result and store its ID
207208
const prompt = interpolateTemplate(systemPrompt, row);
208-
createRowResult({
209+
const pendingResult = createRowResult({
209210
run_id: this.runId,
210211
original_row_index: rowIndex,
211212
model_id: modelId,
@@ -245,12 +246,8 @@ export class BulkEvaluator {
245246
};
246247
}
247248

248-
// Update row result with success
249-
createRowResult({
250-
run_id: this.runId,
251-
original_row_index: rowIndex,
252-
model_id: modelId,
253-
prompt_used: prompt,
249+
// Update row result with success (don't create a new one)
250+
updateRowResult(pendingResult.id, {
254251
output_text: modelResponse.response,
255252
status: 'completed',
256253
duration_ms: modelResponse.executionTime,
@@ -270,13 +267,9 @@ export class BulkEvaluator {
270267
error instanceof Error ? error.message : error
271268
);
272269

273-
// Update row result with failure
270+
// Update row result with failure (don't create a new one)
274271
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
275-
createRowResult({
276-
run_id: this.runId,
277-
original_row_index: rowIndex,
278-
model_id: modelId,
279-
prompt_used: prompt,
272+
updateRowResult(pendingResult.id, {
280273
status: 'failed',
281274
error_message: errorMessage,
282275
});

src/lib/db/bulk-db.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,25 @@ export function getRowResultForModel(
469469
return stmt.get(runId, rowIndex, modelId) as RowResult | null;
470470
}
471471

472+
/**
473+
* Get all results for a specific row index in a run.
474+
* @param runId - Run ID
475+
* @param rowIndex - Original row index
476+
* @param db - Optional database instance
477+
* @returns Array of row results for all models
478+
*/
479+
export function getRowResultsByIndex(
480+
runId: string,
481+
rowIndex: number,
482+
db?: Database.Database
483+
): RowResult[] {
484+
const database = db || getBulkDatabase();
485+
const stmt = database.prepare(
486+
'SELECT * FROM row_results WHERE run_id = ? AND original_row_index = ? ORDER BY model_id'
487+
);
488+
return stmt.all(runId, rowIndex) as RowResult[];
489+
}
490+
472491
/**
473492
* Update a row result's status and output.
474493
* @param id - Result ID

src/pages/api/bulk/row-details.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Bulk Evaluation Row Details API Endpoint
3+
* GET /api/bulk/row-details
4+
*
5+
* Fetches all model results and original row data for a specific row in a bulk evaluation run.
6+
* Used by the detail drawer to display comprehensive row information.
7+
*/
8+
9+
import type { APIRoute } from 'astro';
10+
import { getRunWithResults, getRowResultsByIndex } from '@lib/db/bulk-db';
11+
import { badRequest, notFound, createErrorResponse } from '@lib/api-error-handler';
12+
import { createLogger } from '@lib/logger';
13+
14+
const logger = createLogger('API:Bulk:RowDetails');
15+
16+
/**
17+
* GET /api/bulk/row-details?run_id={id}&row_index={index}
18+
* Get all results and original data for a specific row
19+
*
20+
* Query params:
21+
* - run_id (required): The evaluation run ID
22+
* - row_index (required): The row index (0-based)
23+
*
24+
* Response: 200 with { allRowResults: RowResult[], rowData: Record<string, string> }
25+
* 400 with { error: string, code: string }
26+
* 404 with { error: string }
27+
*/
28+
export const GET: APIRoute = async ({ url }) => {
29+
const startTime = Date.now();
30+
31+
try {
32+
const runId = url.searchParams.get('run_id');
33+
const rowIndexStr = url.searchParams.get('row_index');
34+
35+
if (!runId) {
36+
logger.logApiRequest('GET', '/api/bulk/row-details', 400, Date.now() - startTime);
37+
return badRequest('run_id query parameter is required', 'INVALID_INPUT');
38+
}
39+
40+
if (!rowIndexStr) {
41+
logger.logApiRequest('GET', '/api/bulk/row-details', 400, Date.now() - startTime);
42+
return badRequest('row_index query parameter is required', 'INVALID_INPUT');
43+
}
44+
45+
const rowIndex = parseInt(rowIndexStr, 10);
46+
if (isNaN(rowIndex) || rowIndex < 0) {
47+
logger.logApiRequest('GET', '/api/bulk/row-details', 400, Date.now() - startTime);
48+
return badRequest('row_index must be a non-negative integer', 'INVALID_INPUT');
49+
}
50+
51+
// Get run with dataset
52+
const run = getRunWithResults(runId);
53+
if (!run) {
54+
logger.logApiRequest('GET', '/api/bulk/row-details', 404, Date.now() - startTime);
55+
return notFound('Evaluation run');
56+
}
57+
58+
// Parse CSV data to get the specific row
59+
const csvRows = JSON.parse(run.dataset.csv_data) as Record<string, unknown>[];
60+
if (rowIndex >= csvRows.length) {
61+
logger.logApiRequest('GET', '/api/bulk/row-details', 404, Date.now() - startTime);
62+
return notFound('Row data');
63+
}
64+
65+
const rowData = csvRows[rowIndex];
66+
67+
// Get all results for this row
68+
const allRowResults = getRowResultsByIndex(runId, rowIndex);
69+
70+
logger.logApiRequest('GET', '/api/bulk/row-details', 200, Date.now() - startTime);
71+
72+
return new Response(
73+
JSON.stringify({
74+
allRowResults,
75+
rowData,
76+
}),
77+
{
78+
status: 200,
79+
headers: { 'Content-Type': 'application/json' },
80+
}
81+
);
82+
} catch (error) {
83+
logger.logApiError('GET', '/api/bulk/row-details', error as Error);
84+
return createErrorResponse(error);
85+
}
86+
};

src/pages/bulk-eval/[id].astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ const progress = run.total_rows > 0 ? (run.processed_rows / run.total_rows) * 10
329329
allRowResults: RowResult[];
330330
rowData: Record<string, string>;
331331
}> {
332-
const response = await fetch(`/api/bulk/result?run_id=${runId}&row_index=${rowIndex}`);
332+
const response = await fetch(`/api/bulk/row-details?run_id=${runId}&row_index=${rowIndex}`);
333333

334334
if (!response.ok) {
335335
throw new Error('Failed to fetch row details');

0 commit comments

Comments
 (0)