File size: 7,504 Bytes
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import type {
  ApiDocument,
  DocumentStatus,
  ExamModeOutput,
  FlashcardResponseItem,
  FlashcardsPayload,
  NotesOutput,
  PreviousPaperAnalysisResponse,
  QuizQuestionResponse,
  QuizQuestionsPayload,
  StudyPathResponse,
  VideoPlanV2Response,
  VideoRenderJob,
  VideoRenderJobStatus,
  VideoScenePlanResponse,
} from "@/lib/api/types";

type UnknownRecord = Record<string, unknown>;

function isRecord(value: unknown): value is UnknownRecord {
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

function readString(value: unknown, fallback = "") {
  return typeof value === "string" && value.trim() ? value : fallback;
}

function readStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) return [];
  return value.map((item) => String(item)).filter((item) => item.trim().length > 0);
}

export function normalizeDocument(value: unknown): ApiDocument {
  const record = isRecord(value) ? value : {};
  const now = new Date().toISOString();
  return {
    id: readString(record.id, `doc-local-${Date.now()}`),
    user_id: readString(record.user_id, "demo-user-student"),
    title: readString(record.title, "Study source"),
    file_name: readString(record.file_name, "source.pdf"),
    file_type: readString(record.file_type, "PDF"),
    subject: typeof record.subject === "string" ? record.subject : null,
    chapter: typeof record.chapter === "string" ? record.chapter : null,
    syllabus: typeof record.syllabus === "string" ? record.syllabus : null,
    status: normalizeDocumentStatus(record.status),
    // extracted_text removed — raw OCR text is not returned by the API
    extracted_text_length: typeof record.extracted_text_length === "number" ? record.extracted_text_length : null,
    chunk_count: typeof record.chunk_count === "number" ? record.chunk_count : 0,
    created_at: readString(record.created_at, now),
  };
}

export function normalizeDocumentStatus(value: unknown): DocumentStatus {
  if (value === "ready" || value === "processing" || value === "failed" || value === "uploaded") {
    return value;
  }
  return "ready";
}

export function normalizeNotes(value: unknown): NotesOutput {
  const record = isRecord(value) ? value : {};
  // Backend returns exam_keywords; exam_focus is a frontend-side alias
  const examKeywords = readStringArray(record.exam_keywords);
  return {
    title: readString(record.title, "Smart Notes"),
    summary: typeof record.summary === "string" ? record.summary : undefined,
    student_level_summary: typeof record.student_level_summary === "string" ? record.student_level_summary : undefined,
    key_points: readStringArray(record.key_points).length ? readStringArray(record.key_points) : ["Review the main ideas first."],
    important_keywords: readStringArray(record.important_keywords),
    important_definitions: readStringArray(record.important_definitions),
    formulas: readStringArray(record.formulas),
    diagrams_to_practice: readStringArray(record.diagrams_to_practice),
    memory_tricks: readStringArray(record.memory_tricks),
    exam_keywords: examKeywords,
    exam_focus: readStringArray(record.exam_focus).length ? readStringArray(record.exam_focus) : examKeywords,
    possible_exam_questions: readStringArray(record.possible_exam_questions),
    last_minute_revision: readStringArray(record.last_minute_revision),
    quick_check: readStringArray(record.quick_check),
    source_summary: typeof record.source_summary === "string" ? record.source_summary : undefined,
    scoring_keywords: readStringArray(record.scoring_keywords),
    common_mistakes: readStringArray(record.common_mistakes),
    ten_minute_revision: readStringArray(record.ten_minute_revision),
    next_study_action: typeof record.next_study_action === "string" ? record.next_study_action : undefined,
    caution_note: typeof record.caution_note === "string" ? record.caution_note : undefined,
  };
}

export function normalizeQuiz(value: unknown): QuizQuestionResponse[] {
  const payload: QuizQuestionsPayload | unknown = value;
  if (Array.isArray(payload)) return payload.filter(isRecord).map(normalizeQuizQuestion);
  if (isRecord(payload) && Array.isArray(payload.questions)) {
    return payload.questions.filter(isRecord).map(normalizeQuizQuestion);
  }
  return [];
}

function normalizeQuizQuestion(value: UnknownRecord): QuizQuestionResponse {
  return {
    question: readString(value.question, "Practice question"),
    type: readString(value.type, "MCQ"),
    options: readStringArray(value.options),
    answer: readString(value.answer, "Review the explanation."),
    explanation: typeof value.explanation === "string" ? value.explanation : undefined,
    difficulty: readString(value.difficulty, "Practice"),
    topic: typeof value.topic === "string" ? value.topic : undefined,
    why_this_matters: typeof value.why_this_matters === "string" ? value.why_this_matters : undefined,
    common_trap: typeof value.common_trap === "string" ? value.common_trap : undefined,
  };
}

export function normalizeFlashcards(value: unknown): FlashcardResponseItem[] {
  const payload: FlashcardsPayload | unknown = value;
  if (Array.isArray(payload)) return payload.filter(isRecord).map(normalizeFlashcard);
  if (isRecord(payload) && Array.isArray(payload.cards)) {
    return payload.cards.filter(isRecord).map(normalizeFlashcard);
  }
  return [];
}

function normalizeFlashcard(value: UnknownRecord): FlashcardResponseItem {
  return {
    front: readString(value.front, "Key idea"),
    back: readString(value.back, "Short explanation"),
    hint: typeof value.hint === "string" ? value.hint : undefined,
    tag: typeof value.tag === "string" ? value.tag : undefined,
    topic: typeof value.topic === "string" ? value.topic : undefined,
    memory_hook: typeof value.memory_hook === "string" ? value.memory_hook : undefined,
    exam_use: typeof value.exam_use === "string" ? value.exam_use : undefined,
  };
}

export function normalizePYQAnalysis(value: unknown): PreviousPaperAnalysisResponse | null {
  return isRecord(value) && Array.isArray(value.repeated_topics)
    ? (value as PreviousPaperAnalysisResponse)
    : null;
}

export function normalizeStudyPlan(value: unknown): StudyPathResponse | null {
  return isRecord(value) && Array.isArray(value.blocks) ? (value as StudyPathResponse) : null;
}

export function normalizeExamAnswer(value: unknown): ExamModeOutput | null {
  return isRecord(value) ? (value as ExamModeOutput) : null;
}

export function normalizeVideoPlan(value: unknown): VideoScenePlanResponse | VideoPlanV2Response | null {
  if (!isRecord(value)) return null;
  if (Array.isArray(value.scenes) || Array.isArray(value.sections)) {
    return value as VideoScenePlanResponse | VideoPlanV2Response;
  }
  return null;
}

export function normalizeRenderJob(value: unknown): VideoRenderJob | null {
  return isRecord(value) && typeof value.id === "string" ? (value as VideoRenderJob) : null;
}

export function normalizeRenderStatus(value: unknown): VideoRenderJobStatus {
  if (
    value === "ready" ||
    value === "failed" ||
    value === "queued" ||
    value === "running" ||
    value === "planning" ||
    value === "script_ready" ||
    value === "audio_generating" ||
    value === "visuals_generating" ||
    value === "rendering" ||
    value === "uploading" ||
    value === "complete"
  ) {
    return value;
  }
  if (value === "completed") return "complete";
  if (value === "processing") return "running";
  return "queued";
}