@vue-skuilder/common-ui 0.2.28 → 0.2.29

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.
@@ -1,7 +1,8 @@
1
1
  import { DiagnosticsInterpreters, LearnerDataset } from '@vue-skuilder/db/diagnostics';
2
2
  /**
3
- * One learner: sessions (list, then a session's detail) and card history,
4
- * with session timelines linking into card history. Navigation is internal;
3
+ * One learner: sessions (list, then a session's detail), card history, and,
4
+ * when the dataset carries the course's strategy docs, the pipeline with this
5
+ * learner's run stats. Session timelines link into card history. Navigation is internal;
5
6
  * hosts that want URLs can compose the three views themselves.
6
7
  */
7
8
  type __VLS_Props = {
@@ -1 +1 @@
1
- {"version":3,"file":"LearnerDiagnostics.vue.d.ts","sourceRoot":"","sources":["../../../src/admin/components/LearnerDiagnostics.vue"],"names":[],"mappings":"AAuEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAK5F;;;;GAIG;AACH,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACxC,CAAC;;AAkMF,wBAOG"}
1
+ {"version":3,"file":"LearnerDiagnostics.vue.d.ts","sourceRoot":"","sources":["../../../src/admin/components/LearnerDiagnostics.vue"],"names":[],"mappings":"AAwFA,OAAO,EAAoB,KAAK,uBAAuB,EAAE,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAMnH;;;;;GAKG;AACH,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACxC,CAAC;;AAmOF,wBAOG"}
@@ -0,0 +1,18 @@
1
+ import { ContentNavigationStrategyData } from '@vue-skuilder/db';
2
+ import { PipelineRunStats } from '@vue-skuilder/db/diagnostics';
3
+ /**
4
+ * A course's navigation pipeline as it assembles in this app: each stage in
5
+ * run order, which generators and filters run in it, and where each came from.
6
+ * With `stats` (from `pipelineRunStats`), each piece also shows what it did
7
+ * across a learner's persisted runs. Stats join to the plan by name.
8
+ */
9
+ type __VLS_Props = {
10
+ strategies: ContentNavigationStrategyData[];
11
+ courseId: string;
12
+ stats?: PipelineRunStats;
13
+ /** Whose runs the stats cover, e.g. a username. */
14
+ statsLabel?: string;
15
+ };
16
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
17
+ export default _default;
18
+ //# sourceMappingURL=PipelineOverview.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PipelineOverview.vue.d.ts","sourceRoot":"","sources":["../../../src/admin/components/PipelineOverview.vue"],"names":[],"mappings":"AAyYA,OAAO,EAGL,KAAK,6BAA6B,EAGnC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAKL,KAAK,gBAAgB,EAGtB,MAAM,8BAA8B,CAAC;AAGtC;;;;;GAKG;AACH,KAAK,WAAW,GAAG;IACjB,UAAU,EAAE,6BAA6B,EAAE,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;;AA0oBF,wBAOG"}
@@ -1 +1 @@
1
- {"version":3,"file":"RunDetail.vue.d.ts","sourceRoot":"","sources":["../../../src/admin/components/RunDetail.vue"],"names":[],"mappings":"AAyKA,OAAO,KAAK,EAA4B,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAErG,gDAAgD;AAChD,KAAK,WAAW,GAAG;IAAE,GAAG,EAAE,sBAAsB,CAAA;CAAE,CAAC;;;;;;AAoXnD,wBAQG"}
1
+ {"version":3,"file":"RunDetail.vue.d.ts","sourceRoot":"","sources":["../../../src/admin/components/RunDetail.vue"],"names":[],"mappings":"AA6LA,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,8BAA8B,CAAC;AAEtC,gDAAgD;AAChD,KAAK,WAAW,GAAG;IAAE,GAAG,EAAE,sBAAsB,CAAA;CAAE,CAAC;;;;;;AA8YnD,wBAQG"}
@@ -19,4 +19,5 @@ export { default as LearnerSessionDetail } from './components/LearnerSessionDeta
19
19
  export { default as LearnerCardHistory } from './components/LearnerCardHistory.vue';
20
20
  export { default as CardDossierDetail } from './components/CardDossierDetail.vue';
21
21
  export { default as RunDetail } from './components/RunDetail.vue';
22
+ export { default as PipelineOverview } from './components/PipelineOverview.vue';
22
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,kBAAkB,GACxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,kBAAkB,GACxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,mCAAmC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"UserRegistration.vue.d.ts","sourceRoot":"","sources":["../../../src/components/auth/UserRegistration.vue"],"names":[],"mappings":"AA8EA;AAAA,OAgSO,EAAmB,QAAQ,EAAE,MAAM,KAAK,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAO,MAAM,sBAAsB,CAAC;;;cAU3B,QAAQ,CAAC,CAAC,OAAO,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;;;;;;;;;;;;;;;;;;UAuBrD,eAAe,GAAG,IAAI;WACO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBA2dw6O,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAn3E,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAn3E,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAn3E,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;yBAld3zT,OAAO;;;;;qBASX,MAAM;2BAGA,MAAM;;;;;;;;cA7CT,QAAQ,CAAC,CAAC,OAAO,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;;;;;;;AALzE,wBAoMG"}
1
+ {"version":3,"file":"UserRegistration.vue.d.ts","sourceRoot":"","sources":["../../../src/components/auth/UserRegistration.vue"],"names":[],"mappings":"AA8EA;AAAA,OAgSO,EAAmB,QAAQ,EAAE,MAAM,KAAK,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAO,MAAM,sBAAsB,CAAC;;;cAU3B,QAAQ,CAAC,CAAC,OAAO,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;;;;;;;;;;;;;;;;;;UAuBrD,eAAe,GAAG,IAAI;WACO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBA2dy/W,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAn3E,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAn3E,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAn3E,CAAC;;;;;;;;;;;;;wBAAm7D,CAAC;yBAAuB,CAAC;;;;;;;;;yBAA0Y,CAAC;4BAA0B,CAAC;;;;;;;;;;;;;;yBAld54b,OAAO;;;;;qBASX,MAAM;2BAGA,MAAM;;;;;;;;cA7CT,QAAQ,CAAC,CAAC,OAAO,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;;;;;;;AALzE,wBAoMG"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAuthStore.d.ts","sourceRoot":"","sources":["../../src/stores/useAuthStore.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGjE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,eAAe,GAAG,SAAS,CAAC;IACnC,oBAAoB,EAAE;QACpB,IAAI,EAAE,OAAO,CAAC;QACd,QAAQ,EAAE,OAAO,CAAC;QAClB,aAAa,EAAE,OAAO,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAiB3E;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgFs1lC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;kBA3GxtqC,OAAO;sBACH,OAAO;2BACF,OAAO;6BACL,OAAO;;wBAEV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsGs1lC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;kBA3GxtqC,OAAO;sBACH,OAAO;2BACF,OAAO;6BACL,OAAO;;wBAEV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsGs1lC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;kBA3GxtqC,OAAO;sBACH,OAAO;2BACF,OAAO;6BACL,OAAO;;wBAEV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsGs1lC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;;;yBA1CvsqC,OAAO;uBAIT,OAAO;;;;;EAqC/B,CAAC"}
1
+ {"version":3,"file":"useAuthStore.d.ts","sourceRoot":"","sources":["../../src/stores/useAuthStore.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGjE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,eAAe,GAAG,SAAS,CAAC;IACnC,oBAAoB,EAAE;QACpB,IAAI,EAAE,OAAO,CAAC;QACd,QAAQ,EAAE,OAAO,CAAC;QAClB,aAAa,EAAE,OAAO,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAiB3E;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgFu6tC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;kBA3GzyyC,OAAO;sBACH,OAAO;2BACF,OAAO;6BACL,OAAO;;wBAEV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsGu6tC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;kBA3GzyyC,OAAO;sBACH,OAAO;2BACF,OAAO;6BACL,OAAO;;wBAEV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsGu6tC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;kBA3GzyyC,OAAO;sBACH,OAAO;2BACF,OAAO;6BACL,OAAO;;wBAEV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsGu6tC,CAAC;;;;;;;;;;;;;oBAAm7D,CAAC;qBAAuB,CAAC;;;;;;;;;qBAA0Y,CAAC;wBAA0B,CAAC;;;;;;yBA1CxxyC,OAAO;uBAIT,OAAO;;;;;EAqC/B,CAAC"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.2.28",
6
+ "version": "0.2.29",
7
7
  "sideEffects": false,
8
8
  "main": "./dist/common-ui.umd.js",
9
9
  "module": "./dist/common-ui.es.js",
@@ -47,8 +47,8 @@
47
47
  "@highlightjs/vue-plugin": "^2.1.2",
48
48
  "@mdi/font": "^7.3.67",
49
49
  "@vojtechlanka/vue-tags-input": "^3",
50
- "@vue-skuilder/common": "0.2.28",
51
- "@vue-skuilder/db": "0.2.28",
50
+ "@vue-skuilder/common": "0.2.29",
51
+ "@vue-skuilder/db": "0.2.29",
52
52
  "highlight.js": "^11.0.1",
53
53
  "marked": "^15.0.6",
54
54
  "moment": "^2.29.4",
@@ -84,5 +84,5 @@
84
84
  "vite-plugin-dts": "^4.5.3",
85
85
  "vitest": "^4.1.0"
86
86
  },
87
- "stableVersion": "0.2.28"
87
+ "stableVersion": "0.2.29"
88
88
  }
@@ -3,6 +3,7 @@
3
3
  <v-tabs v-model="tab" density="compact" class="mb-4">
4
4
  <v-tab value="sessions">Sessions ({{ dataset.sessions.length }})</v-tab>
5
5
  <v-tab value="cards">Cards ({{ dataset.cardHistories.length }})</v-tab>
6
+ <v-tab v-if="dataset.strategies" value="pipeline">Pipeline</v-tab>
6
7
  </v-tabs>
7
8
 
8
9
  <template v-if="tab === 'sessions'">
@@ -25,20 +26,35 @@
25
26
  />
26
27
  </template>
27
28
 
28
- <learner-card-history v-else :dataset="dataset" :interpreters="interpreters" :focus="focusCard" />
29
+ <learner-card-history
30
+ v-else-if="tab === 'cards'"
31
+ :dataset="dataset"
32
+ :interpreters="interpreters"
33
+ :focus="focusCard"
34
+ />
35
+
36
+ <pipeline-overview
37
+ v-else-if="tab === 'pipeline' && dataset.strategies"
38
+ :strategies="dataset.strategies"
39
+ :course-id="dataset.courseId"
40
+ :stats="pipelineStats"
41
+ :stats-label="dataset.username"
42
+ />
29
43
  </div>
30
44
  </template>
31
45
 
32
46
  <script setup lang="ts">
33
- import { ref, watch } from 'vue';
34
- import type { DiagnosticsInterpreters, LearnerDataset } from '@vue-skuilder/db/diagnostics';
47
+ import { computed, ref, watch } from 'vue';
48
+ import { pipelineRunStats, type DiagnosticsInterpreters, type LearnerDataset } from '@vue-skuilder/db/diagnostics';
35
49
  import LearnerCardHistory from './LearnerCardHistory.vue';
36
50
  import LearnerSessionDetail from './LearnerSessionDetail.vue';
37
51
  import LearnerSessionList from './LearnerSessionList.vue';
52
+ import PipelineOverview from './PipelineOverview.vue';
38
53
 
39
54
  /**
40
- * One learner: sessions (list, then a session's detail) and card history,
41
- * with session timelines linking into card history. Navigation is internal;
55
+ * One learner: sessions (list, then a session's detail), card history, and,
56
+ * when the dataset carries the course's strategy docs, the pipeline with this
57
+ * learner's run stats. Session timelines link into card history. Navigation is internal;
42
58
  * hosts that want URLs can compose the three views themselves.
43
59
  */
44
60
  const props = defineProps<{
@@ -46,9 +62,10 @@ const props = defineProps<{
46
62
  interpreters?: DiagnosticsInterpreters;
47
63
  }>();
48
64
 
49
- const tab = ref<'sessions' | 'cards'>('sessions');
65
+ const tab = ref<'sessions' | 'cards' | 'pipeline'>('sessions');
50
66
  const sessionId = ref<string | null>(null);
51
67
  const focusCard = ref<string | undefined>(undefined);
68
+ const pipelineStats = computed(() => pipelineRunStats(props.dataset));
52
69
 
53
70
  function openCard(cardId: string): void {
54
71
  // Re-set through undefined so following the same card twice still refocuses.
@@ -0,0 +1,390 @@
1
+ <template>
2
+ <div class="sk-pipeline">
3
+ <div class="d-flex flex-wrap align-center ga-2 mb-3">
4
+ <v-chip size="small" variant="tonal" :color="plan.kind === 'default' ? 'warning' : 'primary'">
5
+ {{
6
+ plan.kind === 'default' ? 'default pipeline: the course has no strategy docs' : 'assembled from strategy docs'
7
+ }}
8
+ </v-chip>
9
+ <span class="text-caption">
10
+ {{ plan.generators.length }} generator(s) · {{ plan.filters.length }} filter(s)
11
+ </span>
12
+ <span v-if="stats" class="text-caption text-medium-emphasis">
13
+ · run stats{{ statsLabel ? ` for ${statsLabel}` : '' }}: {{ stats.runs }} runs over
14
+ {{ stats.sessions }} sessions, {{ fmtTime(stats.firstRunAt) }} – {{ fmtTime(stats.lastRunAt) }}
15
+ </span>
16
+ </div>
17
+
18
+ <v-alert
19
+ v-for="s in plan.skipped"
20
+ :key="s.strategy._id"
21
+ type="warning"
22
+ variant="tonal"
23
+ density="compact"
24
+ class="mb-2"
25
+ >
26
+ <strong>{{ s.strategy.name }}</strong> (<code>{{ s.strategy.implementingClass }}</code
27
+ >) is {{ s.reason }} in this app, so it would not run here. A course's own navigators only plan correctly in the
28
+ app that registers them.
29
+ </v-alert>
30
+ <v-alert v-for="w in plan.warnings" :key="w" type="warning" variant="tonal" density="compact" class="mb-2">
31
+ {{ w }}
32
+ </v-alert>
33
+ <v-alert v-for="n in notes" :key="n" type="info" variant="tonal" density="compact" class="mb-2">
34
+ {{ n }}
35
+ </v-alert>
36
+
37
+ <div v-for="(stage, i) in PIPELINE_STAGES" :key="stage.key">
38
+ <div class="sk-stage">
39
+ <div class="d-flex align-center mb-1">
40
+ <span class="sk-stage-num">{{ i + 1 }}</span>
41
+ <span class="font-weight-medium">{{ stage.title }}</span>
42
+ </div>
43
+ <div class="text-caption text-medium-emphasis mb-2">{{ stage.summary }}</div>
44
+
45
+ <template v-if="stage.key === 'generate'">
46
+ <div v-for="g in plan.generators" :key="g.name" class="sk-nav">
47
+ <div class="d-flex flex-wrap align-center ga-2">
48
+ <span class="font-weight-medium">{{ g.name }}</span>
49
+ <code class="text-caption">{{ g.implementingClass }}</code>
50
+ <v-chip size="x-small" variant="outlined" :color="ORIGIN[g.origin].color">
51
+ {{ ORIGIN[g.origin].label }}
52
+ </v-chip>
53
+ </div>
54
+ <div v-if="g.strategy?.description" class="text-caption mt-1">{{ g.strategy.description }}</div>
55
+
56
+ <div v-if="stats" class="text-caption mt-1">
57
+ <template v-if="genStat(g.name)">
58
+ {{ fmtNum(genStat(g.name)!.meanCards) }} candidates/run ({{ fmtNum(genStat(g.name)!.meanNew) }} new,
59
+ {{ fmtNum(genStat(g.name)!.meanReview) }} review) · top score
60
+ {{ genStat(g.name)!.topScoreLast.toFixed(2) }} last, {{ genStat(g.name)!.topScoreMax.toFixed(2) }} max
61
+ <template v-if="stats.runsWithSelection > 0">
62
+ · <strong>{{ genStat(g.name)!.selected }}</strong> selected
63
+ <template v-if="genStat(g.name)!.firstAttempts > 0">
64
+ · first attempts {{ genStat(g.name)!.firstAttemptsCorrect }}/{{ genStat(g.name)!.firstAttempts }}
65
+ correct
66
+ <span :class="accuracyClass(accuracyOf(genStat(g.name)!))">
67
+ ({{ Math.round(100 * accuracyOf(genStat(g.name)!)!) }}%)
68
+ </span>
69
+ </template>
70
+ </template>
71
+ </template>
72
+ <span v-else class="text-medium-emphasis">absent from these runs</span>
73
+ </div>
74
+
75
+ <div v-if="prettyConfig(g.strategy?.serializedData)" class="mt-1">
76
+ <a class="sk-config-link text-caption" @click="toggleConfig(`g:${g.name}`)">
77
+ {{ openConfigs.has(`g:${g.name}`) ? 'Hide' : 'Show' }} config
78
+ </a>
79
+ <pre v-if="openConfigs.has(`g:${g.name}`)" class="sk-config">{{
80
+ prettyConfig(g.strategy?.serializedData)
81
+ }}</pre>
82
+ </div>
83
+ </div>
84
+ </template>
85
+
86
+ <template v-else-if="stage.key === 'filter'">
87
+ <div v-if="plan.filters.length === 0" class="text-caption text-medium-emphasis">No filters.</div>
88
+ <div v-for="f in plan.filters" :key="f.name" class="sk-nav">
89
+ <div class="d-flex flex-wrap align-center ga-2">
90
+ <span class="font-weight-medium">{{ f.name }}</span>
91
+ <code class="text-caption">{{ f.implementingClass }}</code>
92
+ <v-chip size="x-small" variant="outlined" :color="ORIGIN[f.origin].color">
93
+ {{ ORIGIN[f.origin].label }}
94
+ </v-chip>
95
+ <v-chip v-if="f.strategy?.learnable" size="x-small" variant="outlined">learnable weight</v-chip>
96
+ <v-chip
97
+ size="x-small"
98
+ variant="tonal"
99
+ :color="filterKind(f).kind === 'gate' ? 'deep-orange' : 'blue-grey'"
100
+ :title="filterKind(f).help"
101
+ >
102
+ {{ filterKind(f).label }}
103
+ </v-chip>
104
+ </div>
105
+ <div v-if="f.strategy?.description" class="text-caption mt-1">{{ f.strategy.description }}</div>
106
+ <div v-else-if="!f.strategy" class="text-caption mt-1 text-medium-emphasis">
107
+ Built in code; no strategy document.
108
+ </div>
109
+
110
+ <div v-if="stats" class="mt-1">
111
+ <template v-if="filterStat(f.name)">
112
+ <div class="sk-impact-bar">
113
+ <div
114
+ v-for="part in impactParts(filterStat(f.name)!)"
115
+ :key="part.key"
116
+ :class="`bg-${part.color}`"
117
+ :style="{ width: `${part.share * 100}%` }"
118
+ :title="`${part.key}: ${part.count}`"
119
+ />
120
+ </div>
121
+ <div class="text-caption">
122
+ over {{ filterStat(f.name)!.runs }} runs:
123
+ <span v-for="part in impactParts(filterStat(f.name)!)" :key="part.key" class="mr-2">
124
+ <span :class="`text-${part.color}`">●</span> {{ part.key }} {{ fmtPct(part.share) }}
125
+ </span>
126
+ </div>
127
+ </template>
128
+ <span v-else class="text-caption text-medium-emphasis">absent from these runs</span>
129
+ </div>
130
+
131
+ <div v-if="prettyConfig(f.strategy?.serializedData)" class="mt-1">
132
+ <a class="sk-config-link text-caption" @click="toggleConfig(`f:${f.name}`)">
133
+ {{ openConfigs.has(`f:${f.name}`) ? 'Hide' : 'Show' }} config
134
+ </a>
135
+ <pre v-if="openConfigs.has(`f:${f.name}`)" class="sk-config">{{
136
+ prettyConfig(f.strategy?.serializedData)
137
+ }}</pre>
138
+ </div>
139
+ </div>
140
+ </template>
141
+
142
+ <template v-else-if="stage.key === 'regulate'">
143
+ <div class="sk-nav">
144
+ <div class="d-flex flex-wrap align-center ga-2">
145
+ <span class="font-weight-medium">Review mass</span>
146
+ <v-chip size="x-small" variant="outlined">on review cards</v-chip>
147
+ </div>
148
+ <div class="text-caption mt-1">
149
+ Each due review weighs its SRS urgency (0–1) times the filters' net effect on it. Above
150
+ {{ plan.regulators.reviewMass.healthyMass }} card-equivalents, every review is multiplied by ×{{
151
+ plan.regulators.reviewMass.rate
152
+ }}
153
+ per {{ plan.regulators.reviewMass.healthyMass }} in excess (uncapped). Reviews without an urgency weigh
154
+ their score against {{ plan.regulators.reviewMass.referenceScore }}.
155
+ </div>
156
+ <div v-if="regStat('review-mass')" class="text-caption mt-1">
157
+ {{ regStatText(regStat('review-mass')!) }}
158
+ </div>
159
+ </div>
160
+ <div class="sk-nav">
161
+ <div class="d-flex flex-wrap align-center ga-2">
162
+ <span class="font-weight-medium">Intake</span>
163
+ <v-chip size="x-small" variant="outlined">on new cards no gate penalized</v-chip>
164
+ </div>
165
+ <div class="text-caption mt-1">
166
+ ×{{ plan.regulators.intake.rate }} per {{ plan.regulators.intake.scaleHours }} hours since a new card was
167
+ last presented (uncapped). The clock stops while no eligible new card exists.
168
+ </div>
169
+ <div v-if="regStat('intake')" class="text-caption mt-1">{{ regStatText(regStat('intake')!) }}</div>
170
+ </div>
171
+ <div v-if="stats && stats.regulators.length === 0" class="text-caption text-medium-emphasis">
172
+ None of these runs recorded regulator readings (recorded from 0.2.29).
173
+ </div>
174
+ </template>
175
+
176
+ <template v-else-if="stage.key === 'select' && stats && stats.runsWithSelection > 0">
177
+ <div class="text-caption">
178
+ {{ stats.selectedUnattributed }} selected card(s) had no generator: forced in by a hint's
179
+ <code>requireCards</code>.
180
+ </div>
181
+ </template>
182
+ </div>
183
+ <div v-if="i < PIPELINE_STAGES.length - 1" class="sk-stage-arrow">
184
+ <v-icon size="small">mdi-arrow-down</v-icon>
185
+ </div>
186
+ </div>
187
+
188
+ <div v-if="stats && stats.runsWithSelection === 0" class="text-caption text-medium-emphasis mt-3">
189
+ None of these runs recorded which cards they selected (recorded from 0.2.28), so selection and first-attempt
190
+ counts are not shown.
191
+ </div>
192
+ </div>
193
+ </template>
194
+
195
+ <script setup lang="ts">
196
+ import { computed, ref } from 'vue';
197
+ import {
198
+ PIPELINE_STAGES,
199
+ planPipeline,
200
+ type ContentNavigationStrategyData,
201
+ type PlannedNavigator,
202
+ type PlannedNavigatorOrigin,
203
+ } from '@vue-skuilder/db';
204
+ import {
205
+ regulatorText,
206
+ type FilterKind,
207
+ type FilterRunStat,
208
+ type GeneratorRunStat,
209
+ type PipelineRunStats,
210
+ type RegulatorName,
211
+ type RegulatorRunStat,
212
+ } from '@vue-skuilder/db/diagnostics';
213
+ import { accuracyClass, fmtTime } from '../display';
214
+
215
+ /**
216
+ * A course's navigation pipeline as it assembles in this app: each stage in
217
+ * run order, which generators and filters run in it, and where each came from.
218
+ * With `stats` (from `pipelineRunStats`), each piece also shows what it did
219
+ * across a learner's persisted runs. Stats join to the plan by name.
220
+ */
221
+ const props = defineProps<{
222
+ strategies: ContentNavigationStrategyData[];
223
+ courseId: string;
224
+ stats?: PipelineRunStats;
225
+ /** Whose runs the stats cover, e.g. a username. */
226
+ statsLabel?: string;
227
+ }>();
228
+
229
+ const ORIGIN: Record<PlannedNavigatorOrigin, { label: string; color: string }> = {
230
+ 'strategy-doc': { label: 'strategy doc', color: 'primary' },
231
+ 'assembler-default': { label: 'added by the assembler', color: 'secondary' },
232
+ 'default-pipeline': { label: 'default pipeline', color: 'warning' },
233
+ };
234
+
235
+ const regStat = (name: RegulatorName): RegulatorRunStat | undefined =>
236
+ props.stats?.regulators.find((r) => r.name === name);
237
+
238
+ function regStatText(r: RegulatorRunStat): string {
239
+ return (
240
+ `In these runs: active (×>1) in ${r.runsActive}/${r.runs}, mean ×${r.meanMultiplier.toFixed(2)}, ` +
241
+ `max ×${r.maxMultiplier.toFixed(2)}. Last: ${regulatorText(r.last)}.`
242
+ );
243
+ }
244
+
245
+ const KIND_HELP: Record<FilterKind, string> = {
246
+ gate: 'Gate: its penalties mean not ready yet; regulators will not lift what it penalized.',
247
+ signal: 'Signal: its penalties express preference.',
248
+ };
249
+
250
+ /**
251
+ * A filter's kind: as its class declares it (the plan), else as recorded by
252
+ * runs. Undeclared filters run as gates.
253
+ */
254
+ function filterKind(f: PlannedNavigator): { kind: FilterKind; label: string; help: string } {
255
+ const kind = f.kind ?? filterStat(f.name)?.kind;
256
+ if (kind) return { kind, label: kind, help: KIND_HELP[kind] };
257
+ return {
258
+ kind: 'gate',
259
+ label: 'gate (undeclared)',
260
+ help: `${KIND_HELP.gate} Its class declares no kind, so it counts as a gate.`,
261
+ };
262
+ }
263
+
264
+ const plan = computed(() => planPipeline(props.strategies, props.courseId));
265
+
266
+ const genStats = computed(() => new Map((props.stats?.generators ?? []).map((g) => [g.name, g])));
267
+ const filterStats = computed(() => new Map((props.stats?.filters ?? []).map((f) => [f.name, f])));
268
+ const genStat = (name: string): GeneratorRunStat | undefined => genStats.value.get(name);
269
+ const filterStat = (name: string): FilterRunStat | undefined => filterStats.value.get(name);
270
+
271
+ /** The plan's notes, plus what the run stats show about pieces that did nothing. */
272
+ const notes = computed(() => {
273
+ const out = [...plan.value.notes];
274
+ const stats = props.stats;
275
+ if (!stats) return out;
276
+ for (const f of plan.value.filters) {
277
+ const s = filterStat(f.name);
278
+ if (s && s.boosted + s.penalized + s.removed === 0) {
279
+ out.push(`${f.name} changed no candidate's score in ${s.runs} runs.`);
280
+ }
281
+ }
282
+ if (stats.runsWithSelection > 0) {
283
+ for (const g of plan.value.generators) {
284
+ const s = genStat(g.name);
285
+ if (s && s.runs > 0 && s.selected === 0) {
286
+ out.push(`${g.name} proposed candidates in ${s.runs} runs, and none was selected.`);
287
+ }
288
+ }
289
+ }
290
+ return out;
291
+ });
292
+
293
+ function accuracyOf(g: GeneratorRunStat): number | null {
294
+ return g.firstAttempts > 0 ? g.firstAttemptsCorrect / g.firstAttempts : null;
295
+ }
296
+
297
+ function impactParts(f: FilterRunStat) {
298
+ const total = f.boosted + f.penalized + f.passed + f.removed || 1;
299
+ return [
300
+ { key: 'boosted', count: f.boosted, color: 'success' },
301
+ { key: 'penalized', count: f.penalized, color: 'warning' },
302
+ { key: 'passed', count: f.passed, color: 'grey' },
303
+ { key: 'removed', count: f.removed, color: 'error' },
304
+ ].map((p) => ({ ...p, share: p.count / total }));
305
+ }
306
+
307
+ function fmtNum(n: number): string {
308
+ return n >= 10 ? Math.round(n).toString() : n.toFixed(1);
309
+ }
310
+
311
+ function fmtPct(share: number): string {
312
+ if (share === 0) return '0%';
313
+ return share < 0.01 ? '<1%' : `${Math.round(share * 100)}%`;
314
+ }
315
+
316
+ const openConfigs = ref(new Set<string>());
317
+
318
+ function toggleConfig(key: string): void {
319
+ const next = new Set(openConfigs.value);
320
+ if (next.has(key)) next.delete(key);
321
+ else next.add(key);
322
+ openConfigs.value = next;
323
+ }
324
+
325
+ /** A strategy's serializedData, pretty-printed when it's JSON; '' when there's nothing to show. */
326
+ function prettyConfig(serialized: string | undefined): string {
327
+ if (!serialized) return '';
328
+ try {
329
+ const parsed: unknown = JSON.parse(serialized);
330
+ const text = JSON.stringify(parsed, null, 2);
331
+ return text === '{}' ? '' : text;
332
+ } catch {
333
+ return serialized;
334
+ }
335
+ }
336
+ </script>
337
+
338
+ <style scoped>
339
+ .sk-stage {
340
+ border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
341
+ border-radius: 6px;
342
+ padding: 10px 12px;
343
+ }
344
+ .sk-stage-num {
345
+ display: inline-flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ width: 20px;
349
+ height: 20px;
350
+ border-radius: 50%;
351
+ margin-right: 8px;
352
+ font-size: 0.72rem;
353
+ background: rgba(var(--v-theme-primary), 0.15);
354
+ color: rgb(var(--v-theme-primary));
355
+ }
356
+ .sk-stage-arrow {
357
+ display: flex;
358
+ justify-content: center;
359
+ padding: 2px 0;
360
+ opacity: 0.5;
361
+ }
362
+ .sk-nav {
363
+ border-left: 2px solid rgba(var(--v-theme-primary), 0.25);
364
+ padding: 4px 0 4px 10px;
365
+ margin: 6px 0 6px 2px;
366
+ }
367
+ .sk-impact-bar {
368
+ display: flex;
369
+ height: 6px;
370
+ border-radius: 3px;
371
+ overflow: hidden;
372
+ background: rgba(var(--v-theme-on-surface), 0.06);
373
+ max-width: 420px;
374
+ margin: 4px 0 2px;
375
+ }
376
+ .sk-config-link {
377
+ cursor: pointer;
378
+ border-bottom: 1px dotted rgba(var(--v-theme-on-surface), 0.3);
379
+ }
380
+ .sk-config {
381
+ font-size: 0.72rem;
382
+ background: rgba(var(--v-theme-on-surface), 0.04);
383
+ border-radius: 4px;
384
+ padding: 6px 8px;
385
+ margin: 4px 0 0;
386
+ max-height: 260px;
387
+ overflow: auto;
388
+ white-space: pre-wrap;
389
+ }
390
+ </style>
@@ -30,6 +30,7 @@
30
30
  <thead>
31
31
  <tr>
32
32
  <th></th>
33
+ <th>kind</th>
33
34
  <th class="text-right">boosted</th>
34
35
  <th class="text-right">penalized</th>
35
36
  <th class="text-right">passed</th>
@@ -39,6 +40,7 @@
39
40
  <tbody>
40
41
  <tr v-for="f in run.filters" :key="f.name">
41
42
  <td>{{ f.name }}</td>
43
+ <td>{{ f.kind ?? '—' }}</td>
42
44
  <td class="text-right">{{ f.boosted }}</td>
43
45
  <td class="text-right">{{ f.penalized }}</td>
44
46
  <td class="text-right">{{ f.passed }}</td>
@@ -48,6 +50,16 @@
48
50
  </v-table>
49
51
  </div>
50
52
 
53
+ <div v-if="run.regulators?.length" class="mb-2">
54
+ <div class="text-caption font-weight-medium">regulators</div>
55
+ <div v-for="r in run.regulators" :key="r.name" class="sk-mini-line">
56
+ <span :class="r.multiplier > 1 ? 'text-warning' : 'text-medium-emphasis'">
57
+ ×{{ r.multiplier.toFixed(2) }}
58
+ </span>
59
+ {{ regulatorText(r) }}
60
+ </div>
61
+ </div>
62
+
51
63
  <div v-if="run.cards?.length" class="mb-2">
52
64
  <div class="text-caption font-weight-medium">selection</div>
53
65
  <v-table density="compact" class="sk-mini-table">
@@ -117,7 +129,11 @@
117
129
 
118
130
  <script setup lang="ts">
119
131
  import { computed } from 'vue';
120
- import type { StudySessionRunCardTrail, StudySessionRunSummary } from '@vue-skuilder/db/diagnostics';
132
+ import {
133
+ regulatorText,
134
+ type StudySessionRunCardTrail,
135
+ type StudySessionRunSummary,
136
+ } from '@vue-skuilder/db/diagnostics';
121
137
 
122
138
  /** The persisted detail of one pipeline run. */
123
139
  const props = defineProps<{ run: StudySessionRunSummary }>();
@@ -146,6 +162,10 @@ function fmtScore(n: number): string {
146
162
  .sk-mini-table :deep(th) {
147
163
  font-size: 0.72rem;
148
164
  }
165
+ .sk-mini-line {
166
+ font-size: 0.72rem;
167
+ padding-left: 8px;
168
+ }
149
169
  .sk-hint-block {
150
170
  font-size: 0.72rem;
151
171
  background: rgba(var(--v-theme-on-surface), 0.04);
@@ -25,3 +25,4 @@ export { default as LearnerSessionDetail } from './components/LearnerSessionDeta
25
25
  export { default as LearnerCardHistory } from './components/LearnerCardHistory.vue';
26
26
  export { default as CardDossierDetail } from './components/CardDossierDetail.vue';
27
27
  export { default as RunDetail } from './components/RunDetail.vue';
28
+ export { default as PipelineOverview } from './components/PipelineOverview.vue';