Skip to content

Commit bd159f8

Browse files
authored
course pathfinding (#688)
- **refactor: group contentNavigation items** - **add NavigationStrategy base types** - **make courseDB impl compliant w/ StrategyManager** - **update w/ interface changes** - **add `coursedbInterface` to ContentNavigator inputs** - **add ELO contentNavigator ...** - **add getter for courseIDs** - **use refactored default ELO contentNavigator** - **use enum switch for implementation picking** - **remove hard-coded ELO getPendingReviews**
2 parents b708028 + dbcfc78 commit bd159f8

File tree

7 files changed

+348
-144
lines changed

7 files changed

+348
-144
lines changed

packages/db/src/core/interfaces/courseDB.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { StudySessionNewItem, StudySessionItem } from './contentSource';
33
import { TagStub, Tag } from '../types/types-legacy';
44
import { SkuilderCourseData } from '@vue-skuilder/common/dist/db';
55
import { DataLayerResult } from '../types/db';
6+
import { NavigationStrategyManager } from './navigationStrategyManager';
67

78
/**
89
* Course content and management
@@ -26,12 +27,14 @@ export interface CourseInfo {
2627
registeredUsers: number;
2728
}
2829

29-
export interface CourseDBInterface {
30+
export interface CourseDBInterface extends NavigationStrategyManager {
3031
/**
3132
* Get course config
3233
*/
3334
getCourseConfig(): Promise<CourseConfig>;
3435

36+
getCourseID(): string;
37+
3538
/**
3639
* Set course config
3740
*/
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
2+
3+
/**
4+
* A NavigationStrategyManager is an entity which may contain multiple strategies.
5+
*
6+
* This interface defines strategy CRUD.
7+
*/
8+
export interface NavigationStrategyManager {
9+
/**
10+
* Get the navigation strategy for a given course
11+
* @returns The navigation strategy for the course
12+
*/
13+
getNavigationStrategy(id: string): Promise<ContentNavigationStrategyData>;
14+
15+
/**
16+
* Get all available navigation strategies
17+
* @returns An array of all available navigation strategies
18+
*/
19+
getAllNavigationStrategies(): Promise<ContentNavigationStrategyData[]>;
20+
21+
/**
22+
* Add a new navigation strategy
23+
* @param data The data for the new navigation strategy
24+
* @returns A promise that resolves when the strategy has been added
25+
*/
26+
addNavigationStrategy(data: ContentNavigationStrategyData): Promise<void>;
27+
28+
/**
29+
* Update an existing navigation strategy
30+
* @param id The ID of the navigation strategy to update
31+
* @param data The new data for the navigation strategy
32+
* @returns A promise that resolves when the update is complete
33+
*/
34+
updateNavigationStrategy(id: string, data: ContentNavigationStrategyData): Promise<void>;
35+
36+
/**
37+
* @returns A content navigation strategy suitable to the current context.
38+
*/
39+
surfaceNavigationStrategy(): Promise<ContentNavigationStrategyData>;
40+
41+
// [ ] addons here like:
42+
// - determining Navigation Strategy from context of current user
43+
// - determining weighted averages of navigation strategies
44+
// - expressing A/B testing results of 'ecosystem of strategies'
45+
// - etc etc
46+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { ScheduledCard } from '../types/user';
2+
import { CourseDBInterface } from '../interfaces/courseDB';
3+
import { UserDBInterface } from '../interfaces/userDB';
4+
import { ContentNavigator } from './index';
5+
import { CourseElo } from '@vue-skuilder/common';
6+
import { StudySessionReviewItem, StudySessionNewItem } from '..';
7+
8+
export default class ELONavigator extends ContentNavigator {
9+
user: UserDBInterface;
10+
course: CourseDBInterface;
11+
12+
constructor(
13+
user: UserDBInterface,
14+
course: CourseDBInterface
15+
// The ELO strategy is non-parameterized.
16+
//
17+
// It instead relies on existing meta data from the course and user with respect to
18+
//
19+
//
20+
// strategy?: ContentNavigationStrategyData
21+
) {
22+
super();
23+
this.user = user;
24+
this.course = course;
25+
}
26+
27+
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
28+
type ratedReview = ScheduledCard & CourseElo;
29+
30+
const reviews = await this.user.getPendingReviews(this.course.getCourseID()); // todo: this adds a db round trip - should be server side
31+
const elo = await this.course.getCardEloData(reviews.map((r) => r.cardId));
32+
33+
const ratedReviews = reviews.map((r, i) => {
34+
const ratedR: ratedReview = {
35+
...r,
36+
...elo[i],
37+
};
38+
return ratedR;
39+
});
40+
41+
ratedReviews.sort((a, b) => {
42+
return a.global.score - b.global.score;
43+
});
44+
45+
return ratedReviews.map((r) => {
46+
return {
47+
...r,
48+
contentSourceType: 'course',
49+
contentSourceID: this.course.getCourseID(),
50+
cardID: r.cardId,
51+
courseID: r.courseId,
52+
qualifiedID: `${r.courseId}-${r.cardId}`,
53+
reviewID: r._id,
54+
status: 'review',
55+
};
56+
});
57+
}
58+
59+
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
60+
const activeCards = await this.user.getActiveCards();
61+
return (
62+
await this.course.getCardsCenteredAtELO({ limit: limit, elo: 'user' }, (c: string) => {
63+
if (activeCards.some((ac) => c.includes(ac))) {
64+
return false;
65+
} else {
66+
return true;
67+
}
68+
})
69+
).map((c) => {
70+
return {
71+
...c,
72+
status: 'new',
73+
};
74+
});
75+
}
76+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
StudyContentSource,
3+
UserDBInterface,
4+
CourseDBInterface,
5+
StudySessionReviewItem,
6+
StudySessionNewItem,
7+
} from '..';
8+
import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
9+
import { ScheduledCard } from '../types/user';
10+
11+
export enum Navigators {
12+
ELO = 'elo',
13+
}
14+
15+
/**
16+
* A content-navigator provides runtime steering of study sessions.
17+
*/
18+
export abstract class ContentNavigator implements StudyContentSource {
19+
/**
20+
*
21+
* @param user
22+
* @param strategyData
23+
* @returns the runtime object used to steer a study session.
24+
*/
25+
static create(
26+
user: UserDBInterface,
27+
course: CourseDBInterface,
28+
strategyData: ContentNavigationStrategyData
29+
): ContentNavigator {
30+
const implementingClass = strategyData.implementingClass;
31+
let NavigatorImpl;
32+
33+
// Try different extension variations
34+
const variations = ['', '.ts', '.js'];
35+
36+
for (const ext of variations) {
37+
try {
38+
NavigatorImpl = require(`./${implementingClass}${ext}`).default;
39+
break; // Break the loop if loading succeeds
40+
} catch (e) {
41+
// Continue to next variation if this one fails
42+
console.log(`Failed to load with extension ${ext}:`, e);
43+
}
44+
}
45+
46+
if (!NavigatorImpl) {
47+
throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
48+
}
49+
50+
return new NavigatorImpl(user, course, strategyData);
51+
}
52+
53+
abstract getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
54+
abstract getNewCards(n?: number): Promise<StudySessionNewItem[]>;
55+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { DocType, SkuilderCourseData } from './types-legacy';
2+
3+
/**
4+
*
5+
*/
6+
export interface ContentNavigationStrategyData extends SkuilderCourseData {
7+
id: string;
8+
docType: DocType.NAVIGATION_STRATEGY;
9+
name: string;
10+
description: string;
11+
/**
12+
The name of the class that implements the navigation strategy at runtime.
13+
*/
14+
implementingClass: string;
15+
16+
/**
17+
A representation of the strategy's parameterization - to be deserialized
18+
by the implementing class's constructor at runtime.
19+
*/
20+
serializedData: string;
21+
}

packages/db/src/core/types/types-legacy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export enum DocType {
1717
CARDRECORD = 'CARDRECORD',
1818
SCHEDULED_CARD = 'SCHEDULED_CARD',
1919
TAG = 'TAG',
20+
NAVIGATION_STRATEGY = 'NAVIGATION_STRATEGY',
2021
}
2122

2223
/**

0 commit comments

Comments
 (0)