diff --git a/app/(main)/assessment/results/[runId]/page.tsx b/app/(main)/assessment/results/[runId]/page.tsx index 12242bc2..410f81d4 100644 --- a/app/(main)/assessment/results/[runId]/page.tsx +++ b/app/(main)/assessment/results/[runId]/page.tsx @@ -1,21 +1,22 @@ "use client"; -import { useEffect, useState } from "react"; -import { useParams, useSearchParams } from "next/navigation"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; import dynamic from "next/dynamic"; import { Loader } from "@/app/components/ui"; -import { useToast } from "@/app/hooks/useToast"; -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"; +import ResultsToolbar from "@/app/components/assessment/results/ResultsToolbar"; +import { useRunResults } from "@/app/hooks"; +import type { AssessmentMethodValue } from "@/app/lib/types/assessment"; +import { + downloadCsv, + jsonResultsToTableData, +} from "@/app/lib/assessment/results"; const SpreadsheetView = dynamic( () => import("@/app/components/assessment/SpreadsheetView"), { ssr: false, loading: () => ( -
+
), @@ -25,85 +26,45 @@ const SpreadsheetView = dynamic( 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 router = useRouter(); - const [headers, setHeaders] = useState(null); - const [rows, setRows] = useState(null); - const [error, setError] = useState(null); - - const runId = Number(params?.runId); - const title = searchParams.get("title") ?? `Run ${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[] } | Record[] - >(`/api/assessment/runs/${runId}/results?export_format=json`, apiKey); - const results: Record[] = 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]); + const assessmentId = params.runId; + const method = + (searchParams.get("method") as AssessmentMethodValue | null) ?? "BATCH"; + const title = searchParams.get("title") ?? "Run results"; + const { results, headers, rows, isLoading, error } = useRunResults( + assessmentId ? { assessment_id: assessmentId, method } : null, + ); if (error) { return ( -
+

{error}

); } - if (!headers || !rows) { + if (isLoading) { return ( -
+
); } return ( - +
+ router.push("/assessment")} + onDownload={() => { + const full = jsonResultsToTableData(results); + downloadCsv(title, [full.headers, ...full.rows]); + }} + /> + + +
); } diff --git a/app/(main)/settings/credentials/page.tsx b/app/(main)/settings/credentials/page.tsx index 3f40730b..c464ea3b 100644 --- a/app/(main)/settings/credentials/page.tsx +++ b/app/(main)/settings/credentials/page.tsx @@ -17,7 +17,12 @@ import { Credential, ProviderDef, } from "@/app/lib/types/credentials"; -import { getExistingForProvider } from "@/app/lib/utils"; +import { + buildCredentialPayload, + getExistingForProvider, + missingCredentialFields, + populateCredentialForm, +} from "@/app/lib/utils"; import ProviderSidebar from "@/app/components/settings/ProviderSidebar"; import CredentialForm from "@/app/components/settings/credentials/CredentialForm"; import { apiFetch } from "@/app/lib/apiClient"; @@ -46,23 +51,9 @@ export default function CredentialsPage() { // Re-populate form when provider or credentials change useEffect(() => { const existing = getExistingForProvider(selectedProvider, credentials); - if (existing) { - setExistingCredential(existing); - setIsActive(existing.is_active); - const populated: Record = {}; - selectedProvider.fields.forEach((f) => { - populated[f.key] = existing.credential[f.key] || ""; - }); - setFormValues(populated); - } else { - setExistingCredential(null); - setIsActive(true); - const blank: Record = {}; - selectedProvider.fields.forEach((f) => { - blank[f.key] = ""; - }); - setFormValues(blank); - } + setExistingCredential(existing); + setIsActive(existing ? existing.is_active : true); + setFormValues(populateCredentialForm(selectedProvider, existing)); }, [selectedProvider, credentials]); const loadCredentials = async () => { @@ -80,45 +71,47 @@ export default function CredentialsPage() { } }; - const buildCredentialBody = (isUpdate: boolean) => { - const innerPayload: Record = {}; - selectedProvider.fields.forEach((f) => { - innerPayload[f.key] = formValues[f.key].trim(); - }); - return { - provider: selectedProvider.credentialKey, - is_active: isActive, - credential: isUpdate - ? innerPayload - : { [selectedProvider.credentialKey]: innerPayload }, - }; - }; - const handleSave = async () => { if (!isAuthenticated) { toast.error("Please add an API key in Keystore first"); return; } - const missing = selectedProvider.fields.filter( - (f) => !formValues[f.key]?.trim(), - ); + const missing = missingCredentialFields(selectedProvider, formValues); if (missing.length > 0) { toast.error(`Please fill in: ${missing.map((f) => f.label).join(", ")}`); return; } + const built = buildCredentialPayload(selectedProvider, formValues); + if (built.error) { + toast.error(built.error); + return; + } + if (Object.keys(built.payload).length === 0) { + toast.error("No changes to save"); + return; + } + setIsSaving(true); try { if (existingCredential) { await apiFetch("/api/credentials", apiKeys[0]?.key ?? "", { method: "PATCH", - body: JSON.stringify(buildCredentialBody(true)), + body: JSON.stringify({ + provider: selectedProvider.credentialKey, + is_active: isActive, + credential: built.payload, + }), }); toast.success(`${selectedProvider.name} credentials updated`); } else { await apiFetch("/api/credentials", apiKeys[0]?.key ?? "", { method: "POST", - body: JSON.stringify(buildCredentialBody(false)), + body: JSON.stringify({ + provider: selectedProvider.credentialKey, + is_active: isActive, + credential: { [selectedProvider.credentialKey]: built.payload }, + }), }); toast.success(`${selectedProvider.name} credentials saved`); } @@ -134,21 +127,8 @@ export default function CredentialsPage() { const handleCancel = () => { const existing = getExistingForProvider(selectedProvider, credentials); - if (existing) { - setIsActive(existing.is_active); - const populated: Record = {}; - selectedProvider.fields.forEach((f) => { - populated[f.key] = existing.credential[f.key] || ""; - }); - setFormValues(populated); - } else { - const blank: Record = {}; - selectedProvider.fields.forEach((f) => { - blank[f.key] = ""; - }); - setFormValues(blank); - setIsActive(true); - } + setIsActive(existing ? existing.is_active : true); + setFormValues(populateCredentialForm(selectedProvider, existing)); }; const handleDelete = async () => { diff --git a/app/api/assessment/batch/[assessment_id]/route.ts b/app/api/assessment/batch/[assessment_id]/route.ts new file mode 100644 index 00000000..0ddf3074 --- /dev/null +++ b/app/api/assessment/batch/[assessment_id]/route.ts @@ -0,0 +1,19 @@ +import { NextRequest } from "next/server"; +import { proxyErrorResponse, proxyJsonResponse } from "@/app/api/_routeProxy"; +import type { RouteContext } from "@/app/lib/types/assessment"; + +export async function GET( + request: NextRequest, + context: RouteContext<"assessment_id">, +) { + try { + const { assessment_id } = await context.params; + return await proxyJsonResponse( + request, + `/api/v1/assessments/${assessment_id}`, + { method: "GET" }, + ); + } catch (error: unknown) { + return proxyErrorResponse("Assessment batch detail proxy error:", error); + } +} diff --git a/app/api/assessment/batch/route.ts b/app/api/assessment/batch/route.ts new file mode 100644 index 00000000..c324e0ef --- /dev/null +++ b/app/api/assessment/batch/route.ts @@ -0,0 +1,33 @@ +import { NextRequest } from "next/server"; +import { + proxyErrorResponse, + proxyJsonResponse, + withQueryParams, +} from "@/app/api/_routeProxy"; + +const BATCH_ENDPOINT = "/api/v1/assessments"; + +export async function GET(request: NextRequest) { + try { + const queryParams = new URLSearchParams(request.nextUrl.searchParams); + return await proxyJsonResponse( + request, + withQueryParams(BATCH_ENDPOINT, queryParams), + { method: "GET" }, + ); + } catch (error: unknown) { + return proxyErrorResponse("Assessment batch list proxy error:", error); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + return await proxyJsonResponse(request, BATCH_ENDPOINT, { + method: "POST", + body: JSON.stringify(body), + }); + } catch (error: unknown) { + return proxyErrorResponse("Assessment batch create proxy error:", error); + } +} diff --git a/app/components/assessment/AssessmentChildRunCard.tsx b/app/components/assessment/AssessmentChildRunCard.tsx deleted file mode 100644 index 2faa07d1..00000000 --- a/app/components/assessment/AssessmentChildRunCard.tsx +++ /dev/null @@ -1,171 +0,0 @@ -"use client"; - -import { useToast } from "@/app/hooks/useToast"; -import PostProcessingPanel from "./PostProcessingPanel"; -import ChildRunStageProgress from "./ChildRunStageProgress"; -import ChildRunActions from "./ChildRunActions"; -import { - getStageProgress, - isCompletedStatus, - isFailedStatus, -} from "@/app/lib/assessment/results"; -import { formatRelativeTime } from "@/app/lib/utils"; -import type { - AssessmentChildRun, - ConfigRunDetail, - ExportFormat, - PostProcessingConfig, -} from "@/app/lib/types/assessment"; - -interface AssessmentChildRunCardProps { - childRun: AssessmentChildRun; - configDetailsByKey: Record; - configLoadingKeys: Record; - configErrorKeys: Record; - rerunningId: number | null; - resumingId: number | null; - downloadingId: string | null; - onPreview: (runId: number, label: string) => void; - onRunDownload: (runId: number, format: ExportFormat) => void; - onResume: (run: AssessmentChildRun) => void; - onRerun: (run: AssessmentChildRun) => void; - onFetchRunColumns: (runId: number) => Promise; - onSavePostProcessing: ( - runId: number, - config: PostProcessingConfig | null, - ) => Promise; -} - -export default function AssessmentChildRunCard({ - childRun, - configDetailsByKey, - configLoadingKeys, - configErrorKeys, - rerunningId, - resumingId, - downloadingId, - onPreview, - onRunDownload, - onResume, - onRerun, - onFetchRunColumns, - onSavePostProcessing, -}: AssessmentChildRunCardProps) { - const toast = useToast(); - - const isFailedChild = isFailedStatus(childRun.status); - const isCompletedChild = isCompletedStatus(childRun.status); - const stageProgress = getStageProgress(childRun); - const isRerunning = rerunningId === childRun.id; - const isResuming = resumingId === childRun.id; - const configKey = - childRun.config_id && childRun.config_version - ? `${childRun.config_id}:${childRun.config_version}` - : null; - const configDetail = configKey ? configDetailsByKey[configKey] : null; - const isConfigLoading = configKey - ? Boolean(configLoadingKeys[configKey]) - : false; - const configError = configKey ? configErrorKeys[configKey] : null; - const fallbackName = childRun.config_id - ? `Config ${childRun.config_id.slice(0, 8)}` - : "Configuration"; - const configName = configDetail?.name || fallbackName; - const previewLabel = `${configName}${childRun.config_version ? ` v${childRun.config_version}` : ""}`; - - return ( -
-
-
-
-
- - {configName} - - {childRun.config_version !== null && ( - - v{childRun.config_version} - - )} - {configDetail?.provider && configDetail?.model && ( - - {configDetail.provider}/{configDetail.model} - - )} -
- -
- {isConfigLoading - ? "Loading configuration details..." - : configDetail?.description || - configDetail?.commitMessage || - "No description available for this configuration."} -
- -
- {childRun.total_items} items - {childRun.updated_at && ( - {formatRelativeTime(childRun.updated_at)} - )} - {childRun.config_id && ( - - ID {childRun.config_id.slice(0, 8)} - - )} -
- {childRun.prefilter_total_rows != null && ( -
- Prefilter: {childRun.prefilter_total_passed ?? 0}/ - {childRun.prefilter_total_rows} passed - {childRun.prefilter_total_rejected != null && - childRun.prefilter_total_rejected > 0 && ( - - · {childRun.prefilter_total_rejected} rejected - - )} -
- )} - - - - {configError && ( -
- {configError} -
- )} - {isFailedChild && childRun.error_message && ( -
- {childRun.error_message} -
- )} -
- - -
-
- {isCompletedChild && ( - onFetchRunColumns(childRun.id)} - onSave={async (cfg: PostProcessingConfig) => { - await onSavePostProcessing(childRun.id, cfg); - toast.success( - "Post-processing saved. Re-open preview to see updated results.", - ); - }} - /> - )} -
- ); -} diff --git a/app/components/assessment/ChildRunActions.tsx b/app/components/assessment/ChildRunActions.tsx deleted file mode 100644 index 53289a49..00000000 --- a/app/components/assessment/ChildRunActions.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import { Button } from "@/app/components/ui"; -import { EyeIcon } from "@/app/components/icons"; -import DownloadDropdown from "./DownloadDropdown"; -import { - formatStatusLabel, - getResultTone, - hasViewableResults, - isCompletedStatus, - isFailedStatus, -} from "@/app/lib/assessment/results"; -import { STATUS_BADGE_CLASSES } from "@/app/lib/assessment/constants"; -import type { - AssessmentChildRun, - ExportFormat, -} from "@/app/lib/types/assessment"; - -interface ChildRunActionsProps { - childRun: AssessmentChildRun; - previewLabel: string; - isResuming: boolean; - isRerunning: boolean; - downloadingId: string | null; - onPreview: (runId: number, label: string) => void; - onRunDownload: (runId: number, format: ExportFormat) => void; - onResume: (run: AssessmentChildRun) => void; - onRerun: (run: AssessmentChildRun) => void; -} - -export default function ChildRunActions({ - childRun, - previewLabel, - isResuming, - isRerunning, - downloadingId, - onPreview, - onRunDownload, - onResume, - onRerun, -}: ChildRunActionsProps) { - const childStatusClass = STATUS_BADGE_CLASSES[getResultTone(childRun.status)]; - const isFailedChild = isFailedStatus(childRun.status); - const isCompletedChild = isCompletedStatus(childRun.status); - const canPreview = hasViewableResults(childRun); - const canResume = Boolean(childRun.stage); - - return ( -
- - {formatStatusLabel(childRun.status)} - - {canPreview && ( - - )} - {isCompletedChild && ( - onRunDownload(childRun.id, fmt)} - loading={downloadingId === `run-${childRun.id}`} - /> - )} - {isFailedChild && canResume && ( - - )} - {isFailedChild && ( - - )} -
- ); -} diff --git a/app/components/assessment/ColumnMapperStep.tsx b/app/components/assessment/ColumnMapperStep.tsx deleted file mode 100644 index fd29e869..00000000 --- a/app/components/assessment/ColumnMapperStep.tsx +++ /dev/null @@ -1,401 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Button, Select } from "@/app/components/ui"; -import { - ASSESSMENT_ROLE_OPTION_MAP, - ASSESSMENT_ROLE_OPTIONS, - ATTACHMENT_FORMATS, -} from "@/app/lib/assessment/constants"; -import type { - Attachment, - ColumnConfig, - ColumnRole, - ColumnMapperStepProps, -} from "@/app/lib/types/assessment"; -import { buildColumnConfigs, colorMapping } from "@/app/lib/utils/assessment"; - -export default function ColumnMapperStep({ - columns, - columnMapping, - setColumnMapping, - onNext, - onBack, -}: ColumnMapperStepProps) { - const [columnConfigs, setColumnConfigs] = useState(() => - buildColumnConfigs(columns, columnMapping), - ); - - useEffect(() => { - setColumnConfigs(buildColumnConfigs(columns, columnMapping)); - }, [columns, columnMapping]); - - const updateRole = (index: number, role: ColumnRole) => { - if (role === "ground_truth") { - return; - } - - setColumnConfigs((prev) => { - const current = prev[index]; - const next = [...prev]; - - if (role !== "attachment") { - next[index] = { role }; - return next; - } - - next[index] = { - role, - attachmentType: current?.attachmentType || "mixed", - attachmentFormat: current?.attachmentFormat || "url", - }; - return next; - }); - }; - - const updateAttachmentType = ( - index: number, - type: "image" | "pdf" | "mixed", - ) => { - setColumnConfigs((prev) => { - const next = [...prev]; - next[index] = { - ...prev[index], - role: "attachment", - attachmentType: type, - attachmentFormat: "url", - }; - return next; - }); - }; - - const updateAttachmentFormat = (index: number, format: string) => { - setColumnConfigs((prev) => { - const next = [...prev]; - next[index] = { - ...prev[index], - role: "attachment", - attachmentFormat: format, - }; - return next; - }); - }; - - const patchAttachment = (index: number, patch: Partial) => { - setColumnConfigs((prev) => { - const next = [...prev]; - next[index] = { ...prev[index], role: "attachment", ...patch }; - return next; - }); - }; - - const handleNext = () => { - const textColumns: string[] = []; - const attachments: Attachment[] = []; - - columnConfigs.forEach((config, index) => { - const column = columns[index]; - if (!column) return; - - if (config.role === "text") { - textColumns.push(column); - } else if ( - config.role === "attachment" && - config.attachmentType && - config.attachmentFormat - ) { - const attachment: Attachment = { - column, - type: config.attachmentType, - format: config.attachmentFormat as Attachment["format"], - }; - if (config.attachmentType === "mixed" && config.attachmentTypeColumn) { - const map: Record = {}; - const split = (s?: string) => - (s || "") - .split(",") - .map((v) => v.trim()) - .filter(Boolean); - split(config.attachmentImageValues).forEach( - (v) => (map[v] = "image"), - ); - split(config.attachmentPdfValues).forEach((v) => (map[v] = "pdf")); - if (Object.keys(map).length > 0) { - attachment.type_column = config.attachmentTypeColumn; - attachment.type_value_map = map; - } - } - attachments.push(attachment); - } - }); - - setColumnMapping({ textColumns, attachments, groundTruthColumns: [] }); - onNext(); - }; - - const mappedCount = columnConfigs.filter( - (config) => config.role !== "unmapped", - ).length; - const hasMappedColumn = columnConfigs.some( - (config) => config.role === "text" || config.role === "attachment", - ); - - return ( -
-
-
-
-

- Map Columns -

-

- Choose a role for each column. -

-
-
- {mappedCount}/{columns.length} mapped -
-
- - {columns.length === 0 ? ( -
-

- No columns found. -

-

- Go back and select a dataset first. -

-
- ) : ( -
- {columns.map((column, index) => { - const config = columnConfigs[index] || { - role: "unmapped" as ColumnRole, - }; - const activeOption = - ASSESSMENT_ROLE_OPTION_MAP[config.role] || - ASSESSMENT_ROLE_OPTION_MAP.unmapped; - const roleVisuals = colorMapping(activeOption.value); - - return ( -
-
-
-
-
- - - {column} - -
-
- -
- {ASSESSMENT_ROLE_OPTIONS.map((option) => { - const isGroundTruth = option.value === "ground_truth"; - const isActive = config.role === option.value; - return ( - - ); - })} -
-
- - {config.role === "attachment" && ( - <> -
- -
- - {(config.attachmentType || "mixed") === "mixed" && ( -
- - Mixed: pick a column whose value tells each - row's type, then list which values mean image - vs PDF. - - - -
- )} -
- )} - - )} -
-
- ); - })} -
- )} -
- -
-
- - -
- - {hasMappedColumn - ? "Ready to continue." - : "Map at least one Text or Attachment column."} - - -
-
-
-
- ); -} diff --git a/app/components/assessment/ConfigPanel.tsx b/app/components/assessment/ConfigPanel.tsx deleted file mode 100644 index ad3edabe..00000000 --- a/app/components/assessment/ConfigPanel.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import { Button } from "@/app/components/ui"; -import { DatabaseIcon } from "@/app/components/icons"; -import { ASSESSMENT_CONFIG_STEPS } from "@/app/lib/assessment/constants"; -import type { ConfigPanelProps } from "@/app/lib/types/assessment"; -import ColumnMapperStep from "./ColumnMapperStep"; -import PrefilterStep from "./PrefilterStep"; -import PostProcessingStep from "./PostProcessingStep"; -import PromptAndConfigStep from "./PromptAndConfigStep"; -import ReviewStep from "./ReviewStep"; -import Stepper from "./Stepper"; - -export default function ConfigPanel({ - canSubmitAssessment, - columns, - columnMapping, - completedSteps, - configStep, - configs, - datasetId, - experimentName, - formState, - hasDataset, - isSubmitting, - prefilterConfig, - outputSchema, - systemInstruction, - promptTemplate, - postProcessingConfig, - setPostProcessingConfig, - sampleRow, - setActiveTabToDatasets, - setColumnMapping, - setConfigStep, - setConfigs, - setExperimentName, - setPrefilterConfig, - setOutputSchema, - setSystemInstruction, - setPromptTemplate, - submitBlockerMessage, - onSubmit, - onStepComplete, -}: ConfigPanelProps) { - if (!hasDataset) { - return ( -
-
- -

- No dataset selected -

-

- Select a dataset first from the Datasets tab -

- -
-
- ); - } - - return ( - <> - -
-
- onStepComplete(1)} - onBack={setActiveTabToDatasets} - /> -
- -
- a.column)} - prefilterConfig={prefilterConfig} - setPrefilterConfig={setPrefilterConfig} - onNext={() => onStepComplete(2)} - onBack={() => setConfigStep(1)} - /> -
- -
- onStepComplete(3)} - onBack={() => setConfigStep(2)} - /> -
- -
- onStepComplete(4)} - onBack={() => setConfigStep(3)} - /> -
- -
- setConfigStep(4)} - onEditStep={setConfigStep} - /> -
-
- - ); -} diff --git a/app/components/assessment/DatasetsTab.tsx b/app/components/assessment/DatasetsTab.tsx deleted file mode 100644 index b45fd705..00000000 --- a/app/components/assessment/DatasetsTab.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client"; - -import { Button, Modal } from "@/app/components/ui"; -import { useAssessmentDatasetsTab } from "@/app/hooks/useAssessmentDatasetsTab"; -import type { DatasetsTabProps } from "@/app/lib/types/assessment"; -import { WarningIcon } from "@/app/components/icons"; -import DataViewModal from "@/app/components/assessment/DataViewModal"; -import CreatePanel from "@/app/components/assessment/datasets/CreatePanel"; -import DatasetList from "@/app/components/assessment/datasets/DatasetList"; - -export default function DatasetsTab(props: DatasetsTabProps) { - const { datasetId, onNext } = props; - const { - datasets, - isLoading, - isLoadingColumns, - viewingId, - canProceed, - datasetName, - datasetDescription, - uploadedFile, - isDragging, - isUploading, - fileInputRef, - viewModalData, - confirmDeleteId, - deletingId, - datasetPendingDelete, - setDatasetName, - setDatasetDescription, - setUploadedFile, - setIsDragging, - setConfirmDeleteId, - setViewModalData, - handleFileSelect, - resetForm, - handleCreateDataset, - handleDatasetSelect, - handleViewDataset, - handleDeleteDataset, - handleDrop, - } = useAssessmentDatasetsTab(props); - - return ( -
- - - { - event.preventDefault(); - setIsDragging(true); - }} - onDragLeave={() => setIsDragging(false)} - onDrop={handleDrop} - onRemoveFile={() => { - setUploadedFile(null); - if (fileInputRef.current) fileInputRef.current.value = ""; - }} - onResetForm={resetForm} - onCreateDataset={handleCreateDataset} - /> - - {viewModalData && ( - setViewModalData(null)} - /> - )} - - {confirmDeleteId !== null && ( - setConfirmDeleteId(null)} - maxWidth="max-w-md" - maxHeight="max-h-[90vh]" - showClose={false} - > -
-
-
- - - -
-
-

- Delete dataset -

-

- Are you sure you want to delete -

-

- {datasetPendingDelete?.dataset_name}? -

-

- This action cannot be undone. -

-
-
-
-
- - -
-
- )} -
- ); -} diff --git a/app/components/assessment/DownloadDropdown.tsx b/app/components/assessment/DownloadDropdown.tsx deleted file mode 100644 index 201e4168..00000000 --- a/app/components/assessment/DownloadDropdown.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { createPortal } from "react-dom"; -import { Button, Loader } from "@/app/components/ui"; -import { ChevronDownIcon, DownloadIcon } from "@/app/components/icons"; -import { DOWNLOAD_MENU_WIDTH } from "@/app/lib/assessment/constants"; -import type { ExportFormat } from "@/app/lib/types/assessment"; - -interface DownloadDropdownProps { - onDownload: (format: ExportFormat) => void; - disabled?: boolean; - loading?: boolean; -} - -export default function DownloadDropdown({ - onDownload, - disabled, - loading, -}: DownloadDropdownProps) { - const [open, setOpen] = useState(false); - const triggerRef = useRef(null); - const menuRef = useRef(null); - const [rect, setRect] = useState<{ top: number; left: number } | null>(null); - - // Position the portal menu under the trigger, right-aligned. - useLayoutEffect(() => { - if (!open || !triggerRef.current) return; - const r = triggerRef.current.getBoundingClientRect(); - setRect({ top: r.bottom + 4, left: r.right - DOWNLOAD_MENU_WIDTH }); - }, [open]); - - useEffect(() => { - if (!open) return; - const handlePointer = (e: MouseEvent) => { - const t = e.target as Node; - if (!triggerRef.current?.contains(t) && !menuRef.current?.contains(t)) { - setOpen(false); - } - }; - // Fixed menu can't follow scroll — close instead of drift. - const handleScrollOrResize = () => setOpen(false); - document.addEventListener("mousedown", handlePointer); - window.addEventListener("scroll", handleScrollOrResize, true); - window.addEventListener("resize", handleScrollOrResize); - return () => { - document.removeEventListener("mousedown", handlePointer); - window.removeEventListener("scroll", handleScrollOrResize, true); - window.removeEventListener("resize", handleScrollOrResize); - }; - }, [open]); - - return ( -
- - {open && - rect && - typeof document !== "undefined" && - createPortal( -
- {( - [ - ["csv", "CSV File"], - ["xlsx", "Excel Sheet"], - ] as const - ).map(([fmt, label]) => ( - - ))} -
, - document.body, - )} -
- ); -} diff --git a/app/components/assessment/EvaluationsTab.tsx b/app/components/assessment/EvaluationsTab.tsx deleted file mode 100644 index d9372758..00000000 --- a/app/components/assessment/EvaluationsTab.tsx +++ /dev/null @@ -1,265 +0,0 @@ -"use client"; - -// Assessment Evaluations tab — shows run cards with status, retry, and CSV export. -import { RunsListSkeleton } from "@/app/components"; -import { Button, Select } from "@/app/components/ui"; -import { useToast } from "@/app/hooks/useToast"; -import { - DatabaseIcon, - ClipboardIcon, - RefreshIcon, -} from "@/app/components/icons"; -import DownloadDropdown from "./DownloadDropdown"; -import AssessmentChildRunCard from "./AssessmentChildRunCard"; -import { - canRetryStatus, - formatStatusLabel, - getResultTone, -} from "@/app/lib/assessment/results"; -import { - ASSESSMENT_CARD_CLASSES, - STATUS_BADGE_CLASSES, - STATUS_FILTER_OPTIONS, -} from "@/app/lib/assessment/constants"; -import { formatRelativeTime } from "@/app/lib/utils"; -import type { EvaluationsTabProps } from "@/app/lib/types/assessment"; -import useAssessmentResults from "@/app/hooks/useAssessmentResults"; - -export default function EvaluationsTab({ onForbidden }: EvaluationsTabProps) { - const toast = useToast(); - const { - assessments, - filteredRuns, - childRunsByAssessment, - configDetailsByKey, - configLoadingKeys, - configErrorKeys, - isLoading, - statusFilter, - setStatusFilter, - rerunningId, - resumingId, - retryingAssessmentId, - expandedId, - downloadingId, - loadAssessments, - handleExpand, - handleRetryAssessment, - handleRerun, - handleResume, - handlePreview, - handleAssessmentDownload, - handleRunDownload, - handleSavePostProcessing, - handleFetchRunColumns, - } = useAssessmentResults({ onForbidden, toast }); - - return ( -
-
-
-

- Evaluation Runs -

-
-