Files
encoach_frontend_v4/src/services/coursePlan.service.ts
Yamen Ahmad c2a07a6e5c
Some checks failed
Deploy Frontend to Staging / Deploy frontend to staging (push) Has been cancelled
feat(course-plan): RAG sources + multi-modal media + assignments + student view
Builds the §24 product on top of the LangGraph runtime from §22:

Phase A (Sources / RAG)
  - encoach.course.plan.source model (file | url | text)
  - SourceIndexer extracts PDF (pypdf), DOCX (python-docx), HTML, plain
    text and embeds chunks via the existing pgvector pipeline scoped to
    plan_id, so resources.search only returns the plan's own corpus
  - Endpoints: list/create/upload/reindex/delete + plan-scoped retrieval

Phase B (Deliverables)
  - services.deliverables.compute_deliverables walks the plan, derives
    {planned, generated, ready} per week from material + media state
  - GET /api/ai/course-plan/<id>/deliverables drives the new wizard
    preview step and the live progress strip on the detail page

Phase C (Multi-modal media)
  - encoach.course.plan.media model + MediaService:
    audio: AWS Polly (default) or ElevenLabs
    image: OpenAI DALL-E 3, capped per plan via system parameter
    video: local ffmpeg subprocess (image + audio -> MP4 1280x720)
  - Three new agent tools (media.synthesize_audio / generate_image /
    compose_video), wired into course_week_materials and a new
    course_media_director agent
  - Endpoints per material + week-level batch generator

Phase D (Assignments)
  - encoach.course.plan.assignment supports mode='batch' (op.batch) or
    mode='students' (res.users), with due_date + message + state
  - REST endpoints to list / create / delete assignments

Phase E (Student view)
  - /api/student/course-plans + /api/student/course-plans/<id>
    enforce visibility via assignment.expand_user_ids()
  - New /student/course-plans list + read-only drilldown rendering
    audio/image/video tiles from /web/content/<attachment_id>

Cross-cutting
  - encoach.ai.tool.category: + media (so the new tools register)
  - encoach.embedding gains a plan_id filter for plan-scoped RAG
  - Wizard adds Sources + Multimedia steps; AdminCoursePlanDetail
    rewritten with DeliverablesStrip + SourcesCard + AssignmentsCard +
    per-material MediaDrawer
  - ~280 new EN + AR i18n keys (full RTL coverage)
  - smoke_course_plan.py exercises every phase via odoo-bin shell;
    last run: PASS A/B/D/E + DALL-E 3 image (753 KB), Polly audio
    fails cleanly when AWS creds aren't configured (expected)

Documentation: §24 added to docs/PROJECT_SUMMARY.md with phase-by-phase
artefact list, endpoints, smoke test, ops notes, and gotchas.

Made-with: Cursor
2026-04-25 17:13:01 +04:00

219 lines
6.6 KiB
TypeScript

