/** * API تدریسینو — تنها جایی که فرانت باید برای صدا زدنِ بک‌اند به آن نگاه کند. * * import { api } from '@/api'; * const home = await api.home(); * * هیچ‌جای اپ نباید مستقیم fetch بزند: مسیرِ خام، مدیریتِ توکن و رفتارِ * درستِ خطا همه اینجا یک‌جا جمع‌اند. * * هر مسیر و هر نامِ فیلد از OpenAPI واقعیِ سرویس گرفته شده. جاهایی که * غافلگیرکننده‌اند کامنت خورده‌اند، چون اشتباهشان خطای ۴۰۰/۴۰۴ می‌دهد که * از پیامش علت معلوم نیست. */ import { ApiError, createHttpClient, localTokenStorage } from './client'; import type { ClientOptions, HttpClient, RequestOptions, Tokens } from './client'; import type { AnswerResponse, AskResponse, Grade, HomeResponse, LessonIntro, NodeLessons, Plan, Question, ReportReason, ReportResponse, UserProfile, } from './types'; export { ApiError, localTokenStorage, createHttpClient }; export type { Tokens, HttpClient, ClientOptions, RequestOptions }; export * from './types'; /** پرسش از دستیار کند است (تولیدِ متن)؛ ۱۵ ثانیه‌ی پیش‌فرض کم است. */ const ASK_TIMEOUT_MS = 45_000; export function createApi(options: ClientOptions = {}) { const http = createHttpClient(options); return { http, /* ── ورود ─────────────────────────────────────────────────────────── */ auth: { /** «شروع سریع» بدونِ شماره. توکنِ مهمان refreshToken ندارد. */ async guest(deviceId: string) { const data = await http.post<{ userId: string; token: string; isNewUser: boolean; isGuest: boolean; }>('/auth/session', { type: 'guest', deviceId }); http.storage.set({ token: data.token, refreshToken: '' }); return data; }, /** گامِ ۱ — در حالتِ توسعه `devOtp` هم برمی‌گردد. */ requestOtp(phone: string) { return http.post<{ success: boolean; devOtp?: string }>( '/auth/otp/request', { phone }, ); }, /** گامِ ۲ — نامِ فیلد در سرور `otp` است، نه `code`. */ async login(phone: string, otp: string) { const data = await http.post<{ userId: string } & Tokens>( '/auth/login', { phone, otp }, ); http.storage.set({ token: data.token, refreshToken: data.refreshToken }); return data; }, logout() { http.storage.set(null); }, isSignedIn() { return Boolean(http.storage.get()?.token); }, }, /* ── کاربر ────────────────────────────────────────────────────────── */ profile: { get: (opts?: RequestOptions) => http.get('/user/profile', opts), /** * فقط فیلدهایی که می‌فرستید عوض می‌شوند. صفحه‌ی ثبت‌نام سن می‌پرسد، پس * `age` هم پذیرفته می‌شود و سرور خودش به سالِ تولدِ شمسی تبدیلش می‌کند — * `age` و `birthYear` را با هم نفرستید. */ update: (patch: Partial> & { age?: number }) => http.patch('/user/profile', patch), setGrade: (gradeId: number) => http.post<{ success: boolean; gradeId: number }>('/user/grade', { gradeId }), }, /** xp/سکه/استریک اینجاست، نه در profile. */ summary: (opts?: RequestOptions) => http.get>('/user/summary', opts), /** پایه‌ها را هاردکد نکنید — اضافه می‌شوند. */ grades: (opts?: RequestOptions) => http.get<{ grades: Grade[] }>('/grades', opts).then((r) => r.grades), /** بدونِ gradeId، پایه‌ی ذخیره‌شده‌ی خودِ کاربر استفاده می‌شود. */ home: (gradeId?: number, opts?: RequestOptions) => http.get(gradeId ? `/home?gradeId=${gradeId}` : '/home', opts), /* ── درس و آزمون ──────────────────────────────────────────────────── */ lessons: { intro: (lessonId: number | string, opts?: RequestOptions) => http.get(`/lessons/${lessonId}/intro`, opts), questions: (lessonId: number | string, opts?: RequestOptions) => http.get<{ questions: Question[] }>(`/lessons/${lessonId}/questions`, opts) .then((r) => r.questions), complete: (lessonId: number | string) => http.create>(`/lessons/${lessonId}/complete`, {}), }, nodes: { /** * قدم‌های یک مبحث برای نقشه‌ی مهارت — عنوان، نوع، زیرنویس و «انجام شد». * `home()` فقط شناسه‌ی درس‌ها را می‌دهد؛ بدونِ این، برای هر کارت یک * درخواستِ intro جدا لازم بود. */ lessons: (nodeId: number | string, opts?: RequestOptions) => http.get(`/nodes/${nodeId}/lessons`, opts), }, questions: { /** * ثبتِ پاسخ. مسیر زیرِ /questions است نه /lessons، و نامِ فیلد * `selectedOptionId` است. کلیدِ یکتا می‌گیرد تا دابل‌کلیکِ کاربر یا * تکرارِ خودکارِ شبکه دو بار XP ندهد و دو بار «قلب» کم نکند. */ answer: (questionId: number | string, selectedOptionId: number | string) => http.create(`/questions/${questionId}/answer`, { selectedOptionId }), /** «این سوال ایراد دارد» — گزارشِ تکراریِ همان کاربر ردیفِ تازه نمی‌سازد. */ report: (questionId: number | string, reason: ReportReason, note = '') => http.post(`/questions/${questionId}/report`, { reason, note }), }, /* ── دستیار ───────────────────────────────────────────────────────── */ ai: { /** * فقط `lessonId` کافی‌ست: سرور خودش می‌فهمد این درس مالِ کدام پایه و * فصل است و پاسخ را به کتاب مستند می‌کند. * * ⚠️ نامِ فیلدِ پرسش `userQuestion` است، نه `question`. */ ask: ( userQuestion: string, ctx: { lessonId?: number | string; conversationId?: string; gradeId?: number } = {}, opts?: RequestOptions, ) => http.post('/ai/ask', { userQuestion, ...ctx }, { timeoutMs: ASK_TIMEOUT_MS, ...opts, }), /** ⚠️ `isHelpful` بولین است، نه رشته‌ی up/down. `reason` اختیاری. */ feedback: (messageId: number | string, isHelpful: boolean, reason?: string) => http.post<{ success: boolean }>( `/ai/messages/${messageId}/feedback`, { isHelpful, reason }, ), }, /* ── اشتراک ───────────────────────────────────────────────────────── */ plans: (opts?: RequestOptions) => http.get<{ plans: Plan[] }>('/subscription/plans', opts).then((r) => r.plans), }; } export type Api = ReturnType; /** نمونه‌ی پیش‌فرض — برای حالتِ عادی همین کافی‌ست. */ export const api = createApi();