import { delay, demoUser, } from "@/lib/frontend-demo-data"; import { getLocalPreviousPaperAnalysis, getLocalPreviousPapers, getLocalPreviousQuestions, saveLocalPreviousPaper, } from "@/lib/frontend-local-store"; import { apiGet, apiPost, apiUpload } from "@/lib/api/client"; import { shouldUseDemoData, withDemoFallback } from "@/lib/api/mode"; import type { LocalOfflinePreviousPaper, PreviousPaper, PreviousPaperAnalysisResponse, PreviousPaperUploadInput, PreviousQuestion, PreviousQuestionExtractRequest, PreviousQuestionExtractResponse, StructuredPreviousPaperAnalyzeRequest, } from "@/lib/api/types"; export type PYQDiscoveryCandidate = { title: string; url: string; snippet: string; year: number | null; subject: string | null; board: string | null; class_level: string | null; source_domain: string; source_title: string | null; confidence_score: number; official_source: boolean; file_hash: string | null; notes: string | null; warnings: string[]; }; export type PYQDiscoverySearchPayload = { board?: string | null; class_level?: string | null; subject?: string | null; year_start?: number | null; year_end?: number | null; manual_urls?: string[]; }; export type PYQDiscoverySearchResponse = { enabled: boolean; provider: string; configured: boolean; message: string; candidates: PYQDiscoveryCandidate[]; }; export type PYQDiscoveryImportResponse = { id: string; title: string; year: number | null; subject: string | null; verification_status: string; confidence_score: number | null; official_source: boolean; source_url: string | null; deduped: boolean; }; export type PyqAnalysisRequest = { subject?: string | null; board?: string | null; class_level?: string | null; exam?: string | null; chapter?: string | null; topic?: string | null; paper_ids?: string[]; source_id?: string | null; years?: number[]; }; export type PyqAnalysis = { available: boolean; status: "available" | "unavailable" | "awaiting_verification"; provenance: "live" | "sample"; message: string; disclaimer: "Signals, not guaranteed prediction."; subject: string | null; board: string | null; class_level?: string | null; repeated_topics: Array<{ topic: string; chapter: string; count: number; years: number[]; question_ids: string[]; }>; question_type_distribution: Array<{ type: string; count: number; percentage: number }>; marks_distribution: Array<{ marks: number; count: number; percentage: number }>; chapter_priorities: Array<{ chapter: string; total_questions: number; total_marks: number; topics: string[]; }>; evidence: Array<{ question_id: string; paper_id: string; source_id: string; paper_title: string; year: number | null; question_excerpt: string; marks: number | null; chapter: string; topic: string; source_url: string | null; }>; revision_plan: string[]; data_coverage: { papers: number; questions: number; years: number[] }; }; function domainFromUrl(value: string) { try { return new URL(value).hostname.replace(/^www\./, ""); } catch { return ""; } } function yearFromText(value: string) { const match = value.match(/\b(19|20)\d{2}\b/); return match ? Number(match[0]) : null; } function buildLocalDiscoveryFallback(payload: PYQDiscoverySearchPayload): PYQDiscoverySearchResponse { const candidates = (payload.manual_urls ?? []) .map((url): PYQDiscoveryCandidate | null => { const sourceDomain = domainFromUrl(url); if (!sourceDomain) return null; const year = yearFromText(url) ?? payload.year_end ?? null; const officialSource = /(dhsekerala|scertkerala|keralapareekshabhavan|pareekshabhavan|kerala\.gov)\./i.test(sourceDomain); return { title: `${payload.subject || "Previous paper"} ${year ?? ""}`.trim(), url, snippet: "Public PYQ link supplied for review before import.", year, subject: payload.subject ?? null, board: payload.board ?? null, class_level: payload.class_level ?? null, source_domain: sourceDomain, source_title: sourceDomain, confidence_score: officialSource ? 0.82 : 0.55, official_source: officialSource, file_hash: null, notes: officialSource ? "Official/public source domain detected." : "Review this source before using it for analysis.", warnings: [], }; }) .filter((candidate): candidate is PYQDiscoveryCandidate => Boolean(candidate)); return { enabled: false, provider: "disabled", configured: false, message: candidates.length ? "Review the public PYQ links below before importing them for analysis." : "Automatic PYQ discovery is not configured yet. Paste a public PYQ link, upload a paper, or broaden your search.", candidates, }; } function buildLocalQuestionExtraction(paperId: string): PreviousQuestionExtractResponse { const questions = getLocalPreviousQuestions(paperId); return { previous_paper_id: paperId, model_used: "DocDoe local preview", question_count: questions.length, questions, }; } export async function analyzePreviousPapers(payload: PyqAnalysisRequest) { return apiPost("/pyq/analyze", payload); } export function loadSamplePreviousPaperAnalysis(): PyqAnalysis { return { available: true, status: "available", provenance: "sample", message: "Example layout using clearly labelled sample evidence.", disclaimer: "Signals, not guaranteed prediction.", subject: "Biology", board: "Kerala HSE", class_level: "+2", repeated_topics: [{ topic: "Cell division", chapter: "Cell Cycle", count: 2, years: [2022, 2023], question_ids: ["sample-q-1", "sample-q-2"], }], question_type_distribution: [ { type: "short", count: 1, percentage: 50 }, { type: "long", count: 1, percentage: 50 }, ], marks_distribution: [ { marks: 2, count: 1, percentage: 50 }, { marks: 5, count: 1, percentage: 50 }, ], chapter_priorities: [{ chapter: "Cell Cycle", total_questions: 2, total_marks: 7, topics: ["Cell division"], }], evidence: [{ question_id: "sample-q-1", paper_id: "sample-paper-2022", source_id: "sample-paper-2022", paper_title: "Sample Biology Paper 2022", year: 2022, question_excerpt: "Compare mitosis and meiosis.", marks: 5, chapter: "Cell Cycle", topic: "Cell division", source_url: null, }], revision_plan: ["Revise Cell Cycle, then practise one short and one long answer."], data_coverage: { papers: 2, questions: 2, years: [2022, 2023] }, }; } export async function listPreviousPapers() { if (shouldUseDemoData()) return delay(getLocalPreviousPapers()); return withDemoFallback( () => apiGet("/previous-papers"), () => delay(getLocalPreviousPapers()), ); } export async function uploadPreviousPaper(input: PreviousPaperUploadInput) { if (shouldUseDemoData()) { const paper: LocalOfflinePreviousPaper = { id: `paper-local-${Date.now()}`, user_id: input.userId ?? demoUser.id, title: input.title || input.file.name.replace(/\.[^.]+$/, "") || "Uploaded previous paper", subject: input.subject || "Physics", syllabus: input.syllabus || "Indian Board Exams", year: input.year ?? new Date().getFullYear(), file_name: input.file.name, file_type: input.file.type || "PDF", file_path: "/demo/local-previous-paper", status: "ready", extracted_text: null, extraction_error: null, extracted_questions_json: [], created_at: new Date().toISOString(), }; return delay(saveLocalPreviousPaper(paper)); } const formData = new FormData(); formData.append("file", input.file); if (input.title) formData.append("title", input.title); if (input.subject) formData.append("subject", input.subject); if (input.syllabus) formData.append("syllabus", input.syllabus); if (input.year) formData.append("year", String(input.year)); return withDemoFallback( () => apiUpload("/previous-papers/upload", formData, { timeoutMs: 60000 }), async () => { const paper: LocalOfflinePreviousPaper = { id: `paper-local-${Date.now()}`, user_id: input.userId ?? demoUser.id, title: input.title || input.file.name.replace(/\.[^.]+$/, "") || "Uploaded previous paper", subject: input.subject || "Study", syllabus: input.syllabus || "Indian Board Exams", year: input.year ?? new Date().getFullYear(), file_name: input.file.name, file_type: input.file.type || "PDF", file_path: "/demo/local-previous-paper", status: "ready", extracted_text: null, extraction_error: null, extracted_questions_json: [], created_at: new Date().toISOString(), }; return saveLocalPreviousPaper(paper); }, ); } export async function listPreviousQuestions(paperId: string) { if (shouldUseDemoData(paperId)) { return delay(getLocalPreviousQuestions(paperId)); } return withDemoFallback( () => apiGet(`/previous-papers/${paperId}/questions`), () => delay(getLocalPreviousQuestions(paperId)), ); } export async function extractPreviousQuestions( paperId: string, payload: PreviousQuestionExtractRequest = {}, ) { if (shouldUseDemoData(paperId)) { void payload; return delay(buildLocalQuestionExtraction(paperId)); } return withDemoFallback( () => apiPost( `/previous-papers/${paperId}/extract-questions`, payload, ), () => delay(buildLocalQuestionExtraction(paperId)), ); } export async function analyzeStructuredPreviousPapers( payload: StructuredPreviousPaperAnalyzeRequest, ) { if (shouldUseDemoData()) { return delay( getLocalPreviousPaperAnalysis( payload.paper_ids ?? [], payload.subject, payload.syllabus, ), ); } return withDemoFallback( () => apiPost( "/previous-papers/analyze-structured", payload, ), () => delay( getLocalPreviousPaperAnalysis( payload.paper_ids ?? [], payload.subject, payload.syllabus, ), ), ); } export async function searchPYQDiscovery(payload: PYQDiscoverySearchPayload) { const fallback = buildLocalDiscoveryFallback(payload); return withDemoFallback( () => apiPost( "/pyq-discovery/search", payload, { timeoutMs: 30000 }, ), () => delay(fallback), ); } export async function importPYQDiscoveryCandidate(candidate: PYQDiscoveryCandidate) { const fallback = () => { const paper: LocalOfflinePreviousPaper = { id: `paper-local-${Date.now()}`, user_id: demoUser.id, title: candidate.title || "Imported previous paper", subject: candidate.subject || "Physics", syllabus: candidate.board || "Indian Board Exams", year: candidate.year ?? new Date().getFullYear(), file_name: candidate.url.split("/").pop() || "previous-paper.pdf", file_type: "PDF", file_path: candidate.url, status: "ready", extracted_text: null, extraction_error: null, extracted_questions_json: [], created_at: new Date().toISOString(), }; const saved = saveLocalPreviousPaper(paper); return { id: saved.id, title: saved.title, year: saved.year, subject: saved.subject, verification_status: "candidate", confidence_score: candidate.confidence_score, official_source: candidate.official_source, source_url: candidate.url, deduped: false, } satisfies PYQDiscoveryImportResponse; }; if (shouldUseDemoData()) return delay(fallback()); return withDemoFallback( () => apiPost( "/pyq-discovery/import", candidate, { timeoutMs: 30000 }, ), () => delay(fallback()), ); } // ─── Real Kerala previous-year papers (verified public archives) ────────────── export type RealPyqPaper = { title: string; url: string; years?: number[]; medium?: string; source: string; }; export type RealPyqPapersResponse = { available: boolean; level: string; board: string; subject: string | null; papers: RealPyqPaper[]; years_covered: number[]; trust_note: string; }; /** Fetch REAL downloadable Kerala previous-year question papers (not AI-generated). */ export async function getRealPyqPapers(params?: { subject?: string | null; classLevel?: string | null; board?: string | null; }): Promise { const q = new URLSearchParams(); if (params?.subject) q.set("subject", params.subject); if (params?.classLevel) q.set("class_level", params.classLevel); if (params?.board) q.set("board", params.board); const suffix = q.toString() ? `?${q.toString()}` : ""; return apiGet(`/pyq/papers${suffix}`); }