import { api } from "@/lib/api-client";
import type {
CoursePlan,
CoursePlanAssignment,
CoursePlanDeliverables,
CoursePlanGenerateBrief,
CoursePlanMaterial,
CoursePlanMedia,
CoursePlanSource,
CoursePlanSourceKind,
} from "@/types";
/**
* REST helpers for the AI course plan generator.
*
* The backend routes are mounted under `/api/ai/course-plan`; see
* `backend/custom_addons/encoach_ai_course/controllers/course_plan.py`.
*/
export const coursePlanService = {
// ---------------------------------------------------------------------
// Plan lifecycle
// ---------------------------------------------------------------------
async list(params?: {
page?: number;
size?: number;
search?: string;
}): Promise<{ items: CoursePlan[]; page: { page: number; size: number; total: number } }> {
return api.get("/ai/course-plan", params);
},
async get(planId: number): Promise<{ data: CoursePlan }> {
return api.get(`/ai/course-plan/${planId}`);
},
async generate(brief: CoursePlanGenerateBrief): Promise<{ data: CoursePlan }> {
return api.post("/ai/course-plan", brief);
},
async remove(planId: number): Promise<{ success: boolean }> {
return api.delete(`/ai/course-plan/${planId}`);
},
async generateWeekMaterials(
planId: number,
weekNumber: number,
): Promise<{ items: CoursePlanMaterial[]; count: number }> {
return api.post(`/ai/course-plan/${planId}/weeks/${weekNumber}/materials`);
},
async listWeekMaterials(
planId: number,
weekNumber: number,
): Promise<{ items: CoursePlanMaterial[]; count: number }> {
return api.get(`/ai/course-plan/${planId}/weeks/${weekNumber}/materials`);
},
// ---------------------------------------------------------------------
// Phase A — Sources
// ---------------------------------------------------------------------
async listSources(planId: number): Promise<{ items: CoursePlanSource[]; count: number }> {
return api.get(`/ai/course-plan/${planId}/sources`);
},
async createSource(
planId: number,
payload:
| { kind: "url"; url: string; name?: string; auto_index?: boolean }
| { kind: "text"; inline_text: string; name?: string; auto_index?: boolean },
): Promise<{ data: CoursePlanSource }> {
return api.post(`/ai/course-plan/${planId}/sources`, payload);
},
async uploadSource(
planId: number,
file: File,
opts?: { name?: string; auto_index?: boolean },
): Promise<{ data: CoursePlanSource }> {
const fd = new FormData();
fd.append("file", file);
fd.append("kind", "file" satisfies CoursePlanSourceKind);
if (opts?.name) fd.append("name", opts.name);
if (opts?.auto_index !== undefined) {
fd.append("auto_index", opts.auto_index ? "1" : "0");
}
return api.upload(`/ai/course-plan/${planId}/sources`, fd);
},
async reindexSource(
planId: number,
sourceId: number,
): Promise<{ data: CoursePlanSource }> {
return api.post(
`/ai/course-plan/${planId}/sources/${sourceId}/index`,
);
},
async deleteSource(
planId: number,
sourceId: number,
): Promise<{ success: boolean }> {
return api.delete(`/ai/course-plan/${planId}/sources/${sourceId}`);
},
// ---------------------------------------------------------------------
// Phase B — Deliverables / progress
// ---------------------------------------------------------------------
async deliverables(planId: number): Promise<CoursePlanDeliverables> {
return api.get(`/ai/course-plan/${planId}/deliverables`);
},
// ---------------------------------------------------------------------
// Phase C — Multimedia
// ---------------------------------------------------------------------
async listMaterialMedia(
materialId: number,
): Promise<{ items: CoursePlanMedia[]; count: number }> {
return api.get(`/ai/course-plan/material/${materialId}/media`);
},
async generateAudio(
materialId: number,
payload?: {
voice?: string;
language?: string;
gender?: "male" | "female";
provider?: "polly" | "elevenlabs";
},
): Promise<{ data: CoursePlanMedia }> {
return api.post(
`/ai/course-plan/material/${materialId}/media/audio`,
payload ?? {},
);
},
async generateImage(
materialId: number,
payload?: {
prompt?: string;
size?: "1024x1024" | "1792x1024" | "1024x1792";
style?: "natural" | "vivid";
quality?: "standard" | "hd";
},
): Promise<{ data: CoursePlanMedia }> {
return api.post(
`/ai/course-plan/material/${materialId}/media/image`,
payload ?? {},
);
},
async composeVideo(materialId: number): Promise<{ data: CoursePlanMedia }> {
return api.post(
`/ai/course-plan/material/${materialId}/media/video`,
);
},
async deleteMedia(mediaId: number): Promise<{ success: boolean }> {
return api.delete(`/ai/course-plan/media/${mediaId}`);
},
async generateWeekMedia(
planId: number,
weekNumber: number,
payload?: { kinds?: Array<"audio" | "image" | "video"> },
): Promise<{ items: Array<CoursePlanMedia | { error: string; material_id: number }>; count: number }> {
return api.post(
`/ai/course-plan/${planId}/weeks/${weekNumber}/media`,
payload ?? {},
);
},
// ---------------------------------------------------------------------
// Phase D — Assignments
// ---------------------------------------------------------------------
async listAssignments(
planId: number,
): Promise<{ items: CoursePlanAssignment[]; count: number }> {
return api.get(`/ai/course-plan/${planId}/assignments`);
},
async createAssignment(
planId: number,
payload:
| {
mode: "batch";
batch_id: number;
due_date?: string | null;
message?: string;
}
| {
mode: "students";
student_user_ids: number[];
due_date?: string | null;
message?: string;
},
): Promise<{ data: CoursePlanAssignment }> {
return api.post(`/ai/course-plan/${planId}/assignments`, payload);
},
async deleteAssignment(
planId: number,
assignmentId: number,
): Promise<{ success: boolean }> {
return api.delete(
`/ai/course-plan/${planId}/assignments/${assignmentId}`,
);
},
// ---------------------------------------------------------------------
// Phase E — Student-side
// ---------------------------------------------------------------------
async studentList(): Promise<{ items: CoursePlan[]; count: number }> {
return api.get("/student/course-plans");
},
async studentGet(planId: number): Promise<{ data: CoursePlan }> {
return api.get(`/student/course-plans/${planId}`);
},
};