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:
@@ -400,7 +400,6 @@
|
||||
onValueChange={(nextValue) => onQuestionTypeChange(question.id, nextValue)}
|
||||
triggerQuestionType={question?.questionType}
|
||||
types={availableQuestionTypes}
|
||||
{selfPacedCourse}
|
||||
/>
|
||||
{/snippet}
|
||||
</ExerciseQuestion.QuestionRenderer>
|
||||
|
||||
+72
-39
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import { PREMIUM_QUESTION_TYPE_KEYS, QUESTION_TYPES } from '$features/ui/question/constants';
|
||||
import { PremiumIcon } from '@cio/ui/custom/moving-icons';
|
||||
import { getExerciseEditorQuestionTypeLabel } from './question-type-utils';
|
||||
@@ -16,51 +15,85 @@
|
||||
onValueChange: (value: string) => void;
|
||||
triggerQuestionType: Question['questionType'] | undefined;
|
||||
types: QuestionTypeEntry[];
|
||||
selfPacedCourse?: boolean;
|
||||
}
|
||||
|
||||
let { value, onValueChange, triggerQuestionType, types, selfPacedCourse = false }: Props = $props();
|
||||
let { value, onValueChange, triggerQuestionType, types }: Props = $props();
|
||||
|
||||
const autoGradableTypes = $derived(types.filter((typeEntry) => typeEntry.autoGradable));
|
||||
const manuallyGradedTypes = $derived(types.filter((typeEntry) => !typeEntry.autoGradable));
|
||||
</script>
|
||||
|
||||
{#snippet sectionTitle({ title, description })}
|
||||
<div>
|
||||
<Select.Label>
|
||||
<p class="font-semibold">
|
||||
{$t(title)}
|
||||
</p>
|
||||
<p class="ui:text-muted-foreground ui:mt-1 ui:text-xs">
|
||||
{$t(description)}
|
||||
</p>
|
||||
</Select.Label>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet upgradeButton({ typeEntry })}
|
||||
<button
|
||||
type="button"
|
||||
class="ui:flex ui:w-full ui:cursor-pointer ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:py-1.5 ui:pl-2 ui:pr-8 ui:text-sm ui:outline-hidden ui:hover:bg-accent ui:hover:text-accent-foreground ui:relative"
|
||||
onclick={(clickEvent) => {
|
||||
clickEvent.preventDefault();
|
||||
clickEvent.stopPropagation();
|
||||
openUpgradeModal();
|
||||
}}
|
||||
title={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.premium_question_type')}
|
||||
>
|
||||
<PremiumIcon size={16} class="ui:text-blue-700 ui:dark:text-white ui:shrink-0" />
|
||||
<span>{getExerciseEditorQuestionTypeLabel(typeEntry)}</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
<Select.Root type="single" {value} {onValueChange}>
|
||||
<Select.Trigger class="w-full min-w-0 sm:w-[180px]">
|
||||
{getExerciseEditorQuestionTypeLabel(triggerQuestionType)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each types as typeEntry (typeEntry.key)}
|
||||
{#if $isFreePlan && PREMIUM_QUESTION_TYPE_KEYS.has(typeEntry.key)}
|
||||
<button
|
||||
type="button"
|
||||
class="ui:flex ui:w-full ui:cursor-pointer ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:py-1.5 ui:pl-2 ui:pr-8 ui:text-sm ui:outline-hidden ui:hover:bg-accent ui:hover:text-accent-foreground ui:relative"
|
||||
onclick={(clickEvent) => {
|
||||
clickEvent.preventDefault();
|
||||
clickEvent.stopPropagation();
|
||||
openUpgradeModal();
|
||||
}}
|
||||
title={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.premium_question_type')}
|
||||
>
|
||||
<PremiumIcon size={16} class="ui:text-blue-700 ui:dark:text-white ui:shrink-0" />
|
||||
<span>{getExerciseEditorQuestionTypeLabel(typeEntry)}</span>
|
||||
</button>
|
||||
{:else}
|
||||
<Select.Item value={typeEntry.id.toString()} label={getExerciseEditorQuestionTypeLabel(typeEntry)}>
|
||||
{#snippet children({ selected: _sel })}
|
||||
<span class="flex w-full min-w-0 items-center justify-between gap-2">
|
||||
<span class="truncate">{getExerciseEditorQuestionTypeLabel(typeEntry)}</span>
|
||||
{#if selfPacedCourse}
|
||||
<Badge
|
||||
variant={typeEntry.autoGradable ? 'success' : 'warning'}
|
||||
class="ui:shrink-0 ui:text-[10px] ui:font-normal"
|
||||
>
|
||||
{typeEntry.autoGradable
|
||||
? $t('course.navItem.lessons.exercises.all_exercises.edit_mode.question_type_auto_gradable')
|
||||
: $t('course.navItem.lessons.exercises.all_exercises.edit_mode.question_type_manual_grading')}
|
||||
</Badge>
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
<Select.Content class="max-h-[300px]!">
|
||||
<Select.Group>
|
||||
{@render sectionTitle({
|
||||
title: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_type_auto_gradable',
|
||||
description: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_type_auto_gradable_description'
|
||||
})}
|
||||
|
||||
{#each autoGradableTypes as typeEntry (typeEntry.key)}
|
||||
{#if $isFreePlan && PREMIUM_QUESTION_TYPE_KEYS.has(typeEntry.key)}
|
||||
{@render upgradeButton({ typeEntry })}
|
||||
{:else}
|
||||
<Select.Item value={typeEntry.id.toString()} label={getExerciseEditorQuestionTypeLabel(typeEntry)}>
|
||||
{getExerciseEditorQuestionTypeLabel(typeEntry)}
|
||||
</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select.Group>
|
||||
|
||||
{#if manuallyGradedTypes.length > 0}
|
||||
<Select.Separator />
|
||||
|
||||
<Select.Group>
|
||||
{@render sectionTitle({
|
||||
title: 'course.navItem.lessons.exercises.all_exercises.edit_mode.question_type_manual_grading',
|
||||
description:
|
||||
'course.navItem.lessons.exercises.all_exercises.edit_mode.question_type_manual_grading_description'
|
||||
})}
|
||||
|
||||
{#each manuallyGradedTypes as typeEntry (typeEntry.key)}
|
||||
{#if $isFreePlan && PREMIUM_QUESTION_TYPE_KEYS.has(typeEntry.key)}
|
||||
{@render upgradeButton({ typeEntry })}
|
||||
{:else}
|
||||
<Select.Item value={typeEntry.id.toString()} label={getExerciseEditorQuestionTypeLabel(typeEntry)}>
|
||||
{getExerciseEditorQuestionTypeLabel(typeEntry)}
|
||||
</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select.Group>
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
Reference in New Issue
Block a user