@vue-skuilder/common-ui 0.2.27 → 0.2.28
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 +2 -0
- package/dist/admin/common-ui-admin.es.js +1394 -0
- package/dist/admin/common-ui-admin.es.js.map +1 -0
- package/dist/admin/components/CardDossierDetail.vue.d.ts +9 -0
- package/dist/admin/components/CardDossierDetail.vue.d.ts.map +1 -0
- package/dist/admin/components/LearnerCardHistory.vue.d.ts +13 -0
- package/dist/admin/components/LearnerCardHistory.vue.d.ts.map +1 -0
- package/dist/admin/components/LearnerDatasetSource.vue.d.ts +24 -0
- package/dist/admin/components/LearnerDatasetSource.vue.d.ts.map +1 -0
- package/dist/admin/components/LearnerDiagnostics.vue.d.ts +13 -0
- package/dist/admin/components/LearnerDiagnostics.vue.d.ts.map +1 -0
- package/dist/admin/components/LearnerSessionDetail.vue.d.ts +14 -0
- package/dist/admin/components/LearnerSessionDetail.vue.d.ts.map +1 -0
- package/dist/admin/components/LearnerSessionList.vue.d.ts +13 -0
- package/dist/admin/components/LearnerSessionList.vue.d.ts.map +1 -0
- package/dist/admin/components/RunDetail.vue.d.ts +12 -0
- package/dist/admin/components/RunDetail.vue.d.ts.map +1 -0
- package/dist/admin/composables/useLearnerDataset.d.ts +26 -0
- package/dist/admin/composables/useLearnerDataset.d.ts.map +1 -0
- package/dist/admin/display.d.ts +20 -0
- package/dist/admin/display.d.ts.map +1 -0
- package/dist/admin/index.d.ts +22 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/components/CardBrowser.vue.d.ts +5 -5
- package/dist/components/CardHistoryViewer.vue.d.ts +1 -1
- package/dist/components/CardSearchResults.vue.d.ts +1 -1
- package/dist/components/CourseCardBrowser.vue.d.ts +9 -9
- package/dist/components/CourseInformation.vue.d.ts +18 -18
- package/dist/components/CourseTagFilterWidget.vue.d.ts +1 -1
- package/dist/components/SessionControllerDebug.vue.d.ts +1 -1
- package/dist/components/SnackbarService.d.ts +1 -1
- package/dist/components/StudySession.types.d.ts +1 -1
- package/dist/components/StudySession.vue.d.ts +2 -2
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/UserTagPreferences.vue.d.ts +1 -1
- package/dist/components/auth/UserRegistration.vue.d.ts +74 -74
- package/dist/components/auth/UserRegistration.vue.d.ts.map +1 -1
- package/dist/components/cardRendering/CardLoader.vue.d.ts +6 -6
- package/dist/components/cardRendering/CardViewer.vue.d.ts +2 -2
- package/dist/components/cardRendering/MarkdownRenderer.vue.d.ts +6 -6
- package/dist/components/cardRendering/MdTokenRenderer.vue.d.ts +6 -6
- package/dist/components/studentInputs/BaseUserInput.d.ts +2 -2
- package/dist/components/studentInputs/MultipleChoiceOption.vue.d.ts +6 -6
- package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +1 -1
- package/dist/components/studentInputs/RadioMultipleChoice.vue.d.ts +9 -9
- package/dist/components/studentInputs/TrueFalse.vue.d.ts +9 -9
- package/dist/components/studentInputs/UserInputNumber.vue.d.ts +2 -2
- package/dist/components/studentInputs/UserInputString.vue.d.ts +3 -3
- package/dist/components/studentInputs/fillInInput.vue.d.ts +3 -3
- package/dist/composables/CompositionViewable.d.ts +2 -2
- package/dist/composables/Displayable.d.ts +1 -1
- package/dist/stores/useAuthStore.d.ts +82 -82
- package/dist/stores/useAuthStore.d.ts.map +1 -1
- package/dist/stores/useConfigStore.d.ts +1 -1
- package/package.json +18 -5
- package/src/admin/components/CardDossierDetail.vue +99 -0
- package/src/admin/components/LearnerCardHistory.vue +196 -0
- package/src/admin/components/LearnerDatasetSource.vue +97 -0
- package/src/admin/components/LearnerDiagnostics.vue +68 -0
- package/src/admin/components/LearnerSessionDetail.vue +439 -0
- package/src/admin/components/LearnerSessionList.vue +175 -0
- package/src/admin/components/RunDetail.vue +166 -0
- package/src/admin/composables/useLearnerDataset.ts +66 -0
- package/src/admin/display.ts +73 -0
- package/src/admin/index.ts +27 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sk-run-detail">
|
|
3
|
+
<div v-if="run.generators?.length" class="mb-2">
|
|
4
|
+
<div class="text-caption font-weight-medium">generators</div>
|
|
5
|
+
<v-table density="compact" class="sk-mini-table">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th></th>
|
|
9
|
+
<th class="text-right">cards</th>
|
|
10
|
+
<th class="text-right">new</th>
|
|
11
|
+
<th class="text-right">review</th>
|
|
12
|
+
<th class="text-right">top</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<tr v-for="g in run.generators" :key="g.name">
|
|
17
|
+
<td>{{ g.name }}</td>
|
|
18
|
+
<td class="text-right">{{ g.cardCount }}</td>
|
|
19
|
+
<td class="text-right">{{ g.newCount }}</td>
|
|
20
|
+
<td class="text-right">{{ g.reviewCount }}</td>
|
|
21
|
+
<td class="text-right">{{ g.topScore.toFixed(2) }}</td>
|
|
22
|
+
</tr>
|
|
23
|
+
</tbody>
|
|
24
|
+
</v-table>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div v-if="run.filters?.length" class="mb-2">
|
|
28
|
+
<div class="text-caption font-weight-medium">filters</div>
|
|
29
|
+
<v-table density="compact" class="sk-mini-table">
|
|
30
|
+
<thead>
|
|
31
|
+
<tr>
|
|
32
|
+
<th></th>
|
|
33
|
+
<th class="text-right">boosted</th>
|
|
34
|
+
<th class="text-right">penalized</th>
|
|
35
|
+
<th class="text-right">passed</th>
|
|
36
|
+
<th class="text-right">removed</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<tr v-for="f in run.filters" :key="f.name">
|
|
41
|
+
<td>{{ f.name }}</td>
|
|
42
|
+
<td class="text-right">{{ f.boosted }}</td>
|
|
43
|
+
<td class="text-right">{{ f.penalized }}</td>
|
|
44
|
+
<td class="text-right">{{ f.passed }}</td>
|
|
45
|
+
<td class="text-right" :class="f.removed > 0 ? 'text-error' : ''">{{ f.removed }}</td>
|
|
46
|
+
</tr>
|
|
47
|
+
</tbody>
|
|
48
|
+
</v-table>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div v-if="run.cards?.length" class="mb-2">
|
|
52
|
+
<div class="text-caption font-weight-medium">selection</div>
|
|
53
|
+
<v-table density="compact" class="sk-mini-table">
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th></th>
|
|
57
|
+
<th>card</th>
|
|
58
|
+
<th>origin</th>
|
|
59
|
+
<th>generator</th>
|
|
60
|
+
<th class="text-right">score</th>
|
|
61
|
+
</tr>
|
|
62
|
+
</thead>
|
|
63
|
+
<tbody>
|
|
64
|
+
<tr v-for="c in run.cards" :key="c.cardId" :class="c.selected ? '' : 'text-medium-emphasis'">
|
|
65
|
+
<td>
|
|
66
|
+
<v-icon v-if="c.selected" size="x-small" color="success">mdi-check</v-icon>
|
|
67
|
+
<span v-else title="runner-up">·</span>
|
|
68
|
+
</td>
|
|
69
|
+
<td>
|
|
70
|
+
<a class="sk-card-link" @click="emit('open-card', c.cardId)">{{ c.cardId }}</a>
|
|
71
|
+
</td>
|
|
72
|
+
<td>{{ c.origin }}</td>
|
|
73
|
+
<td>{{ c.generator ?? '—' }}</td>
|
|
74
|
+
<td class="text-right">{{ fmtScore(c.score) }}</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</tbody>
|
|
77
|
+
</v-table>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div v-for="t in trails" :key="t.label" class="mb-2">
|
|
81
|
+
<div class="text-caption font-weight-medium">
|
|
82
|
+
{{ t.label }}:
|
|
83
|
+
<a class="sk-card-link" @click="emit('open-card', t.card.cardId)">{{ t.card.cardId }}</a>
|
|
84
|
+
<span class="text-medium-emphasis">
|
|
85
|
+
({{ t.card.origin }}, {{ t.card.generator ?? '?' }}) final {{ fmtScore(t.card.score) }}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
<v-table density="compact" class="sk-mini-table">
|
|
89
|
+
<tbody>
|
|
90
|
+
<tr v-for="(p, i) in t.card.trail" :key="i">
|
|
91
|
+
<td class="text-no-wrap">{{ p.action }}</td>
|
|
92
|
+
<td class="text-no-wrap">{{ p.strategyName }}</td>
|
|
93
|
+
<td class="text-right text-no-wrap">{{ fmtScore(p.score) }}</td>
|
|
94
|
+
<td class="text-caption">{{ p.reason }}</td>
|
|
95
|
+
</tr>
|
|
96
|
+
</tbody>
|
|
97
|
+
</v-table>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div v-if="run.hints" class="mb-2">
|
|
101
|
+
<div class="text-caption font-weight-medium">hints</div>
|
|
102
|
+
<pre class="sk-hint-block">{{ JSON.stringify(run.hints, null, 2) }}</pre>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div v-if="run.discardedTail" class="text-caption text-medium-emphasis">
|
|
106
|
+
discarded tail: {{ run.discardedTail.count }} cards
|
|
107
|
+
<template v-if="run.discardedTail.scoreRange">
|
|
108
|
+
· score [{{ run.discardedTail.scoreRange[0].toFixed(2) }}, {{ run.discardedTail.scoreRange[1].toFixed(2) }}]
|
|
109
|
+
</template>
|
|
110
|
+
<template v-if="run.discardedTail.eloRange">
|
|
111
|
+
· ELO [{{ Math.round(run.discardedTail.eloRange[0]) }}, {{ Math.round(run.discardedTail.eloRange[1]) }}]
|
|
112
|
+
</template>
|
|
113
|
+
<div v-if="run.discardedTail.note">{{ run.discardedTail.note }}</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script setup lang="ts">
|
|
119
|
+
import { computed } from 'vue';
|
|
120
|
+
import type { StudySessionRunCardTrail, StudySessionRunSummary } from '@vue-skuilder/db/diagnostics';
|
|
121
|
+
|
|
122
|
+
/** The persisted detail of one pipeline run. */
|
|
123
|
+
const props = defineProps<{ run: StudySessionRunSummary }>();
|
|
124
|
+
const emit = defineEmits<{ 'open-card': [cardId: string] }>();
|
|
125
|
+
|
|
126
|
+
const trails = computed(() => {
|
|
127
|
+
const u = props.run.unselectedNew;
|
|
128
|
+
const out: Array<{ label: string; card: StudySessionRunCardTrail }> = [];
|
|
129
|
+
if (u?.nextInLine) out.push({ label: 'next new card in line', card: u.nextInLine });
|
|
130
|
+
if (u?.topGenerated) out.push({ label: 'top-generated new card (sunk)', card: u.topGenerated });
|
|
131
|
+
return out;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function fmtScore(n: number): string {
|
|
135
|
+
return Math.abs(n) >= 0.01 || n === 0 ? n.toFixed(2) : n.toExponential(1);
|
|
136
|
+
}
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<style scoped>
|
|
140
|
+
.sk-run-detail {
|
|
141
|
+
border-left: 2px solid rgba(var(--v-theme-primary), 0.25);
|
|
142
|
+
padding-left: 10px;
|
|
143
|
+
margin-left: 2px;
|
|
144
|
+
}
|
|
145
|
+
.sk-mini-table :deep(td),
|
|
146
|
+
.sk-mini-table :deep(th) {
|
|
147
|
+
font-size: 0.72rem;
|
|
148
|
+
}
|
|
149
|
+
.sk-hint-block {
|
|
150
|
+
font-size: 0.72rem;
|
|
151
|
+
background: rgba(var(--v-theme-on-surface), 0.04);
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
padding: 6px 8px;
|
|
154
|
+
margin: 0;
|
|
155
|
+
max-height: 140px;
|
|
156
|
+
overflow: auto;
|
|
157
|
+
white-space: pre-wrap;
|
|
158
|
+
}
|
|
159
|
+
.sk-card-link {
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
border-bottom: 1px dotted rgba(var(--v-theme-on-surface), 0.3);
|
|
162
|
+
}
|
|
163
|
+
.sk-card-link:hover {
|
|
164
|
+
color: rgb(var(--v-theme-primary));
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ref, shallowRef } from 'vue';
|
|
2
|
+
import {
|
|
3
|
+
dumpFileName,
|
|
4
|
+
fetchLearnerDump,
|
|
5
|
+
fromDump,
|
|
6
|
+
parseLearnerDump,
|
|
7
|
+
type LearnerDataset,
|
|
8
|
+
type LearnerDump,
|
|
9
|
+
} from '@vue-skuilder/db/diagnostics';
|
|
10
|
+
|
|
11
|
+
/** Where to read a learner from live. Auth is the signed-in admin's couch session cookie. */
|
|
12
|
+
export interface LearnerCouchSource {
|
|
13
|
+
/** Couch base URL, e.g. `https://example.com/couch`. */
|
|
14
|
+
couchUrl: string;
|
|
15
|
+
username: string;
|
|
16
|
+
courseId: string;
|
|
17
|
+
/** Course cards to include. Default `touched`. */
|
|
18
|
+
cards?: 'touched' | 'all' | 'none';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load one learner's dataset for admin views, live from couch or from a dump
|
|
23
|
+
* file (`skuilder dump-user`, or a file saved from here). Keeps the raw dump
|
|
24
|
+
* so a view can hand exactly what it showed back out as a file.
|
|
25
|
+
*/
|
|
26
|
+
export function useLearnerDataset() {
|
|
27
|
+
const dump = shallowRef<LearnerDump | null>(null);
|
|
28
|
+
const dataset = shallowRef<LearnerDataset | null>(null);
|
|
29
|
+
const loading = ref(false);
|
|
30
|
+
const error = ref<string | null>(null);
|
|
31
|
+
|
|
32
|
+
async function load(read: () => Promise<LearnerDump>, courseId?: string): Promise<void> {
|
|
33
|
+
loading.value = true;
|
|
34
|
+
error.value = null;
|
|
35
|
+
try {
|
|
36
|
+
const d = await read();
|
|
37
|
+
dataset.value = fromDump(d, courseId);
|
|
38
|
+
dump.value = d;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
error.value = e instanceof Error ? e.message : String(e);
|
|
41
|
+
} finally {
|
|
42
|
+
loading.value = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadFromCouch(source: LearnerCouchSource): Promise<void> {
|
|
47
|
+
return load(() => fetchLearnerDump({ ...source, auth: { kind: 'cookie' } }), source.courseId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadFromFile(file: File, courseId?: string): Promise<void> {
|
|
51
|
+
return load(async () => parseLearnerDump(await file.text()), courseId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { dump, dataset, loading, error, loadFromCouch, loadFromFile };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Save a dump under the same name `skuilder dump-user` would give it. */
|
|
58
|
+
export function downloadLearnerDump(dump: LearnerDump): void {
|
|
59
|
+
const blob = new Blob([JSON.stringify(dump, null, 2)], { type: 'application/json' });
|
|
60
|
+
const url = URL.createObjectURL(blob);
|
|
61
|
+
const a = document.createElement('a');
|
|
62
|
+
a.href = url;
|
|
63
|
+
a.download = dumpFileName(dump);
|
|
64
|
+
a.click();
|
|
65
|
+
URL.revokeObjectURL(url);
|
|
66
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Display-only helpers shared by the admin views: colours, icons, and
|
|
2
|
+
// locale-formatted times. Anything a text export also needs lives in
|
|
3
|
+
// @vue-skuilder/db/diagnostics instead.
|
|
4
|
+
import { ref, type Ref } from 'vue';
|
|
5
|
+
import type { SessionFlag } from '@vue-skuilder/db/diagnostics';
|
|
6
|
+
|
|
7
|
+
export const FLAG_UI: Record<SessionFlag, { icon: string; color: string }> = {
|
|
8
|
+
churned: { icon: 'mdi-autorenew', color: 'warning' },
|
|
9
|
+
'out-of-content': { icon: 'mdi-cup-off-outline', color: 'error' },
|
|
10
|
+
walked: { icon: 'mdi-exit-run', color: 'error' },
|
|
11
|
+
stalled: { icon: 'mdi-arrow-collapse-horizontal', color: 'grey' },
|
|
12
|
+
grinding: { icon: 'mdi-alert-octagon-outline', color: 'warning' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function statusColor(status: string): string {
|
|
16
|
+
if (status === 'closed') return 'success';
|
|
17
|
+
if (status === 'abandoned') return 'warning';
|
|
18
|
+
return 'error'; // 'open': the tab went away before close
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function accuracyClass(a: number | null): string {
|
|
22
|
+
if (a === null) return 'text-disabled';
|
|
23
|
+
if (a >= 0.8) return 'text-success';
|
|
24
|
+
if (a < 0.5) return 'text-error';
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deltaClass(d: number | null): string {
|
|
29
|
+
if (d === null || d === 0) return 'text-disabled';
|
|
30
|
+
return d > 0 ? 'text-success' : 'text-error';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function deltaColor(d: number): string {
|
|
34
|
+
if (d > 0) return 'success';
|
|
35
|
+
if (d < 0) return 'error';
|
|
36
|
+
return 'grey';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function fmtTime(iso: string | null | undefined): string {
|
|
40
|
+
if (!iso) return '—';
|
|
41
|
+
const d = new Date(iso);
|
|
42
|
+
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Time of day only: a session timeline reads as a within-sitting sequence. */
|
|
46
|
+
export function fmtClock(iso: string): string {
|
|
47
|
+
const d = new Date(iso);
|
|
48
|
+
return Number.isNaN(d.getTime()) ? iso : d.toLocaleTimeString(undefined, { hour12: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function shortId(id: string, max = 28): string {
|
|
52
|
+
return id.length > max ? `…${id.slice(-(max - 2))}` : id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Copy text, and remember which control asked, briefly, for a tick. */
|
|
56
|
+
export function useCopy(): {
|
|
57
|
+
copied: Ref<string | null>;
|
|
58
|
+
copy: (text: string, key: string) => Promise<void>;
|
|
59
|
+
} {
|
|
60
|
+
const copied = ref<string | null>(null);
|
|
61
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
62
|
+
async function copy(text: string, key: string): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
await navigator.clipboard.writeText(text);
|
|
65
|
+
copied.value = key;
|
|
66
|
+
if (timer) clearTimeout(timer);
|
|
67
|
+
timer = setTimeout(() => (copied.value = null), 1400);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn('[admin] clipboard write failed', e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { copied, copy };
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@vue-skuilder/common-ui/admin`: admin and diagnostics components.
|
|
3
|
+
*
|
|
4
|
+
* A separate entry (built by vite.config.admin.js) so learner-facing bundles
|
|
5
|
+
* don't carry it. Views here render a `LearnerDataset` from
|
|
6
|
+
* `@vue-skuilder/db/diagnostics`, whether it came from couch or a dump file,
|
|
7
|
+
* and take the course's `DiagnosticsInterpreters` for domain meaning.
|
|
8
|
+
*
|
|
9
|
+
* Built CSS: import '@vue-skuilder/common-ui/admin/style' where these render.
|
|
10
|
+
*
|
|
11
|
+
* Import shared common-ui pieces from '@vue-skuilder/common-ui', not
|
|
12
|
+
* '@cui/...', so the admin bundle doesn't duplicate them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
useLearnerDataset,
|
|
17
|
+
downloadLearnerDump,
|
|
18
|
+
type LearnerCouchSource,
|
|
19
|
+
} from './composables/useLearnerDataset';
|
|
20
|
+
|
|
21
|
+
export { default as LearnerDatasetSource } from './components/LearnerDatasetSource.vue';
|
|
22
|
+
export { default as LearnerDiagnostics } from './components/LearnerDiagnostics.vue';
|
|
23
|
+
export { default as LearnerSessionList } from './components/LearnerSessionList.vue';
|
|
24
|
+
export { default as LearnerSessionDetail } from './components/LearnerSessionDetail.vue';
|
|
25
|
+
export { default as LearnerCardHistory } from './components/LearnerCardHistory.vue';
|
|
26
|
+
export { default as CardDossierDetail } from './components/CardDossierDetail.vue';
|
|
27
|
+
export { default as RunDetail } from './components/RunDetail.vue';
|