Skip to content

Commit 7cee482

Browse files
authored
Merge branch 'master' into strategy-ui-platform
2 parents c80ba6d + 9079c53 commit 7cee482

File tree

23 files changed

+1886
-180
lines changed

23 files changed

+1886
-180
lines changed

packages/common-ui/src/components/SkMouseTrap.vue

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
<template>
22
<v-dialog v-if="display" max-width="500px" transition="dialog-transition">
33
<template #activator="{ props }">
4-
<v-btn icon color="primary" v-bind="props"> ? </v-btn>
4+
<v-btn icon color="primary" v-bind="props">
5+
<v-icon>mdi-keyboard</v-icon>
6+
</v-btn>
57
</template>
68

79
<v-card>
8-
<v-toolbar color="teal" dark>
9-
<v-toolbar-title>Shortcut keys for this card:</v-toolbar-title>
10-
<v-spacer></v-spacer>
10+
<v-toolbar color="teal" dark dense>
11+
<v-toolbar-title class="text-subtitle-1">Shortcut keys:</v-toolbar-title>
1112
</v-toolbar>
12-
<v-list>
13-
<v-list-item v-for="hk in commands" :key="Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey">
14-
<v-btn variant="outlined" color="black">
13+
<v-list dense>
14+
<v-list-item
15+
v-for="hk in commands"
16+
:key="Array.isArray(hk.hotkey) ? hk.hotkey.join(',') : hk.hotkey"
17+
class="py-1"
18+
>
19+
<v-btn variant="outlined" color="primary" class="text-white" size="small">
1520
{{ Array.isArray(hk.hotkey) ? hk.hotkey[0] : hk.hotkey }}
1621
</v-btn>
1722
<v-spacer></v-spacer>
18-
<span class="text-right">
19-
{{ hk.command }}
20-
</span>
23+
<span class="text-caption ml-2">{{ hk.command }}</span>
2124
</v-list-item>
2225
</v-list>
2326
</v-card>

packages/common-ui/src/components/StudySession.vue

