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:
@@ -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'];
|
||||
|
||||
Reference in New Issue
Block a user