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:
Rotimi Best
2026-06-07 18:43:05 +01:00
committed by GitHub
parent a2e70779a0
commit d5b07d356e
496 changed files with 19050 additions and 8312 deletions
@@ -2,8 +2,6 @@ const WIDGET_ATTR = 'data-cio-widget';
const WIDGET_VALUE = 'course-widget';
const WIDGET_KEY_ATTR = 'data-widget-key';
const API_BASE_ATTR = 'data-api-base-url';
const LOADING_CLASS = 'cio-embed-loading';
const EMBED_MIN_HEIGHT_PX = 360;
export type CourseWidgetEmbedApi = {
mount: (element: HTMLElement, options: { publicKey: string; apiBaseUrl?: string }) => { destroy: () => void };
@@ -18,50 +16,6 @@ declare global {
}
}
let stylesInjected = false;
function injectSpinnerStyles(): void {
if (stylesInjected) return;
stylesInjected = true;
const css = `
@keyframes cio-embed-spin { to { transform: rotate(360deg); } }
.${LOADING_CLASS} {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-height: ${EMBED_MIN_HEIGHT_PX}px;
padding: 1rem;
color: oklch(0.556 0 0);
}
.${LOADING_CLASS} svg {
animation: cio-embed-spin 0.8s linear infinite;
flex-shrink: 0;
}
`;
const styleEl = document.createElement('style');
styleEl.setAttribute('data-cio-embed', 'course-widget-loading');
styleEl.textContent = css;
document.head.appendChild(styleEl);
}
const LOADER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v4"/><path d="m16.24 7.76 2.83-2.83"/><path d="M22 12h-4"/><path d="m16.24 16.24 2.83 2.83"/><path d="M12 22v-4"/><path d="m7.76 16.24-2.83 2.83"/><path d="M2 12h4"/><path d="m7.76 7.76-2.83-2.83"/></svg>`;
function renderLoading(container: HTMLElement): void {
injectSpinnerStyles();
const root = document.createElement('div');
root.className = LOADING_CLASS;
root.setAttribute('role', 'status');
root.setAttribute('aria-live', 'polite');
root.setAttribute('aria-label', 'Loading');
root.innerHTML = LOADER_SVG;
container.replaceChildren(root);
}
function collectTargets(): Array<{ element: HTMLElement; publicKey: string; apiBaseUrl?: string }> {
return [...document.querySelectorAll<HTMLElement>(`[${WIDGET_ATTR}="${WIDGET_VALUE}"]`)]
.map((element) => ({
@@ -86,8 +40,6 @@ function manualMount(
element: HTMLElement,
options: { publicKey: string; apiBaseUrl?: string }
): { destroy: () => void } {
renderLoading(element);
let cancelled = false;
let destroyWidget: (() => void) | null = null;
@@ -108,10 +60,6 @@ function manualMount(
function init(): void {
const targets = collectTargets();
for (const target of targets) {
renderLoading(target.element);
}
window.CIO = window.CIO ?? {};
window.CIO.courseWidget = {
mount: manualMount