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,71 @@
|
||||
<script lang="ts">
|
||||
import type { CourseLandingPageLabels, OrgLandingPageTheme } from './types';
|
||||
import { courseLandingTokens } from './course-landing-page.tokens';
|
||||
import * as UnderlineTabs from '../underline-tabs';
|
||||
|
||||
interface SectionNavItem {
|
||||
/** DOM id of the section to scroll to (without leading #). */
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
variant: OrgLandingPageTheme;
|
||||
items: SectionNavItem[];
|
||||
labels?: CourseLandingPageLabels;
|
||||
}
|
||||
|
||||
let { variant, items }: Props = $props();
|
||||
|
||||
const t = $derived(courseLandingTokens(variant));
|
||||
|
||||
let activeId = $state<string>('');
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (items.length === 0) return;
|
||||
|
||||
const elements = items.map(({ id }) => document.getElementById(id)).filter((el): el is HTMLElement => el !== null);
|
||||
|
||||
if (elements.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
||||
if (visible[0]) activeId = visible[0].target.id;
|
||||
},
|
||||
{
|
||||
rootMargin: '-120px 0px -55% 0px',
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1]
|
||||
}
|
||||
);
|
||||
|
||||
elements.forEach((el) => observer.observe(el));
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
|
||||
function handleScroll(id: string) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - 24;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if items.length > 1}
|
||||
<nav class={t.sectionNavShell}>
|
||||
<div class={t.sectionNavInner}>
|
||||
<UnderlineTabs.Root bind:value={activeId} class="ui:w-full">
|
||||
<UnderlineTabs.List class={t.sectionNavList}>
|
||||
{#each items as item (item.id)}
|
||||
<UnderlineTabs.Trigger value={item.id} class={t.sectionNavItem} onclick={() => handleScroll(item.id)}>
|
||||
{item.label}
|
||||
</UnderlineTabs.Trigger>
|
||||
{/each}
|
||||
</UnderlineTabs.List>
|
||||
</UnderlineTabs.Root>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user