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
@@ -1,19 +1,23 @@
import type {
AssignAudienceCoursesRequest,
CreateLinkInviteRequest,
DeleteAudienceMemberRequest,
DeleteTeamRequest,
DomainRequestRequest,
GetAudienceRequest,
GetLinkInviteRequest,
GetOrgPublicCoursesRequest,
ImportAudienceRequest,
InviteTeamRequest,
OrgLinkInvite,
OrgPublicCourses,
OrganizationAudience,
OrganizationAudiencePagination,
OrganizationAudienceQuery,
OrganizationTeamMembers,
ResendAudienceInviteRequest,
RevokeAudienceInviteRequest
RevokeAudienceInviteRequest,
ToggleLinkInviteRequest
} from '../utils/types';
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
import type {
@@ -553,6 +557,44 @@ class OrgApi extends BaseApiWithErrors {
}
});
}
linkInvite = $state<OrgLinkInvite>(null);
async getLinkInvite() {
return this.execute<GetLinkInviteRequest>({
requestFn: () => classroomio.organization['link-invite'].$get(),
logContext: 'fetching link invite',
onSuccess: (response) => {
this.linkInvite = response.data;
}
});
}
async generateLinkInvite(roleId: number) {
return this.execute<CreateLinkInviteRequest>({
requestFn: () => classroomio.organization['link-invite'].$post({ json: { roleId } }),
logContext: 'generating link invite',
onSuccess: (response) => {
const wasNew = !this.linkInvite;
this.linkInvite = response.data;
if (wasNew) {
snackbar.success('snackbar.link_invite.generated');
}
}
});
}
async toggleLinkInvite(isRevoked: boolean) {
return this.execute<ToggleLinkInviteRequest>({
requestFn: () => classroomio.organization['link-invite'].$patch({ json: { isRevoked } }),
logContext: 'toggling link invite',
onSuccess: (response) => {
this.linkInvite = response.data;
snackbar.success(isRevoked ? 'snackbar.link_invite.disabled' : 'snackbar.link_invite.enabled');
}
});
}
}
export const orgApi = new OrgApi();
@@ -1,5 +1,13 @@
<script module lang="ts">
let pending: Promise<typeof import('@cio/ui/base/chart')> | null = null;
function loadChart() {
pending ??= import('@cio/ui/base/chart');
return pending;
}
</script>
<script lang="ts">
import * as Chart from '@cio/ui/base/chart';
import type * as Chart from '@cio/ui/base/chart';
import { t } from '$lib/utils/functions/translations';
import type { LoginActivityData } from '$features/org/utils/types';
@@ -40,9 +48,11 @@
<div class="flex h-full flex-col justify-center">
{#if hasData}
<Chart.ChartContainer class="h-[260px] w-full" config={chartConfig}>
<Chart.BarChart {data} x="day" axis="x" {series} />
</Chart.ChartContainer>
{#await loadChart() then C}
<C.ChartContainer class="h-[260px] w-full" config={chartConfig}>
<C.BarChart {data} x="day" axis="x" {series} />
</C.ChartContainer>
{/await}
{:else}
<div class="ui:text-muted-foreground flex h-[260px] items-center justify-center text-sm">
{$t('dashboard.login_activity_no_data')}
@@ -1,31 +1,41 @@
import {
BoldCourseLanding,
BoldLandingHero,
BoldLandingNav,
BoldLandingPage,
ClassicCourseLanding,
ClassicLandingHero,
ClassicLandingNav,
ClassicLandingPage,
CorporateCourseLanding,
CorporateLandingHero,
CorporateLandingNav,
CorporateLandingPage,
EditorialCourseLanding,
EditorialLandingHero,
EditorialLandingNav,
EditorialLandingPage,
MinimalCourseLanding,
MinimalLandingHero,
MinimalLandingNav,
MinimalLandingPage,
SaasCourseLanding,
SaasLandingHero,
SaasLandingNav,
SaasLandingPage,
StudioCourseLanding,
StudioLandingHero,
StudioLandingNav,
StudioLandingPage,
TechCourseLanding,
TechLandingHero,
TechLandingNav,
TechLandingPage,
TerminalCourseLanding,
TerminalLandingHero,
TerminalLandingNav,
TerminalLandingPage,
VibrantCourseLanding,
VibrantLandingHero,
VibrantLandingNav,
VibrantLandingPage
@@ -44,6 +54,19 @@ export const landingPageThemeComponents = {
vibrant: VibrantLandingPage
} as const;
export const courseLandingPageThemeComponents = {
minimal: MinimalCourseLanding,
bold: BoldCourseLanding,
classic: ClassicCourseLanding,
saas: SaasCourseLanding,
tech: TechCourseLanding,
studio: StudioCourseLanding,
corporate: CorporateCourseLanding,
terminal: TerminalCourseLanding,
editorial: EditorialCourseLanding,
vibrant: VibrantCourseLanding
} as const;
export const landingPageNavComponents = {
minimal: MinimalLandingNav,
bold: BoldLandingNav,
@@ -12,7 +12,7 @@ import type {
OrgLandingPageTheme
} from '$lib/utils/types/org';
import type { OrgPublicCourses } from './types';
import type { OrgLandingPageProps } from '@cio/ui/custom/org-landing-page/types';
import type { OrgLandingPageProps, LandingPageThemeBundle } from '@cio/ui/custom/org-landing-page/types';
import { resolveLandingPageLinkIcon } from '@cio/ui/custom/org-landing-page/landing-page-link-icons';
import {
labelMatchesSocialPlatform,
@@ -36,11 +36,11 @@ export const defaultLandingPageHero: OrgLandingPageHero = {
heading: 'Become a Certified AI Engineer',
subheading: 'Master the skills, earn your certification, and prove your expertise with hands-on training programs.',
primaryAction: {
label: 'Get Started',
href: '#courses'
label: 'Start Learning',
href: '/login'
},
secondaryAction: {
label: 'View Programs',
label: 'Browse',
href: '/courses'
},
image: '/images/learn-on-cio.jpg'
@@ -83,8 +83,8 @@ export const defaultLandingPageSettings: OrgLandingPageJson = {
theme: 'minimal',
hero: defaultLandingPageHero,
navItems: [
{ label: 'Programs', href: '#courses' },
{ label: 'Contact', href: '#contact' }
{ label: 'Courses', href: '/courses' }
// { label: 'Contact', href: '#contact' }
],
footer: createDefaultFooterConfig()
};
@@ -710,29 +710,34 @@ export function buildOrgLandingPageProps(
export type LandingPageThemeKey = (typeof landingPageThemes)[number];
export function importThemeComponent(theme: LandingPageThemeKey) {
switch (theme) {
case 'bold':
return import('@cio/ui/custom/org-landing-page/bold.svelte');
case 'classic':
return import('@cio/ui/custom/org-landing-page/classic.svelte');
case 'saas':
return import('@cio/ui/custom/org-landing-page/saas.svelte');
case 'tech':
return import('@cio/ui/custom/org-landing-page/tech.svelte');
case 'studio':
return import('@cio/ui/custom/org-landing-page/studio.svelte');
case 'corporate':
return import('@cio/ui/custom/org-landing-page/corporate.svelte');
case 'terminal':
return import('@cio/ui/custom/org-landing-page/terminal.svelte');
case 'editorial':
return import('@cio/ui/custom/org-landing-page/editorial.svelte');
case 'vibrant':
return import('@cio/ui/custom/org-landing-page/vibrant.svelte');
default:
return import('@cio/ui/custom/org-landing-page/minimal.svelte');
}
export const DEFAULT_LANDING_PAGE_THEME: LandingPageThemeKey = 'minimal';
const THEME_BUNDLE_LOADERS: Record<LandingPageThemeKey, () => Promise<LandingPageThemeBundle>> = {
minimal: () => import('@cio/ui/custom/org-landing-page/minimal'),
bold: () => import('@cio/ui/custom/org-landing-page/bold'),
classic: () => import('@cio/ui/custom/org-landing-page/classic'),
saas: () => import('@cio/ui/custom/org-landing-page/saas'),
tech: () => import('@cio/ui/custom/org-landing-page/tech'),
studio: () => import('@cio/ui/custom/org-landing-page/studio'),
corporate: () => import('@cio/ui/custom/org-landing-page/corporate'),
terminal: () => import('@cio/ui/custom/org-landing-page/terminal'),
editorial: () => import('@cio/ui/custom/org-landing-page/editorial'),
vibrant: () => import('@cio/ui/custom/org-landing-page/vibrant')
};
export function importThemeBundle(theme: LandingPageThemeKey): Promise<LandingPageThemeBundle> {
const load = THEME_BUNDLE_LOADERS[theme] ?? THEME_BUNDLE_LOADERS[DEFAULT_LANDING_PAGE_THEME];
return load();
}
export async function importThemeComponent(theme: LandingPageThemeKey) {
const mod = await importThemeBundle(theme);
return { default: mod.org };
}
export async function importCourseLandingPageTheme(theme: LandingPageThemeKey) {
const mod = await importThemeBundle(theme);
return { default: mod.course };
}
const NAV_SNIPPET_THEMES: ReadonlySet<LandingPageThemeKey> = new Set([
@@ -757,123 +762,10 @@ export function themeHeaderShellClass(_theme: LandingPageThemeKey): string {
export { themeStyle } from '@cio/ui/custom/org-landing-page/theme-style';
export async function importThemeCourseCard(theme: LandingPageThemeKey) {
switch (theme) {
case 'bold':
return (await import('@cio/ui/custom/org-landing-page/bold/course-card.svelte')).default;
case 'classic':
return (await import('@cio/ui/custom/org-landing-page/classic/course-card.svelte')).default;
case 'saas':
return (await import('@cio/ui/custom/org-landing-page/saas/course-card.svelte')).default;
case 'tech':
return (await import('@cio/ui/custom/org-landing-page/tech/course-card.svelte')).default;
case 'studio':
return (await import('@cio/ui/custom/org-landing-page/studio/course-card.svelte')).default;
case 'corporate':
return (await import('@cio/ui/custom/org-landing-page/corporate/course-card.svelte')).default;
case 'terminal':
return (await import('@cio/ui/custom/org-landing-page/terminal/course-card.svelte')).default;
case 'editorial':
return (await import('@cio/ui/custom/org-landing-page/editorial/course-card.svelte')).default;
case 'vibrant':
return (await import('@cio/ui/custom/org-landing-page/vibrant/course-card.svelte')).default;
default:
return (await import('@cio/ui/custom/org-landing-page/minimal/course-card.svelte')).default;
}
}
export function themeCourseGridClass(theme: LandingPageThemeKey): string {
switch (theme) {
case 'corporate':
case 'tech':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:border-t ui:border-l ui:border-[var(--landing-border)]';
case 'saas':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-px ui:bg-[var(--landing-border)] ui:border ui:border-[var(--landing-border)]';
case 'minimal':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-6';
case 'studio':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-3';
case 'terminal':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-[14px]';
case 'editorial':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-4';
case 'vibrant':
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-5';
default:
return 'ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-6';
}
return (await importThemeBundle(theme)).courseCard;
}
export async function importThemeNavHero(theme: LandingPageThemeKey) {
switch (theme) {
case 'bold': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/bold/nav.svelte'),
import('@cio/ui/custom/org-landing-page/bold/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'classic': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/classic/nav.svelte'),
import('@cio/ui/custom/org-landing-page/classic/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'saas': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/saas/nav.svelte'),
import('@cio/ui/custom/org-landing-page/saas/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'tech': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/tech/nav.svelte'),
import('@cio/ui/custom/org-landing-page/tech/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'studio': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/studio/nav.svelte'),
import('@cio/ui/custom/org-landing-page/studio/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'corporate': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/corporate/nav.svelte'),
import('@cio/ui/custom/org-landing-page/corporate/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'terminal': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/terminal/nav.svelte'),
import('@cio/ui/custom/org-landing-page/terminal/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'editorial': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/editorial/nav.svelte'),
import('@cio/ui/custom/org-landing-page/editorial/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
case 'vibrant': {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/vibrant/nav.svelte'),
import('@cio/ui/custom/org-landing-page/vibrant/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
default: {
const [nav, hero] = await Promise.all([
import('@cio/ui/custom/org-landing-page/minimal/nav.svelte'),
import('@cio/ui/custom/org-landing-page/minimal/hero.svelte')
]);
return { NavComponent: nav.default, HeroComponent: hero.default };
}
}
const mod = await importThemeBundle(theme);
return { NavComponent: mod.nav, HeroComponent: mod.hero };
}
@@ -128,6 +128,14 @@ export type AssignAudienceCoursesData = AssignAudienceCoursesSuccess['data'];
export type ResendAudienceInviteRequest = (typeof classroomio.organization)['audience']['resend-invite']['$post'];
export type RevokeAudienceInviteRequest = (typeof classroomio.organization)['audience']['revoke-invite']['$post'];
// Link invite types
export type GetLinkInviteRequest = (typeof classroomio.organization)['link-invite']['$get'];
export type CreateLinkInviteRequest = (typeof classroomio.organization)['link-invite']['$post'];
export type ToggleLinkInviteRequest = (typeof classroomio.organization)['link-invite']['$patch'];
export type GetLinkInviteSuccess = Extract<InferResponseType<GetLinkInviteRequest>, { success: true }>;
export type OrgLinkInvite = GetLinkInviteSuccess['data'];
// Token auth types
export type CreateTokenAuthRequest = (typeof classroomio.organization)['token-auth']['$post'];
export type GetTokenAuthRequest = (typeof classroomio.organization)['token-auth']['$get'];