@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.
- package/dist/admin/admin.css +1 -1
- package/dist/admin/common-ui-admin.es.js +748 -390
- package/dist/admin/common-ui-admin.es.js.map +1 -1
- package/dist/admin/components/LearnerDiagnostics.vue.d.ts +3 -2
- package/dist/admin/components/LearnerDiagnostics.vue.d.ts.map +1 -1
- package/dist/admin/components/PipelineOverview.vue.d.ts +18 -0
- package/dist/admin/components/PipelineOverview.vue.d.ts.map +1 -0
- package/dist/admin/components/RunDetail.vue.d.ts.map +1 -1
- package/dist/admin/index.d.ts +1 -0
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/components/auth/UserRegistration.vue.d.ts.map +1 -1
- package/dist/stores/useAuthStore.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/admin/components/LearnerDiagnostics.vue +23 -6
- package/src/admin/components/PipelineOverview.vue +390 -0
- package/src/admin/components/RunDetail.vue +21 -1
- package/src/admin/index.ts +1 -0
|
@@ -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)
|
|
4
|
-
*
|
|
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":"
|
|
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":"
|
|
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"}
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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.
|
|
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.
|
|
51
|
-
"@vue-skuilder/db": "0.2.
|
|
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.
|
|
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
|
|
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
|
|
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)
|
|
41
|
-
*
|
|
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
|
|
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);
|
package/src/admin/index.ts
CHANGED
|
@@ -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';
|