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
@@ -23,10 +23,10 @@
];
const agents = [
{ name: 'Cursor', logo: '/cursor-logo.png' },
{ name: 'Claude Code', logo: '/claude-logo.svg' },
{ name: 'Codex', logo: '/codex-logo.png' },
{ name: 'OpenCode', logo: '/opencode-logo.svg' }
{ name: 'Cursor', logo: 'https://assets.cdn.clsrio.com/www/home/cursor-logo.png' },
{ name: 'Claude Code', logo: 'https://assets.cdn.clsrio.com/www/home/claude-logo.svg' },
{ name: 'Codex', logo: 'https://assets.cdn.clsrio.com/www/home/codex-logo.png' },
{ name: 'OpenCode', logo: 'https://assets.cdn.clsrio.com/www/home/opencode-logo.svg' }
];
</script>
@@ -47,7 +47,7 @@
<div class="mx-auto mt-10 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/ai-course-builder.jpeg"
src="https://assets.cdn.clsrio.com/www/home/ai-course-builder.jpeg"
alt="Course builder — plan, approve, implement. Templates, in-editor chat, and MCP."
class="block h-auto w-full"
loading="lazy"
@@ -152,7 +152,7 @@
<div class="mx-auto mt-10 w-full max-w-[960px] overflow-hidden rounded-lg">
<div style="padding:{videoAspect} 0 0 0;position:relative;">
<iframe
src="https://player.vimeo.com/video/{vimeoVideoId}?badge=0&autopause=0&player_id=0&app_id=58479&autoplay=1&muted=1&loop=1"
src="https://player.vimeo.com/video/{vimeoVideoId}?badge=0&autopause=0&player_id=0&app_id=58479&loop=1"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share"
referrerpolicy="strict-origin-when-cross-origin"
@@ -145,6 +145,12 @@
>Docs vs Company Academy</a
>
</li>
<li>
<a
href="/public-courses"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Public Courses</a
>
</li>
<li>
<a href="/teach" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Teach with us</a
@@ -177,7 +183,7 @@
<ul class="space-y-3">
<li>
<a
href="https://feedback.classroomio.com"
href="https://feedback.classroomio.com/roadmap"
target="_blank"
rel="noopener"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Roadmap</a
+1 -1
View File
@@ -109,7 +109,7 @@
<div class="relative z-30 mx-auto mt-14 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/hero-academy.jpeg"
src="https://assets.cdn.clsrio.com/www/home/hero-academy.jpeg"
alt="Branded customer academy on a custom domain — catalog, programs, AI tutor"
width="2560"
height="1440"
-1
View File
@@ -27,7 +27,6 @@ export { default as ImagePlaceholder } from './image-placeholder.svelte';
export { default as LanguageReel } from './language-reel.svelte';
export { default as LearnerSection } from './learner-section.svelte';
export { default as Modal } from './modal.svelte';
export { default as MoreFeatures } from './more-features.svelte';
export { default as McpServerSection } from './mcp-server-section.svelte';
export { default as MultiAcademySection } from './multi-academy-section.svelte';
export { default as Navigation } from './navigation.svelte';
@@ -1,104 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import { folder, globe, team, transcription } from '$lib/emojis';
import { tools } from '$lib/utils/constants/tools';
let selected = $state(0);
let interval: NodeJS.Timer = $state();
const moreFeatures = [
{
icon: globe,
title: 'Customizable Landing Page',
description: 'A minimalistic landing page template out of the box to share your courses with the world',
image: '/landing-page-builder.webp'
},
{
icon: team,
title: 'Team management',
description: 'Extend invitations and manage your teaching institution seamlessly through ClassroomIO.',
image: '/team-management.png'
},
// {
// icon: transcription,
// title: 'Auto Video Transcription',
// description:
// 'AI automatically transcribes videos, making content searchable for seamless student access.',
// image: '/video-transcription.png'
// },
{
icon: folder,
title: 'Downloadable Lesson PDF',
description: 'Students can download lessons automatically for offline access.',
image: '/download-pdf.png'
}
];
const freeTools = tools.filter((item) => item.showFeature);
onMount(() => {
if (window.innerWidth > 768) {
interval = setInterval(() => {
selected = moreFeatures[selected + 1] ? selected + 1 : 0;
}, 3000);
}
});
</script>
<!-- More Features -->
<section id="morefeatures" class="bg-gray-50 px-3 py-[10%] lg:px-0">
<div class="mx-0 lg:mx-[12%]">
<div class="mx-4 mb-14 w-full lg:mx-0 lg:w-4/5">
<h2 class="mt-2 text-3xl tracking-tight">There is More...</h2>
<p class="mt-4 w-[94%] text-lg text-gray-500 lg:w-4/5">
ClassroomIO is packed with useful features while we try to make it easy to use.
</p>
</div>
<div class="flex flex-col-reverse gap-3 md:gap-10 lg:flex-row" id="image-container">
<div class="mt-5 flex w-[95%] overflow-auto p-3 lg:mt-0 lg:block lg:w-[45%] lg:overflow-hidden">
{#each moreFeatures as moreFeature, i}
<button
class="rounded-md p-4 {i === selected &&
'selected'} mb-3 min-w-[75vw] cursor-pointer text-start lg:min-w-[unset]"
onclick={() => {
selected = i;
clearInterval(interval);
}}
>
<div class="flex items-center gap-1">
<img loading="lazy" width="32" height="32" src={moreFeature.icon} alt="" class="w-8" />
<p class="text-lg">
{moreFeature.title}
</p>
</div>
<p class="text-sm">{moreFeature.description}</p>
</button>
{/each}
</div>
<!-- Video -->
<div class="h-full w-full p-3 lg:w-2/3 lg:p-0">
{#key selected}
<img
width="100%"
height="100%"
loading="lazy"
id="image"
in:fly={{ x: 100, duration: 800 }}
class="flex w-full flex-col items-center"
src={moreFeatures[selected].image}
alt={moreFeatures[selected].title}
/>
{/key}
</div>
</div>
</div>
</section>
<style>
#morefeatures .selected {
background-image: linear-gradient(45deg, rgb(253, 239, 132), rgb(247, 198, 169), rgb(21, 186, 196));
}
</style>
@@ -105,10 +105,10 @@
href: '/mcp-recipes'
},
{
key: 'github',
title: 'GitHub',
subtitle: 'Star, read the source, fork.',
href: '/github'
key: 'storybook',
title: 'Storybook',
subtitle: 'Browse and reuse UI components.',
href: 'https://ui.classroomio.dev'
}
];
@@ -305,7 +305,12 @@
<div class="hidden! flex-row items-center justify-between gap-3 lg:flex!">
<a href="/discord" target="_blank" class="flex items-center transition-opacity duration-200 hover:opacity-80">
<img loading="lazy" alt="discord logo" src="/discord-blue.png" class="h-5 w-6 cursor-pointer" />
<img
loading="lazy"
alt="discord logo"
src="https://assets.cdn.clsrio.com/www/home/discord-blue.png"
class="h-5 w-6 cursor-pointer"
/>
</a>
<div class="flex items-center">
<a
@@ -316,7 +321,7 @@
<img
loading="lazy"
alt="github logo"
src="/github-mark.png"
src="https://assets.cdn.clsrio.com/www/home/github-mark.png"
class="h-5 w-5 cursor-pointer transition-transform duration-200 group-hover:scale-110"
/>
<span
@@ -402,7 +407,12 @@
target="_blank"
class="flex w-full items-center rounded-md px-4 py-4 text-left text-sm transition-all duration-200 hover:bg-gray-100 md:text-lg"
>
<img loading="lazy" alt="discord logo" src="/discord-blue.png" class="mr-2 h-5 w-6 cursor-pointer" />
<img
loading="lazy"
alt="discord logo"
src="https://assets.cdn.clsrio.com/www/home/discord-blue.png"
class="mr-2 h-5 w-6 cursor-pointer"
/>
<span>Discord</span>
</a>
<a
@@ -413,7 +423,7 @@
<img
loading="lazy"
alt="github logo"
src="/github-mark.png"
src="https://assets.cdn.clsrio.com/www/home/github-mark.png"
class="h-5 w-5 cursor-pointer transition-transform duration-200 group-hover:scale-110"
/>
<span class="ml-3 transition-colors duration-200 group-hover:text-black">Github</span>
@@ -0,0 +1,44 @@
import { dev } from '$app/environment';
const USERJOT_APP_ID = 'cm4a6vcmp00jpmdb5n66rmkzz';
let isInitialized = false;
function isWidgetAllowed(): boolean {
return !dev;
}
function ensureSdkLoaded(): void {
if (window.uj) return;
window.$ujq = window.$ujq || [];
window.uj =
window.uj ||
(new Proxy({} as Window['uj'], {
get:
(_, prop) =>
(...args: unknown[]) =>
window.$ujq.push([prop, ...args])
}) as Window['uj']);
const script = document.createElement('script');
script.type = 'module';
script.async = true;
script.src = 'https://cdn.userjot.com/sdk/v2/uj.js';
document.head.appendChild(script);
}
export function initUserJot(): void {
if (isInitialized) return;
if (!isWidgetAllowed()) return;
ensureSdkLoaded();
window.uj.init(USERJOT_APP_ID, {
trigger: 'custom',
position: 'right',
theme: 'auto'
});
isInitialized = true;
}
+19
View File
@@ -0,0 +1,19 @@
declare global {
interface Window {
$ujq: unknown[];
uj: {
init: (
appId: string,
options?: {
widget?: boolean;
position?: 'left' | 'right';
theme?: 'auto' | 'light' | 'dark';
}
) => void;
identify: (user: { id: string; email?: string; firstName?: string; lastName?: string; avatar?: string }) => void;
[method: string]: (...args: unknown[]) => void;
};
}
}
export {};