Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions app/(main)/assessment/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
"use client";

import { Suspense, useCallback, useMemo, useRef, useState } from "react";
import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useRouter } from "next/navigation";
import Loader from "@/app/components/Loader";
import { useToast } from "@/app/components/Toast";
Expand All @@ -15,6 +22,8 @@ import type {
AssessmentTab,
AssessmentTabId,
ConfigSelection,
PrefilterConfig,
PostProcessingConfig,
SchemaProperty,
} from "@/app/lib/types/assessment";
import PageLayout from "@/app/components/assessment/PageLayout";
Expand Down Expand Up @@ -93,6 +102,10 @@ function PageContent() {
const [systemInstruction, setSystemInstruction] = useState("");
const [outputSchema, setOutputSchema] = useState<SchemaProperty[]>([]);
const [configs, setConfigs] = useState<ConfigSelection[]>([]);
const [prefilterConfig, setPrefilterConfig] =
useState<PrefilterConfig | null>(null);
const [postProcessingConfig, setPostProcessingConfig] =
useState<PostProcessingConfig | null>(null);

const handleForbidden = useCallback(
(options?: { notify?: boolean }) => {
Expand Down Expand Up @@ -141,6 +154,10 @@ function PageContent() {
[setDataset],
);

useEffect(() => {
setPrefilterConfig(null);
}, [datasetId]);

const outputSchemaJson = useMemo(
() => schemaToJsonSchema(outputSchema),
[outputSchema],
Expand Down Expand Up @@ -183,13 +200,20 @@ function PageContent() {
system_instruction: systemInstruction.trim() || null,
text_columns: columnMapping.textColumns,
attachments: columnMapping.attachments.map(
({ column, type, format }) => ({ column, type, format }),
({ column, type, format, type_column, type_value_map }) => ({
column,
type,
format,
...(type_column ? { type_column, type_value_map } : {}),
}),
),
output_schema: outputSchemaJson,
configs: configs.map(({ config_id, config_version }) => ({
config_id,
config_version,
})),
prefilter_config: prefilterConfig ?? null,
post_processing_config: postProcessingConfig ?? null,
}),
});

Expand All @@ -202,6 +226,8 @@ function PageContent() {
setPromptTemplate("");
setOutputSchema([]);
setConfigs([]);
setPrefilterConfig(null);
setPostProcessingConfig(null);
setActiveTab("results");
} catch (error) {
if (handleForbiddenError(error, handleForbiddenWithNotify)) return;
Expand All @@ -218,8 +244,10 @@ function PageContent() {
datasetId,
experimentName,
handleForbiddenWithNotify,
prefilterConfig,
outputSchema,
outputSchemaJson,
postProcessingConfig,
promptTemplate,
activeKey,
systemInstruction,
Expand All @@ -238,6 +266,8 @@ function PageContent() {
promptTemplate,
outputSchema,
configs,
prefilterConfig,
postProcessingConfig,
};

const hasDataset = !!datasetId && columns.length > 0;
Expand Down Expand Up @@ -272,7 +302,9 @@ function PageContent() {
const effectiveCompletedConfigSteps = useMemo(() => {
const merged = new Set(completedConfigSteps);
if (hasMapperSelection) merged.add(1);
if (canReachReview) merged.add(2);
if (hasMapperSelection) merged.add(2); // Prefilter is optional and always passable
if (canReachReview) merged.add(3);
if (canReachReview) merged.add(4); // Post Processing is optional and always passable
return merged;
}, [canReachReview, completedConfigSteps, hasMapperSelection]);

Expand All @@ -299,10 +331,12 @@ function PageContent() {
completedSteps: effectiveCompletedConfigSteps,
configStep,
configs,
datasetId,
experimentName,
formState,
hasDataset,
isSubmitting,
prefilterConfig,
outputSchema,
systemInstruction,
promptTemplate,
Expand All @@ -312,9 +346,12 @@ function PageContent() {
setConfigStep,
setConfigs,
setExperimentName,
setPrefilterConfig,
setOutputSchema,
setSystemInstruction,
setPromptTemplate,
postProcessingConfig,
setPostProcessingConfig,
submitBlockerMessage,
onSubmit: handleSubmit,
onStepComplete: handleConfigNext,
Expand Down
109 changes: 109 additions & 0 deletions app/(main)/assessment/results/[runId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation";
import dynamic from "next/dynamic";
import Loader from "@/app/components/Loader";
import { useToast } from "@/app/components/Toast";
import { useAuth } from "@/app/lib/context/AuthContext";
import { apiFetch } from "@/app/lib/apiClient";
import { jsonResultsToTableData } from "@/app/lib/assessment/results";
import { SPREADSHEET_PREVIEW_ROW_LIMIT } from "@/app/lib/assessment/constants";

const SpreadsheetView = dynamic(
() => import("@/app/components/assessment/SpreadsheetView"),
{
ssr: false,
loading: () => (
<div className="w-full h-screen flex items-center justify-center bg-bg-primary">
<Loader size="lg" message="Loading spreadsheet..." />
</div>
),
},
);

export default function AssessmentResultsPage() {
const params = useParams<{ runId: string }>();
const searchParams = useSearchParams();
const toast = useToast();
const { apiKeys, isAuthenticated, isHydrated } = useAuth();
const apiKey = apiKeys[0]?.key ?? "";

const runId = Number(params?.runId);
const title = searchParams.get("title") ?? `Run ${runId}`;

const [headers, setHeaders] = useState<string[] | null>(null);
const [rows, setRows] = useState<string[][] | null>(null);
const [error, setError] = useState<string | null>(null);
Comment on lines +35 to +37
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move these useState related hooks calling above of the runId.


useEffect(() => {
if (!isHydrated) return;
if (!isAuthenticated) {
setError("You must be signed in to view this run.");
return;
}
if (!Number.isFinite(runId) || runId <= 0) {
setError("Invalid run id.");
return;
}

let cancelled = false;
(async () => {
try {
const json = await apiFetch<
{ data?: Record<string, unknown>[] } | Record<string, unknown>[]
>(`/api/assessment/runs/${runId}/results?export_format=json`, apiKey);
const results: Record<string, unknown>[] = Array.isArray(json)
? json
: json.data || [];
const table = jsonResultsToTableData(results, {
rowLimit: SPREADSHEET_PREVIEW_ROW_LIMIT,
});
if (cancelled) return;
if (results.length > SPREADSHEET_PREVIEW_ROW_LIMIT) {
toast.warning(
`Preview capped at ${SPREADSHEET_PREVIEW_ROW_LIMIT} rows. Download CSV for full data.`,
);
}
setHeaders(table.headers);
setRows(table.rows);
} catch (err) {
if (cancelled) return;
const msg =
err instanceof Error ? err.message : "Failed to load results";
setError(msg);
toast.error(msg);
}
})();

return () => {
cancelled = true;
};
}, [apiKey, isAuthenticated, isHydrated, runId, toast]);

if (error) {
return (
<div className="w-full h-screen flex items-center justify-center bg-bg-primary">
<p className="text-sm text-text-secondary">{error}</p>
</div>
);
}

if (!headers || !rows) {
return (
<div className="w-full h-screen flex items-center justify-center bg-bg-primary">
<Loader size="lg" message="Loading results..." />
</div>
);
}

return (
<SpreadsheetView
runId={runId}
title={title}
subtitle={`${rows.length} rows · ${headers.length} columns`}
headers={headers}
rows={rows}
/>
);
}
28 changes: 28 additions & 0 deletions app/api/assessment/runs/[run_id]/resume/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// BFF proxy — POST /api/v1/assessment/runs/:id/resume
import { NextRequest, NextResponse } from "next/server";
import { apiClient } from "@/app/lib/apiClient";
import type { RouteContext } from "@/app/lib/types/assessment";

export async function POST(
request: NextRequest,
context: RouteContext<"run_id">,
) {
try {
const { run_id } = await context.params;
const { status, data } = await apiClient(
request,
`/api/v1/assessment/runs/${run_id}/resume`,
{ method: "POST" },
);

return NextResponse.json(data, { status });
} catch (error: unknown) {
console.error("Assessment run resume proxy error:", error);
return NextResponse.json(
{
error: "Failed to forward assessment run resume request",
},
{ status: 500 },
);
}
}
Loading
Loading