Lines changed: 81 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {
9393
CourseRegistrationDoc,
9494
DataLayerProvider,
9595
UserDBInterface,
96+
ClassroomDBInterface,
9697
} from '@vue-skuilder/db';
9798
import { SessionController, StudySessionRecord } from '@vue-skuilder/db';
9899
import { newInterval } from '@vue-skuilder/db';
@@ -158,7 +159,15 @@ export default defineComponent({
158159
},
159160
},
160161
161-
emits: ['session-finished', 'session-started', 'card-loaded', 'card-response', 'time-changed'],
162+
emits: [
163+
'session-finished',
164+
'session-started',
165+
'card-loaded',
166+
'card-response',
167+
'time-changed',
168+
'session-prepared',
169+
'session-error',
170+
],
162171
163172
data() {
164173
return {
@@ -224,7 +233,9 @@ export default defineComponent({
224233
225234
async created() {
226235
this.userCourseRegDoc = await this.user.getCourseRegistrationsDoc();
227-
this.initSession();
236+
console.log('[StudySession] Created lifecycle hook - starting initSession');
237+
await this.initSession();
238+
console.log('[StudySession] InitSession completed in created hook');
228239
},
229240
230241
methods: {
@@ -271,58 +282,84 @@ export default defineComponent({
271282
},
272283
273284
async initSession() {
274-
console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);
275-
276-
this.sessionContentSources = (
277-
await Promise.all(
278-
this.contentSources.map(async (s) => {
279-
try {
280-
return await getStudySource(s, this.user);
281-
} catch (e) {
282-
console.error(`Failed to load study source: ${s.type}/${s.id}`, e);
283-
return null;
284-
}
285-
})
286-
)
287-
).filter((s) => s !== null);
285+
let sessionClassroomDBs: ClassroomDBInterface[] = [];
286+
try {
287+
console.log(`[StudySession] starting study session w/ sources: ${JSON.stringify(this.contentSources)}`);
288+
console.log('[StudySession] Beginning preparation process');
289+
290+
this.sessionContentSources = (
291+
await Promise.all(
292+
this.contentSources.map(async (s) => {
293+
try {
294+
return await getStudySource(s, this.user);
295+
} catch (e) {
296+
console.error(`Failed to load study source: ${s.type}/${s.id}`, e);
297+
return null;
298+
}
299+
})
300+
)
301+
).filter((s) => s !== null);
288302
289-
this.timeRemaining = this.sessionTimeLimit * 60;
303+
this.timeRemaining = this.sessionTimeLimit * 60;
290304
291-
const sessionClassroomDBs = await Promise.all(
292-
this.contentSources
293-
.filter((s) => s.type === 'classroom')
294-
.map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))
295-
);
305+
sessionClassroomDBs = await Promise.all(
306+
this.contentSources
307+
.filter((s) => s.type === 'classroom')
308+
.map(async (c) => await this.dataLayer.getClassroomDB(c.id, 'student'))
309+
);
296310
297-
sessionClassroomDBs.forEach((db) => {
298-
// db.setChangeFcn(this.handleClassroomMessage());
299-
});
311+
sessionClassroomDBs.forEach((db) => {
312+
// db.setChangeFcn(this.handleClassroomMessage());
313+
});
300314
301-
this.sessionController = new SessionController(this.sessionContentSources, 60 * this.sessionTimeLimit);
302-
this.sessionController.sessionRecord = this.sessionRecord;
315+
this.sessionController = new SessionController(this.sessionContentSources, 60 * this.sessionTimeLimit);
316+
this.sessionController.sessionRecord = this.sessionRecord;
303317
304-
await this.sessionController.prepareSession();
305-
this.intervalHandler = setInterval(this.tick, 1000);
318+
await this.sessionController.prepareSession();
319+
this.intervalHandler = setInterval(this.tick, 1000);
306320
307-
this.sessionPrepared = true;
321+
this.sessionPrepared = true;
308322
309-
this.contentSources
310-
.filter((s) => s.type === 'course')
311-
.forEach(
312-
async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)
313-
);
323+
console.log('[StudySession] Session preparation complete, emitting session-prepared event');
324+
this.$emit('session-prepared');
325+
console.log('[StudySession] Event emission completed');
326+
} catch (error) {
327+
console.error('[StudySession] Error during session preparation:', error);
328+
// Notify parent component about the error
329+
this.$emit('session-error', { message: 'Failed to prepare study session', error });
330+
}
314331
315-
console.log(`[StudySession] Session created:
316-
${this.sessionController.toString()}
317-
User courses: ${this.contentSources
332+
try {
333+
this.contentSources
318334
.filter((s) => s.type === 'course')
319-
.map((c) => c.id)
320-
.toString()}
321-
User classrooms: ${sessionClassroomDBs.map((db) => db._id)}
322-
`);
335+
.forEach(
336+
async (c) => (this.courseNames[c.id] = (await this.dataLayer.getCoursesDB().getCourseConfig(c.id)).name)
337+
);
338+
339+
console.log(`[StudySession] Session created:
340+
${this.sessionController?.toString() || 'Session controller not initialized'}
341+
User courses: ${this.contentSources
342+
.filter((s) => s.type === 'course')
343+
.map((c) => c.id)
344+
.toString()}
345+
User classrooms: ${sessionClassroomDBs.map((db: any) => db._id).toString() || 'No classrooms'}
346+
`);
347+
} catch (error) {
348+
console.error('[StudySession] Error during final session setup:', error);
349+
}
323350
324-
this.$emit('session-started');
325-
this.loadCard(this.sessionController.nextCard());
351+
if (this.sessionController) {
352+
try {
353+
this.$emit('session-started');
354+
this.loadCard(this.sessionController.nextCard());
355+
} catch (error) {
356+
console.error('[StudySession] Error loading next card:', error);
357+
this.$emit('session-error', { message: 'Failed to load study card', error });
358+
}
359+
} else {
360+
console.error('[StudySession] Cannot load card: session controller not initialized');
361+
this.$emit('session-error', { message: 'Study session initialization failed' });
362+
}
326363
},
327364
328365
countCardViews(course_id: string, card_id: string): number {

packages/common-ui/src/components/studentInputs/UserInputString.vue

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
<template>
2-
<v-container class="pa-0">
3-
<v-text-field
2+
<span class="user-input-container">
3+
<input
44
v-model="answer"
55
:autofocus="autofocus"
6-
:prepend-icon="prependIcon"
76
type="text"
8-
variant="underlined"
9-
single-line
10-
hide-details
7+
class="user-input-string"
8+
ref="input"
119
@keyup.enter="submitAnswer(answer)"
12-
></v-text-field>
13-
</v-container>
10+
/>
11+
</span>
1412
</template>
1513

1614
<script lang="ts">
@@ -61,3 +59,31 @@ export default defineComponent({
6159
},
6260
});
6361
</script>
62+
63+
<style scoped>
64+
.user-input-container {
65+
display: inline-block;
66+
min-width: 6em;
67+
vertical-align: baseline;
68+
}
69+
70+
.user-input-string {
71+
display: inline-block;
72+
background: transparent;
73+
border: none;
74+
border-bottom: 1px solid currentColor;
75+
color: currentColor;
76+
font-family: inherit;
77+
font-size: inherit;
78+
line-height: inherit;
79+
padding: 0;
80+
margin: 0;
81+
text-align: center;
82+
width: 100%;
83+
outline: none;
84+
}
85+
86+
.user-input-string:focus {
87+
border-bottom: 2px solid currentColor;
88+
}
89+
</style>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { ParsedCard } from './types.js';
2+
3+
/**
4+
* Configuration for the bulk card parser
5+
*/
6+
export interface CardParserConfig {
7+
/** Custom tag identifier (defaults to 'tags:') */
8+
tagIdentifier?: string;
9+
/** Custom ELO identifier (defaults to 'elo:') */
10+
eloIdentifier?: string;
11+
}
12+
13+
/**
14+
* Default configuration for the card parser
15+
*/
16+
const DEFAULT_PARSER_CONFIG: CardParserConfig = {
17+
tagIdentifier: 'tags:',
18+
eloIdentifier: 'elo:',
19+
};
20+
21+
/**
22+
* Card delimiter used to separate cards in bulk input
23+
*/
24+
export const CARD_DELIMITER = '\n---\n---\n';
25+
26+
/**
27+
* Parses a single card string into a structured object
28+
*
29+
* @param cardString - Raw string containing card content
30+
* @param config - Optional parser configuration
31+
* @returns ParsedCard object or null if parsing fails
32+
*/
33+
export function parseCard(
34+
cardString: string,
35+
config: CardParserConfig = DEFAULT_PARSER_CONFIG
36+
): ParsedCard | null {
37+
const trimmedCardString = cardString.trim();
38+
if (!trimmedCardString) {
39+
return null;
40+
}
41+
42+
const lines = trimmedCardString.split('\n');
43+
let tags: string[] = [];
44+
let elo: number | undefined = undefined;
45+
const markdownLines = [...lines];
46+
47+
// Process the lines from bottom to top to handle metadata
48+
let metadataLines = 0;
49+
50+
// Get the configured identifiers
51+
const tagId = config.tagIdentifier || DEFAULT_PARSER_CONFIG.tagIdentifier;
52+
const eloId = config.eloIdentifier || DEFAULT_PARSER_CONFIG.eloIdentifier;
53+
54+
// Check the last few lines for metadata (tags and elo)
55+
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 2; i--) {
56+
const line = lines[i].trim();
57+
58+
// Check for tags
59+
if (line.toLowerCase().startsWith(tagId!.toLowerCase())) {
60+
tags = line
61+
.substring(tagId!.length)
62+
.split(',')
63+
.map((tag) => tag.trim())
64+
.filter((tag) => tag);
65+
metadataLines++;
66+
}
67+
// Check for ELO
68+
else if (line.toLowerCase().startsWith(eloId!.toLowerCase())) {
69+
const eloValue = line.substring(eloId!.length).trim();
70+
const parsedElo = parseInt(eloValue, 10);
71+
if (!isNaN(parsedElo)) {
72+
elo = parsedElo;
73+
}
74+
metadataLines++;
75+
}
76+
}
77+
78+
// Remove metadata lines from the end of the content
79+
if (metadataLines > 0) {
80+
markdownLines.splice(markdownLines.length - metadataLines);
81+
}
82+
83+
const markdown = markdownLines.join('\n').trim();
84+
if (!markdown) {
85+
// Card must have some markdown content
86+
return null;
87+
}
88+
89+
return { markdown, tags, elo };
90+
}
91+
92+
/**
93+
* Splits a bulk text input into individual card strings
94+
*
95+
* @param bulkText - Raw string containing multiple cards
96+
* @returns Array of card strings
97+
*/
98+
export function splitCardsText(bulkText: string): string[] {
99+
return bulkText
100+
.split(CARD_DELIMITER)
101+
.map((card) => card.trim())
102+
.filter((card) => card); // Filter out empty strings
103+
}
104+
105+
/**
106+
* Parses a bulk text input into an array of structured ParsedCard objects.
107+
*
108+
* @param bulkText - Raw string containing multiple cards.
109+
* @param config - Optional parser configuration.
110+
* @returns Array of ParsedCard objects. Filters out cards that fail to parse.
111+
*/
112+
export function parseBulkTextToCards(
113+
bulkText: string,
114+
config: CardParserConfig = DEFAULT_PARSER_CONFIG
115+
): ParsedCard[] {
116+
const cardStrings = splitCardsText(bulkText);
117+
const parsedCards: ParsedCard[] = [];
118+
119+
for (const cardString of cardStrings) {
120+
const parsedCard = parseCard(cardString, config);
121+
if (parsedCard) {
122+
parsedCards.push(parsedCard);
123+
}
124+
}
125+
return parsedCards;
126+
}
127+
128+
/**
129+
* Validates if a bulk text input has valid format
130+
*
131+
* @param bulkText - Raw string containing multiple cards
132+
* @returns true if valid, false otherwise
133+
*/
134+
export function isValidBulkFormat(bulkText: string): boolean {
135+
const cardStrings = splitCardsText(bulkText);
136+
return cardStrings.length > 0 && cardStrings.some((card) => !!card.trim());
137+
}

0 commit comments

Comments
 (0)