@shaxpir/duiduidui-models 1.7.2 → 1.7.4

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.
@@ -34,7 +34,7 @@ export declare class Metric extends Content {
34
34
  get maxDate(): CompactDate;
35
35
  get length(): number;
36
36
  get(index: number): MetricEntry;
37
- sumBetween(minDate: CompactDate, maxDate: CompactDate): number;
37
+ sumBetween(minDate?: CompactDate | null, maxDate?: CompactDate | null): number;
38
38
  forDate(date: CompactDate): number;
39
39
  nonZeroDayCount(): number;
40
40
  setDateAmount(date: CompactDate, amount: number): void;
@@ -97,8 +97,16 @@ class Metric extends Content_1.Content {
97
97
  }
98
98
  sumBetween(minDate, maxDate) {
99
99
  this.checkDisposed("Metric.sumBetween");
100
+ // Use payload min/max dates as defaults for null parameters
101
+ const effectiveMinDate = minDate || this.payload.min_date;
102
+ const effectiveMaxDate = maxDate || this.payload.max_date;
103
+ // If we still don't have dates (empty metric), return 0
104
+ if (!effectiveMinDate || !effectiveMaxDate) {
105
+ return 0;
106
+ }
107
+ // Sum between the effective dates
100
108
  let sum = 0;
101
- const dates = shaxpir_common_1.Time.datesBetween(minDate, maxDate);
109
+ const dates = shaxpir_common_1.Time.datesBetween(effectiveMinDate, effectiveMaxDate);
102
110
  for (let i = 0, len = dates.length; i < len; i++) {
103
111
  const date = dates[i];
104
112
  sum += this.forDate(date);
@@ -30,6 +30,7 @@ export interface ProgressPayload {
30
30
  skill_level: UncertainValue;
31
31
  cognitive_load: number;
32
32
  streaks?: StreakData;
33
+ total_review_count: number;
33
34
  }
34
35
  export interface ProgressBody extends ContentBody {
35
36
  meta: ContentMeta;
@@ -55,4 +56,6 @@ export declare class Progress extends Content {
55
56
  setCognitiveLoad(value: number): void;
56
57
  getStreaks(): StreakData | undefined;
57
58
  setStreaks(streaks: StreakData): void;
59
+ getTotalReviewCount(): number;
60
+ setTotalReviewCount(count: number): void;
58
61
  }
@@ -31,7 +31,8 @@ class Progress extends Content_1.Content {
31
31
  upper: 1000 // Could potentially be advanced
32
32
  }
33
33
  },
34
- cognitive_load: 0
34
+ cognitive_load: 0,
35
+ total_review_count: 0 // Start with zero lifetime reviews
35
36
  }
36
37
  });
37
38
  }
@@ -134,5 +135,17 @@ class Progress extends Content_1.Content {
134
135
  batch.commit();
135
136
  }
136
137
  }
138
+ getTotalReviewCount() {
139
+ this.checkDisposed("Progress.getTotalReviewCount");
140
+ return this.payload.total_review_count;
141
+ }
142
+ setTotalReviewCount(count) {
143
+ this.checkDisposed("Progress.setTotalReviewCount");
144
+ if (this.payload.total_review_count !== count) {
145
+ const batch = new Operation_1.BatchOperation(this);
146
+ batch.setPathValue(['payload', 'total_review_count'], count);
147
+ batch.commit();
148
+ }
149
+ }
137
150
  }
138
151
  exports.Progress = Progress;
@@ -23,7 +23,7 @@ export interface SessionConfig {
23
23
  auto_play_speech?: boolean;
24
24
  };
25
25
  }
