@jiy/quizme 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 +29 -2
- package/dist/cli/config.js +19 -1
- package/dist/generation/prompts/quiz.js +57 -0
- package/dist/generation/prompts/why.js +30 -0
- package/dist/generation/schema.js +1 -1
- package/dist/providers/claudeAgent.js +138 -80
- package/dist/sources/claudeSession.js +80 -12
- package/dist/storage/index.js +20 -5
- package/dist/storage/json.js +227 -0
- package/dist/ui/App.js +7 -3
- package/dist/ui/components/AppHeader.js +5 -2
- package/dist/ui/components/SelectList.js +1 -1
- package/dist/ui/components/Spinner.js +16 -0
- package/dist/ui/components/StatusBar.js +1 -2
- package/dist/ui/components/TextInput.js +7 -4
- package/dist/ui/components/WelcomeBanner.js +3 -4
- package/dist/ui/screens/QuizScreen.js +12 -12
- package/dist/ui/screens/SettingsScreen.js +127 -1
- package/dist/ui/screens/SetupScreen.js +23 -14
- package/dist/ui/theme.js +6 -3
- package/package.json +5 -8
- package/dist/platform/fs.js +0 -17
- package/dist/platform/paths.js +0 -27
- package/dist/storage/sqlite.js +0 -294
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { Box,
|
|
3
|
+
import { Box, useInput } from "ink";
|
|
4
4
|
import { AppHeader } from "../components/AppHeader.js";
|
|
5
5
|
import { SelectList } from "../components/SelectList.js";
|
|
6
6
|
import { StatusBar } from "../components/StatusBar.js";
|
|
7
|
-
import { hintLine
|
|
7
|
+
import { hintLine } from "../theme.js";
|
|
8
8
|
const LEVELS = [
|
|
9
9
|
{ id: "junior", label: "Junior" },
|
|
10
10
|
{ id: "mid", label: "Mid-level" },
|
|
11
11
|
{ id: "senior", label: "Senior" },
|
|
12
12
|
{ id: "staff", label: "Staff+" }
|
|
13
13
|
];
|
|
14
|
+
const LANGUAGES = [
|
|
15
|
+
{ id: "zh-CN", label: "中文" },
|
|
16
|
+
{ id: "en", label: "English" }
|
|
17
|
+
];
|
|
14
18
|
export function SetupScreen({ onComplete }) {
|
|
15
19
|
const [step, setStep] = useState("language");
|
|
16
|
-
const [
|
|
20
|
+
const [languageIndex, setLanguageIndex] = useState(1); // default English
|
|
17
21
|
const [levelIndex, setLevelIndex] = useState(1);
|
|
22
|
+
const isZh = LANGUAGES[languageIndex].id === "zh-CN";
|
|
18
23
|
useInput((input, key) => {
|
|
19
24
|
if (step === "language") {
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
setStep("level");
|
|
25
|
+
if (key.upArrow) {
|
|
26
|
+
setLanguageIndex((i) => Math.max(0, i - 1));
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
if (key.downArrow) {
|
|
30
|
+
setLanguageIndex((i) => Math.min(LANGUAGES.length - 1, i + 1));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const num = Number(input);
|
|
34
|
+
if (num >= 1 && num <= LANGUAGES.length) {
|
|
35
|
+
setLanguageIndex(num - 1);
|
|
28
36
|
return;
|
|
29
37
|
}
|
|
30
38
|
if (key.return) {
|
|
@@ -48,7 +56,7 @@ export function SetupScreen({ onComplete }) {
|
|
|
48
56
|
}
|
|
49
57
|
if (key.return) {
|
|
50
58
|
onComplete({
|
|
51
|
-
language,
|
|
59
|
+
language: LANGUAGES[languageIndex].id,
|
|
52
60
|
level: LEVELS[levelIndex].id,
|
|
53
61
|
dailyGoal: 5,
|
|
54
62
|
soundEnabled: false,
|
|
@@ -57,11 +65,12 @@ export function SetupScreen({ onComplete }) {
|
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
});
|
|
60
|
-
const isZh = language === "zh-CN";
|
|
61
68
|
if (step === "language") {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
isZh ? "
|
|
69
|
+
const languageItems = LANGUAGES.map((l) => ({ id: l.id, label: l.label }));
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(AppHeader, { title: "QuizMe", subtitle: isZh ? "首次设置 · 选择语言" : "First run · Choose language" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(SelectList, { items: languageItems, selectedIndex: languageIndex, showIndex: true }) }), _jsx(StatusBar, { status: isZh ? "语言" : "Language", hints: hintLine([
|
|
71
|
+
isZh ? "↑↓ 选择" : "↑↓ select",
|
|
72
|
+
isZh ? "Enter 确认" : "enter confirm",
|
|
73
|
+
isZh ? "1-2 快捷" : "1-2 shortcut"
|
|
65
74
|
]) })] }));
|
|
66
75
|
}
|
|
67
76
|
const levelItems = LEVELS.map((l) => ({ id: l.id, label: l.label }));
|
package/dist/ui/theme.js
CHANGED
|
@@ -16,12 +16,15 @@ export const theme = {
|
|
|
16
16
|
error: "#FF6B80",
|
|
17
17
|
warning: "#FFC107",
|
|
18
18
|
promptBorder: "#888888",
|
|
19
|
-
|
|
19
|
+
selectionFg: "#6CB6FF",
|
|
20
20
|
userMessageBg: "#373737"
|
|
21
21
|
};
|
|
22
22
|
export const symbols = {
|
|
23
|
-
pointer: "
|
|
24
|
-
pointerIdle: " "
|
|
23
|
+
pointer: "❯",
|
|
24
|
+
pointerIdle: " ",
|
|
25
|
+
success: "✓",
|
|
26
|
+
error: "✘",
|
|
27
|
+
cursor: "▌"
|
|
25
28
|
};
|
|
26
29
|
export function hintLine(parts) {
|
|
27
30
|
return parts.filter(Boolean).join(" · ");
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiy/quizme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Interactive CLI quiz practice for developers using Claude Code context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"quizme": "./dist/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/cli/index.js",
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
11
13
|
"scripts": {
|
|
12
14
|
"start": "node --import tsx ./src/cli/index.ts",
|
|
13
15
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -37,19 +39,14 @@
|
|
|
37
39
|
"ink"
|
|
38
40
|
],
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"better-sqlite3": "^12.11.1",
|
|
41
42
|
"ink": "^5.2.1",
|
|
42
43
|
"react": "^18.3.1"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
46
46
|
"@types/node": "^26.0.1",
|
|
47
47
|
"@types/react": "^19.2.17",
|
|
48
48
|
"tsx": "^4.22.4",
|
|
49
49
|
"typescript": "^6.0.3"
|
|
50
50
|
},
|
|
51
|
-
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
|
|
52
|
-
"pnpm": {
|
|
53
|
-
"onlyBuiltDependencies": ["better-sqlite3"]
|
|
54
|
-
}
|
|
51
|
+
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
|
|
55
52
|
}
|
package/dist/platform/fs.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
export function ensureDir(dirPath) {
|
|
4
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
5
|
-
}
|
|
6
|
-
export function readJson(filePath, fallback = null) {
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return fallback;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export function writeJson(filePath, value) {
|
|
15
|
-
ensureDir(path.dirname(filePath));
|
|
16
|
-
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
17
|
-
}
|
package/dist/platform/paths.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
export function getAppDataDir() {
|
|
4
|
-
if (process.env.QUIZME_DATA_DIR) {
|
|
5
|
-
return process.env.QUIZME_DATA_DIR;
|
|
6
|
-
}
|
|
7
|
-
const home = os.homedir();
|
|
8
|
-
if (process.platform === "darwin") {
|
|
9
|
-
return path.join(home, "Library", "Application Support", "quizme");
|
|
10
|
-
}
|
|
11
|
-
if (process.platform === "win32") {
|
|
12
|
-
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "quizme");
|
|
13
|
-
}
|
|
14
|
-
return path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), "quizme");
|
|
15
|
-
}
|
|
16
|
-
export function slugifyProjectPath(projectPath) {
|
|
17
|
-
return projectPath.replace(/[:\\/]+/g, "-").replace(/_/g, "-");
|
|
18
|
-
}
|
|
19
|
-
export function getClaudeRoots(cwd = process.cwd()) {
|
|
20
|
-
return [
|
|
21
|
-
path.join(cwd, ".claude"),
|
|
22
|
-
path.join(os.homedir(), ".claude")
|
|
23
|
-
];
|
|
24
|
-
}
|
|
25
|
-
export function getClaudeProjectsDir() {
|
|
26
|
-
return path.join(os.homedir(), ".claude", "projects");
|
|
27
|
-
}
|
package/dist/storage/sqlite.js
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import Database from "better-sqlite3";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { ensureDir } from "../platform/fs.js";
|
|
4
|
-
/**
|
|
5
|
-
* SQLite-backed store. Uses `better-sqlite3` (synchronous, in-process) instead
|
|
6
|
-
* of shelling out to the `sqlite3` CLI — eliminates a subprocess spawn per
|
|
7
|
-
* query and removes the external binary dependency.
|
|
8
|
-
*/
|
|
9
|
-
export class SqliteStore {
|
|
10
|
-
dbPath;
|
|
11
|
-
db;
|
|
12
|
-
stmtCache = new Map();
|
|
13
|
-
constructor(dbPath) {
|
|
14
|
-
this.dbPath = dbPath;
|
|
15
|
-
ensureDir(path.dirname(dbPath));
|
|
16
|
-
this.db = new Database(dbPath);
|
|
17
|
-
this.db.pragma("busy_timeout = 5000");
|
|
18
|
-
}
|
|
19
|
-
stmt(sql) {
|
|
20
|
-
let prepared = this.stmtCache.get(sql);
|
|
21
|
-
if (!prepared) {
|
|
22
|
-
prepared = this.db.prepare(sql);
|
|
23
|
-
this.stmtCache.set(sql, prepared);
|
|
24
|
-
}
|
|
25
|
-
return prepared;
|
|
26
|
-
}
|
|
27
|
-
init() {
|
|
28
|
-
this.db.exec(`
|
|
29
|
-
PRAGMA busy_timeout=5000;
|
|
30
|
-
CREATE TABLE IF NOT EXISTS config (
|
|
31
|
-
key TEXT PRIMARY KEY,
|
|
32
|
-
value_json TEXT NOT NULL
|
|
33
|
-
);
|
|
34
|
-
CREATE TABLE IF NOT EXISTS questions (
|
|
35
|
-
id TEXT PRIMARY KEY,
|
|
36
|
-
source_type TEXT NOT NULL,
|
|
37
|
-
topic TEXT NOT NULL,
|
|
38
|
-
difficulty INTEGER NOT NULL,
|
|
39
|
-
payload_json TEXT NOT NULL,
|
|
40
|
-
created_at TEXT NOT NULL
|
|
41
|
-
);
|
|
42
|
-
CREATE TABLE IF NOT EXISTS attempts (
|
|
43
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
-
question_id TEXT NOT NULL,
|
|
45
|
-
selected TEXT NOT NULL,
|
|
46
|
-
correct INTEGER NOT NULL,
|
|
47
|
-
duration_ms INTEGER NOT NULL,
|
|
48
|
-
tags_json TEXT NOT NULL,
|
|
49
|
-
created_at TEXT NOT NULL
|
|
50
|
-
);
|
|
51
|
-
CREATE TABLE IF NOT EXISTS profile_signals (
|
|
52
|
-
tag TEXT PRIMARY KEY,
|
|
53
|
-
score REAL NOT NULL,
|
|
54
|
-
confidence REAL NOT NULL,
|
|
55
|
-
trend TEXT NOT NULL,
|
|
56
|
-
correct_count INTEGER NOT NULL,
|
|
57
|
-
wrong_count INTEGER NOT NULL,
|
|
58
|
-
updated_at TEXT NOT NULL
|
|
59
|
-
);
|
|
60
|
-
CREATE TABLE IF NOT EXISTS profile_preferences (
|
|
61
|
-
tag TEXT PRIMARY KEY,
|
|
62
|
-
kind TEXT NOT NULL,
|
|
63
|
-
note TEXT NOT NULL,
|
|
64
|
-
updated_at TEXT NOT NULL
|
|
65
|
-
);
|
|
66
|
-
CREATE TABLE IF NOT EXISTS why_threads (
|
|
67
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
-
question_id TEXT NOT NULL,
|
|
69
|
-
turns_json TEXT NOT NULL,
|
|
70
|
-
updated_at TEXT NOT NULL
|
|
71
|
-
);
|
|
72
|
-
CREATE TABLE IF NOT EXISTS review_items (
|
|
73
|
-
question_id TEXT PRIMARY KEY,
|
|
74
|
-
resolved INTEGER NOT NULL,
|
|
75
|
-
last_result INTEGER NOT NULL,
|
|
76
|
-
updated_at TEXT NOT NULL
|
|
77
|
-
);
|
|
78
|
-
`);
|
|
79
|
-
}
|
|
80
|
-
setConfig(key, value) {
|
|
81
|
-
this.stmt(`INSERT INTO config(key, value_json)
|
|
82
|
-
VALUES (@key, @value)
|
|
83
|
-
ON CONFLICT(key) DO UPDATE SET value_json=excluded.value_json;`).run({ key, value: JSON.stringify(value) });
|
|
84
|
-
}
|
|
85
|
-
getConfig(key, fallback = null) {
|
|
86
|
-
const row = this.stmt(`SELECT value_json FROM config WHERE key=@key LIMIT 1;`).get({ key });
|
|
87
|
-
return row ? JSON.parse(row.value_json) : fallback;
|
|
88
|
-
}
|
|
89
|
-
saveQuestion(question, sourceType) {
|
|
90
|
-
this.stmt(`INSERT OR REPLACE INTO questions(id, source_type, topic, difficulty, payload_json, created_at)
|
|
91
|
-
VALUES (@id, @sourceType, @topic, @difficulty, @payload, datetime('now'));`).run({
|
|
92
|
-
id: question.id,
|
|
93
|
-
sourceType,
|
|
94
|
-
topic: question.topic,
|
|
95
|
-
difficulty: Number(question.difficulty || 1),
|
|
96
|
-
payload: JSON.stringify(question)
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
listRecentQuestions(limit = 20) {
|
|
100
|
-
const rows = this.stmt(`SELECT payload_json FROM questions
|
|
101
|
-
ORDER BY datetime(created_at) DESC
|
|
102
|
-
LIMIT @limit;`).all({ limit: Number(limit) });
|
|
103
|
-
return rows.map((row) => JSON.parse(row.payload_json));
|
|
104
|
-
}
|
|
105
|
-
recordAttempt({ questionId, selected, correct, durationMs, tags }) {
|
|
106
|
-
this.stmt(`INSERT INTO attempts(question_id, selected, correct, duration_ms, tags_json, created_at)
|
|
107
|
-
VALUES (@questionId, @selected, @correct, @durationMs, @tags, datetime('now'));`).run({
|
|
108
|
-
questionId,
|
|
109
|
-
selected,
|
|
110
|
-
correct: correct ? 1 : 0,
|
|
111
|
-
durationMs: Number(durationMs),
|
|
112
|
-
tags: JSON.stringify(tags || [])
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
updateSignal(tag, wasCorrect) {
|
|
116
|
-
const delta = wasCorrect ? 0.08 : -0.1;
|
|
117
|
-
const trend = delta > 0 ? "up" : "down";
|
|
118
|
-
// Single atomic UPSERT: read current values via subquery, clamp, write back.
|
|
119
|
-
this.stmt(`INSERT INTO profile_signals(tag, score, confidence, trend, correct_count, wrong_count, updated_at)
|
|
120
|
-
VALUES (
|
|
121
|
-
@tag,
|
|
122
|
-
MIN(0.95, MAX(0.05, COALESCE((SELECT score FROM profile_signals WHERE tag=@tag), 0.5) + @delta)),
|
|
123
|
-
MIN(0.98, COALESCE((SELECT confidence FROM profile_signals WHERE tag=@tag), 0.2) + 0.08),
|
|
124
|
-
@trend,
|
|
125
|
-
COALESCE((SELECT correct_count FROM profile_signals WHERE tag=@tag), 0) + @correctInc,
|
|
126
|
-
COALESCE((SELECT wrong_count FROM profile_signals WHERE tag=@tag), 0) + @wrongInc,
|
|
127
|
-
datetime('now')
|
|
128
|
-
)
|
|
129
|
-
ON CONFLICT(tag) DO UPDATE SET
|
|
130
|
-
score=excluded.score,
|
|
131
|
-
confidence=excluded.confidence,
|
|
132
|
-
trend=excluded.trend,
|
|
133
|
-
correct_count=excluded.correct_count,
|
|
134
|
-
wrong_count=excluded.wrong_count,
|
|
135
|
-
updated_at=excluded.updated_at;`).run({
|
|
136
|
-
tag,
|
|
137
|
-
delta,
|
|
138
|
-
trend,
|
|
139
|
-
correctInc: wasCorrect ? 1 : 0,
|
|
140
|
-
wrongInc: wasCorrect ? 0 : 1
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
getProfileSignals() {
|
|
144
|
-
const rows = this.stmt(`SELECT tag, score, confidence, trend, correct_count, wrong_count
|
|
145
|
-
FROM profile_signals
|
|
146
|
-
ORDER BY score DESC, confidence DESC;`).all();
|
|
147
|
-
return rows.map((row) => ({
|
|
148
|
-
tag: row.tag,
|
|
149
|
-
score: row.score,
|
|
150
|
-
confidence: row.confidence,
|
|
151
|
-
trend: row.trend,
|
|
152
|
-
correctCount: row.correct_count,
|
|
153
|
-
wrongCount: row.wrong_count
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
getStats() {
|
|
157
|
-
const counts = this.stmt(`SELECT
|
|
158
|
-
(SELECT COUNT(*) FROM attempts) AS total,
|
|
159
|
-
(SELECT COUNT(*) FROM attempts WHERE correct=1) AS correct,
|
|
160
|
-
(SELECT COUNT(*) FROM attempts WHERE date(created_at)=date('now','localtime')) AS today,
|
|
161
|
-
(SELECT COUNT(*) FROM review_items WHERE resolved=0) AS reviewPending,
|
|
162
|
-
(SELECT COUNT(*) FROM why_threads) AS whyCount;`).get();
|
|
163
|
-
const dayList = this.stmt(`SELECT DISTINCT date(created_at, 'localtime') AS day
|
|
164
|
-
FROM attempts
|
|
165
|
-
ORDER BY day DESC;`).all().map((row) => row.day);
|
|
166
|
-
const weekRows = this.stmt(`SELECT date(created_at, 'localtime') AS day, COUNT(*) AS count
|
|
167
|
-
FROM attempts
|
|
168
|
-
WHERE datetime(created_at) >= datetime('now', '-6 day', 'localtime')
|
|
169
|
-
GROUP BY day
|
|
170
|
-
ORDER BY day;`).all().map((row) => [row.day, String(row.count)]);
|
|
171
|
-
const longest = this.stmt(`WITH days AS (
|
|
172
|
-
SELECT DISTINCT date(created_at, 'localtime') AS day FROM attempts
|
|
173
|
-
),
|
|
174
|
-
streaks AS (
|
|
175
|
-
SELECT
|
|
176
|
-
day,
|
|
177
|
-
julianday(day) - ROW_NUMBER() OVER (ORDER BY day) AS grp
|
|
178
|
-
FROM days
|
|
179
|
-
)
|
|
180
|
-
SELECT COALESCE(MAX(cnt), 0) AS cnt
|
|
181
|
-
FROM (SELECT COUNT(*) AS cnt FROM streaks GROUP BY grp);`).get();
|
|
182
|
-
const attemptsTotal = Number(counts?.total ?? 0);
|
|
183
|
-
const attemptsCorrect = Number(counts?.correct ?? 0);
|
|
184
|
-
const todayCount = Number(counts?.today ?? 0);
|
|
185
|
-
const reviewPending = Number(counts?.reviewPending ?? 0);
|
|
186
|
-
const whyCount = Number(counts?.whyCount ?? 0);
|
|
187
|
-
let currentStreak = 0;
|
|
188
|
-
let cursor = new Date();
|
|
189
|
-
for (const day of dayList) {
|
|
190
|
-
const expected = cursor.toISOString().slice(0, 10);
|
|
191
|
-
if (day !== expected) {
|
|
192
|
-
if (currentStreak === 0) {
|
|
193
|
-
cursor.setDate(cursor.getDate() - 1);
|
|
194
|
-
const yesterday = cursor.toISOString().slice(0, 10);
|
|
195
|
-
if (day !== yesterday) {
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
currentStreak += 1;
|
|
204
|
-
cursor.setDate(cursor.getDate() - 1);
|
|
205
|
-
}
|
|
206
|
-
const xp = attemptsCorrect * 10 + (attemptsTotal - attemptsCorrect) * 4 + whyCount * 3;
|
|
207
|
-
const level = Math.floor(xp / 100) + 1;
|
|
208
|
-
return {
|
|
209
|
-
attemptsTotal,
|
|
210
|
-
attemptsCorrect,
|
|
211
|
-
todayCount,
|
|
212
|
-
reviewPending,
|
|
213
|
-
whyCount,
|
|
214
|
-
currentStreak,
|
|
215
|
-
longestStreak: Number(longest?.cnt ?? 0),
|
|
216
|
-
xp,
|
|
217
|
-
level,
|
|
218
|
-
accuracy: attemptsTotal ? attemptsCorrect / attemptsTotal : 0,
|
|
219
|
-
weekRows
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
upsertReviewItem(questionId, resolved) {
|
|
223
|
-
this.stmt(`INSERT INTO review_items(question_id, resolved, last_result, updated_at)
|
|
224
|
-
VALUES (@questionId, @resolved, @lastResult, datetime('now'))
|
|
225
|
-
ON CONFLICT(question_id) DO UPDATE SET
|
|
226
|
-
resolved=excluded.resolved,
|
|
227
|
-
last_result=excluded.last_result,
|
|
228
|
-
updated_at=excluded.updated_at;`).run({ questionId, resolved: resolved ? 1 : 0, lastResult: resolved ? 1 : 0 });
|
|
229
|
-
}
|
|
230
|
-
listReviewQuestionIds(limit = 5) {
|
|
231
|
-
const rows = this.stmt(`SELECT question_id FROM review_items
|
|
232
|
-
WHERE resolved=0
|
|
233
|
-
ORDER BY datetime(updated_at) DESC
|
|
234
|
-
LIMIT @limit;`).all({ limit: Number(limit) });
|
|
235
|
-
return rows.map((row) => row.question_id);
|
|
236
|
-
}
|
|
237
|
-
appendWhyThread(questionId, turns) {
|
|
238
|
-
this.stmt(`INSERT INTO why_threads(question_id, turns_json, updated_at)
|
|
239
|
-
VALUES (@questionId, @turns, datetime('now'));`).run({ questionId, turns: JSON.stringify(turns) });
|
|
240
|
-
}
|
|
241
|
-
listProfilePreferences() {
|
|
242
|
-
const rows = this.stmt(`SELECT tag, kind, note, updated_at
|
|
243
|
-
FROM profile_preferences
|
|
244
|
-
ORDER BY datetime(updated_at) DESC;`).all();
|
|
245
|
-
return rows.map((row) => ({
|
|
246
|
-
tag: row.tag,
|
|
247
|
-
kind: row.kind,
|
|
248
|
-
note: row.note || undefined,
|
|
249
|
-
updatedAt: row.updated_at
|
|
250
|
-
}));
|
|
251
|
-
}
|
|
252
|
-
upsertProfilePreference(pref) {
|
|
253
|
-
this.stmt(`INSERT INTO profile_preferences(tag, kind, note, updated_at)
|
|
254
|
-
VALUES (@tag, @kind, @note, datetime('now'))
|
|
255
|
-
ON CONFLICT(tag) DO UPDATE SET
|
|
256
|
-
kind=excluded.kind,
|
|
257
|
-
note=excluded.note,
|
|
258
|
-
updated_at=excluded.updated_at;`).run({ tag: pref.tag, kind: pref.kind, note: pref.note ?? "" });
|
|
259
|
-
}
|
|
260
|
-
deleteProfilePreference(tag) {
|
|
261
|
-
this.stmt(`DELETE FROM profile_preferences WHERE tag=@tag;`).run({ tag });
|
|
262
|
-
}
|
|
263
|
-
clearProfilePreferences() {
|
|
264
|
-
this.db.exec("DELETE FROM profile_preferences;");
|
|
265
|
-
}
|
|
266
|
-
clearAttemptHistory() {
|
|
267
|
-
this.db.exec(`
|
|
268
|
-
DELETE FROM attempts;
|
|
269
|
-
DELETE FROM review_items;
|
|
270
|
-
`);
|
|
271
|
-
}
|
|
272
|
-
clearProfileSignals() {
|
|
273
|
-
this.db.exec("DELETE FROM profile_signals;");
|
|
274
|
-
}
|
|
275
|
-
clearQuestionBank() {
|
|
276
|
-
this.db.exec("DELETE FROM questions;");
|
|
277
|
-
}
|
|
278
|
-
clearAll() {
|
|
279
|
-
this.db.exec(`
|
|
280
|
-
DELETE FROM attempts;
|
|
281
|
-
DELETE FROM questions;
|
|
282
|
-
DELETE FROM profile_signals;
|
|
283
|
-
DELETE FROM profile_preferences;
|
|
284
|
-
DELETE FROM why_threads;
|
|
285
|
-
DELETE FROM review_items;
|
|
286
|
-
`);
|
|
287
|
-
}
|
|
288
|
-
clearWhyThreads() {
|
|
289
|
-
this.db.exec("DELETE FROM why_threads;");
|
|
290
|
-
}
|
|
291
|
-
close() {
|
|
292
|
-
this.db.close();
|
|
293
|
-
}
|
|
294
|
-
}
|