@shaxpir/duiduidui-models 1.9.13 → 1.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/Collection.d.ts +158 -0
- package/dist/models/Collection.js +315 -0
- package/dist/models/Condition.d.ts +12 -11
- package/dist/models/Condition.js +35 -35
- package/dist/models/Content.d.ts +2 -1
- package/dist/models/ContentKind.d.ts +1 -0
- package/dist/models/ContentKind.js +1 -0
- package/dist/models/Device.d.ts +2 -2
- package/dist/models/SearchState.d.ts +2 -2
- package/dist/models/Session.d.ts +2 -2
- package/dist/models/Workspace.d.ts +4 -4
- package/dist/models/Workspace.js +2 -2
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Doc } from '@shaxpir/sharedb/lib/client';
|
|
2
|
+
import { CompactDateTime } from "@shaxpir/shaxpir-common";
|
|
3
|
+
import { ShareSync } from '../repo';
|
|
4
|
+
import { Conditions } from './Condition';
|
|
5
|
+
import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
|
|
6
|
+
import { UncertainValue } from './Progress';
|
|
7
|
+
/**
|
|
8
|
+
* Display metadata for a Collection, used in the UI.
|
|
9
|
+
*/
|
|
10
|
+
export interface CollectionDisplay {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
icon?: string;
|
|
14
|
+
sort_order: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A snapshot of a term's proficiency within this collection.
|
|
18
|
+
* Stored in term_scores map, keyed by termId.
|
|
19
|
+
*/
|
|
20
|
+
export interface CollectionTermScore {
|
|
21
|
+
text: string;
|
|
22
|
+
sense_rank: number;
|
|
23
|
+
theta: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The payload for a Collection document.
|
|
27
|
+
*
|
|
28
|
+
* A Collection represents a scoped subset of the corpus defined by filters,
|
|
29
|
+
* combined with a competency model tracking the user's mastery within that scope.
|
|
30
|
+
*/
|
|
31
|
+
export interface CollectionPayload {
|
|
32
|
+
collection_key: string;
|
|
33
|
+
conditions: Conditions;
|
|
34
|
+
display: CollectionDisplay;
|
|
35
|
+
proficiency: UncertainValue;
|
|
36
|
+
term_scores: {
|
|
37
|
+
[termId: string]: CollectionTermScore;
|
|
38
|
+
};
|
|
39
|
+
total_cards: number;
|
|
40
|
+
last_activity_utc: CompactDateTime | null;
|
|
41
|
+
is_visible: boolean;
|
|
42
|
+
is_builtin: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface CollectionBody extends ContentBody {
|
|
45
|
+
meta: ContentMeta;
|
|
46
|
+
payload: CollectionPayload;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parameters for creating a new Collection.
|
|
50
|
+
*/
|
|
51
|
+
export interface CollectionCreateParams {
|
|
52
|
+
collectionKey: string;
|
|
53
|
+
conditions: Conditions;
|
|
54
|
+
display: CollectionDisplay;
|
|
55
|
+
isBuiltin?: boolean;
|
|
56
|
+
isVisible?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare class Collection extends Content {
|
|
59
|
+
/**
|
|
60
|
+
* Generate a deterministic Collection ID from userId and collectionKey.
|
|
61
|
+
* The same user + collectionKey always produces the same ID.
|
|
62
|
+
*/
|
|
63
|
+
static makeCollectionId(userId: ContentId, collectionKey: string): ContentId;
|
|
64
|
+
constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
|
|
65
|
+
/**
|
|
66
|
+
* Create a new Collection for a user.
|
|
67
|
+
*/
|
|
68
|
+
static create(userId: ContentId, params: CollectionCreateParams): Collection;
|
|
69
|
+
get payload(): CollectionPayload;
|
|
70
|
+
get collectionKey(): string;
|
|
71
|
+
get conditions(): Conditions;
|
|
72
|
+
get display(): CollectionDisplay;
|
|
73
|
+
get name(): string;
|
|
74
|
+
get description(): string;
|
|
75
|
+
get icon(): string | undefined;
|
|
76
|
+
get sortOrder(): number;
|
|
77
|
+
get proficiency(): UncertainValue;
|
|
78
|
+
get proficiencyRating(): number;
|
|
79
|
+
get proficiencyRatingDeviation(): number;
|
|
80
|
+
get proficiencyVolatility(): number;
|
|
81
|
+
get termScores(): {
|
|
82
|
+
[termId: string]: CollectionTermScore;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Get the number of unique terms the user has reviewed in this collection.
|
|
86
|
+
*/
|
|
87
|
+
get termsReviewedCount(): number;
|
|
88
|
+
/**
|
|
89
|
+
* Get the term score for a specific term, or undefined if not reviewed.
|
|
90
|
+
*/
|
|
91
|
+
getTermScore(termId: string): CollectionTermScore | undefined;
|
|
92
|
+
get totalCards(): number;
|
|
93
|
+
get lastActivityUtc(): CompactDateTime | null;
|
|
94
|
+
get isVisible(): boolean;
|
|
95
|
+
get isBuiltin(): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Check if this collection has any proficiency data yet.
|
|
98
|
+
*/
|
|
99
|
+
get hasProgress(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Calculate the percentage of terms that are "mastered" (theta >= 0.8).
|
|
102
|
+
* Returns 0 if no terms have been reviewed.
|
|
103
|
+
*/
|
|
104
|
+
get masteryPercentage(): number;
|
|
105
|
+
/**
|
|
106
|
+
* Calculate the percentage of total cards that have been reviewed.
|
|
107
|
+
* Returns 0 if total_cards is 0 or not set.
|
|
108
|
+
*/
|
|
109
|
+
get reviewedPercentage(): number;
|
|
110
|
+
/**
|
|
111
|
+
* Get counts of terms by grade (based on theta ranges).
|
|
112
|
+
*/
|
|
113
|
+
getGradeDistribution(): {
|
|
114
|
+
A: number;
|
|
115
|
+
B: number;
|
|
116
|
+
C: number;
|
|
117
|
+
D: number;
|
|
118
|
+
F: number;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Update the Glicko-2 proficiency rating.
|
|
122
|
+
*/
|
|
123
|
+
setProficiency(value: UncertainValue): void;
|
|
124
|
+
/**
|
|
125
|
+
* Update or add a term score in the collection.
|
|
126
|
+
*/
|
|
127
|
+
setTermScore(termId: string, score: CollectionTermScore): void;
|
|
128
|
+
/**
|
|
129
|
+
* Update just the theta value for an existing term score.
|
|
130
|
+
* Does nothing if the term doesn't exist in term_scores.
|
|
131
|
+
*/
|
|
132
|
+
updateTermTheta(termId: string, theta: number): void;
|
|
133
|
+
/**
|
|
134
|
+
* Remove a term score from the collection.
|
|
135
|
+
*/
|
|
136
|
+
removeTermScore(termId: string): void;
|
|
137
|
+
/**
|
|
138
|
+
* Update the cached total card count for this collection.
|
|
139
|
+
*/
|
|
140
|
+
setTotalCards(count: number): void;
|
|
141
|
+
/**
|
|
142
|
+
* Update the last activity timestamp.
|
|
143
|
+
*/
|
|
144
|
+
setLastActivityUtc(timestamp: CompactDateTime): void;
|
|
145
|
+
/**
|
|
146
|
+
* Update visibility of this collection.
|
|
147
|
+
*/
|
|
148
|
+
setVisible(visible: boolean): void;
|
|
149
|
+
/**
|
|
150
|
+
* Update display metadata.
|
|
151
|
+
*/
|
|
152
|
+
setDisplay(display: CollectionDisplay): void;
|
|
153
|
+
/**
|
|
154
|
+
* Update the condition definition.
|
|
155
|
+
* Note: Changing conditions on a builtin collection is not recommended.
|
|
156
|
+
*/
|
|
157
|
+
setConditions(conditions: Conditions): void;
|
|
158
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Collection = void 0;
|
|
4
|
+
const shaxpir_common_1 = require("@shaxpir/shaxpir-common");
|
|
5
|
+
const repo_1 = require("../repo");
|
|
6
|
+
const Content_1 = require("./Content");
|
|
7
|
+
const ContentKind_1 = require("./ContentKind");
|
|
8
|
+
const Operation_1 = require("./Operation");
|
|
9
|
+
class Collection extends Content_1.Content {
|
|
10
|
+
/**
|
|
11
|
+
* Generate a deterministic Collection ID from userId and collectionKey.
|
|
12
|
+
* The same user + collectionKey always produces the same ID.
|
|
13
|
+
*/
|
|
14
|
+
static makeCollectionId(userId, collectionKey) {
|
|
15
|
+
return shaxpir_common_1.CachingHasher.makeMd5Base62Hash(`${ContentKind_1.ContentKind.COLLECTION}-${userId}-${collectionKey}`);
|
|
16
|
+
}
|
|
17
|
+
constructor(doc, shouldAcquire, shareSync) {
|
|
18
|
+
super(doc, shouldAcquire, shareSync);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a new Collection for a user.
|
|
22
|
+
*/
|
|
23
|
+
static create(userId, params) {
|
|
24
|
+
const now = shaxpir_common_1.ClockService.getClock().now();
|
|
25
|
+
const collectionId = Collection.makeCollectionId(userId, params.collectionKey);
|
|
26
|
+
return repo_1.ShareSyncFactory.get().createContent({
|
|
27
|
+
meta: {
|
|
28
|
+
ref: collectionId,
|
|
29
|
+
kind: ContentKind_1.ContentKind.COLLECTION,
|
|
30
|
+
id: collectionId,
|
|
31
|
+
owner: userId,
|
|
32
|
+
created_at: now,
|
|
33
|
+
updated_at: now
|
|
34
|
+
},
|
|
35
|
+
payload: {
|
|
36
|
+
collection_key: params.collectionKey,
|
|
37
|
+
conditions: params.conditions,
|
|
38
|
+
display: params.display,
|
|
39
|
+
// Initial Glicko-2 proficiency - high uncertainty, no data yet
|
|
40
|
+
proficiency: {
|
|
41
|
+
rating: 0, // Start at zero (no demonstrated skill)
|
|
42
|
+
rating_deviation: 50, // High initial uncertainty
|
|
43
|
+
volatility: 0.06 // Moderate initial volatility
|
|
44
|
+
},
|
|
45
|
+
// Empty term scores map - populated as user reviews cards
|
|
46
|
+
term_scores: {},
|
|
47
|
+
// Progress starts at zero
|
|
48
|
+
total_cards: 0,
|
|
49
|
+
last_activity_utc: null,
|
|
50
|
+
// Visibility defaults
|
|
51
|
+
is_visible: params.isVisible ?? true,
|
|
52
|
+
is_builtin: params.isBuiltin ?? false
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// ============================================================
|
|
57
|
+
// Payload accessor
|
|
58
|
+
// ============================================================
|
|
59
|
+
get payload() {
|
|
60
|
+
this.checkDisposed("Collection.payload");
|
|
61
|
+
return this.doc.data.payload;
|
|
62
|
+
}
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Identity getters
|
|
65
|
+
// ============================================================
|
|
66
|
+
get collectionKey() {
|
|
67
|
+
this.checkDisposed("Collection.collectionKey");
|
|
68
|
+
return this.payload.collection_key;
|
|
69
|
+
}
|
|
70
|
+
// ============================================================
|
|
71
|
+
// Condition definition getters
|
|
72
|
+
// ============================================================
|
|
73
|
+
get conditions() {
|
|
74
|
+
this.checkDisposed("Collection.conditions");
|
|
75
|
+
return this.payload.conditions;
|
|
76
|
+
}
|
|
77
|
+
// ============================================================
|
|
78
|
+
// Display metadata getters
|
|
79
|
+
// ============================================================
|
|
80
|
+
get display() {
|
|
81
|
+
this.checkDisposed("Collection.display");
|
|
82
|
+
return this.payload.display;
|
|
83
|
+
}
|
|
84
|
+
get name() {
|
|
85
|
+
this.checkDisposed("Collection.name");
|
|
86
|
+
return this.payload.display.name;
|
|
87
|
+
}
|
|
88
|
+
get description() {
|
|
89
|
+
this.checkDisposed("Collection.description");
|
|
90
|
+
return this.payload.display.description;
|
|
91
|
+
}
|
|
92
|
+
get icon() {
|
|
93
|
+
this.checkDisposed("Collection.icon");
|
|
94
|
+
return this.payload.display.icon;
|
|
95
|
+
}
|
|
96
|
+
get sortOrder() {
|
|
97
|
+
this.checkDisposed("Collection.sortOrder");
|
|
98
|
+
return this.payload.display.sort_order;
|
|
99
|
+
}
|
|
100
|
+
// ============================================================
|
|
101
|
+
// Proficiency getters
|
|
102
|
+
// ============================================================
|
|
103
|
+
get proficiency() {
|
|
104
|
+
this.checkDisposed("Collection.proficiency");
|
|
105
|
+
return shaxpir_common_1.Struct.clone(this.payload.proficiency);
|
|
106
|
+
}
|
|
107
|
+
get proficiencyRating() {
|
|
108
|
+
this.checkDisposed("Collection.proficiencyRating");
|
|
109
|
+
return this.payload.proficiency.rating;
|
|
110
|
+
}
|
|
111
|
+
get proficiencyRatingDeviation() {
|
|
112
|
+
this.checkDisposed("Collection.proficiencyRatingDeviation");
|
|
113
|
+
return this.payload.proficiency.rating_deviation;
|
|
114
|
+
}
|
|
115
|
+
get proficiencyVolatility() {
|
|
116
|
+
this.checkDisposed("Collection.proficiencyVolatility");
|
|
117
|
+
return this.payload.proficiency.volatility;
|
|
118
|
+
}
|
|
119
|
+
// ============================================================
|
|
120
|
+
// Term scores getters
|
|
121
|
+
// ============================================================
|
|
122
|
+
get termScores() {
|
|
123
|
+
this.checkDisposed("Collection.termScores");
|
|
124
|
+
return this.payload.term_scores;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get the number of unique terms the user has reviewed in this collection.
|
|
128
|
+
*/
|
|
129
|
+
get termsReviewedCount() {
|
|
130
|
+
this.checkDisposed("Collection.termsReviewedCount");
|
|
131
|
+
return Object.keys(this.payload.term_scores).length;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the term score for a specific term, or undefined if not reviewed.
|
|
135
|
+
*/
|
|
136
|
+
getTermScore(termId) {
|
|
137
|
+
this.checkDisposed("Collection.getTermScore");
|
|
138
|
+
return this.payload.term_scores[termId];
|
|
139
|
+
}
|
|
140
|
+
// ============================================================
|
|
141
|
+
// Progress statistics getters
|
|
142
|
+
// ============================================================
|
|
143
|
+
get totalCards() {
|
|
144
|
+
this.checkDisposed("Collection.totalCards");
|
|
145
|
+
return this.payload.total_cards;
|
|
146
|
+
}
|
|
147
|
+
get lastActivityUtc() {
|
|
148
|
+
this.checkDisposed("Collection.lastActivityUtc");
|
|
149
|
+
return this.payload.last_activity_utc;
|
|
150
|
+
}
|
|
151
|
+
// ============================================================
|
|
152
|
+
// Visibility and type getters
|
|
153
|
+
// ============================================================
|
|
154
|
+
get isVisible() {
|
|
155
|
+
this.checkDisposed("Collection.isVisible");
|
|
156
|
+
return this.payload.is_visible;
|
|
157
|
+
}
|
|
158
|
+
get isBuiltin() {
|
|
159
|
+
this.checkDisposed("Collection.isBuiltin");
|
|
160
|
+
return this.payload.is_builtin;
|
|
161
|
+
}
|
|
162
|
+
// ============================================================
|
|
163
|
+
// Computed properties
|
|
164
|
+
// ============================================================
|
|
165
|
+
/**
|
|
166
|
+
* Check if this collection has any proficiency data yet.
|
|
167
|
+
*/
|
|
168
|
+
get hasProgress() {
|
|
169
|
+
this.checkDisposed("Collection.hasProgress");
|
|
170
|
+
return Object.keys(this.payload.term_scores).length > 0;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Calculate the percentage of terms that are "mastered" (theta >= 0.8).
|
|
174
|
+
* Returns 0 if no terms have been reviewed.
|
|
175
|
+
*/
|
|
176
|
+
get masteryPercentage() {
|
|
177
|
+
this.checkDisposed("Collection.masteryPercentage");
|
|
178
|
+
const scores = Object.values(this.payload.term_scores);
|
|
179
|
+
if (scores.length === 0)
|
|
180
|
+
return 0;
|
|
181
|
+
const mastered = scores.filter(s => s.theta >= 0.8).length;
|
|
182
|
+
return Math.round((mastered / scores.length) * 100);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Calculate the percentage of total cards that have been reviewed.
|
|
186
|
+
* Returns 0 if total_cards is 0 or not set.
|
|
187
|
+
*/
|
|
188
|
+
get reviewedPercentage() {
|
|
189
|
+
this.checkDisposed("Collection.reviewedPercentage");
|
|
190
|
+
if (this.payload.total_cards === 0)
|
|
191
|
+
return 0;
|
|
192
|
+
const reviewed = Object.keys(this.payload.term_scores).length;
|
|
193
|
+
return Math.round((reviewed / this.payload.total_cards) * 100);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get counts of terms by grade (based on theta ranges).
|
|
197
|
+
*/
|
|
198
|
+
getGradeDistribution() {
|
|
199
|
+
this.checkDisposed("Collection.getGradeDistribution");
|
|
200
|
+
const scores = Object.values(this.payload.term_scores);
|
|
201
|
+
return {
|
|
202
|
+
A: scores.filter(s => s.theta >= 0.8).length,
|
|
203
|
+
B: scores.filter(s => s.theta >= 0.6 && s.theta < 0.8).length,
|
|
204
|
+
C: scores.filter(s => s.theta >= 0.4 && s.theta < 0.6).length,
|
|
205
|
+
D: scores.filter(s => s.theta >= 0.2 && s.theta < 0.4).length,
|
|
206
|
+
F: scores.filter(s => s.theta < 0.2).length
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// ============================================================
|
|
210
|
+
// Mutators
|
|
211
|
+
// ============================================================
|
|
212
|
+
/**
|
|
213
|
+
* Update the Glicko-2 proficiency rating.
|
|
214
|
+
*/
|
|
215
|
+
setProficiency(value) {
|
|
216
|
+
this.checkDisposed("Collection.setProficiency");
|
|
217
|
+
if (!shaxpir_common_1.Struct.equals(this.payload.proficiency, value)) {
|
|
218
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
219
|
+
batch.setPathValue(['payload', 'proficiency', 'rating'], value.rating);
|
|
220
|
+
batch.setPathValue(['payload', 'proficiency', 'rating_deviation'], value.rating_deviation);
|
|
221
|
+
batch.setPathValue(['payload', 'proficiency', 'volatility'], value.volatility);
|
|
222
|
+
batch.commit();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Update or add a term score in the collection.
|
|
227
|
+
*/
|
|
228
|
+
setTermScore(termId, score) {
|
|
229
|
+
this.checkDisposed("Collection.setTermScore");
|
|
230
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
231
|
+
batch.setPathValue(['payload', 'term_scores', termId], score);
|
|
232
|
+
batch.commit();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Update just the theta value for an existing term score.
|
|
236
|
+
* Does nothing if the term doesn't exist in term_scores.
|
|
237
|
+
*/
|
|
238
|
+
updateTermTheta(termId, theta) {
|
|
239
|
+
this.checkDisposed("Collection.updateTermTheta");
|
|
240
|
+
if (this.payload.term_scores[termId]) {
|
|
241
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
242
|
+
batch.setPathValue(['payload', 'term_scores', termId, 'theta'], theta);
|
|
243
|
+
batch.commit();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Remove a term score from the collection.
|
|
248
|
+
*/
|
|
249
|
+
removeTermScore(termId) {
|
|
250
|
+
this.checkDisposed("Collection.removeTermScore");
|
|
251
|
+
if (this.payload.term_scores[termId]) {
|
|
252
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
253
|
+
batch.removeValueAtPath(['payload', 'term_scores', termId]);
|
|
254
|
+
batch.commit();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Update the cached total card count for this collection.
|
|
259
|
+
*/
|
|
260
|
+
setTotalCards(count) {
|
|
261
|
+
this.checkDisposed("Collection.setTotalCards");
|
|
262
|
+
if (this.payload.total_cards !== count) {
|
|
263
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
264
|
+
batch.setPathValue(['payload', 'total_cards'], count);
|
|
265
|
+
batch.commit();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Update the last activity timestamp.
|
|
270
|
+
*/
|
|
271
|
+
setLastActivityUtc(timestamp) {
|
|
272
|
+
this.checkDisposed("Collection.setLastActivityUtc");
|
|
273
|
+
if (this.payload.last_activity_utc !== timestamp) {
|
|
274
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
275
|
+
batch.setPathValue(['payload', 'last_activity_utc'], timestamp);
|
|
276
|
+
batch.commit();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Update visibility of this collection.
|
|
281
|
+
*/
|
|
282
|
+
setVisible(visible) {
|
|
283
|
+
this.checkDisposed("Collection.setVisible");
|
|
284
|
+
if (this.payload.is_visible !== visible) {
|
|
285
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
286
|
+
batch.setPathValue(['payload', 'is_visible'], visible);
|
|
287
|
+
batch.commit();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Update display metadata.
|
|
292
|
+
*/
|
|
293
|
+
setDisplay(display) {
|
|
294
|
+
this.checkDisposed("Collection.setDisplay");
|
|
295
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
296
|
+
batch.setPathValue(['payload', 'display', 'name'], display.name);
|
|
297
|
+
batch.setPathValue(['payload', 'display', 'description'], display.description);
|
|
298
|
+
batch.setPathValue(['payload', 'display', 'sort_order'], display.sort_order);
|
|
299
|
+
if (display.icon !== undefined) {
|
|
300
|
+
batch.setPathValue(['payload', 'display', 'icon'], display.icon);
|
|
301
|
+
}
|
|
302
|
+
batch.commit();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Update the condition definition.
|
|
306
|
+
* Note: Changing conditions on a builtin collection is not recommended.
|
|
307
|
+
*/
|
|
308
|
+
setConditions(conditions) {
|
|
309
|
+
this.checkDisposed("Collection.setConditions");
|
|
310
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
311
|
+
batch.setPathValue(['payload', 'conditions'], conditions);
|
|
312
|
+
batch.commit();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
exports.Collection = Collection;
|
|
@@ -47,11 +47,12 @@ export interface ComponentCondition extends BaseCondition {
|
|
|
47
47
|
mode: 'any' | 'all';
|
|
48
48
|
}
|
|
49
49
|
export type AnyCondition = TagCondition | GradeCondition | ThetaCondition | TemporalCondition | CountCondition | StarredCondition | DifficultyCondition | HasTermCondition | ComponentCondition;
|
|
50
|
-
export interface
|
|
50
|
+
export interface Conditions {
|
|
51
51
|
all?: AnyCondition[];
|
|
52
52
|
any?: AnyCondition[];
|
|
53
53
|
none?: AnyCondition[];
|
|
54
54
|
}
|
|
55
|
+
export type ConditionFilters = Conditions;
|
|
55
56
|
export declare const Condition: {
|
|
56
57
|
tag: (name: string) => TagCondition;
|
|
57
58
|
grade: (grade: "A" | "B" | "C" | "D" | "F") => GradeCondition;
|
|
@@ -89,44 +90,44 @@ export declare const Condition: {
|
|
|
89
90
|
/**
|
|
90
91
|
* Check if starred condition is required (in 'all' section)
|
|
91
92
|
*/
|
|
92
|
-
requiresStarred: (
|
|
93
|
+
requiresStarred: (conditions?: Conditions) => boolean;
|
|
93
94
|
/**
|
|
94
95
|
* Check if starred condition is optional (in 'any' section)
|
|
95
96
|
*/
|
|
96
|
-
allowsStarred: (
|
|
97
|
+
allowsStarred: (conditions?: Conditions) => boolean;
|
|
97
98
|
/**
|
|
98
99
|
* Check if starred condition is excluded (in 'none' section)
|
|
99
100
|
*/
|
|
100
|
-
excludesStarred: (
|
|
101
|
+
excludesStarred: (conditions?: Conditions) => boolean;
|
|
101
102
|
/**
|
|
102
103
|
* Check if starred condition exists anywhere in filters
|
|
103
104
|
*/
|
|
104
|
-
hasStarred: (
|
|
105
|
+
hasStarred: (conditions?: Conditions) => boolean;
|
|
105
106
|
/**
|
|
106
107
|
* Check if a specific tag is required (in 'all' section)
|
|
107
108
|
*/
|
|
108
|
-
requiresTag: (
|
|
109
|
+
requiresTag: (conditions: Conditions | undefined, tag: string) => boolean;
|
|
109
110
|
/**
|
|
110
111
|
* Check if a specific tag is optional (in 'any' section)
|
|
111
112
|
*/
|
|
112
|
-
allowsTag: (
|
|
113
|
+
allowsTag: (conditions: Conditions | undefined, tag: string) => boolean;
|
|
113
114
|
/**
|
|
114
115
|
* Check if a specific tag is excluded (in 'none' section)
|
|
115
116
|
*/
|
|
116
|
-
excludesTag: (
|
|
117
|
+
excludesTag: (conditions: Conditions | undefined, tag: string) => boolean;
|
|
117
118
|
/**
|
|
118
119
|
* Check if a specific grade is required (in 'all' section)
|
|
119
120
|
*/
|
|
120
|
-
requiresGrade: (
|
|
121
|
+
requiresGrade: (conditions: Conditions | undefined, grade: "A" | "B" | "C" | "D" | "F") => boolean;
|
|
121
122
|
/**
|
|
122
123
|
* Check if any condition of a specific type exists in filters
|
|
123
124
|
*/
|
|
124
|
-
hasConditionType: (
|
|
125
|
+
hasConditionType: (conditions: Conditions | undefined, type: string) => boolean;
|
|
125
126
|
/**
|
|
126
127
|
* Validate filters for logical inconsistencies
|
|
127
128
|
* Returns an array of error messages, empty if valid
|
|
128
129
|
*/
|
|
129
|
-
validate: (
|
|
130
|
+
validate: (conditions?: Conditions) => string[];
|
|
130
131
|
hasStarredInAll: (filters?: ConditionFilters) => boolean;
|
|
131
132
|
hasStarredInAny: (filters?: ConditionFilters) => boolean;
|
|
132
133
|
hasStarredInNone: (filters?: ConditionFilters) => boolean;
|
package/dist/models/Condition.js
CHANGED
|
@@ -49,84 +49,84 @@ exports.Condition = {
|
|
|
49
49
|
/**
|
|
50
50
|
* Check if starred condition is required (in 'all' section)
|
|
51
51
|
*/
|
|
52
|
-
requiresStarred: (
|
|
53
|
-
return !!(
|
|
52
|
+
requiresStarred: (conditions) => {
|
|
53
|
+
return !!(conditions?.all?.some(c => c.type === 'starred' && c.value === true));
|
|
54
54
|
},
|
|
55
55
|
/**
|
|
56
56
|
* Check if starred condition is optional (in 'any' section)
|
|
57
57
|
*/
|
|
58
|
-
allowsStarred: (
|
|
59
|
-
return !!(
|
|
58
|
+
allowsStarred: (conditions) => {
|
|
59
|
+
return !!(conditions?.any?.some(c => c.type === 'starred' && c.value === true));
|
|
60
60
|
},
|
|
61
61
|
/**
|
|
62
62
|
* Check if starred condition is excluded (in 'none' section)
|
|
63
63
|
*/
|
|
64
|
-
excludesStarred: (
|
|
65
|
-
return !!(
|
|
64
|
+
excludesStarred: (conditions) => {
|
|
65
|
+
return !!(conditions?.none?.some(c => c.type === 'starred' && c.value === true));
|
|
66
66
|
},
|
|
67
67
|
/**
|
|
68
68
|
* Check if starred condition exists anywhere in filters
|
|
69
69
|
*/
|
|
70
|
-
hasStarred: (
|
|
71
|
-
return exports.Condition.requiresStarred(
|
|
72
|
-
exports.Condition.allowsStarred(
|
|
73
|
-
exports.Condition.excludesStarred(
|
|
70
|
+
hasStarred: (conditions) => {
|
|
71
|
+
return exports.Condition.requiresStarred(conditions) ||
|
|
72
|
+
exports.Condition.allowsStarred(conditions) ||
|
|
73
|
+
exports.Condition.excludesStarred(conditions);
|
|
74
74
|
},
|
|
75
75
|
/**
|
|
76
76
|
* Check if a specific tag is required (in 'all' section)
|
|
77
77
|
*/
|
|
78
|
-
requiresTag: (
|
|
79
|
-
return !!(
|
|
78
|
+
requiresTag: (conditions, tag) => {
|
|
79
|
+
return !!(conditions?.all?.some(c => c.type === 'tag' && c.tag === tag));
|
|
80
80
|
},
|
|
81
81
|
/**
|
|
82
82
|
* Check if a specific tag is optional (in 'any' section)
|
|
83
83
|
*/
|
|
84
|
-
allowsTag: (
|
|
85
|
-
return !!(
|
|
84
|
+
allowsTag: (conditions, tag) => {
|
|
85
|
+
return !!(conditions?.any?.some(c => c.type === 'tag' && c.tag === tag));
|
|
86
86
|
},
|
|
87
87
|
/**
|
|
88
88
|
* Check if a specific tag is excluded (in 'none' section)
|
|
89
89
|
*/
|
|
90
|
-
excludesTag: (
|
|
91
|
-
return !!(
|
|
90
|
+
excludesTag: (conditions, tag) => {
|
|
91
|
+
return !!(conditions?.none?.some(c => c.type === 'tag' && c.tag === tag));
|
|
92
92
|
},
|
|
93
93
|
/**
|
|
94
94
|
* Check if a specific grade is required (in 'all' section)
|
|
95
95
|
*/
|
|
96
|
-
requiresGrade: (
|
|
97
|
-
return !!(
|
|
96
|
+
requiresGrade: (conditions, grade) => {
|
|
97
|
+
return !!(conditions?.all?.some(c => c.type === 'grade' && c.grade === grade));
|
|
98
98
|
},
|
|
99
99
|
/**
|
|
100
100
|
* Check if any condition of a specific type exists in filters
|
|
101
101
|
*/
|
|
102
|
-
hasConditionType: (
|
|
103
|
-
return !!(
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
hasConditionType: (conditions, type) => {
|
|
103
|
+
return !!(conditions?.all?.some(c => c.type === type) ||
|
|
104
|
+
conditions?.any?.some(c => c.type === type) ||
|
|
105
|
+
conditions?.none?.some(c => c.type === type));
|
|
106
106
|
},
|
|
107
107
|
/**
|
|
108
108
|
* Validate filters for logical inconsistencies
|
|
109
109
|
* Returns an array of error messages, empty if valid
|
|
110
110
|
*/
|
|
111
|
-
validate: (
|
|
112
|
-
if (!
|
|
111
|
+
validate: (conditions) => {
|
|
112
|
+
if (!conditions)
|
|
113
113
|
return [];
|
|
114
114
|
const errors = [];
|
|
115
115
|
// Check for contradictory starred conditions
|
|
116
|
-
if (exports.Condition.requiresStarred(
|
|
116
|
+
if (exports.Condition.requiresStarred(conditions) && exports.Condition.excludesStarred(conditions)) {
|
|
117
117
|
errors.push('Cannot require starred items and exclude starred items at the same time');
|
|
118
118
|
}
|
|
119
119
|
// Check for contradictory tags
|
|
120
|
-
const allTags =
|
|
121
|
-
const noneTags =
|
|
120
|
+
const allTags = conditions.all?.filter(c => c.type === 'tag').map(c => c.tag) || [];
|
|
121
|
+
const noneTags = conditions.none?.filter(c => c.type === 'tag').map(c => c.tag) || [];
|
|
122
122
|
allTags.forEach(tag => {
|
|
123
123
|
if (noneTags.includes(tag)) {
|
|
124
124
|
errors.push(`Tag "${tag}" cannot be both required and excluded`);
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
127
|
// Check for contradictory grades
|
|
128
|
-
const allGrades =
|
|
129
|
-
const noneGrades =
|
|
128
|
+
const allGrades = conditions.all?.filter(c => c.type === 'grade').map(c => c.grade) || [];
|
|
129
|
+
const noneGrades = conditions.none?.filter(c => c.type === 'grade').map(c => c.grade) || [];
|
|
130
130
|
allGrades.forEach(grade => {
|
|
131
131
|
if (noneGrades.includes(grade)) {
|
|
132
132
|
errors.push(`Grade "${grade}" cannot be both required and excluded`);
|
|
@@ -137,24 +137,24 @@ exports.Condition = {
|
|
|
137
137
|
errors.push(`Cannot require multiple grades: ${allGrades.join(', ')} - an item can only have one grade`);
|
|
138
138
|
}
|
|
139
139
|
// Check for contradictory has_term conditions
|
|
140
|
-
const requiresTerm =
|
|
141
|
-
const excludesTerm =
|
|
140
|
+
const requiresTerm = conditions.all?.some(c => c.type === 'has_term' && c.value === true);
|
|
141
|
+
const excludesTerm = conditions.all?.some(c => c.type === 'has_term' && c.value === false);
|
|
142
142
|
if (requiresTerm && excludesTerm) {
|
|
143
143
|
errors.push('Cannot require both has_term and no_term');
|
|
144
144
|
}
|
|
145
145
|
// Check for implicit term requirements conflicting with no_term
|
|
146
146
|
const termDependentTypes = ['grade', 'theta', 'starred', 'temporal', 'count'];
|
|
147
|
-
const hasTermDependentInAll =
|
|
148
|
-
const hasTermDependentInAny =
|
|
147
|
+
const hasTermDependentInAll = conditions.all?.some(c => termDependentTypes.includes(c.type));
|
|
148
|
+
const hasTermDependentInAny = conditions.any?.some(c => termDependentTypes.includes(c.type));
|
|
149
149
|
if (excludesTerm && hasTermDependentInAll) {
|
|
150
|
-
const conflictingTypes =
|
|
150
|
+
const conflictingTypes = conditions.all
|
|
151
151
|
?.filter(c => termDependentTypes.includes(c.type))
|
|
152
152
|
.map(c => c.type)
|
|
153
153
|
.join(', ');
|
|
154
154
|
errors.push(`Conditions [${conflictingTypes}] require a term record, but has_term is false`);
|
|
155
155
|
}
|
|
156
156
|
if (excludesTerm && hasTermDependentInAny) {
|
|
157
|
-
const conflictingTypes =
|
|
157
|
+
const conflictingTypes = conditions.any
|
|
158
158
|
?.filter(c => termDependentTypes.includes(c.type))
|
|
159
159
|
.map(c => c.type)
|
|
160
160
|
.join(', ');
|
package/dist/models/Content.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Doc } from '@shaxpir/sharedb/lib/client';
|
|
|
2
2
|
import { CompactDateTime, MultiTime } from '@shaxpir/shaxpir-common';
|
|
3
3
|
import { ShareSync } from '../repo';
|
|
4
4
|
import { BillingPayload } from './Billing';
|
|
5
|
+
import { CollectionPayload } from './Collection';
|
|
5
6
|
import { ContentKind } from './ContentKind';
|
|
6
7
|
import { DevicePayload } from './Device';
|
|
7
8
|
import { ImagePayload } from './Image';
|
|
@@ -37,7 +38,7 @@ export interface ContentMeta {
|
|
|
37
38
|
created_at: MultiTime;
|
|
38
39
|
updated_at: MultiTime;
|
|
39
40
|
}
|
|
40
|
-
export type ContentPayload = BillingPayload | DevicePayload | ImagePayload | MetricPayload | ProfilePayload | ProgressPayload | SessionPayload | SocialPayload | TermPayload | UserPayload | WorkspacePayload;
|
|
41
|
+
export type ContentPayload = BillingPayload | CollectionPayload | DevicePayload | ImagePayload | MetricPayload | ProfilePayload | ProgressPayload | SessionPayload | SocialPayload | TermPayload | UserPayload | WorkspacePayload;
|
|
41
42
|
export declare abstract class Content extends Model {
|
|
42
43
|
static ID_LENGTH: number;
|
|
43
44
|
constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
|
|
@@ -6,6 +6,7 @@ var ContentKind;
|
|
|
6
6
|
// No models yet for these, but they're definitely coming
|
|
7
7
|
ContentKind["BILLING"] = "billing";
|
|
8
8
|
// These are all the current Content subclasses
|
|
9
|
+
ContentKind["COLLECTION"] = "collection";
|
|
9
10
|
ContentKind["DEVICE"] = "device";
|
|
10
11
|
ContentKind["METRIC"] = "metric";
|
|
11
12
|
ContentKind["PROFILE"] = "profile";
|
package/dist/models/Device.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Doc } from '@shaxpir/sharedb/lib/client';
|
|
|
2
2
|
import { CompactDateTime } from "@shaxpir/shaxpir-common";
|
|
3
3
|
import { ShareSync } from '../repo';
|
|
4
4
|
import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
|
|
5
|
-
import {
|
|
5
|
+
import { Conditions } from './Condition';
|
|
6
6
|
import { BuiltInDbState, DatabaseVersion } from '../util/Database';
|
|
7
7
|
import { SearchHistory, SearchState } from './SearchState';
|
|
8
8
|
export interface LastSync {
|
|
@@ -34,7 +34,7 @@ export interface DevicePayload {
|
|
|
34
34
|
chinese_font?: string;
|
|
35
35
|
raw_search_text?: string;
|
|
36
36
|
star_filter?: boolean;
|
|
37
|
-
conditions?:
|
|
37
|
+
conditions?: Conditions;
|
|
38
38
|
download_queue?: DownloadQueueItem[];
|
|
39
39
|
upload_queue?: UploadQueueItem[];
|
|
40
40
|
builtin_db?: BuiltInDbState;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Conditions } from './Condition';
|
|
2
2
|
/**
|
|
3
3
|
* Represents the data portion of a search state.
|
|
4
4
|
* This is the shareable/persistable part of a search - it excludes
|
|
@@ -12,7 +12,7 @@ import { ConditionFilters } from './Condition';
|
|
|
12
12
|
export interface SearchState {
|
|
13
13
|
query?: string;
|
|
14
14
|
senseRank?: number;
|
|
15
|
-
conditions?:
|
|
15
|
+
conditions?: Conditions;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Maximum number of entries in the search history stack.
|
package/dist/models/Session.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Doc } from '@shaxpir/sharedb/lib/client';
|
|
2
2
|
import { MultiTime } from "@shaxpir/shaxpir-common";
|
|
3
3
|
import { ShareSync } from '../repo';
|
|
4
|
-
import {
|
|
4
|
+
import { Conditions } from './Condition';
|
|
5
5
|
import { Content, ContentBody, ContentId, ContentMeta, ContentRef } from "./Content";
|
|
6
6
|
import { Review } from './Review';
|
|
7
7
|
import { UncertainValue } from './Progress';
|
|
@@ -12,7 +12,7 @@ export interface SessionConfig {
|
|
|
12
12
|
};
|
|
13
13
|
selection?: {
|
|
14
14
|
difficulty_ramp?: number;
|
|
15
|
-
filters?:
|
|
15
|
+
filters?: Conditions;
|
|
16
16
|
strategy?: {
|
|
17
17
|
name: string;
|
|
18
18
|
constants?: Record<string, any>;
|
|
@@ -3,7 +3,7 @@ import { CompactDate, CompactDateTime, MultiTime } from "@shaxpir/shaxpir-common
|
|
|
3
3
|
import { ShareSync } from '../repo';
|
|
4
4
|
import { ArrayView } from './ArrayView';
|
|
5
5
|
import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
|
|
6
|
-
import {
|
|
6
|
+
import { Conditions } from './Condition';
|
|
7
7
|
import { Session } from './Session';
|
|
8
8
|
export interface JourneyEntry {
|
|
9
9
|
key: string;
|
|
@@ -16,7 +16,7 @@ export interface WorkspacePayload {
|
|
|
16
16
|
journey: {
|
|
17
17
|
[key: string]: CompactDateTime;
|
|
18
18
|
};
|
|
19
|
-
global_conditions:
|
|
19
|
+
global_conditions: Conditions;
|
|
20
20
|
uploaded_avatars: ContentId[];
|
|
21
21
|
default_voice?: VoicePreference;
|
|
22
22
|
}
|
|
@@ -30,7 +30,7 @@ export declare class Workspace extends Content {
|
|
|
30
30
|
private _uploadedAvatarsView;
|
|
31
31
|
constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
|
|
32
32
|
get payload(): WorkspacePayload;
|
|
33
|
-
getGlobalConditions():
|
|
33
|
+
getGlobalConditions(): Conditions;
|
|
34
34
|
static makeWorkspaceId(userId: ContentId): ContentId;
|
|
35
35
|
/**
|
|
36
36
|
* Validate that a WorkspacePayload has all required fields.
|
|
@@ -46,7 +46,7 @@ export declare class Workspace extends Content {
|
|
|
46
46
|
hasJourneyDate(journeyKey: string): boolean;
|
|
47
47
|
getJourneyDate(journeyKey: string): CompactDateTime;
|
|
48
48
|
setJourneyDate(journeyKey: string, journeyDate: CompactDateTime): void;
|
|
49
|
-
setGlobalConditions(
|
|
49
|
+
setGlobalConditions(conditions: Conditions): void;
|
|
50
50
|
get defaultVoice(): VoicePreference;
|
|
51
51
|
setDefaultVoice(value: VoicePreference): void;
|
|
52
52
|
}
|
package/dist/models/Workspace.js
CHANGED
|
@@ -130,10 +130,10 @@ class Workspace extends Content_1.Content {
|
|
|
130
130
|
batch.setPathValue(['payload', 'journey', journeyKey], journeyDate);
|
|
131
131
|
batch.commit();
|
|
132
132
|
}
|
|
133
|
-
setGlobalConditions(
|
|
133
|
+
setGlobalConditions(conditions) {
|
|
134
134
|
this.checkDisposed("Workspace.setGlobalConditions");
|
|
135
135
|
const batch = new Operation_1.BatchOperation(this);
|
|
136
|
-
batch.setPathValue(['payload', 'global_conditions'],
|
|
136
|
+
batch.setPathValue(['payload', 'global_conditions'], conditions);
|
|
137
137
|
batch.commit();
|
|
138
138
|
}
|
|
139
139
|
get defaultVoice() {
|
package/dist/models/index.d.ts
CHANGED
package/dist/models/index.js
CHANGED
|
@@ -19,6 +19,7 @@ __exportStar(require("./ArrayView"), exports);
|
|
|
19
19
|
__exportStar(require("./BayesianScore"), exports);
|
|
20
20
|
__exportStar(require("./Billing"), exports);
|
|
21
21
|
__exportStar(require("./ChangeModel"), exports);
|
|
22
|
+
__exportStar(require("./Collection"), exports);
|
|
22
23
|
__exportStar(require("./Condition"), exports);
|
|
23
24
|
__exportStar(require("./Content"), exports);
|
|
24
25
|
__exportStar(require("./ContentKind"), exports);
|