From 9a33a82e460f4abf24865c5e0170efbf5eb36e41 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Mon, 4 May 2026 22:08:46 +0300 Subject: [PATCH] Remove AI peer review hooks, service, and related types from the application. This aligns the frontend with recent backend changes, eliminating unused code and dependencies related to AI peer review functionality. --- hooks/useAiPeerReview.ts | 268 ------------------- services/aiPeerReview.service.ts | 156 ----------- types/aiPeerReview.ts | 437 +------------------------------ 3 files changed, 11 insertions(+), 850 deletions(-) delete mode 100644 hooks/useAiPeerReview.ts delete mode 100644 services/aiPeerReview.service.ts diff --git a/hooks/useAiPeerReview.ts b/hooks/useAiPeerReview.ts deleted file mode 100644 index e7dc04667..000000000 --- a/hooks/useAiPeerReview.ts +++ /dev/null @@ -1,268 +0,0 @@ -'use client'; - -import { useCallback, useEffect, useState } from 'react'; -import { - AiPeerReviewService, - type CreateProposalReviewPayload, - type RfpBriefRefreshPayload, -} from '@/services/aiPeerReview.service'; -import type { - ExecutiveSummaryResponse, - GrantComparisonResponse, - ProposalReview, - RfpSummary, - RfpSummaryMissing, -} from '@/types/aiPeerReview'; -import { isRfpSummaryMissing } from '@/types/aiPeerReview'; - -// ── useProposalReview ───────────────────────────────────────────────────────── - -interface UseProposalReviewState { - review: ProposalReview | null; - isLoading: boolean; - error: string | null; -} - -type RefetchProposalReviewFn = () => Promise; -type UseProposalReviewReturn = [UseProposalReviewState, RefetchProposalReviewFn]; - -/** - * Fetches a single AI proposal review by id (no polling). - */ -export function useProposalReview(reviewId: number | string | null): UseProposalReviewReturn { - const [review, setReview] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const fetch = useCallback(async () => { - if (reviewId == null) return; - try { - setIsLoading(true); - setError(null); - const detail = await AiPeerReviewService.getProposalReview(reviewId); - setReview(detail); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to fetch proposal review'; - setError(message); - setReview(null); - } finally { - setIsLoading(false); - } - }, [reviewId]); - - useEffect(() => { - if (reviewId != null) { - void fetch(); - } else { - setReview(null); - setError(null); - } - }, [reviewId, fetch]); - - return [{ review, isLoading, error }, fetch]; -} - -// ── useCreateProposalReview ─────────────────────────────────────────────────── - -interface UseCreateProposalReviewState { - review: ProposalReview | null; - isLoading: boolean; - error: string | null; -} - -type CreateProposalReviewFn = (payload: CreateProposalReviewPayload) => Promise; -type UseCreateProposalReviewReturn = [UseCreateProposalReviewState, CreateProposalReviewFn]; - -export function useCreateProposalReview(): UseCreateProposalReviewReturn { - const [review, setReview] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const createReview = useCallback(async (payload: CreateProposalReviewPayload) => { - setIsLoading(true); - setError(null); - try { - const response = await AiPeerReviewService.createProposalReview(payload); - setReview(response); - return response; - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to create proposal review'; - setError(message); - throw err; - } finally { - setIsLoading(false); - } - }, []); - - return [{ review, isLoading, error }, createReview]; -} - -// ── useGrantComparison ──────────────────────────────────────────────────────── - -interface UseGrantComparisonState { - data: GrantComparisonResponse | null; - isLoading: boolean; - error: string | null; -} - -type RefetchGrantComparisonFn = () => Promise; -type UseGrantComparisonReturn = [UseGrantComparisonState, RefetchGrantComparisonFn]; - -export function useGrantComparison(grantId: number | string | null): UseGrantComparisonReturn { - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const fetch = useCallback(async () => { - if (grantId == null) return; - try { - setIsLoading(true); - setError(null); - const response = await AiPeerReviewService.getGrantComparison(grantId); - setData(response); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to fetch grant comparison'; - setError(message); - setData(null); - } finally { - setIsLoading(false); - } - }, [grantId]); - - useEffect(() => { - if (grantId != null) { - void fetch(); - } else { - setData(null); - setError(null); - } - }, [grantId, fetch]); - - return [{ data, isLoading, error }, fetch]; -} - -// ── useRfpSummary ───────────────────────────────────────────────────────────── - -interface UseRfpSummaryState { - summary: RfpSummary | RfpSummaryMissing | null; - isLoading: boolean; - error: string | null; - /** True when API returned the 404 "no row yet" envelope (see `isRfpSummaryMissing`). */ - isMissing: boolean; -} - -type RefetchRfpSummaryFn = () => Promise; -type UseRfpSummaryReturn = [UseRfpSummaryState, RefetchRfpSummaryFn]; - -export function useRfpSummary(grantId: number | string | null): UseRfpSummaryReturn { - const [summary, setSummary] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const fetch = useCallback(async () => { - if (grantId == null) return; - try { - setIsLoading(true); - setError(null); - const response = await AiPeerReviewService.getRfpSummary(grantId); - setSummary(response); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to fetch RFP summary'; - setError(message); - setSummary(null); - } finally { - setIsLoading(false); - } - }, [grantId]); - - useEffect(() => { - if (grantId != null) { - void fetch(); - } else { - setSummary(null); - setError(null); - } - }, [grantId, fetch]); - - const isMissing = summary != null && isRfpSummaryMissing(summary); - - return [{ summary, isLoading, error, isMissing }, fetch]; -} - -// ── useRefreshRfpSummary ────────────────────────────────────────────────────── - -interface UseRefreshRfpSummaryState { - summary: RfpSummary | null; - isLoading: boolean; - error: string | null; -} - -type RefreshRfpSummaryFn = ( - grantId: number | string, - payload?: RfpBriefRefreshPayload -) => Promise; - -type UseRefreshRfpSummaryReturn = [UseRefreshRfpSummaryState, RefreshRfpSummaryFn]; - -export function useRefreshRfpSummary(): UseRefreshRfpSummaryReturn { - const [summary, setSummary] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const refresh = useCallback(async (gid: number | string, payload?: RfpBriefRefreshPayload) => { - setIsLoading(true); - setError(null); - try { - const response = await AiPeerReviewService.refreshRfpSummary(gid, payload); - setSummary(response); - return response; - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to refresh RFP summary'; - setError(message); - throw err; - } finally { - setIsLoading(false); - } - }, []); - - return [{ summary, isLoading, error }, refresh]; -} - -// ── useGenerateExecutiveSummary ─────────────────────────────────────────────── - -interface UseGenerateExecutiveSummaryState { - result: ExecutiveSummaryResponse | null; - isLoading: boolean; - error: string | null; -} - -type GenerateExecutiveSummaryFn = (grantId: number | string) => Promise; -type UseGenerateExecutiveSummaryReturn = [ - UseGenerateExecutiveSummaryState, - GenerateExecutiveSummaryFn, -]; - -export function useGenerateExecutiveSummary(): UseGenerateExecutiveSummaryReturn { - const [result, setResult] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const generate = useCallback(async (grantId: number | string) => { - setIsLoading(true); - setError(null); - try { - const response = await AiPeerReviewService.generateExecutiveSummary(grantId); - setResult(response); - return response; - } catch (err: unknown) { - const message = - err instanceof Error ? err.message : 'Failed to generate executive comparison summary'; - setError(message); - throw err; - } finally { - setIsLoading(false); - } - }, []); - - return [{ result, isLoading, error }, generate]; -} diff --git a/services/aiPeerReview.service.ts b/services/aiPeerReview.service.ts deleted file mode 100644 index bdd60ae12..000000000 --- a/services/aiPeerReview.service.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ApiClient } from './client'; -import { ApiError } from './types'; -import { - transformExecutiveSummary, - transformGrantComparisonResponse, - transformProposalReview, - transformRfpSummary, - type ExecutiveSummaryResponse, - type GrantComparisonResponse, - type ProposalReview, - type RfpSummary, - type RfpSummaryMissing, -} from '@/types/aiPeerReview'; - -// ── API enums / unions ───────────────────────────── - -export type ReviewStatus = 'pending' | 'processing' | 'completed' | 'failed'; - -export type OverallRating = 'excellent' | 'good' | 'poor'; - -export type OverallConfidence = 'High' | 'Medium' | 'Low'; - -export type CategoryScoreLabel = 'High' | 'Medium' | 'Low' | 'N/A'; - -export type ItemDecisionValue = 'Yes' | 'No' | 'Partial' | 'N/A'; - -export type RfpStatus = 'pending' | 'processing' | 'completed' | 'failed'; - -export type EditorialCategoryScore = 'high' | 'medium' | 'low'; - -export type CategoryKey = - | 'overall_impact' - | 'importance_significance_innovation' - | 'rigor_and_feasibility' - | 'additional_review_criteria' - | 'funding_opportunity_fit' - | 'methods_rigor' - | 'statistical_analysis_plan' - | 'feasibility_and_execution' - | 'scientific_impact' - | 'clinical_or_translational_impact' - | 'societal_and_broader_impact'; - -// ── Request payloads ────────────────────────────── - -export interface CreateProposalReviewPayload { - unified_document_id: number; - grant_id?: number | null; -} - -export interface RfpBriefRefreshPayload { - force?: boolean; -} - -const BASE_PATH = '/api/ai_peer_review'; - -const NO_RFP_SUMMARY_DETAIL = 'No RFP summary yet.'; - -function isRfpSummaryNotFoundBody(body: Record): boolean { - return body.detail === NO_RFP_SUMMARY_DETAIL; -} - -export class AiPeerReviewService { - /** - * POST /api/ai_peer_review/proposal-review/ - */ - static async createProposalReview(payload: CreateProposalReviewPayload): Promise { - const raw = await ApiClient.post>( - `${BASE_PATH}/proposal-review/`, - payload - ); - return transformProposalReview(raw); - } - - /** - * GET /api/ai_peer_review/proposal-review// - */ - static async getProposalReview(reviewId: number | string): Promise { - const raw = await ApiClient.get>( - `${BASE_PATH}/proposal-review/${reviewId}/` - ); - return transformProposalReview(raw); - } - - /** - * GET /api/ai_peer_review/proposal-review/grant// - */ - static async getGrantComparison(grantId: number | string): Promise { - const raw = await ApiClient.get>( - `${BASE_PATH}/proposal-review/grant/${grantId}/` - ); - return transformGrantComparisonResponse(raw); - } - - /** - * GET /api/ai_peer_review/rfp// - * 404 with body `{ detail: "No RFP summary yet.", grant_id, status: null, ... }` → RfpSummaryMissing - */ - static async getRfpSummary(grantId: number | string): Promise { - try { - const raw = await ApiClient.get>(`${BASE_PATH}/rfp/${grantId}/`); - return transformRfpSummary(raw); - } catch (error) { - if (error instanceof ApiError && error.status === 404) { - const body = (error.errors ?? {}) as Record; - if (isRfpSummaryNotFoundBody(body)) { - const id = body.grant_id; - const gid = - typeof id === 'number' && Number.isFinite(id) - ? id - : typeof grantId === 'number' - ? grantId - : Number(grantId); - return { - grantId: Number.isFinite(gid) ? gid : 0, - status: null, - summaryContent: typeof body.summary_content === 'string' ? body.summary_content : '', - executiveComparisonSummary: - typeof body.executive_comparison_summary === 'string' - ? body.executive_comparison_summary - : '', - detail: typeof body.detail === 'string' ? body.detail : NO_RFP_SUMMARY_DETAIL, - }; - } - } - throw error; - } - } - - /** - * POST /api/ai_peer_review/rfp// - */ - static async refreshRfpSummary( - grantId: number | string, - payload?: RfpBriefRefreshPayload - ): Promise { - const raw = await ApiClient.post>( - `${BASE_PATH}/rfp/${grantId}/`, - payload ?? {} - ); - return transformRfpSummary(raw); - } - - /** - * POST /api/ai_peer_review/rfp//executive-summary/ - */ - static async generateExecutiveSummary( - grantId: number | string - ): Promise { - const raw = await ApiClient.post>( - `${BASE_PATH}/rfp/${grantId}/executive-summary/`, - {} - ); - return transformExecutiveSummary(raw); - } -} diff --git a/types/aiPeerReview.ts b/types/aiPeerReview.ts index 5c77654fc..7d1b489c0 100644 --- a/types/aiPeerReview.ts +++ b/types/aiPeerReview.ts @@ -1,114 +1,8 @@ -import { createTransformer } from './transformer'; -import type { - CategoryKey, - CategoryScoreLabel, - EditorialCategoryScore, - ItemDecisionValue, - OverallConfidence, - OverallRating, - ReviewStatus, - RfpStatus, -} from '@/services/aiPeerReview.service'; +import { createTransformer, type BaseTransformed } from './transformer'; -export type { - CategoryKey, - CategoryScoreLabel, - EditorialCategoryScore, - ItemDecisionValue, - OverallConfidence, - OverallRating, - ReviewStatus, - RfpStatus, -} from '@/services/aiPeerReview.service'; +export type ReviewStatus = 'pending' | 'processing' | 'completed' | 'failed'; -// ── Constants ─────────────────────────────────── - -export const CATEGORY_KEYS: readonly CategoryKey[] = [ - 'overall_impact', - 'importance_significance_innovation', - 'rigor_and_feasibility', - 'additional_review_criteria', - 'funding_opportunity_fit', - 'methods_rigor', - 'statistical_analysis_plan', - 'feasibility_and_execution', - 'scientific_impact', - 'clinical_or_translational_impact', - 'societal_and_broader_impact', -]; - -export const CATEGORY_ITEMS: Record = { - overall_impact: ['novelty', 'rigor', 'reproducibility', 'field_impact'], - importance_significance_innovation: [ - 'hypothesis_strength', - 'work_novelty', - 'question_importance', - 'advances_knowledge', - ], - rigor_and_feasibility: [ - 'study_design', - 'methodology', - 'timeline_feasibility', - 'team_qualifications', - 'research_environment', - 'budget_appropriateness_justification', - ], - additional_review_criteria: [ - 'human_or_animal_protections', - 'resubmission_critiques_addressed', - 'open_science_adherence', - 'ai_use_disclosed', - 'conflicts_of_interest_disclosed', - ], - funding_opportunity_fit: ['fit_modality', 'fit_aims', 'fit_deliverables', 'fit_scope'], - methods_rigor: [ - 'methods_detail', - 'parameters_specified', - 'controls_defined', - 'model_choice_justified', - 'outcomes_linked_to_aims', - ], - statistical_analysis_plan: [ - 'analysis_present', - 'power_analysis', - 'multiple_comparisons', - 'metrics_defined', - 'analysis_matches_design', - ], - feasibility_and_execution: [ - 'recruitment_feasible', - 'procedures_feasible', - 'timeline_milestones', - 'team_environment', - 'ethics_data_quality', - ], - scientific_impact: ['advances_understanding', 'generalizability', 'opens_new_directions'], - clinical_or_translational_impact: ['clinical_pathway', 'unmet_need', 'milestones_defined'], - societal_and_broader_impact: [ - 'societal_challenge', - 'public_communication', - 'commercial_potential', - ], -}; - -export const OPTIONAL_CATEGORIES: readonly CategoryKey[] = [ - 'statistical_analysis_plan', - 'clinical_or_translational_impact', - 'societal_and_broader_impact', -]; - -// ── Domain types ──────────────────────────────────────────────── - -export interface ItemDecision { - decision: ItemDecisionValue; - justification: string; -} - -export interface CategoryBlock { - score: CategoryScoreLabel; - rationale: string; - items: Record; -} +export type OverallRating = 'excellent' | 'good' | 'poor'; export type KeyInsightItemType = 'strength' | 'weakness'; @@ -125,100 +19,13 @@ export interface KeyInsightData { items: KeyInsightItem[]; } -export interface ProposalReviewResultData { - overallSummary: string; - overallRating: OverallRating | null; - overallRationale: string; - overallConfidence: OverallConfidence | null; - overallScoreNumeric: number | null; - fatalFlaws: string[]; - categories: Partial>; -} - -export interface EditorialCategory { - categoryCode: CategoryKey; - score: EditorialCategoryScore; -} - -export interface EditorialFeedback { - id: number; - unifiedDocumentId: number; - createdById: number | null; - updatedById: number | null; - categories: EditorialCategory[]; - expertInsights: string; - createdDate: string; - updatedDate: string; -} - -export interface ProposalReview { - id: number; - unifiedDocumentId: number; - grantId: number | null; - createdById: number | null; +type ProposalReviewFields = { status: ReviewStatus; overallRating: OverallRating | null; - overallRationale: string; - overallConfidence: OverallConfidence | null; - overallScoreNumeric: number | null; - resultData: ProposalReviewResultData | null; keyInsight: KeyInsightData | null; - errorMessage: string; - progress: number; - currentStep: string; - llmModel: string; - processingTime: number | null; - createdDate: string; - updatedDate: string; - editorialFeedback: EditorialFeedback | null; - alreadyExists?: boolean; -} - -export interface GrantComparisonRow { - unifiedDocumentId: number; - proposalTitle: string; - reviewId: number | null; - status: ReviewStatus | null; - overallRating: OverallRating | null; - overallScoreNumeric: number | null; - categories: Record | null; - editorialFeedback: EditorialFeedback | null; -} - -export interface GrantComparisonResponse { - grantId: number; - proposals: GrantComparisonRow[]; - executiveSummary: string; -} - -export interface RfpSummary { - id: number; - grantId: number; - status: RfpStatus; - summaryContent: string; - executiveComparisonSummary: string; - executiveComparisonUpdatedDate: string | null; - errorMessage: string; - llmModel: string; - processingTime: number | null; - createdDate: string; - updatedDate: string; - alreadyExists?: boolean; -} - -export interface RfpSummaryMissing { - grantId: number; - status: null; - summaryContent: string; - executiveComparisonSummary: string; - detail: string; -} +}; -export interface ExecutiveSummaryResponse { - grantId: number; - executiveSummary: string; - updatedDate: string; -} +export type ProposalReview = ProposalReviewFields & BaseTransformed; // ── Helpers ───────────────────────────────────────────────────────────────── @@ -232,38 +39,6 @@ function str(v: unknown, fallback = ''): string { return typeof v === 'string' ? v : fallback; } -function transformItemDecision(raw: Record | null | undefined): ItemDecision { - if (!raw || typeof raw !== 'object') { - return { decision: 'N/A', justification: '' }; - } - const decision = (raw.decision ?? raw.item_decision) as ItemDecisionValue | undefined; - const allowed: ItemDecisionValue[] = ['Yes', 'No', 'Partial', 'N/A']; - return { - decision: decision && allowed.includes(decision) ? decision : 'N/A', - justification: str(raw.justification ?? raw.justification_text), - }; -} - -function transformCategoryBlock( - raw: Record | null | undefined -): CategoryBlock | null { - if (!raw || typeof raw !== 'object') return null; - const score = raw.score as CategoryScoreLabel | undefined; - const allowedScores: CategoryScoreLabel[] = ['High', 'Medium', 'Low', 'N/A']; - const itemsRaw = raw.items; - const items: Record = {}; - if (itemsRaw && typeof itemsRaw === 'object' && !Array.isArray(itemsRaw)) { - for (const [key, val] of Object.entries(itemsRaw as Record)) { - items[key] = transformItemDecision(val as Record); - } - } - return { - score: score && allowedScores.includes(score) ? score : 'N/A', - rationale: str(raw.rationale), - items, - }; -} - function transformKeyInsightItemRow(raw: Record): KeyInsightItem | null { const itemTypeRaw = raw.item_type; if (itemTypeRaw !== 'strength' && itemTypeRaw !== 'weakness') return null; @@ -290,214 +65,24 @@ export function transformKeyInsight(raw: unknown): KeyInsightData | null { items.sort((a, b) => a.order - b.order || a.id - b.id); const tldr = str(o.tldr); if (items.length === 0 && !tldr) return null; - return { tldr, items }; -} - -export function transformResultData(raw: unknown): ProposalReviewResultData | null { - if (raw == null || typeof raw !== 'object') return null; - const o = raw as Record; - const keys = Object.keys(o); - if (keys.length === 0) return null; - const categoriesRaw = o.categories; - const categories: Partial> = {}; - if (categoriesRaw && typeof categoriesRaw === 'object' && !Array.isArray(categoriesRaw)) { - const categoriesRecord = categoriesRaw as Record; - for (const key of Object.keys(categoriesRecord)) { - if (!(CATEGORY_KEYS as readonly string[]).includes(key)) continue; - const block = transformCategoryBlock(categoriesRecord[key] as Record); - if (block) categories[key as CategoryKey] = block; - } - } - - const or = o.overall_rating as OverallRating | undefined; - const ratings: OverallRating[] = ['excellent', 'good', 'poor']; - const oc = o.overall_confidence as OverallConfidence | undefined; - const confidences: OverallConfidence[] = ['High', 'Medium', 'Low']; - - return { - overallSummary: str(o.overall_summary), - overallRating: or && ratings.includes(or) ? or : null, - overallRationale: str(o.overall_rationale), - overallConfidence: oc && confidences.includes(oc) ? oc : null, - overallScoreNumeric: numOrNull(o.overall_score_numeric), - fatalFlaws: Array.isArray(o.fatal_flaws) - ? (o.fatal_flaws as unknown[]).map((x) => String(x)) - : [], - categories, - }; -} - -function transformEditorialCategory(raw: Record): EditorialCategory | null { - const code = raw.category_code as string | undefined; - if (!code || !(CATEGORY_KEYS as readonly string[]).includes(code)) return null; - const scoreRaw = (raw.score as string | undefined)?.toLowerCase(); - const scores: EditorialCategoryScore[] = ['high', 'medium', 'low']; - const score = - scoreRaw && scores.includes(scoreRaw as EditorialCategoryScore) - ? (scoreRaw as EditorialCategoryScore) - : 'low'; - return { categoryCode: code as CategoryKey, score }; -} - -export function transformEditorialFeedback(raw: unknown): EditorialFeedback | null { - if (raw == null || typeof raw !== 'object') return null; - const o = raw as Record; - const catsRaw = o.categories; - const categories: EditorialCategory[] = []; - if (Array.isArray(catsRaw)) { - for (const row of catsRaw) { - if (row && typeof row === 'object') { - const c = transformEditorialCategory(row as Record); - if (c) categories.push(c); - } - } - } - return { - id: numOrNull(o.id) ?? 0, - unifiedDocumentId: numOrNull(o.unified_document_id) ?? 0, - createdById: numOrNull(o.created_by_id), - updatedById: numOrNull(o.updated_by_id), - categories, - expertInsights: str(o.expert_insights), - createdDate: str(o.created_date), - updatedDate: str(o.updated_date), - }; + return { tldr, items }; } const reviewStatuses: ReviewStatus[] = ['pending', 'processing', 'completed', 'failed']; -const rfpStatuses: RfpStatus[] = ['pending', 'processing', 'completed', 'failed']; function parseReviewStatus(v: unknown): ReviewStatus { return reviewStatuses.includes(v as ReviewStatus) ? (v as ReviewStatus) : 'pending'; } -function parseRfpStatus(v: unknown): RfpStatus { - return rfpStatuses.includes(v as RfpStatus) ? (v as RfpStatus) : 'pending'; -} - function parseOverallRating(v: unknown): OverallRating | null { if (v == null || v === '') return null; const r = String(v).toLowerCase() as OverallRating; return ['excellent', 'good', 'poor'].includes(r) ? r : null; } -function parseOverallConfidence(v: unknown): OverallConfidence | null { - if (v == null || v === '') return null; - const c = v as OverallConfidence; - return ['High', 'Medium', 'Low'].includes(c) ? c : null; -} - -export const transformProposalReview = createTransformer((raw) => { - const status = parseReviewStatus(raw.status); - const rd = transformResultData(raw.result_data); - const keyInsight = transformKeyInsight(raw.key_insight); - - return { - id: numOrNull(raw.id) ?? 0, - unifiedDocumentId: numOrNull(raw.unified_document_id) ?? 0, - grantId: numOrNull(raw.grant_id), - createdById: numOrNull(raw.created_by_id), - status, - overallRating: parseOverallRating(raw.overall_rating), - overallRationale: str(raw.overall_rationale), - overallConfidence: parseOverallConfidence(raw.overall_confidence), - overallScoreNumeric: numOrNull(raw.overall_score_numeric), - resultData: rd, - keyInsight, - errorMessage: str(raw.error_message), - progress: Number.isFinite(Number(raw.progress)) ? Number(raw.progress) : 0, - currentStep: str(raw.current_step), - llmModel: str(raw.llm_model), - processingTime: (() => { - const p = raw.processing_time; - if (p == null || p === '') return null; - const n = Number(p); - return Number.isFinite(n) ? n : null; - })(), - createdDate: str(raw.created_date), - updatedDate: str(raw.updated_date), - editorialFeedback: transformEditorialFeedback(raw.editorial_feedback), - }; -}); - -function transformComparisonCategories( - raw: unknown -): Record | null { - if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) return null; - const out = {} as Record; - const allowed: CategoryScoreLabel[] = ['High', 'Medium', 'Low', 'N/A']; - for (const key of CATEGORY_KEYS) { - const v = (raw as Record)[key]; - if (v == null) { - out[key] = null; - } else if ( - typeof v === 'string' && - (allowed.includes(v as CategoryScoreLabel) || v === 'N/A') - ) { - out[key] = v as CategoryScoreLabel; - } else { - out[key] = null; - } - } - return out; -} - -export const transformGrantComparisonRow = createTransformer((raw) => { - const st = raw.status; - return { - unifiedDocumentId: numOrNull(raw.unified_document_id) ?? 0, - proposalTitle: str(raw.proposal_title), - reviewId: numOrNull(raw.review_id), - status: st == null || st === '' ? null : parseReviewStatus(st), - overallRating: parseOverallRating(raw.overall_rating), - overallScoreNumeric: numOrNull(raw.overall_score_numeric), - categories: transformComparisonCategories(raw.categories), - editorialFeedback: transformEditorialFeedback(raw.editorial_feedback), - }; -}); - -export const transformGrantComparisonResponse = createTransformer( - (raw) => ({ - grantId: numOrNull(raw.grant_id) ?? 0, - proposals: Array.isArray(raw.proposals) - ? raw.proposals.map((p: any) => transformGrantComparisonRow(p)) - : [], - executiveSummary: str(raw.executive_summary), - }) -); - -export const transformRfpSummary = createTransformer((raw) => ({ - id: numOrNull(raw.id) ?? 0, - grantId: numOrNull(raw.grant_id) ?? 0, - status: parseRfpStatus(raw.status), - summaryContent: str(raw.summary_content), - executiveComparisonSummary: str(raw.executive_comparison_summary), - executiveComparisonUpdatedDate: - raw.executive_comparison_updated_date != null && raw.executive_comparison_updated_date !== '' - ? str(raw.executive_comparison_updated_date) - : null, - errorMessage: str(raw.error_message), - llmModel: str(raw.llm_model), - processingTime: (() => { - const p = raw.processing_time; - if (p == null || p === '') return null; - const n = Number(p); - return Number.isFinite(n) ? n : null; - })(), - createdDate: str(raw.created_date), - updatedDate: str(raw.updated_date), - alreadyExists: raw.already_exists !== undefined ? Boolean(raw.already_exists) : undefined, +export const transformProposalReview = createTransformer((raw) => ({ + status: parseReviewStatus(raw.status), + overallRating: parseOverallRating(raw.overall_rating), + keyInsight: transformKeyInsight(raw.key_insight), })); - -export const transformExecutiveSummary = createTransformer( - (raw) => ({ - grantId: numOrNull(raw.grant_id) ?? 0, - executiveSummary: str(raw.executive_summary), - updatedDate: str(raw.updated_date), - }) -); - -export function isRfpSummaryMissing(x: RfpSummary | RfpSummaryMissing): x is RfpSummaryMissing { - return x.status === null; -}