feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features: - HLS video encoding/streaming (hls routes, hls-encoder) and a media transcripts pipeline (transcripts route + media-transcript queries) - AI assistant chat vs. run model: agent-course-generation worker/queue, run-monitor pane, chat-changes card, content ask-ai bar - Per-theme course-page previews across org landing-page themes (bold, classic, corporate, editorial, etc.) with a shared course token/component set - View-as-student mode and login-link / link-invite invite flows Refactor: - Extract packages/core consolidating config (env, storage, dashboard-url), services and utils previously duplicated under apps/api/src/config - Remove legacy agent service modules (chat-tools, chat-context, context-window, document, fetch-url, lesson-content) - Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding - Add format script to tenant-router so turbo run format covers it
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { AppError, ErrorCodes } from '@cio/utils/errors';
|
||||
import { sanitizeOptionalHtml } from '../utils/sanitize-html';
|
||||
import type { TLessonLanguage, TLocale, TNewLessonLanguage } from '@cio/db/types';
|
||||
import {
|
||||
deleteLessonLanguage,
|
||||
getLessonLanguageByLessonIdAndLocale,
|
||||
getLessonLanguagesByLessonId,
|
||||
updateLessonLanguage,
|
||||
upsertLessonLanguage
|
||||
} from '@cio/db/queries/lesson/language';
|
||||
|
||||
import { getLessonById } from '@cio/db/queries/lesson/lesson';
|
||||
import { touchCourseUpdatedAt } from '@cio/db/queries/course';
|
||||
|
||||
/**
|
||||
* Gets all language translations for a lesson
|
||||
* @param lessonId - The lesson ID
|
||||
* @returns Array of lesson languages
|
||||
*/
|
||||
export async function listLessonLanguages(lessonId: string): Promise<TLessonLanguage[]> {
|
||||
try {
|
||||
const languages = await getLessonLanguagesByLessonId(lessonId);
|
||||
return languages;
|
||||
} catch (error) {
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to fetch lesson languages',
|
||||
ErrorCodes.LESSON_LANGUAGE_FETCH_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single lesson language by locale
|
||||
* @param lessonId - The lesson ID
|
||||
* @param locale - The locale
|
||||
* @returns Lesson language or null if not found
|
||||
*/
|
||||
export async function getLessonLanguage(lessonId: string, locale: TLocale): Promise<TLessonLanguage | null> {
|
||||
try {
|
||||
const language = await getLessonLanguageByLessonIdAndLocale(lessonId, locale);
|
||||
return language;
|
||||
} catch (error) {
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to fetch lesson language',
|
||||
ErrorCodes.LESSON_LANGUAGE_FETCH_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates a lesson language translation
|
||||
* @param lessonId - The lesson ID
|
||||
* @param data - Lesson language data
|
||||
* @returns Created or updated lesson language
|
||||
*/
|
||||
export async function upsertLessonLanguageService(
|
||||
lessonId: string,
|
||||
data: Omit<TNewLessonLanguage, 'lessonId'>
|
||||
): Promise<TLessonLanguage> {
|
||||
try {
|
||||
// Verify lesson exists
|
||||
const lesson = await getLessonById(lessonId);
|
||||
if (!lesson) {
|
||||
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const language = await upsertLessonLanguage({
|
||||
...data,
|
||||
content: sanitizeOptionalHtml(data.content),
|
||||
lessonId
|
||||
});
|
||||
|
||||
if (lesson.courseId) {
|
||||
await touchCourseUpdatedAt(lesson.courseId);
|
||||
}
|
||||
|
||||
return language;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to create or update lesson language',
|
||||
ErrorCodes.LESSON_LANGUAGE_CREATE_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a lesson language translation
|
||||
* @param lessonId - The lesson ID
|
||||
* @param locale - The locale
|
||||
* @param data - Lesson language update data
|
||||
* @returns Updated lesson language
|
||||
*/
|
||||
export async function updateLessonLanguageService(
|
||||
lessonId: string,
|
||||
locale: TLocale,
|
||||
data: Partial<TNewLessonLanguage>
|
||||
): Promise<TLessonLanguage> {
|
||||
try {
|
||||
// Verify lesson exists
|
||||
const lesson = await getLessonById(lessonId);
|
||||
if (!lesson) {
|
||||
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const existing = await getLessonLanguageByLessonIdAndLocale(lessonId, locale);
|
||||
if (!existing) {
|
||||
throw new AppError('Lesson language not found', ErrorCodes.LESSON_LANGUAGE_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const language = await updateLessonLanguage(lessonId, locale, {
|
||||
...data,
|
||||
content: sanitizeOptionalHtml(data.content)
|
||||
});
|
||||
|
||||
if (lesson.courseId) {
|
||||
await touchCourseUpdatedAt(lesson.courseId);
|
||||
}
|
||||
|
||||
return language;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to update lesson language',
|
||||
ErrorCodes.LESSON_LANGUAGE_UPDATE_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteLessonLanguageService(lessonId: string, locale: TLocale): Promise<TLessonLanguage> {
|
||||
try {
|
||||
const lesson = await getLessonById(lessonId);
|
||||
if (!lesson) {
|
||||
throw new AppError('Lesson not found', ErrorCodes.LESSON_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const existing = await getLessonLanguageByLessonIdAndLocale(lessonId, locale);
|
||||
if (!existing) {
|
||||
throw new AppError('Lesson language not found', ErrorCodes.LESSON_LANGUAGE_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
const deleted = await deleteLessonLanguage(lessonId, locale);
|
||||
if (!deleted) {
|
||||
throw new AppError('Lesson language not found', ErrorCodes.LESSON_LANGUAGE_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
if (lesson.courseId) {
|
||||
await touchCourseUpdatedAt(lesson.courseId);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to delete lesson language',
|
||||
ErrorCodes.LESSON_LANGUAGE_DELETE_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user