algocoach 0.1.7 → 0.1.9
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/cli/index.ts +17 -0
- package/package.json +1 -1
- package/server/routes/plan.ts +1 -1
- package/server/services/ai.ts +52 -15
package/cli/index.ts
CHANGED
|
@@ -5,6 +5,22 @@ import fs from "fs"
|
|
|
5
5
|
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || ".", ".algocoach")
|
|
6
6
|
const ENV_PATH = path.join(CONFIG_DIR, ".env")
|
|
7
7
|
|
|
8
|
+
async function checkVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(await Bun.file(path.join(import.meta.dir, "../package.json")).text())
|
|
11
|
+
const current = pkg.version
|
|
12
|
+
const res = await fetch("https://registry.npmjs.org/algocoach/latest", {
|
|
13
|
+
signal: AbortSignal.timeout(3000),
|
|
14
|
+
})
|
|
15
|
+
const data: any = await res.json()
|
|
16
|
+
const latest = data.version
|
|
17
|
+
if (latest !== current) {
|
|
18
|
+
console.log(` Update available: ${current} → ${latest}`)
|
|
19
|
+
console.log(` Run: npm install -g algocoach@latest\n`)
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
function envTemplate() {
|
|
9
25
|
return [
|
|
10
26
|
"# AlgoCoach Configuration",
|
|
@@ -64,6 +80,7 @@ Then run: algocoach start
|
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
async function cmdStart() {
|
|
83
|
+
checkVersion()
|
|
67
84
|
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
68
85
|
|
|
69
86
|
if (!fs.existsSync(ENV_PATH)) {
|
package/package.json
CHANGED
package/server/routes/plan.ts
CHANGED
|
@@ -106,7 +106,7 @@ async function scheduleReview(userId: string, problem: any, status: string) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async function getDueReviews(userId: string): Promise<any[]> {
|
|
109
|
-
const now =
|
|
109
|
+
const now = Date.now()
|
|
110
110
|
const due = await db.query.problemReview.findMany({
|
|
111
111
|
where: and(eq(problemReview.userId, userId), sql`${problemReview.nextReviewAt} <= ${now}`),
|
|
112
112
|
orderBy: [sql`${problemReview.nextReviewAt} ASC`],
|
package/server/services/ai.ts
CHANGED
|
@@ -168,11 +168,29 @@ export async function selectDailyProblems(params: {
|
|
|
168
168
|
const exclude = [...new Set([...overusedSlugs, ...solvedSlugs, ...extraExclude])]
|
|
169
169
|
|
|
170
170
|
const topicSlugs = parseTopicToSlugs(week.topic)
|
|
171
|
-
let searchResults =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
let searchResults: LeetCodeProblem[] = []
|
|
172
|
+
if (topicSlugs.length > 1) {
|
|
173
|
+
const seen = new Set<string>()
|
|
174
|
+
for (const slug of topicSlugs) {
|
|
175
|
+
const results = await searchLeetCodeProblems({
|
|
176
|
+
topics: [slug],
|
|
177
|
+
excludeSlugs: exclude,
|
|
178
|
+
limit: 30,
|
|
179
|
+
})
|
|
180
|
+
for (const p of results) {
|
|
181
|
+
if (!seen.has(p.titleSlug)) {
|
|
182
|
+
seen.add(p.titleSlug)
|
|
183
|
+
searchResults.push(p)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
searchResults = await searchLeetCodeProblems({
|
|
189
|
+
topics: topicSlugs,
|
|
190
|
+
excludeSlugs: exclude,
|
|
191
|
+
limit: 30,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
176
194
|
|
|
177
195
|
if (!searchResults.length) {
|
|
178
196
|
return {
|
|
@@ -204,23 +222,42 @@ export async function selectDailyProblems(params: {
|
|
|
204
222
|
const hard = searchResults.filter((p) => p.difficulty === "Hard").sort((a, b) => b.acRate - a.acRate)
|
|
205
223
|
|
|
206
224
|
const selected: LeetCodeProblem[] = []
|
|
207
|
-
const
|
|
225
|
+
const usedSlugs = new Set<string>()
|
|
208
226
|
|
|
209
227
|
function pickFromBucket(bucket: LeetCodeProblem[], want: number): LeetCodeProblem[] {
|
|
210
228
|
const result: LeetCodeProblem[] = []
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
result.push(p)
|
|
217
|
-
p.topicTags.forEach((t) => usedTags.add(t.slug))
|
|
218
|
-
}
|
|
229
|
+
const candidates = bucket.filter((p) => !usedSlugs.has(p.titleSlug))
|
|
230
|
+
for (const p of candidates) {
|
|
231
|
+
if (result.length >= want) break
|
|
232
|
+
result.push(p)
|
|
233
|
+
usedSlugs.add(p.titleSlug)
|
|
219
234
|
}
|
|
220
235
|
return result
|
|
221
236
|
}
|
|
222
237
|
|
|
223
|
-
|
|
238
|
+
// When multiple constituent topics, round-robin across them for topic balance
|
|
239
|
+
if (topicSlugs.length > 1 && difficultyFilter === "MIXED") {
|
|
240
|
+
const byTopic = topicSlugs.map((slug) => ({
|
|
241
|
+
slug,
|
|
242
|
+
easy: easy.filter((p) => p.topicTags.some((t) => t.slug === slug)),
|
|
243
|
+
medium: medium.filter((p) => p.topicTags.some((t) => t.slug === slug)),
|
|
244
|
+
hard: hard.filter((p) => p.topicTags.some((t) => t.slug === slug)),
|
|
245
|
+
}))
|
|
246
|
+
|
|
247
|
+
for (let round = 0; round < count; round++) {
|
|
248
|
+
const topic = byTopic[round % byTopic.length]
|
|
249
|
+
const tier = round === 0 ? topic.easy : round === 1 ? topic.medium : topic.hard
|
|
250
|
+
const picked = pickFromBucket(tier, 1)
|
|
251
|
+
if (picked.length) {
|
|
252
|
+
selected.push(...picked)
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
const fallback = [...topic.easy, ...topic.medium, ...topic.hard].filter(
|
|
256
|
+
(p) => !usedSlugs.has(p.titleSlug),
|
|
257
|
+
)
|
|
258
|
+
selected.push(...pickFromBucket(fallback, 1))
|
|
259
|
+
}
|
|
260
|
+
} else if (difficultyFilter !== "MIXED") {
|
|
224
261
|
const pool = difficultyFilter === "EASY" ? easy : difficultyFilter === "MEDIUM" ? medium : hard
|
|
225
262
|
selected.push(...pickFromBucket(pool, count))
|
|
226
263
|
} else {
|