chishiki-sqlite 0.8.0
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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/index.mjs +1 -0
- package/dist/lib/activities.js +210 -0
- package/dist/lib/actors.js +147 -0
- package/dist/lib/database-manager.js +70 -0
- package/dist/lib/database-reset.js +29 -0
- package/dist/lib/generated-activities.js +240 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/learning-content.js +230 -0
- package/dist/lib/manual.js +30 -0
- package/dist/lib/statements.js +342 -0
- package/dist/lib/syllst-importer.js +234 -0
- package/dist/lib/types.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { getDatabaseManager } from './database-manager.js';
|
|
2
|
+
import { ManualSqliteClient } from './manual.js';
|
|
3
|
+
const getDb = () => {
|
|
4
|
+
const client = ManualSqliteClient.getInstance();
|
|
5
|
+
return client.getDb();
|
|
6
|
+
};
|
|
7
|
+
const ensureDatabaseInitialized = async () => {
|
|
8
|
+
const dbManager = getDatabaseManager();
|
|
9
|
+
await dbManager.ensureInitialized();
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Generate a UUID v4
|
|
13
|
+
*/
|
|
14
|
+
const generateUUID = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
15
|
+
const r = (Math.random() * 16) | 0;
|
|
16
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
17
|
+
return v.toString(16);
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the statements table
|
|
21
|
+
*/
|
|
22
|
+
const initializeStatementsTable = async () => {
|
|
23
|
+
const db = getDb();
|
|
24
|
+
if (!db)
|
|
25
|
+
return;
|
|
26
|
+
const tableExists = await db.introspection
|
|
27
|
+
.getTables()
|
|
28
|
+
.then((tables) => tables.some((table) => table.name === 'statements'));
|
|
29
|
+
if (!tableExists) {
|
|
30
|
+
await db.schema
|
|
31
|
+
.createTable('statements')
|
|
32
|
+
.ifNotExists()
|
|
33
|
+
.addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
|
|
34
|
+
.addColumn('statement_id', 'text', col => col.notNull().unique())
|
|
35
|
+
.addColumn('actor_id', 'integer', col => col.notNull())
|
|
36
|
+
.addColumn('verb_id', 'text', col => col.notNull())
|
|
37
|
+
.addColumn('activity_id', 'integer', col => col.notNull())
|
|
38
|
+
.addColumn('result', 'text')
|
|
39
|
+
.addColumn('context', 'text')
|
|
40
|
+
.addColumn('timestamp', 'text', col => col.notNull())
|
|
41
|
+
.addColumn('stored', 'text', col => col.notNull())
|
|
42
|
+
.execute();
|
|
43
|
+
// Create indexes for common queries
|
|
44
|
+
await db.schema.createIndex('idx_statements_actor').ifNotExists().on('statements').column('actor_id').execute();
|
|
45
|
+
await db.schema.createIndex('idx_statements_verb').ifNotExists().on('statements').column('verb_id').execute();
|
|
46
|
+
await db.schema
|
|
47
|
+
.createIndex('idx_statements_activity')
|
|
48
|
+
.ifNotExists()
|
|
49
|
+
.on('statements')
|
|
50
|
+
.column('activity_id')
|
|
51
|
+
.execute();
|
|
52
|
+
await db.schema
|
|
53
|
+
.createIndex('idx_statements_timestamp')
|
|
54
|
+
.ifNotExists()
|
|
55
|
+
.on('statements')
|
|
56
|
+
.column('timestamp')
|
|
57
|
+
.execute();
|
|
58
|
+
await db.schema.createIndex('idx_statements_stored').ifNotExists().on('statements').column('stored').execute();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Record a new xAPI statement
|
|
63
|
+
*/
|
|
64
|
+
const recordStatement = async (statement) => {
|
|
65
|
+
await ensureDatabaseInitialized();
|
|
66
|
+
const db = getDb();
|
|
67
|
+
if (!db) {
|
|
68
|
+
throw new Error('Database not available');
|
|
69
|
+
}
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const insertValues = {
|
|
72
|
+
statement_id: generateUUID(),
|
|
73
|
+
actor_id: statement.actor_id,
|
|
74
|
+
verb_id: statement.verb_id,
|
|
75
|
+
activity_id: statement.activity_id,
|
|
76
|
+
result: statement.result ? JSON.stringify(statement.result) : null,
|
|
77
|
+
context: statement.context ? JSON.stringify(statement.context) : null,
|
|
78
|
+
timestamp: statement.timestamp || now,
|
|
79
|
+
stored: now,
|
|
80
|
+
};
|
|
81
|
+
const result = await db.insertInto('statements').values(insertValues).returningAll().executeTakeFirstOrThrow();
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Get statement by ID
|
|
86
|
+
*/
|
|
87
|
+
const getStatementById = async (id) => {
|
|
88
|
+
await ensureDatabaseInitialized();
|
|
89
|
+
const db = getDb();
|
|
90
|
+
if (!db)
|
|
91
|
+
return null;
|
|
92
|
+
const result = await db.selectFrom('statements').selectAll().where('id', '=', id).executeTakeFirst();
|
|
93
|
+
return result || null;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Get statement by statement_id (UUID)
|
|
97
|
+
*/
|
|
98
|
+
const getStatementByStatementId = async (statementId) => {
|
|
99
|
+
await ensureDatabaseInitialized();
|
|
100
|
+
const db = getDb();
|
|
101
|
+
if (!db)
|
|
102
|
+
return null;
|
|
103
|
+
const result = await db
|
|
104
|
+
.selectFrom('statements')
|
|
105
|
+
.selectAll()
|
|
106
|
+
.where('statement_id', '=', statementId)
|
|
107
|
+
.executeTakeFirst();
|
|
108
|
+
return result || null;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Get statements with filters and pagination
|
|
112
|
+
*/
|
|
113
|
+
const getStatements = async (page = 1, limit = 50, filters) => {
|
|
114
|
+
await ensureDatabaseInitialized();
|
|
115
|
+
const db = getDb();
|
|
116
|
+
if (!db)
|
|
117
|
+
return [];
|
|
118
|
+
let query = db.selectFrom('statements').selectAll().orderBy('timestamp', 'desc');
|
|
119
|
+
if (filters?.actor_id) {
|
|
120
|
+
query = query.where('actor_id', '=', filters.actor_id);
|
|
121
|
+
}
|
|
122
|
+
if (filters?.verb_id) {
|
|
123
|
+
query = query.where('verb_id', '=', filters.verb_id);
|
|
124
|
+
}
|
|
125
|
+
if (filters?.activity_id) {
|
|
126
|
+
query = query.where('activity_id', '=', filters.activity_id);
|
|
127
|
+
}
|
|
128
|
+
if (filters?.since) {
|
|
129
|
+
query = query.where('timestamp', '>=', filters.since);
|
|
130
|
+
}
|
|
131
|
+
if (filters?.until) {
|
|
132
|
+
query = query.where('timestamp', '<=', filters.until);
|
|
133
|
+
}
|
|
134
|
+
const results = await query
|
|
135
|
+
.offset((page - 1) * limit)
|
|
136
|
+
.limit(limit)
|
|
137
|
+
.execute();
|
|
138
|
+
return results;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Get statement count
|
|
142
|
+
*/
|
|
143
|
+
const getStatementCount = async (filters) => {
|
|
144
|
+
await ensureDatabaseInitialized();
|
|
145
|
+
const db = getDb();
|
|
146
|
+
if (!db)
|
|
147
|
+
return 0;
|
|
148
|
+
let query = db.selectFrom('statements').select(db.fn.count('id').as('count'));
|
|
149
|
+
if (filters?.actor_id) {
|
|
150
|
+
query = query.where('actor_id', '=', filters.actor_id);
|
|
151
|
+
}
|
|
152
|
+
if (filters?.verb_id) {
|
|
153
|
+
query = query.where('verb_id', '=', filters.verb_id);
|
|
154
|
+
}
|
|
155
|
+
if (filters?.activity_id) {
|
|
156
|
+
query = query.where('activity_id', '=', filters.activity_id);
|
|
157
|
+
}
|
|
158
|
+
if (filters?.since) {
|
|
159
|
+
query = query.where('timestamp', '>=', filters.since);
|
|
160
|
+
}
|
|
161
|
+
if (filters?.until) {
|
|
162
|
+
query = query.where('timestamp', '<=', filters.until);
|
|
163
|
+
}
|
|
164
|
+
const result = await query.executeTakeFirst();
|
|
165
|
+
return result?.count || 0;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Get statements for an activity
|
|
169
|
+
*/
|
|
170
|
+
const getStatementsByActivityId = async (activityId) => {
|
|
171
|
+
await ensureDatabaseInitialized();
|
|
172
|
+
const db = getDb();
|
|
173
|
+
if (!db)
|
|
174
|
+
return [];
|
|
175
|
+
const results = await db
|
|
176
|
+
.selectFrom('statements')
|
|
177
|
+
.selectAll()
|
|
178
|
+
.where('activity_id', '=', activityId)
|
|
179
|
+
.orderBy('timestamp', 'desc')
|
|
180
|
+
.execute();
|
|
181
|
+
return results;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Get statements by verb
|
|
185
|
+
*/
|
|
186
|
+
const getStatementsByVerb = async (verbId, limit) => {
|
|
187
|
+
await ensureDatabaseInitialized();
|
|
188
|
+
const db = getDb();
|
|
189
|
+
if (!db)
|
|
190
|
+
return [];
|
|
191
|
+
let query = db.selectFrom('statements').selectAll().where('verb_id', '=', verbId).orderBy('timestamp', 'desc');
|
|
192
|
+
if (limit) {
|
|
193
|
+
query = query.limit(limit);
|
|
194
|
+
}
|
|
195
|
+
const results = await query.execute();
|
|
196
|
+
return results;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Get recent statements
|
|
200
|
+
*/
|
|
201
|
+
const getRecentStatements = async (days = 7, limit) => {
|
|
202
|
+
await ensureDatabaseInitialized();
|
|
203
|
+
const db = getDb();
|
|
204
|
+
if (!db)
|
|
205
|
+
return [];
|
|
206
|
+
const cutoffDate = new Date();
|
|
207
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
208
|
+
const cutoffISO = cutoffDate.toISOString();
|
|
209
|
+
let query = db.selectFrom('statements').selectAll().where('timestamp', '>=', cutoffISO).orderBy('timestamp', 'desc');
|
|
210
|
+
if (limit) {
|
|
211
|
+
query = query.limit(limit);
|
|
212
|
+
}
|
|
213
|
+
const results = await query.execute();
|
|
214
|
+
return results;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Get learning statistics
|
|
218
|
+
*/
|
|
219
|
+
const getLearningStats = async (actorId, since) => {
|
|
220
|
+
await ensureDatabaseInitialized();
|
|
221
|
+
const db = getDb();
|
|
222
|
+
if (!db) {
|
|
223
|
+
return {
|
|
224
|
+
totalStatements: 0,
|
|
225
|
+
completedActivities: 0,
|
|
226
|
+
passedActivities: 0,
|
|
227
|
+
failedActivities: 0,
|
|
228
|
+
averageScore: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
let baseQuery = db.selectFrom('statements').where('actor_id', '=', actorId);
|
|
232
|
+
if (since) {
|
|
233
|
+
baseQuery = baseQuery.where('timestamp', '>=', since);
|
|
234
|
+
}
|
|
235
|
+
// Total statements
|
|
236
|
+
const totalResult = await baseQuery.select(db.fn.count('id').as('count')).executeTakeFirst();
|
|
237
|
+
const totalStatements = totalResult?.count || 0;
|
|
238
|
+
// Completed (using verb URI)
|
|
239
|
+
const completedResult = await baseQuery
|
|
240
|
+
.where('verb_id', '=', 'http://adlnet.gov/expapi/verbs/completed')
|
|
241
|
+
.select(db.fn.count('id').as('count'))
|
|
242
|
+
.executeTakeFirst();
|
|
243
|
+
const completedActivities = completedResult?.count || 0;
|
|
244
|
+
// Passed
|
|
245
|
+
const passedResult = await baseQuery
|
|
246
|
+
.where('verb_id', '=', 'http://adlnet.gov/expapi/verbs/passed')
|
|
247
|
+
.select(db.fn.count('id').as('count'))
|
|
248
|
+
.executeTakeFirst();
|
|
249
|
+
const passedActivities = passedResult?.count || 0;
|
|
250
|
+
// Failed
|
|
251
|
+
const failedResult = await baseQuery
|
|
252
|
+
.where('verb_id', '=', 'http://adlnet.gov/expapi/verbs/failed')
|
|
253
|
+
.select(db.fn.count('id').as('count'))
|
|
254
|
+
.executeTakeFirst();
|
|
255
|
+
const failedActivities = failedResult?.count || 0;
|
|
256
|
+
// Average score - need to parse JSON results
|
|
257
|
+
const statementsWithScores = await baseQuery.select(['result']).where('result', 'is not', null).execute();
|
|
258
|
+
let totalScore = 0;
|
|
259
|
+
let scoreCount = 0;
|
|
260
|
+
for (const stmt of statementsWithScores) {
|
|
261
|
+
if (stmt.result) {
|
|
262
|
+
try {
|
|
263
|
+
const result = JSON.parse(stmt.result);
|
|
264
|
+
if (result.score?.scaled !== undefined) {
|
|
265
|
+
totalScore += result.score.scaled;
|
|
266
|
+
scoreCount++;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Ignore invalid JSON
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const averageScore = scoreCount > 0 ? totalScore / scoreCount : null;
|
|
275
|
+
return {
|
|
276
|
+
totalStatements,
|
|
277
|
+
completedActivities,
|
|
278
|
+
passedActivities,
|
|
279
|
+
failedActivities,
|
|
280
|
+
averageScore,
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Delete a statement
|
|
285
|
+
*/
|
|
286
|
+
const deleteStatement = async (id) => {
|
|
287
|
+
await ensureDatabaseInitialized();
|
|
288
|
+
const db = getDb();
|
|
289
|
+
if (!db)
|
|
290
|
+
return;
|
|
291
|
+
await db.deleteFrom('statements').where('id', '=', id).execute();
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Delete statements by activity ID
|
|
295
|
+
*/
|
|
296
|
+
const deleteStatementsByActivityId = async (activityId) => {
|
|
297
|
+
await ensureDatabaseInitialized();
|
|
298
|
+
const db = getDb();
|
|
299
|
+
if (!db)
|
|
300
|
+
return;
|
|
301
|
+
await db.deleteFrom('statements').where('activity_id', '=', activityId).execute();
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Reset the statements table
|
|
305
|
+
*/
|
|
306
|
+
const resetStatementsTable = async () => {
|
|
307
|
+
await ensureDatabaseInitialized();
|
|
308
|
+
const db = getDb();
|
|
309
|
+
if (!db)
|
|
310
|
+
return;
|
|
311
|
+
await db.schema.dropTable('statements').ifExists().execute();
|
|
312
|
+
await initializeStatementsTable();
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Clear all statements
|
|
316
|
+
*/
|
|
317
|
+
const clearAllStatements = async () => {
|
|
318
|
+
await ensureDatabaseInitialized();
|
|
319
|
+
const db = getDb();
|
|
320
|
+
if (!db)
|
|
321
|
+
return;
|
|
322
|
+
await db.deleteFrom('statements').execute();
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Export statements in xAPI format (for external LRS compatibility)
|
|
326
|
+
*/
|
|
327
|
+
const exportStatementsAsXAPI = async (filters) => {
|
|
328
|
+
const statements = await getStatements(1, 10000, filters);
|
|
329
|
+
// This would need to be expanded to include full actor/activity details
|
|
330
|
+
// For now, return raw statements
|
|
331
|
+
return statements.map(s => ({
|
|
332
|
+
id: s.statement_id,
|
|
333
|
+
actor: { id: s.actor_id }, // Would need to fetch full actor
|
|
334
|
+
verb: { id: s.verb_id },
|
|
335
|
+
object: { id: s.activity_id }, // Would need to fetch full activity
|
|
336
|
+
result: s.result ? JSON.parse(s.result) : undefined,
|
|
337
|
+
context: s.context ? JSON.parse(s.context) : undefined,
|
|
338
|
+
timestamp: s.timestamp,
|
|
339
|
+
stored: s.stored,
|
|
340
|
+
}));
|
|
341
|
+
};
|
|
342
|
+
export { initializeStatementsTable, generateUUID, recordStatement, getStatementById, getStatementByStatementId, getStatements, getStatementCount, getStatementsByActivityId, getStatementsByVerb, getRecentStatements, getLearningStats, deleteStatement, deleteStatementsByActivityId, resetStatementsTable, clearAllStatements, exportStatementsAsXAPI, };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYLLST Content Importer
|
|
3
|
+
*
|
|
4
|
+
* Imports learning content from SYLLST MDX format (polyglot-bundles packages)
|
|
5
|
+
* and creates corresponding flashcard activities in Chishiki.
|
|
6
|
+
*/
|
|
7
|
+
import { getDatabaseManager } from './database-manager.js';
|
|
8
|
+
import { ManualSqliteClient } from './manual.js';
|
|
9
|
+
import { addLearningContent } from './learning-content.js';
|
|
10
|
+
import { createActivity } from './activities.js';
|
|
11
|
+
import { storeGeneratedActivity } from './generated-activities.js';
|
|
12
|
+
const getDb = () => {
|
|
13
|
+
const client = ManualSqliteClient.getInstance();
|
|
14
|
+
return client.getDb();
|
|
15
|
+
};
|
|
16
|
+
const ensureDatabaseInitialized = async () => {
|
|
17
|
+
const dbManager = getDatabaseManager();
|
|
18
|
+
await dbManager.ensureInitialized();
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Parse SYLLST MDX content and extract frontmatter + learning items
|
|
22
|
+
*/
|
|
23
|
+
export function parseSyllstMdx(mdxContent) {
|
|
24
|
+
// Extract frontmatter (YAML between --- markers)
|
|
25
|
+
const frontmatterMatch = mdxContent.match(/^---\n([\s\S]*?)\n---/);
|
|
26
|
+
const frontmatterRaw = frontmatterMatch ? frontmatterMatch[1] : '';
|
|
27
|
+
// Simple YAML parsing for frontmatter
|
|
28
|
+
const frontmatter = {};
|
|
29
|
+
const lines = frontmatterRaw.split('\n');
|
|
30
|
+
let currentKey = null;
|
|
31
|
+
let currentArray = [];
|
|
32
|
+
let inArray = false;
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
// Check for array start
|
|
36
|
+
if (trimmed.match(/^(\w+):\s*$/)) {
|
|
37
|
+
const keyMatch = trimmed.match(/^(\w+):\s*$/);
|
|
38
|
+
if (!keyMatch)
|
|
39
|
+
continue;
|
|
40
|
+
const key = keyMatch[1];
|
|
41
|
+
currentKey = key;
|
|
42
|
+
currentArray = [];
|
|
43
|
+
inArray = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Check for array item
|
|
47
|
+
if (inArray && trimmed.startsWith('- ')) {
|
|
48
|
+
currentArray.push(trimmed.substring(2).trim());
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Check for key: value
|
|
52
|
+
const kvMatch = trimmed.match(/^(\w+):\s*(.+)$/);
|
|
53
|
+
if (kvMatch) {
|
|
54
|
+
const [, key, value] = kvMatch;
|
|
55
|
+
// Remove quotes if present
|
|
56
|
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
57
|
+
frontmatter[key] = cleanValue;
|
|
58
|
+
// Handle nested learningObjectives array
|
|
59
|
+
if (key === 'learningObjectives') {
|
|
60
|
+
frontmatter[key] = [];
|
|
61
|
+
}
|
|
62
|
+
currentKey = null;
|
|
63
|
+
inArray = false;
|
|
64
|
+
}
|
|
65
|
+
// End array on empty line or new key
|
|
66
|
+
if (inArray && (trimmed === '' || trimmed.match(/^\w+:/))) {
|
|
67
|
+
if (currentKey) {
|
|
68
|
+
frontmatter[currentKey] = currentArray;
|
|
69
|
+
}
|
|
70
|
+
inArray = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Extract vocabulary items (look for ::vocabulary blocks)
|
|
74
|
+
const vocabularyItems = [];
|
|
75
|
+
const vocabBlockRegex = /::vocabulary\{([^}]+)\}/g;
|
|
76
|
+
let match;
|
|
77
|
+
while ((match = vocabBlockRegex.exec(mdxContent)) !== null) {
|
|
78
|
+
const attrs = match[1];
|
|
79
|
+
const idMatch = attrs.match(/id="([^"]+)"/);
|
|
80
|
+
const wordMatch = attrs.match(/word="([^"]+)"/);
|
|
81
|
+
const translationMatch = attrs.match(/translation="([^"]+)"/);
|
|
82
|
+
const romanizationMatch = attrs.match(/romanization="([^"]+)"/);
|
|
83
|
+
if (idMatch && wordMatch && translationMatch) {
|
|
84
|
+
vocabularyItems.push({
|
|
85
|
+
id: idMatch[1],
|
|
86
|
+
word: wordMatch[1],
|
|
87
|
+
translation: translationMatch[1],
|
|
88
|
+
romanization: romanizationMatch?.[1],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Extract character items (look for ::character blocks)
|
|
93
|
+
const characterItems = [];
|
|
94
|
+
const charBlockRegex = /::character\{([^}]+)\}/g;
|
|
95
|
+
let charMatch;
|
|
96
|
+
while ((charMatch = charBlockRegex.exec(mdxContent)) !== null) {
|
|
97
|
+
const attrs = charMatch[1];
|
|
98
|
+
const idMatch = attrs.match(/id="([^"]+)"/);
|
|
99
|
+
const charMatchRegex = attrs.match(/char="([^"]+)"/);
|
|
100
|
+
const nameMatch = attrs.match(/name="([^"]+)"/);
|
|
101
|
+
const romanizationMatch = attrs.match(/romanization="([^"]+)"/);
|
|
102
|
+
const typeMatch = attrs.match(/type="([^"]+)"/);
|
|
103
|
+
if (idMatch && charMatchRegex && nameMatch) {
|
|
104
|
+
characterItems.push({
|
|
105
|
+
id: idMatch[1],
|
|
106
|
+
char: charMatchRegex[1],
|
|
107
|
+
name: nameMatch[1],
|
|
108
|
+
romanization: romanizationMatch?.[1],
|
|
109
|
+
charType: typeMatch?.[1] || 'unknown',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
frontmatter: frontmatter,
|
|
115
|
+
vocabularyItems,
|
|
116
|
+
characterItems,
|
|
117
|
+
rawMdx: mdxContent,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Import SYLLST MDX content into Chishiki
|
|
122
|
+
* Creates learning content and generates flashcard activities
|
|
123
|
+
*/
|
|
124
|
+
export async function importSyllstContent(mdxContent, syllabusId, lessonNumber) {
|
|
125
|
+
await ensureDatabaseInitialized();
|
|
126
|
+
const parsed = parseSyllstMdx(mdxContent);
|
|
127
|
+
const title = parsed.frontmatter.title || `Lesson ${lessonNumber}`;
|
|
128
|
+
const language = parsed.frontmatter.language || 'unknown';
|
|
129
|
+
// Create learning content entry
|
|
130
|
+
const content = await addLearningContent({
|
|
131
|
+
content_type: 'lesson',
|
|
132
|
+
raw_content: mdxContent,
|
|
133
|
+
title: `${syllabusId} - ${title}`,
|
|
134
|
+
metadata: {
|
|
135
|
+
title: `${syllabusId} - ${title}`,
|
|
136
|
+
source_url: undefined,
|
|
137
|
+
syllabusId,
|
|
138
|
+
lessonNumber,
|
|
139
|
+
language,
|
|
140
|
+
frontmatter: parsed.frontmatter,
|
|
141
|
+
extractedAt: new Date().toISOString(),
|
|
142
|
+
},
|
|
143
|
+
language,
|
|
144
|
+
});
|
|
145
|
+
const contentId = String(content.id);
|
|
146
|
+
const activityIds = [];
|
|
147
|
+
// Collect flashcards for vocabulary
|
|
148
|
+
const vocabFlashcards = [];
|
|
149
|
+
for (const vocab of parsed.vocabularyItems) {
|
|
150
|
+
vocabFlashcards.push({
|
|
151
|
+
id: vocab.id,
|
|
152
|
+
front: vocab.word,
|
|
153
|
+
back: vocab.translation,
|
|
154
|
+
tags: vocab.romanization ? [`roman:${vocab.romanization}`] : undefined,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Store vocabulary flashcard activity
|
|
158
|
+
if (vocabFlashcards.length > 0) {
|
|
159
|
+
const vocabActivity = await createActivity({
|
|
160
|
+
activity_iri: `https://chishiki.app/activities/flashcard/${syllabusId}-vocab-${lessonNumber}`,
|
|
161
|
+
name: `Vocabulary Flashcards: ${title}`,
|
|
162
|
+
description: `Vocabulary flashcards for ${syllabusId} lesson ${lessonNumber}`,
|
|
163
|
+
activity_type: 'flashcard',
|
|
164
|
+
content_id: content.id,
|
|
165
|
+
});
|
|
166
|
+
activityIds.push(String(vocabActivity.id));
|
|
167
|
+
await storeGeneratedActivity({
|
|
168
|
+
content_id: content.id,
|
|
169
|
+
activity_type: 'flashcard',
|
|
170
|
+
activity_data: {
|
|
171
|
+
flashcards: vocabFlashcards,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Collect flashcards for characters
|
|
176
|
+
const charFlashcards = [];
|
|
177
|
+
for (const char of parsed.characterItems) {
|
|
178
|
+
charFlashcards.push({
|
|
179
|
+
id: char.id,
|
|
180
|
+
front: char.char,
|
|
181
|
+
back: char.name,
|
|
182
|
+
tags: [
|
|
183
|
+
`type:${char.charType}`,
|
|
184
|
+
...(char.romanization ? [`roman:${char.romanization}`] : []),
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Store character flashcard activity
|
|
189
|
+
if (charFlashcards.length > 0) {
|
|
190
|
+
const charActivity = await createActivity({
|
|
191
|
+
activity_iri: `https://chishiki.app/activities/flashcard/${syllabusId}-char-${lessonNumber}`,
|
|
192
|
+
name: `Character Flashcards: ${title}`,
|
|
193
|
+
description: `Character flashcards for ${syllabusId} lesson ${lessonNumber}`,
|
|
194
|
+
activity_type: 'flashcard',
|
|
195
|
+
content_id: content.id,
|
|
196
|
+
});
|
|
197
|
+
activityIds.push(String(charActivity.id));
|
|
198
|
+
await storeGeneratedActivity({
|
|
199
|
+
content_id: content.id,
|
|
200
|
+
activity_type: 'flashcard',
|
|
201
|
+
activity_data: {
|
|
202
|
+
flashcards: charFlashcards,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
contentId,
|
|
208
|
+
activityIds,
|
|
209
|
+
vocabularyCount: parsed.vocabularyItems.length,
|
|
210
|
+
characterCount: parsed.characterItems.length,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Batch import multiple SYLLST lessons
|
|
215
|
+
*/
|
|
216
|
+
export async function batchImportSyllstLessons(lessons) {
|
|
217
|
+
const results = [];
|
|
218
|
+
let totalActivities = 0;
|
|
219
|
+
for (const lesson of lessons) {
|
|
220
|
+
const result = await importSyllstContent(lesson.mdx, lesson.syllabusId, lesson.lessonNumber);
|
|
221
|
+
results.push({
|
|
222
|
+
syllabusId: lesson.syllabusId,
|
|
223
|
+
lessonNumber: lesson.lessonNumber,
|
|
224
|
+
contentId: result.contentId,
|
|
225
|
+
activityCount: result.activityIds.length,
|
|
226
|
+
});
|
|
227
|
+
totalActivities += result.activityIds.length;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
totalContent: lessons.length,
|
|
231
|
+
totalActivities,
|
|
232
|
+
results,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../index.mts","../lib/activities.ts","../lib/actors.ts","../lib/database-manager.ts","../lib/database-reset.ts","../lib/generated-activities.ts","../lib/index.ts","../lib/learning-content.ts","../lib/manual.ts","../lib/statements.ts","../lib/syllst-importer.ts","../lib/types.ts"],"version":"5.8.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chishiki-sqlite",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "chrome extension - sqlite opfs integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**"
|
|
9
|
+
],
|
|
10
|
+
"types": "dist/index.d.mts",
|
|
11
|
+
"main": "dist/index.mjs",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"kysely": "^0.28.8",
|
|
17
|
+
"sqlocal": "^0.15.2",
|
|
18
|
+
"@extension/shared": "0.5.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"tsc-alias": "^1.8.16",
|
|
22
|
+
"@extension/tsconfig": "0.6.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean:bundle": "rimraf dist",
|
|
26
|
+
"clean:node_modules": "pnpx rimraf node_modules",
|
|
27
|
+
"clean:turbo": "rimraf .turbo",
|
|
28
|
+
"clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo",
|
|
29
|
+
"ready": "tsc -b && tsc-alias -p tsconfig.json",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "pnpm lint --fix",
|
|
32
|
+
"format": "prettier . --write --ignore-path ../../.prettierignore",
|
|
33
|
+
"type-check": "tsc --noEmit"
|
|
34
|
+
}
|
|
35
|
+
}
|