Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b3f2b9c
feat: AI lecture summary and quiz under the video
Punnawit9285 Oct 8, 2026
77da304
fix: draw the summary ticks and answer picks in the theme's colours
Punnawit9285 Oct 8, 2026
c8b1a83
feat: let admins choose the editors, add images and share the quiz by…
Punnawit9285 Oct 8, 2026
b6fbc2d
feat: open the summary and quiz in a reading window
Punnawit9285 Oct 9, 2026
1d804f5
feat: transcription, captions, verification and change history for th…
Punnawit9285 Oct 10, 2026
0cf6cb5
Merge remote-tracking branch 'upstream/master' into study-aids
Punnawit9285 Oct 10, 2026
20e8668
feat: talk to FlickMan's study aids through GraphQL
Punnawit9285 Oct 10, 2026
7376cc9
feat: Google Drive pictures, ticks that follow the reader, and one ca…
Punnawit9285 Oct 10, 2026
e63e384
fix: show each question in full in the quiz results
Punnawit9285 Oct 10, 2026
ad6b3b2
feat: call the edited version "Edited by PS"
Punnawit9285 Oct 10, 2026
f98dec3
feat: redo the quiz at any point
Punnawit9285 Oct 11, 2026
fc547b1
Merge remote-tracking branch 'upstream/master' into study-aids
Punnawit9285 Oct 11, 2026
882bc46
fix: draw a summary's diagrams line for line
Punnawit9285 Oct 12, 2026
1e6125c
Merge remote-tracking branch 'upstream/master' into study-aids
Punnawit9285 Oct 15, 2026
2f50696
feat: download the transcript as a text file
Punnawit9285 Oct 15, 2026
6ad64c5
fix: put the transcript download before the playback buttons
Punnawit9285 Oct 16, 2026
c2359b3
refactor: keep the study aids AI-only, without editors or captions
Punnawit9285 Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/home/course/course.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
}
<!-- Pomodoro Timer Widget -->
<app-pomodoro-timer></app-pomodoro-timer>
<app-lecture-study-aids [video]="currentVideo" [playback]="playback"></app-lecture-study-aids>
</ion-col>
<ion-col size="12" size-lg [ngClass]="currentVideo ? 'scroll-area' : ''">
<ion-searchbar (ionInput)="onSearchChange($event)" placeholder="Search videos by title or lecturer..."
Expand Down
20 changes: 20 additions & 0 deletions src/app/home/course/course.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {checkmarkOutline, closeOutline, documentAttachOutline, documentTextOutli
import type Player from 'video.js/dist/types/player';
import {ulid} from 'ulid';
import {AsyncPipe, DatePipe, DecimalPipe, NgClass} from '@angular/common';
import {LectureStudyAidsComponent} from '../../shared/lecture-study-aids.component';
import {LecturePlayback} from '../../study-aids/study-aids.model';
import {ModalEvaluationComponent} from './modal-evaluation.component';
import {confirmAiDisclaimer, ModalDocumentComponent} from './modal-document.component';
import {PomodoroTimerComponent} from '../../shared/pomodoro-timer.component';
Expand Down Expand Up @@ -83,6 +85,7 @@ interface SeekBarPointerHandlers {
NgClass,
IonProgressBar,
AsyncPipe,
LectureStudyAidsComponent,
DecimalPipe,
DatePipe,
PomodoroTimerComponent,
Expand Down Expand Up @@ -131,6 +134,12 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy {
pendingVideoToPlay: Lecture | null = null;
isPlayerReady = false;

// Lets the summary and quiz read the video's time and jump to where a topic is taught
readonly playback: LecturePlayback = {
currentTime: () => this.videoPlayer?.currentTime() ?? 0,
seek: seconds => this.seekTo(seconds),
};

constructor() {
addIcons({ download, documentAttachOutline, documentTextOutline, checkmarkOutline, closeOutline, pauseCircleOutline });
}
Expand Down Expand Up @@ -449,6 +458,17 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy {
this.playLog = [];
}

private seekTo(seconds: number) {
if (!this.videoPlayer || !this.currentVideo) {
return;
}
// A jump asked for before the metadata arrives must not be undone by resuming the last position
this.expectVideoTimeJump = false;
this.videoPlayer.currentTime(seconds);
this.videoPlayer.play()?.catch(() => undefined);
this.videoPlayer.el().scrollIntoView({behavior: 'smooth', block: 'nearest'});
}

async setPlaybackSpeed() {
const alert = await this.alertController.create({
header: 'Please enter speed!',
Expand Down
3 changes: 2 additions & 1 deletion src/app/home/course/markdown-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ export const mathExtension: MarkedExtension = {
export const markedWithMath = new Marked(mathExtension);

// Replaces the math placeholders under `root` with KaTeX output. Invalid TeX is shown as the raw source.
// Placeholders already rendered are skipped, so it is safe to call again after more content appears.
export function renderMath(root: HTMLElement) {
root.querySelectorAll<HTMLElement>(`.${INLINE_CLASS}, .${DISPLAY_CLASS}`).forEach(el => {
root.querySelectorAll<HTMLElement>(`.${INLINE_CLASS}:not(.math-rendered), .${DISPLAY_CLASS}:not(.math-rendered)`).forEach(el => {
const tex = el.textContent ?? '';
katex.render(tex, el, {
displayMode: el.classList.contains(DISPLAY_CLASS),
Expand Down
172 changes: 172 additions & 0 deletions src/app/shared/_checklist.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// The checklist's look, shared by the summary and the summary's Changes view.

@mixin layout {
display: block;
font-size: 0.95rem;
line-height: 1.55;

.checklist-title {
margin: 0 0 0.75rem;
font-size: 1.1rem;
font-weight: 700;
text-decoration: underline;
text-underline-offset: 0.2em;
}

.checklist-section + .checklist-section {
margin-top: 1rem;
}

.checklist-heading {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem 0.5rem;
margin: 0 0 0.4rem;
font-size: 1.05rem;
font-weight: 700;
}

.heading-text {
padding: 0.05em 0.35em;
border-radius: 0.25rem;
background: rgba(var(--ion-color-primary-rgb), 0.18);
}

.time-chip {
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.1rem 0.55rem;
border: 1px solid rgba(var(--ion-color-primary-rgb), 0.45);
border-radius: 1rem;
background: transparent;
color: var(--ion-color-primary);
font: inherit;
font-size: 0.75rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}

button.time-chip {
cursor: pointer;

&:hover {
background: rgba(var(--ion-color-primary-rgb), 0.1);
}

&:focus-visible {
outline: 2px solid var(--ion-color-primary);
outline-offset: 2px;
}
}

.checklist-point {
display: grid;
grid-template-columns: 2.4em minmax(0, 1fr);
column-gap: 0.4rem;
padding: 0.12rem 0;
}

&.with-ticks .checklist-point {
grid-template-columns: 1.1rem 2.4em minmax(0, 1fr);
}

/* Drawn from the theme: a native box follows the system's light or dark scheme, not the theme. */
.point-tick {
appearance: none;
display: grid;
place-content: center;
width: 1rem;
height: 1rem;
margin: 0.22em 0 0;
border: 1.5px solid rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.45);
border-radius: 0.25rem;
background: transparent;
cursor: pointer;

&::after {
content: '';
width: 0.28rem;
height: 0.55rem;
margin-top: -0.12rem;
border: solid var(--ion-color-primary-contrast);
border-width: 0 2px 2px 0;
transform: rotate(45deg) scale(0);
}

&:checked {
border-color: var(--ion-color-primary);
background: var(--ion-color-primary);
}

&:checked::after {
transform: rotate(45deg) scale(1);
}

&:focus-visible {
outline: 2px solid var(--ion-color-primary);
outline-offset: 2px;
}
}

.point-number {
text-align: right;
font-variant-numeric: tabular-nums;
}

.is-ticked .point-number,
.is-ticked .point-body {
opacity: 0.55;
}

.point-body {
strong {
color: var(--ion-color-primary);
}

ol,
ul {
margin: 0.1rem 0;
padding-left: 1.6em;
list-style-type: lower-alpha;
}

:is(ol, ul) :is(ol, ul) {
list-style-type: lower-roman;
}

:is(ol, ul) :is(ol, ul) :is(ol, ul) {
list-style-type: decimal;
}
}

/* Lines up with the text of the points around it. */
.checklist-label {
margin: 0.5rem 0 0.2rem calc(2.4em + 0.4rem);
font-weight: 700;
text-decoration: underline;
text-underline-offset: 0.2em;
}

&.with-ticks .checklist-label {
margin-left: calc(1.1rem + 2.4em + 0.8rem);
}

.checklist-block {
margin: 0.4rem 0;
}

/* A diagram drawn in text: every line and space kept, scrolled sideways rather than wrapped. */
pre {
margin: 0.4rem 0;
padding: 0.6em 0.8em;
border-radius: 0.4rem;
background: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.05);
font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
font-size: 0.85em;
line-height: 1.2;
white-space: pre;
overflow-x: auto;
}
}
107 changes: 107 additions & 0 deletions src/app/shared/_study-card.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// The look the study aids card shares with the Pomodoro card above it.
@use 'study-shared';

:host {
// A block, so the settings sheet can tell when it hides the card (see study-aids/displayed.ts).
display: block;
width: 100%;
}

.study-card {
margin: 0.75rem 0 0 0;
border-radius: 0.75rem;
overflow: hidden;
box-shadow: var(--flick-welcome-card-shadow, 0 4px 16px rgba(0, 0, 0, 0.08));
}

.card-head {
display: flex;
align-items: center;
}

.card-toggle {
display: flex;
flex: 1;
align-items: center;
gap: 0.5rem;
min-width: 0;
padding: 0.75rem 0.5rem 0.75rem 1rem;
border: 0;
background: transparent;
color: var(--ion-text-color);
font: inherit;
text-align: left;
cursor: pointer;

&:focus-visible {
outline: 2px solid var(--ion-color-primary);
outline-offset: -2px;
}
}

.card-heading {
display: flex;
flex: 1;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.5rem;
min-width: 0;
}

.card-title {
font-size: 1rem;
font-weight: 600;
}

.card-badge {
padding: 0.1rem 0.5rem;
border-radius: 1rem;
background: rgba(var(--ion-color-warning-rgb), 0.25);
font-size: 0.7rem;
font-weight: 600;
white-space: nowrap;
}

.card-meta {
color: var(--ion-color-medium);
font-size: 0.8rem;
white-space: nowrap;
}

.collapse-icon {
flex: none;
color: var(--ion-color-medium);
font-size: 1.2rem;
}

// Opens the reading window; outside the toggle, since a button cannot hold another.
.card-expand {
display: flex;
align-items: center;
justify-content: center;
flex: none;
width: 2.25rem;
height: 2.25rem;
margin-right: 0.5rem;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--ion-color-medium);
font-size: 1.15rem;
cursor: pointer;

&:hover {
background: rgba(var(--ion-color-primary-rgb), 0.1);
color: var(--ion-color-primary);
}

&:focus-visible {
outline: 2px solid var(--ion-color-primary);
}
}

.card-body {
padding: 0 1rem 1rem;
color: var(--ion-text-color);
}
47 changes: 47 additions & 0 deletions src/app/shared/_study-shared.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// What the summary and quiz show the same way on the card and in the reading window.

.byline {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.25rem 0.75rem;
margin-bottom: 0.75rem;
color: var(--ion-color-medium);
font-size: 0.8rem;
line-height: 1.4;
}

.card-actions {
display: inline-flex;
flex-wrap: wrap;
gap: 0.35rem;
}

.card-note {
margin: 0 0 0.5rem;
color: var(--ion-color-medium);
font-size: 0.85rem;
line-height: 1.45;
}

.card-loading {
display: flex;
justify-content: center;
padding: 0.5rem 0;
}

.small-button {
margin: 0;
--padding-start: 0.6rem;
--padding-end: 0.6rem;
}

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
Loading
Loading