26
- export interface SessionMetrics {
26
+ export interface SessionStats {
27
27
  skill_level: UncertainValue;
28
28
  cognitive_load: number;
29
29
  theta_distribution: {
@@ -58,9 +58,9 @@ export interface SessionPayload {
58
58
  is_complete: boolean;
59
59
  app_version?: string;
60
60
  config?: SessionConfig;
61
- metrics?: {
62
- before?: SessionMetrics;
63
- after?: SessionMetrics;
61
+ stats?: {
62
+ before?: SessionStats;
63
+ after?: SessionStats;
64
64
  };
65
65
  }
66
66
  export interface SessionBody extends ContentBody {
@@ -87,10 +87,10 @@ export declare class Session extends Content {
87
87
  setConfig(config: SessionConfig): void;
88
88
  get isComplete(): boolean;
89
89
  markComplete(): void;
90
- get metricsBefore(): SessionMetrics | undefined;
91
- setMetricsBefore(metrics: SessionMetrics): void;
92
- get metricsAfter(): SessionMetrics | undefined;
93
- setMetricsAfter(metrics: SessionMetrics): void;
90
+ get statsBefore(): SessionStats | undefined;
91
+ setStatsBefore(stats: SessionStats): void;
92
+ get statsAfter(): SessionStats | undefined;
93
+ setStatsAfter(stats: SessionStats): void;
94
94
  isActive(): boolean;
95
95
  get appVersion(): string | undefined;
96
96
  setAppVersion(version: string): void;
@@ -8,6 +8,8 @@ const Content_1 = require("./Content");
8
8
  const ContentKind_1 = require("./ContentKind");
9
9
  const Metric_1 = require("./Metric");
10
10
  const Operation_1 = require("./Operation");
11
+ const Progress_1 = require("./Progress");
12
+ const Streaks_1 = require("./Streaks");
11
13
  const Workspace_1 = require("./Workspace");
12
14
  class Session extends Content_1.Content {
13
15
  constructor(doc, shouldAcquire, shareSync) {
@@ -128,30 +130,30 @@ class Session extends Content_1.Content {
128
130
  batch.setPathValue(['payload', 'is_complete'], true);
129
131
  batch.commit();
130
132
  }
131
- get metricsBefore() {
132
- this.checkDisposed("Session.metricsBefore");
133
- return this.payload.metrics?.before;
133
+ get statsBefore() {
134
+ this.checkDisposed("Session.statsBefore");
135
+ return this.payload.stats?.before;
134
136
  }
135
- setMetricsBefore(metrics) {
136
- this.checkDisposed("Session.setMetricsBefore");
137
+ setStatsBefore(stats) {
138
+ this.checkDisposed("Session.setStatsBefore");
137
139
  const batch = new Operation_1.BatchOperation(this);
138
- if (!this.payload.metrics) {
139
- batch.setPathValue(['payload', 'metrics'], {});
140
+ if (!this.payload.stats) {
141
+ batch.setPathValue(['payload', 'stats'], {});
140
142
  }
141
- batch.setPathValue(['payload', 'metrics', 'before'], metrics);
143
+ batch.setPathValue(['payload', 'stats', 'before'], stats);
142
144
  batch.commit();
143
145
  }
144
- get metricsAfter() {
145
- this.checkDisposed("Session.metricsAfter");
146
- return this.payload.metrics?.after;
146
+ get statsAfter() {
147
+ this.checkDisposed("Session.statsAfter");
148
+ return this.payload.stats?.after;
147
149
  }
148
- setMetricsAfter(metrics) {
149
- this.checkDisposed("Session.setMetricsAfter");
150
+ setStatsAfter(stats) {
151
+ this.checkDisposed("Session.setStatsAfter");
150
152
  const batch = new Operation_1.BatchOperation(this);
151
- if (!this.payload.metrics) {
152
- batch.setPathValue(['payload', 'metrics'], {});
153
+ if (!this.payload.stats) {
154
+ batch.setPathValue(['payload', 'stats'], {});
153
155
  }
154
- batch.setPathValue(['payload', 'metrics', 'after'], metrics);
156
+ batch.setPathValue(['payload', 'stats', 'after'], stats);
155
157
  batch.commit();
156
158
  }
157
159
  isActive() {
@@ -201,7 +203,22 @@ class Session extends Content_1.Content {
201
203
  const reviewCountMetricId = Metric_1.Metric.makeMetricId(userId, Metric_1.MetricName.REVIEW_COUNT);
202
204
  const reviewCountMetric = await shareSync.acquire(ContentKind_1.ContentKind.METRIC, reviewCountMetricId);
203
205
  reviewCountMetric.setDateAmount(date, reviewCountOnDate);
206
+ // Calculate total review count using sumBetween with null arguments (sums all entries)
207
+ const totalReviewCount = reviewCountMetric.sumBetween(null, null);
204
208
  reviewCountMetric.release();
209
+ // Update total review count in Progress
210
+ const progressId = Progress_1.Progress.makeProgressId(userId);
211
+ const progress = await shareSync.acquire(ContentKind_1.ContentKind.PROGRESS, progressId);
212
+ progress.setTotalReviewCount(totalReviewCount);
213
+ // Calculate streaks based on metrics
214
+ const now = shaxpir_common_1.ClockService.getClock().now();
215
+ const today = shaxpir_common_1.Time.dateFrom(now.local_time);
216
+ // Use the Streaks class to calculate streak data
217
+ const streakData = Streaks_1.Streaks.calculateFromMetric(minutesStudyingMetric, today);
218
+ if (streakData) {
219
+ progress.setStreaks(streakData);
220
+ }
221
+ progress.release();
205
222
  }
206
223
  }
207
224
  exports.Session = Session;
@@ -0,0 +1,14 @@
1
+ import { CompactDate } from "@shaxpir/shaxpir-common";
2
+ import { Metric } from './Metric';
3
+ import { StreakData } from './Progress';
4
+ export declare class Streaks {
5
+ /**
6
+ * Calculate streak data from a Metric containing activity dates.
7
+ * Returns daily, weekly, and monthly streak information.
8
+ *
9
+ * @param metric - The Metric model containing activity data
10
+ * @param today - The current date for streak calculation
11
+ * @returns StreakData object with streak information, or null if no activity
12
+ */
13
+ static calculateFromMetric(metric: Metric, today: CompactDate): StreakData | null;
14
+ }
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Streaks = void 0;
4
+ const shaxpir_common_1 = require("@shaxpir/shaxpir-common");
5
+ class Streaks {
6
+ /**
7
+ * Calculate streak data from a Metric containing activity dates.
8
+ * Returns daily, weekly, and monthly streak information.
9
+ *
10
+ * @param metric - The Metric model containing activity data
11
+ * @param today - The current date for streak calculation
12
+ * @returns StreakData object with streak information, or null if no activity
13
+ */
14
+ static calculateFromMetric(metric, today) {
15
+ const minDate = metric.minDate;
16
+ const maxDate = metric.maxDate;
17
+ if (!minDate || !maxDate) {
18
+ return null;
19
+ }
20
+ // Get all dates between min and max using Metric's datesBetween
21
+ const allDates = shaxpir_common_1.Time.datesBetween(minDate, maxDate);
22
+ const activityDates = [];
23
+ // Collect dates with actual activity
24
+ for (const date of allDates) {
25
+ if (metric.forDate(date) > 0) {
26
+ activityDates.push(date);
27
+ }
28
+ }
29
+ if (activityDates.length === 0) {
30
+ return null;
31
+ }
32
+ // Calculate daily streak
33
+ let dailyCurrent = 0;
34
+ let dailyLongest = 0;
35
+ let tempStreak = 0;
36
+ let lastDate = null;
37
+ for (const date of activityDates) {
38
+ if (!lastDate) {
39
+ tempStreak = 1;
40
+ }
41
+ else {
42
+ // Calculate days between dates
43
+ const datesBetween = shaxpir_common_1.Time.datesBetween(lastDate, date);
44
+ const daysBetween = datesBetween.length - 1; // Subtract 1 because datesBetween includes both dates
45
+ if (daysBetween === 1) {
46
+ tempStreak++;
47
+ }
48
+ else {
49
+ // Streak broken
50
+ dailyLongest = Math.max(dailyLongest, tempStreak);
51
+ tempStreak = 1;
52
+ }
53
+ }
54
+ lastDate = date;
55
+ }
56
+ // Check if current streak is still active (includes today or yesterday)
57
+ if (lastDate) {
58
+ const datesSinceLastActivity = shaxpir_common_1.Time.datesBetween(lastDate, today);
59
+ const daysSinceLastActivity = datesSinceLastActivity.length - 1;
60
+ if (daysSinceLastActivity <= 1) {
61
+ dailyCurrent = tempStreak;
62
+ }
63
+ }
64
+ dailyLongest = Math.max(dailyLongest, tempStreak);
65
+ // Calculate weekly streak (at least one activity per week)
66
+ let weeklyCurrent = 0;
67
+ let weeklyStreak = 0;
68
+ // Check for weekly continuity - if there's activity within each 7-day window
69
+ for (let i = activityDates.length - 1; i >= 0; i--) {
70
+ const currentDate = activityDates[i];
71
+ // For the most recent date, start counting
72
+ if (i === activityDates.length - 1) {
73
+ // Check if this was within the last 7 days
74
+ const daysFromToday = shaxpir_common_1.Time.datesBetween(currentDate, today).length - 1;
75
+ if (daysFromToday <= 7) {
76
+ weeklyStreak = 1;
77
+ weeklyCurrent = 1;
78
+ }
79
+ }
80
+ else {
81
+ // Check if there's a gap of more than 7 days
82
+ const nextDate = activityDates[i + 1];
83
+ const daysBetween = shaxpir_common_1.Time.datesBetween(currentDate, nextDate).length - 1;
84
+ if (daysBetween <= 7) {
85
+ weeklyStreak++;
86
+ if (weeklyCurrent > 0) {
87
+ weeklyCurrent = weeklyStreak;
88
+ }
89
+ }
90
+ else {
91
+ // Weekly streak broken
92
+ break;
93
+ }
94
+ }
95
+ }
96
+ // Calculate monthly streak
97
+ // Extract YYYY-MM from CompactDate (format: YYYY-MM-DD)
98
+ const monthlyActivity = new Set();
99
+ for (const date of activityDates) {
100
+ const month = date.substring(0, 7); // Extract YYYY-MM
101
+ monthlyActivity.add(month);
102
+ }
103
+ const sortedMonths = Array.from(monthlyActivity).sort();
104
+ let monthlyCurrent = 0;
105
+ let monthlyStreak = 0;
106
+ let lastMonth = null;
107
+ for (const month of sortedMonths) {
108
+ if (!lastMonth) {
109
+ monthlyStreak = 1;
110
+ }
111
+ else {
112
+ // Check if consecutive month
113
+ const [lastYear, lastMonthNum] = lastMonth.split('-').map(Number);
114
+ const [currentYear, currentMonthNum] = month.split('-').map(Number);
115
+ if ((currentYear === lastYear && currentMonthNum === lastMonthNum + 1) ||
116
+ (currentYear === lastYear + 1 && lastMonthNum === 12 && currentMonthNum === 1)) {
117
+ monthlyStreak++;
118
+ }
119
+ else {
120
+ monthlyStreak = 1;
121
+ }
122
+ }
123
+ lastMonth = month;
124
+ }
125
+ // Check if current month streak is active
126
+ const currentMonth = today.substring(0, 7); // Extract YYYY-MM
127
+ const lastActiveMonth = sortedMonths[sortedMonths.length - 1];
128
+ const lastMonthDate = shaxpir_common_1.Time.plus(today, -30, 'days');
129
+ const previousMonth = shaxpir_common_1.Time.dateFrom(lastMonthDate).substring(0, 7);
130
+ if (lastActiveMonth === currentMonth || lastActiveMonth === previousMonth) {
131
+ monthlyCurrent = monthlyStreak;
132
+ }
133
+ // Get week string for last activity (ISO week format)
134
+ const getWeekString = (date) => {
135
+ // Simple week calculation: YYYY-Www where ww is week of year
136
+ const [year, month, day] = date.split('-').map(Number);
137
+ const dateObj = new Date(year, month - 1, day);
138
+ const startOfYear = new Date(year, 0, 1);
139
+ const dayOfYear = Math.floor((dateObj.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000)) + 1;
140
+ const weekNumber = Math.ceil(dayOfYear / 7);
141
+ return `${year}-W${weekNumber.toString().padStart(2, '0')}`;
142
+ };
143
+ // We know lastDate has a value since activityDates.length > 0
144
+ const finalLastDate = lastDate || activityDates[activityDates.length - 1];
145
+ return {
146
+ daily: {
147
+ current: dailyCurrent,
148
+ longest: dailyLongest
149
+ },
150
+ weekly: {
151
+ current: weeklyCurrent
152
+ },
153
+ monthly: {
154
+ current: monthlyCurrent
155
+ },
156
+ last_activity: {
157
+ date: finalLastDate,
158
+ week: getWeekString(finalLastDate),
159
+ month: finalLastDate.substring(0, 7)
160
+ },
161
+ total_days: activityDates.length
162
+ };
163
+ }
164
+ }
165
+ exports.Streaks = Streaks;
@@ -21,6 +21,7 @@ export * from './Progress';
21
21
  export * from './Review';
22
22
  export * from './Session';
23
23
  export * from './Social';
24
+ export * from './Streaks';
24
25
  export * from './Term';
25
26
  export * from './User';
26
27
  export * from './Workspace';
@@ -38,6 +38,7 @@ __exportStar(require("./Progress"), exports);
38
38
  __exportStar(require("./Review"), exports);
39
39
  __exportStar(require("./Session"), exports);
40
40
  __exportStar(require("./Social"), exports);
41
+ __exportStar(require("./Streaks"), exports);
41
42
  __exportStar(require("./Term"), exports);
42
43
  __exportStar(require("./User"), exports);
43
44
  __exportStar(require("./Workspace"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaxpir/duiduidui-models",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/shaxpir/duiduidui-models"