File size: 5,633 Bytes
88959ff
64004f1
88959ff
 
 
f39674d
88959ff
 
 
 
f39674d
88959ff
 
 
 
 
 
 
f39674d
88959ff
 
 
 
 
f39674d
88959ff
 
 
 
 
 
f39674d
88959ff
 
 
 
 
c7003e8
64004f1
 
 
 
 
 
 
 
 
 
 
 
 
 
88959ff
 
 
 
 
 
 
f39674d
 
88959ff
 
 
 
 
64004f1
88959ff
 
f39674d
 
 
 
88959ff
 
 
64004f1
88959ff
 
f39674d
88959ff
 
 
 
 
 
 
f39674d
88959ff
 
 
 
 
 
 
 
 
f39674d
88959ff
 
 
f39674d
88959ff
 
 
 
 
 
 
 
 
 
f39674d
 
88959ff
 
c7003e8
88959ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f39674d
64004f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// web/src/lib/api.ts

export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
export type Language = 'auto' | 'en' | 'zh';
export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';

export interface User {
  name: string;
  email: string; // use email as user_id
}

const modeMap: Record<LearningMode, string> = {
  concept: 'Concept Explainer',
  socratic: 'Socratic Tutor',
  exam: 'Exam Prep / Quiz',
  assignment: 'Assignment Helper',
  summary: 'Quick Summary',
};

const langMap: Record<Language, string> = {
  auto: 'Auto',
  en: 'English',
  zh: '简体中文',
};

const docTypeMap: Record<FileType, string> = {
  syllabus: 'Syllabus',
  'lecture-slides': 'Lecture Slides',
  'literature-review': 'Literature Review / Paper',
  other: 'Other',
};

async function mustJson<T>(res: Response): Promise<T> {
  if (res.ok) return (await res.json()) as T;
  const txt = await res.text().catch(() => '');
  throw new Error(`${res.status} ${res.statusText}${txt ? ` — ${txt}` : ''}`);
}

// -----------------------------
// Helpers
// -----------------------------
function resolveDocType(docType?: FileType | string): string | undefined {
  if (!docType) return undefined;

  // If docType is one of FileType keys, map it; otherwise assume it's already backend-ready.
  const mapped = (docTypeMap as Record<string, string>)[docType];
  return mapped ?? docType;
}

// -----------------------------
// Core APIs
// -----------------------------
export async function apiLogin(user: User) {
  const res = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: user.name, user_id: user.email }),
  });
  return mustJson<{ ok: boolean; user: { name: string; user_id: string } }>(res);
}

export async function apiChat(params: {
  user: User;
  message: string;
  learningMode: LearningMode;
  language: Language;
  docType?: FileType | string; // optional override
}) {
  const res = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      user_id: params.user.email,
      message: params.message,
      learning_mode: modeMap[params.learningMode],
      language_preference: langMap[params.language],
      doc_type: resolveDocType(params.docType) ?? 'Syllabus',
    }),
  });

  return mustJson<{
    reply: string;
    refs?: Array<{ source_file?: string; section?: string }>;
    latency_ms?: number;
    session_status_md?: string;
  }>(res);
}

export async function apiUpload(params: {
  user: User;
  file: File;
  fileType: FileType;
}) {
  const fd = new FormData();
  fd.append('user_id', params.user.email);
  fd.append('doc_type', docTypeMap[params.fileType]);
  fd.append('file', params.file);

  const res = await fetch('/api/upload', { method: 'POST', body: fd });
  return mustJson<{ ok: boolean; added_chunks: number; status_md: string }>(res);
}

export async function apiExport(params: {
  user: User;
  learningMode: LearningMode;
}) {
  const res = await fetch('/api/export', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      user_id: params.user.email,
      learning_mode: modeMap[params.learningMode],
    }),
  });
  return mustJson<{ markdown: string }>(res);
}

export async function apiSummary(params: {
  user: User;
  learningMode: LearningMode;
  language: Language;
}) {
  const res = await fetch('/api/summary', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      user_id: params.user.email,
      learning_mode: modeMap[params.learningMode],
      language_preference: langMap[params.language],
    }),
  });
  return mustJson<{ markdown: string }>(res);
}

// -----------------------------
// Feedback API (NEW)
// -----------------------------
export type FeedbackRating = 'helpful' | 'not_helpful';

export interface FeedbackParams {
  user: User;

  rating: FeedbackRating;

  // which assistant msg is being rated (optional but recommended)
  assistantMessageId?: string;

  // texts
  assistantText: string;
  userText?: string;

  // UI details
  tags?: string[]; // structured tags/chips
  comment?: string; // free-text

  // context
  refs?: string[];
  learningMode?: LearningMode;
  docType?: FileType | string;

  timestampMs?: number;
}

export async function apiFeedback(params: FeedbackParams) {
  const payload: Record<string, unknown> = {
    user_id: params.user.email,
    rating: params.rating,

    // Only include if present (avoid null noise)
    ...(params.assistantMessageId ? { assistant_message_id: params.assistantMessageId } : {}),

    assistant_text: params.assistantText,
    user_text: (params.userText ?? '').trim(),

    tags: params.tags ?? [],
    comment: (params.comment ?? '').trim(),
    refs: params.refs ?? [],

    learning_mode: params.learningMode ? modeMap[params.learningMode] : undefined,
    doc_type: resolveDocType(params.docType),

    timestamp_ms: params.timestampMs ?? Date.now(),
  };

  // Remove undefined keys (extra safety; FastAPI pydantic will ignore missing but this keeps payload clean)
  Object.keys(payload).forEach((k) => {
    if (payload[k] === undefined) delete payload[k];
  });

  const res = await fetch('/api/feedback', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });

  return mustJson<{ ok: boolean }>(res);
}

// Optional: export maps if other components need them
export const _maps = { modeMap, langMap, docTypeMap };