drill-srs 0.1.0 → 0.1.1
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/README.md +44 -1
- package/docs/decks.png +0 -0
- package/docs/forecast.png +0 -0
- package/docs/home.png +0 -0
- package/docs/question.png +0 -0
- package/docs/summary.png +0 -0
- package/package.json +20 -2
- package/docs/browse-decks.png +0 -0
- package/docs/initial-idea.md +0 -578
- package/docs/question-answer.png +0 -0
- package/docs/summery.png +0 -0
package/README.md
CHANGED
|
@@ -14,15 +14,58 @@ Drill helps you memorize anything using [spaced repetition](https://en.wikipedia
|
|
|
14
14
|
- **Deck organization** - Organize cards into decks using directories
|
|
15
15
|
- **Statistics** - Track learning progress, review forecasts, retention rates
|
|
16
16
|
|
|
17
|
+
## Screenshots
|
|
18
|
+
|
|
19
|
+
<details>
|
|
20
|
+
<summary>Main Menu</summary>
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary>Study Mode - Question</summary>
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
</details>
|
|
30
|
+
|
|
31
|
+
<details>
|
|
32
|
+
<summary>Browse Decks</summary>
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Statistics Summary</summary>
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
</details>
|
|
42
|
+
|
|
43
|
+
<details>
|
|
44
|
+
<summary>Review Forecast</summary>
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
</details>
|
|
48
|
+
|
|
17
49
|
## Installation
|
|
18
50
|
|
|
51
|
+
### Install globally via npm
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g drill-srs
|
|
55
|
+
drill
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Or clone and run locally
|
|
59
|
+
|
|
19
60
|
```bash
|
|
61
|
+
git clone https://github.com/yourusername/drill.git
|
|
62
|
+
cd drill
|
|
20
63
|
npm install
|
|
21
64
|
npm run build
|
|
22
65
|
npm start
|
|
23
66
|
```
|
|
24
67
|
|
|
25
|
-
|
|
68
|
+
For development:
|
|
26
69
|
```bash
|
|
27
70
|
npm run start
|
|
28
71
|
```
|
package/docs/decks.png
ADDED
|
Binary file
|
|
Binary file
|
package/docs/home.png
ADDED
|
Binary file
|
|
Binary file
|
package/docs/summary.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drill-srs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,13 +14,31 @@
|
|
|
14
14
|
"test:watch": "vitest",
|
|
15
15
|
"prepublishOnly": "npm run build"
|
|
16
16
|
},
|
|
17
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"flashcards",
|
|
19
|
+
"spaced-repetition",
|
|
20
|
+
"srs",
|
|
21
|
+
"cli",
|
|
22
|
+
"terminal",
|
|
23
|
+
"learning",
|
|
24
|
+
"memorization",
|
|
25
|
+
"markdown"
|
|
26
|
+
],
|
|
18
27
|
"author": "",
|
|
19
28
|
"license": "ISC",
|
|
20
29
|
"description": "Lightweight file-based spaced repetition system using Markdown flashcards",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/jim-at-jibba/drill.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/jim-at-jibba/drill/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/jim-at-jibba/drill#readme",
|
|
21
38
|
"dependencies": {
|
|
22
39
|
"cli-highlight": "^2.1.11",
|
|
23
40
|
"date-fns": "^4.1.0",
|
|
41
|
+
"drill-srs": "^0.1.0",
|
|
24
42
|
"glob": "^13.0.0",
|
|
25
43
|
"gray-matter": "^4.0.3",
|
|
26
44
|
"ink": "^5.2.1",
|
package/docs/browse-decks.png
DELETED
|
Binary file
|
package/docs/initial-idea.md
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
Perfect! Now I have a solid understanding of the SM-2 algorithm. Let me create a comprehensive analysis document for you:
|
|
2
|
-
|
|
3
|
-
# drill Deep Analysis for JavaScript/Ink Implementation
|
|
4
|
-
|
|
5
|
-
Based on my analysis of the drill repository and spaced repetition algorithms, here's a comprehensive breakdown:
|
|
6
|
-
|
|
7
|
-
## 1. Core Data Structures
|
|
8
|
-
|
|
9
|
-
### Card Model
|
|
10
|
-
```javascript
|
|
11
|
-
// Card data structure (from card.go analysis and markdown format)
|
|
12
|
-
const Card = {
|
|
13
|
-
// Identity
|
|
14
|
-
id: String, // Unique identifier (typically filename or UUID)
|
|
15
|
-
title: String, // Card title (from # heading)
|
|
16
|
-
filePath: String, // Path to markdown file
|
|
17
|
-
deckName: String, // Parent deck name (directory name)
|
|
18
|
-
|
|
19
|
-
// Content
|
|
20
|
-
question: String, // Content under ## Question section
|
|
21
|
-
answer: String, // Content under ## Answer section
|
|
22
|
-
|
|
23
|
-
// Metadata (YAML frontmatter)
|
|
24
|
-
tags: Array<String>, // Categories/topics
|
|
25
|
-
created: Date, // Creation timestamp (YYYY-MM-DD)
|
|
26
|
-
lastReviewed: Date, // Last review timestamp
|
|
27
|
-
nextReview: Date, // Calculated next review date
|
|
28
|
-
|
|
29
|
-
// SM-2 Algorithm Fields
|
|
30
|
-
reviewInterval: Number, // Days until next review (default: 0)
|
|
31
|
-
easeFactor: Number, // Difficulty multiplier (default: 2.5, min: 1.3)
|
|
32
|
-
repetitionCount: Number, // Successful reviews in a row (default: 0)
|
|
33
|
-
difficulty: Number // User perception 0-5 (optional, for stats)
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Deck Model
|
|
38
|
-
```javascript
|
|
39
|
-
const Deck = {
|
|
40
|
-
name: String, // Directory name
|
|
41
|
-
path: String, // Full directory path
|
|
42
|
-
cards: Array<Card>, // All cards in deck
|
|
43
|
-
totalCards: Number, // Card count
|
|
44
|
-
dueCards: Number, // Cards due today
|
|
45
|
-
newCards: Number, // Never reviewed cards
|
|
46
|
-
learningCards: Number // Cards in learning phase
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## 2. File Format Structure
|
|
51
|
-
|
|
52
|
-
### Markdown File Template
|
|
53
|
-
```markdown
|
|
54
|
-
---
|
|
55
|
-
tags: [tag1, tag2, tag3]
|
|
56
|
-
created: 2025-04-02
|
|
57
|
-
last_reviewed: 2025-11-20
|
|
58
|
-
review_interval: 7
|
|
59
|
-
easeFactor: 2.6
|
|
60
|
-
repetitionCount: 3
|
|
61
|
-
difficulty: 4
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
# Card Title
|
|
65
|
-
|
|
66
|
-
## Question
|
|
67
|
-
|
|
68
|
-
Your question content here.
|
|
69
|
-
Can be multiline with **markdown** formatting.
|
|
70
|
-
|
|
71
|
-
## Answer
|
|
72
|
-
|
|
73
|
-
Your answer here with:
|
|
74
|
-
- Lists
|
|
75
|
-
- Code blocks
|
|
76
|
-
- Tables
|
|
77
|
-
- Any markdown content
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Parsing Strategy
|
|
81
|
-
1. **Split frontmatter from content** (between `---` markers)
|
|
82
|
-
2. **Parse YAML** to extract metadata fields
|
|
83
|
-
3. **Extract sections** by finding `## Question` and `## Answer` headers
|
|
84
|
-
4. **Parse title** from first `#` heading
|
|
85
|
-
5. **Preserve markdown** for rich rendering
|
|
86
|
-
|
|
87
|
-
## 3. SM-2 Spaced Repetition Algorithm
|
|
88
|
-
|
|
89
|
-
### Core Algorithm Implementation
|
|
90
|
-
|
|
91
|
-
```javascript
|
|
92
|
-
/**
|
|
93
|
-
* SM-2 Algorithm for calculating next review
|
|
94
|
-
* @param {Card} card - Current card state
|
|
95
|
-
* @param {Number} quality - User rating 0-5
|
|
96
|
-
* @returns {Object} Updated { interval, repetitions, easeFactor, nextReview }
|
|
97
|
-
*/
|
|
98
|
-
function calculateSM2(card, quality) {
|
|
99
|
-
// Validate quality (0-5 scale)
|
|
100
|
-
if (quality < 0 || quality > 5) {
|
|
101
|
-
throw new Error('Quality must be between 0 and 5');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Get current values (with defaults for new cards)
|
|
105
|
-
let repetitions = card.repetitionCount || 0;
|
|
106
|
-
let easeFactor = card.easeFactor || 2.5;
|
|
107
|
-
let interval = card.reviewInterval || 0;
|
|
108
|
-
|
|
109
|
-
// Step 1: Update ease factor based on quality
|
|
110
|
-
// Formula: EF' = EF + (0.1 - (5-q) * (0.08 + (5-q) * 0.02))
|
|
111
|
-
easeFactor = easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
|
|
112
|
-
|
|
113
|
-
// Ensure ease factor never goes below 1.3
|
|
114
|
-
easeFactor = Math.max(1.3, easeFactor);
|
|
115
|
-
|
|
116
|
-
// Step 2: Update repetition count
|
|
117
|
-
if (quality < 3) {
|
|
118
|
-
// Failed recall - reset to beginning
|
|
119
|
-
repetitions = 0;
|
|
120
|
-
interval = 0;
|
|
121
|
-
} else {
|
|
122
|
-
// Successful recall - increment
|
|
123
|
-
repetitions += 1;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Step 3: Calculate new interval based on repetition count
|
|
127
|
-
if (repetitions === 0) {
|
|
128
|
-
interval = 0; // Review immediately
|
|
129
|
-
} else if (repetitions === 1) {
|
|
130
|
-
interval = 1; // Review tomorrow
|
|
131
|
-
} else if (repetitions === 2) {
|
|
132
|
-
interval = 6; // Review in 6 days
|
|
133
|
-
} else {
|
|
134
|
-
// For subsequent reviews, multiply previous interval by ease factor
|
|
135
|
-
interval = Math.round(interval * easeFactor);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Step 4: Calculate next review date
|
|
139
|
-
const now = new Date();
|
|
140
|
-
const nextReview = new Date(now.getTime() + interval * 24 * 60 * 60 * 1000);
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
interval,
|
|
144
|
-
repetitions,
|
|
145
|
-
easeFactor,
|
|
146
|
-
nextReview
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Quality Rating Scale (1-5 in drill, 0-5 in SM-2)
|
|
152
|
-
|
|
153
|
-
```javascript
|
|
154
|
-
const QUALITY_RATINGS = {
|
|
155
|
-
1: 'Blackout', // Complete failure (SM-2: 0)
|
|
156
|
-
2: 'Wrong', // Incorrect but recognized (SM-2: 1-2)
|
|
157
|
-
3: 'Hard', // Correct with difficulty (SM-2: 3)
|
|
158
|
-
4: 'Good', // Correct with some effort (SM-2: 4)
|
|
159
|
-
5: 'Easy' // Perfect recall (SM-2: 5)
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// drill uses 1-5, but you'll need to map to 0-5 internally
|
|
163
|
-
// or adjust the algorithm formulas accordingly
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Algorithm Behavior Patterns
|
|
167
|
-
|
|
168
|
-
1. **New cards** (repetitions = 0):
|
|
169
|
-
- First review: 1 day interval
|
|
170
|
-
- Second review: 6 days interval
|
|
171
|
-
- Subsequent: exponential growth based on ease factor
|
|
172
|
-
|
|
173
|
-
2. **Failed cards** (quality < 3):
|
|
174
|
-
- Reset repetitions to 0
|
|
175
|
-
- Start learning cycle over
|
|
176
|
-
- Ease factor decreases (makes future intervals shorter)
|
|
177
|
-
|
|
178
|
-
3. **Successful cards** (quality ≥ 3):
|
|
179
|
-
- Increment repetitions
|
|
180
|
-
- Interval grows exponentially
|
|
181
|
-
- Ease factor adjusts based on difficulty
|
|
182
|
-
|
|
183
|
-
4. **Ease factor dynamics**:
|
|
184
|
-
- Quality 5 (Easy): EF increases by ~0.1
|
|
185
|
-
- Quality 4 (Good): EF stays roughly same
|
|
186
|
-
- Quality 3 (Hard): EF decreases by ~0.14
|
|
187
|
-
- Quality 1-2: EF decreases significantly
|
|
188
|
-
|
|
189
|
-
## 4. Data Store Architecture
|
|
190
|
-
|
|
191
|
-
### File System Structure
|
|
192
|
-
```
|
|
193
|
-
~/drill/
|
|
194
|
-
├── programming/ # Deck (directory)
|
|
195
|
-
│ ├── algorithms.md # Card
|
|
196
|
-
│ ├── data-structures.md
|
|
197
|
-
│ └── patterns.md
|
|
198
|
-
├── languages/ # Another deck
|
|
199
|
-
│ ├── spanish-basics.md
|
|
200
|
-
│ └── french-verbs.md
|
|
201
|
-
└── history/
|
|
202
|
-
└── world-war-2.md
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Store Operations
|
|
206
|
-
|
|
207
|
-
```javascript
|
|
208
|
-
class CardStore {
|
|
209
|
-
constructor(baseDir) {
|
|
210
|
-
this.baseDir = baseDir; // e.g., ~/drill
|
|
211
|
-
this.decks = new Map();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Load all decks from filesystem
|
|
215
|
-
async loadDecks() {
|
|
216
|
-
// 1. Read directories in baseDir
|
|
217
|
-
// 2. For each directory, create Deck object
|
|
218
|
-
// 3. Read all .md files in directory
|
|
219
|
-
// 4. Parse each markdown file into Card
|
|
220
|
-
// 5. Store in this.decks Map
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Get cards due for review today
|
|
224
|
-
getDueCards(deckName = null) {
|
|
225
|
-
const today = new Date();
|
|
226
|
-
today.setHours(0, 0, 0, 0); // Start of day
|
|
227
|
-
|
|
228
|
-
// Filter cards where nextReview <= today
|
|
229
|
-
// If deckName specified, filter by deck
|
|
230
|
-
// Sort by nextReview (oldest first)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Get new cards (never reviewed)
|
|
234
|
-
getNewCards(deckName = null) {
|
|
235
|
-
// Filter cards where repetitionCount === 0
|
|
236
|
-
// and lastReviewed is null/undefined
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Save card after review
|
|
240
|
-
async saveCard(card) {
|
|
241
|
-
// 1. Construct markdown with updated frontmatter
|
|
242
|
-
// 2. Write to card.filePath
|
|
243
|
-
// 3. Update in-memory deck
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Get statistics
|
|
247
|
-
getStats(deckName = null) {
|
|
248
|
-
// Calculate:
|
|
249
|
-
// - Total cards
|
|
250
|
-
// - Due today
|
|
251
|
-
// - New cards
|
|
252
|
-
// - Learning cards (repetitionCount < 3)
|
|
253
|
-
// - Mature cards (repetitionCount >= 3)
|
|
254
|
-
// - Retention rate
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Markdown Parser
|
|
260
|
-
|
|
261
|
-
```javascript
|
|
262
|
-
function parseMarkdownCard(filePath, content) {
|
|
263
|
-
// 1. Split by frontmatter delimiters (---)
|
|
264
|
-
const parts = content.split(/^---$/m);
|
|
265
|
-
|
|
266
|
-
// 2. Parse YAML frontmatter
|
|
267
|
-
const frontmatter = yaml.parse(parts[1]);
|
|
268
|
-
|
|
269
|
-
// 3. Extract content sections
|
|
270
|
-
const bodyContent = parts[2];
|
|
271
|
-
|
|
272
|
-
// 4. Find title (first # heading)
|
|
273
|
-
const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
|
|
274
|
-
const title = titleMatch ? titleMatch[1] : 'Untitled';
|
|
275
|
-
|
|
276
|
-
// 5. Extract Question section
|
|
277
|
-
const questionMatch = bodyContent.match(/##\s+Question\s*\n([\s\S]*?)(?=##|$)/);
|
|
278
|
-
const question = questionMatch ? questionMatch[1].trim() : '';
|
|
279
|
-
|
|
280
|
-
// 6. Extract Answer section
|
|
281
|
-
const answerMatch = bodyContent.match(/##\s+Answer\s*\n([\s\S]*?)$/);
|
|
282
|
-
const answer = answerMatch ? answerMatch[1].trim() : '';
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
id: path.basename(filePath, '.md'),
|
|
286
|
-
filePath,
|
|
287
|
-
title,
|
|
288
|
-
question,
|
|
289
|
-
answer,
|
|
290
|
-
tags: frontmatter.tags || [],
|
|
291
|
-
created: new Date(frontmatter.created),
|
|
292
|
-
lastReviewed: frontmatter.last_reviewed ? new Date(frontmatter.last_reviewed) : null,
|
|
293
|
-
reviewInterval: frontmatter.review_interval || 0,
|
|
294
|
-
easeFactor: frontmatter.easeFactor || 2.5,
|
|
295
|
-
repetitionCount: frontmatter.repetitionCount || 0,
|
|
296
|
-
nextReview: calculateNextReview(frontmatter)
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### Markdown Writer
|
|
302
|
-
|
|
303
|
-
```javascript
|
|
304
|
-
function serializeCard(card) {
|
|
305
|
-
// 1. Build YAML frontmatter
|
|
306
|
-
const frontmatter = {
|
|
307
|
-
tags: card.tags,
|
|
308
|
-
created: formatDate(card.created),
|
|
309
|
-
last_reviewed: card.lastReviewed ? formatDate(card.lastReviewed) : null,
|
|
310
|
-
review_interval: card.reviewInterval,
|
|
311
|
-
easeFactor: card.easeFactor,
|
|
312
|
-
repetitionCount: card.repetitionCount
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const yaml = stringifyYAML(frontmatter);
|
|
316
|
-
|
|
317
|
-
// 2. Build markdown body
|
|
318
|
-
const markdown = `---
|
|
319
|
-
${yaml}---
|
|
320
|
-
|
|
321
|
-
# ${card.title}
|
|
322
|
-
|
|
323
|
-
## Question
|
|
324
|
-
|
|
325
|
-
${card.question}
|
|
326
|
-
|
|
327
|
-
## Answer
|
|
328
|
-
|
|
329
|
-
${card.answer}
|
|
330
|
-
`;
|
|
331
|
-
|
|
332
|
-
return markdown;
|
|
333
|
-
}
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
## 5. UI Architecture for Ink
|
|
337
|
-
|
|
338
|
-
### Screen Flow
|
|
339
|
-
```
|
|
340
|
-
Main Menu
|
|
341
|
-
├─> Study Session (due cards)
|
|
342
|
-
├─> Browse Decks
|
|
343
|
-
│ └─> Deck Detail
|
|
344
|
-
│ └─> Study Deck
|
|
345
|
-
└─> Statistics
|
|
346
|
-
├─> Summary View
|
|
347
|
-
├─> Deck Review
|
|
348
|
-
└─> Review Forecast
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Key Ink Components
|
|
352
|
-
|
|
353
|
-
```javascript
|
|
354
|
-
// Main App Component
|
|
355
|
-
function App() {
|
|
356
|
-
const [screen, setScreen] = useState('main-menu');
|
|
357
|
-
const [store, setStore] = useState(null);
|
|
358
|
-
|
|
359
|
-
useEffect(() => {
|
|
360
|
-
// Load decks on mount
|
|
361
|
-
const cardStore = new CardStore(config.baseDir);
|
|
362
|
-
await cardStore.loadDecks();
|
|
363
|
-
setStore(cardStore);
|
|
364
|
-
}, []);
|
|
365
|
-
|
|
366
|
-
// Route to different screens
|
|
367
|
-
switch (screen) {
|
|
368
|
-
case 'main-menu':
|
|
369
|
-
return <MainMenu />;
|
|
370
|
-
case 'study':
|
|
371
|
-
return <StudyScreen />;
|
|
372
|
-
case 'browse':
|
|
373
|
-
return <BrowseDecks />;
|
|
374
|
-
case 'stats':
|
|
375
|
-
return <StatsScreen />;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Study Screen Component
|
|
380
|
-
function StudyScreen({ deck, store }) {
|
|
381
|
-
const [currentCard, setCurrentCard] = useState(null);
|
|
382
|
-
const [showAnswer, setShowAnswer] = useState(false);
|
|
383
|
-
const [dueCards, setDueCards] = useState([]);
|
|
384
|
-
|
|
385
|
-
useInput((input, key) => {
|
|
386
|
-
if (input === ' ' && !showAnswer) {
|
|
387
|
-
setShowAnswer(true);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (showAnswer && input >= '1' && input <= '5') {
|
|
391
|
-
handleRating(parseInt(input));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (input === 'q') {
|
|
395
|
-
exit();
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
const handleRating = async (quality) => {
|
|
400
|
-
// Calculate SM-2 update
|
|
401
|
-
const updated = calculateSM2(currentCard, quality);
|
|
402
|
-
|
|
403
|
-
// Update card
|
|
404
|
-
currentCard.reviewInterval = updated.interval;
|
|
405
|
-
currentCard.repetitionCount = updated.repetitions;
|
|
406
|
-
currentCard.easeFactor = updated.easeFactor;
|
|
407
|
-
currentCard.nextReview = updated.nextReview;
|
|
408
|
-
currentCard.lastReviewed = new Date();
|
|
409
|
-
|
|
410
|
-
// Save to file
|
|
411
|
-
await store.saveCard(currentCard);
|
|
412
|
-
|
|
413
|
-
// Move to next card
|
|
414
|
-
loadNextCard();
|
|
415
|
-
setShowAnswer(false);
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
return (
|
|
419
|
-
<Box flexDirection="column">
|
|
420
|
-
<Text bold>{currentCard.title}</Text>
|
|
421
|
-
<Box marginTop={1}>
|
|
422
|
-
<Text>{currentCard.question}</Text>
|
|
423
|
-
</Box>
|
|
424
|
-
|
|
425
|
-
{showAnswer && (
|
|
426
|
-
<Box marginTop={1}>
|
|
427
|
-
<Text color="green">{currentCard.answer}</Text>
|
|
428
|
-
<Box marginTop={1}>
|
|
429
|
-
<Text>Rate your recall: [1] Blackout [2] Wrong [3] Hard [4] Good [5] Easy</Text>
|
|
430
|
-
</Box>
|
|
431
|
-
</Box>
|
|
432
|
-
)}
|
|
433
|
-
|
|
434
|
-
{!showAnswer && (
|
|
435
|
-
<Box marginTop={1}>
|
|
436
|
-
<Text color="gray">Press SPACE to reveal answer</Text>
|
|
437
|
-
</Box>
|
|
438
|
-
)}
|
|
439
|
-
</Box>
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
## 6. Key JavaScript Libraries
|
|
445
|
-
|
|
446
|
-
### Required Dependencies
|
|
447
|
-
|
|
448
|
-
```json
|
|
449
|
-
{
|
|
450
|
-
"dependencies": {
|
|
451
|
-
"ink": "^4.0.0", // Terminal UI framework
|
|
452
|
-
"ink-markdown": "^1.0.0", // Markdown rendering in Ink
|
|
453
|
-
"react": "^18.0.0", // Required by Ink
|
|
454
|
-
"yaml": "^2.0.0", // YAML parsing for frontmatter
|
|
455
|
-
"gray-matter": "^4.0.0", // Frontmatter extraction (alternative)
|
|
456
|
-
"date-fns": "^3.0.0", // Date manipulation
|
|
457
|
-
"glob": "^10.0.0", // File pattern matching
|
|
458
|
-
"ink-text-input": "^5.0.0", // Text input component
|
|
459
|
-
"ink-select-input": "^5.0.0", // Selection lists
|
|
460
|
-
"ink-spinner": "^5.0.0" // Loading indicators
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
## 7. Project Structure
|
|
466
|
-
|
|
467
|
-
```
|
|
468
|
-
gocard-js/
|
|
469
|
-
├── src/
|
|
470
|
-
│ ├── index.js # CLI entry point
|
|
471
|
-
│ ├── models/
|
|
472
|
-
│ │ ├── Card.js # Card class
|
|
473
|
-
│ │ └── Deck.js # Deck class
|
|
474
|
-
│ ├── store/
|
|
475
|
-
│ │ ├── CardStore.js # File system operations
|
|
476
|
-
│ │ ├── parser.js # Markdown parsing
|
|
477
|
-
│ │ └── writer.js # Markdown serialization
|
|
478
|
-
│ ├── srs/
|
|
479
|
-
│ │ └── sm2.js # SM-2 algorithm
|
|
480
|
-
│ ├── ui/
|
|
481
|
-
│ │ ├── App.js # Main Ink app
|
|
482
|
-
│ │ ├── MainMenu.js # Main menu screen
|
|
483
|
-
│ │ ├── StudyScreen.js # Study interface
|
|
484
|
-
│ │ ├── BrowseDecks.js # Deck browser
|
|
485
|
-
│ │ └── StatsScreen.js # Statistics view
|
|
486
|
-
│ └── utils/
|
|
487
|
-
│ ├── config.js # Configuration
|
|
488
|
-
│ └── dates.js # Date helpers
|
|
489
|
-
├── package.json
|
|
490
|
-
└── README.md
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
## 8. Implementation Checklist
|
|
494
|
-
|
|
495
|
-
### Phase 1: Core Functionality
|
|
496
|
-
- [ ] Implement Card and Deck models
|
|
497
|
-
- [ ] Build markdown parser (frontmatter + sections)
|
|
498
|
-
- [ ] Build markdown writer (serialize cards)
|
|
499
|
-
- [ ] Implement CardStore (file operations)
|
|
500
|
-
- [ ] Implement SM-2 algorithm
|
|
501
|
-
|
|
502
|
-
### Phase 2: Basic UI
|
|
503
|
-
- [ ] Create Main Menu with Ink
|
|
504
|
-
- [ ] Build Study Screen (show question → answer → rating)
|
|
505
|
-
- [ ] Implement keyboard shortcuts
|
|
506
|
-
- [ ] Add markdown rendering in terminal
|
|
507
|
-
|
|
508
|
-
### Phase 3: Advanced Features
|
|
509
|
-
- [ ] Deck browser
|
|
510
|
-
- [ ] Statistics screens (summary, deck review, forecast)
|
|
511
|
-
- [ ] Configuration management
|
|
512
|
-
- [ ] Error handling and validation
|
|
513
|
-
|
|
514
|
-
### Phase 4: Polish
|
|
515
|
-
- [ ] Add loading states
|
|
516
|
-
- [ ] Improve markdown rendering (code highlighting)
|
|
517
|
-
- [ ] Add help screens
|
|
518
|
-
- [ ] Write tests
|
|
519
|
-
- [ ] Documentation
|
|
520
|
-
|
|
521
|
-
## 9. Critical Implementation Details
|
|
522
|
-
|
|
523
|
-
### Date Handling
|
|
524
|
-
- Store dates in ISO format (YYYY-MM-DD) in frontmatter
|
|
525
|
-
- Calculate "due" by comparing `nextReview` to current date (start of day)
|
|
526
|
-
- Update `lastReviewed` to current date after each review
|
|
527
|
-
|
|
528
|
-
### File Watching (Optional Enhancement)
|
|
529
|
-
- Watch markdown files for external changes
|
|
530
|
-
- Reload decks when files modified
|
|
531
|
-
- Useful for Git-based workflows
|
|
532
|
-
|
|
533
|
-
### Performance Considerations
|
|
534
|
-
- Cache parsed cards in memory
|
|
535
|
-
- Only re-parse files when modified
|
|
536
|
-
- Use async I/O for all file operations
|
|
537
|
-
|
|
538
|
-
### SM-2 Edge Cases
|
|
539
|
-
- Minimum ease factor: 1.3 (prevents intervals from becoming too short)
|
|
540
|
-
- Quality < 3: Reset repetitions but keep ease factor change
|
|
541
|
-
- First two intervals are fixed (1 day, 6 days)
|
|
542
|
-
|
|
543
|
-
## 10. Example Usage Flow
|
|
544
|
-
|
|
545
|
-
```bash
|
|
546
|
-
# Install
|
|
547
|
-
npm install -g gocard-js
|
|
548
|
-
|
|
549
|
-
# Initialize
|
|
550
|
-
mkdir ~/drill
|
|
551
|
-
gocard-js --dir ~/drill
|
|
552
|
-
|
|
553
|
-
# Screens flow:
|
|
554
|
-
# 1. Main Menu → Select "Study"
|
|
555
|
-
# 2. Study Screen → Shows due card question
|
|
556
|
-
# 3. Press SPACE → Reveals answer
|
|
557
|
-
# 4. Press 1-5 → Rate difficulty, moves to next card
|
|
558
|
-
# 5. When done → Back to main menu
|
|
559
|
-
# 6. Browse Decks → See all decks and stats
|
|
560
|
-
# 7. Statistics → View learning progress
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
***
|
|
564
|
-
|
|
565
|
-
This should give you everything you need to build a faithful JavaScript/Ink implementation of drill! The key is maintaining the same file format (markdown + YAML frontmatter), implementing the SM-2 algorithm correctly, and building a clean Ink-based interface. Would you like me to elaborate on any specific section?
|
|
566
|
-
|
|
567
|
-
Sources
|
|
568
|
-
[1] GitHub - DavidMiserak/drill: A lightweight file-based spaced repetition system (SRS) that uses plain Markdown files for flashcards. Perfect for developers who prefer text files, Git version control, and keyboard-driven interfaces. https://github.com/DavidMiserak/drill
|
|
569
|
-
[2] The Anki SM-2 Spaced Repetition Algorithm https://help.remnote.com/en/articles/6026144-the-anki-sm-2-spaced-repetition-algorithm
|
|
570
|
-
[3] thyagoluciano/sm2: SM-2 is a simple spaced repetition ... https://github.com/thyagoluciano/sm2
|
|
571
|
-
[4] Spaced Repetition Algorithm: A Three‐Day Journey from ... https://www.reddit.com/r/Anki/comments/17u01ge/spaced_repetition_algorithm_a_threeday_journey/
|
|
572
|
-
[5] java - Spaced repetition algorithm from SuperMemo (SM-2) https://stackoverflow.com/questions/49047159/spaced-repetition-algorithm-from-supermemo-sm-2
|
|
573
|
-
[6] SuperMemo https://en.wikipedia.org/wiki/SuperMemo
|
|
574
|
-
[7] The best spaced repetition time intervals according ... - Traverse https://traverse.link/spaced-repetition/the-optimal-spaced-repetition-schedule
|
|
575
|
-
[8] FSRS vs SM2: How Spaced Repetition Algorithms Work https://www.youtube.com/watch?v=v2asudkSFek
|
|
576
|
-
[9] A-Factor https://supermemo.guru/wiki/A-Factor
|
|
577
|
-
[10] Spaced repetition algorithm metric https://supermemopedia.com/wiki/Spaced_repetition_algorithm_metric
|
|
578
|
-
|
package/docs/question-answer.png
DELETED
|
Binary file
|
package/docs/summery.png
DELETED
|
Binary file
|