Download src/lib/api/previous-papers.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/previous-papers.ts
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/lib/api/previous-papers.ts
-
curl -L -o previous-papers.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/lib/api/previous-papers.ts
13.4 kB
| 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<PyqAnalysis, PyqAnalysisRequest>("/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<PreviousPaper[]>("/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<PreviousPaper>("/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<PreviousQuestion[]>(`/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<PreviousQuestionExtractResponse, PreviousQuestionExtractRequest>( | |
| `/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<PreviousPaperAnalysisResponse, StructuredPreviousPaperAnalyzeRequest>( | |
| "/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<PYQDiscoverySearchResponse, PYQDiscoverySearchPayload>( | |
| "/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<PYQDiscoveryImportResponse, PYQDiscoveryCandidate>( | |
| "/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<RealPyqPapersResponse> { | |
| 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<RealPyqPapersResponse>(`/pyq/papers${suffix}`); | |
| } | |