algocoach 0.1.6 → 0.1.7
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/dist/assets/index-Cdh7Qj-d.css +1 -0
- package/dist/assets/index-PSyWQaoU.js +55 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/db/schema.ts +19 -1
- package/server/db/setup.ts +18 -0
- package/server/routes/plan.ts +151 -8
- package/dist/assets/index-C1FVIsAd.js +0 -55
- package/dist/assets/index-C4ELaZwf.css +0 -1
package/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-PSyWQaoU.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cdh7Qj-d.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/db/schema.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
sqliteTable,
|
|
3
3
|
text as sqliteText,
|
|
4
4
|
integer,
|
|
5
|
+
real,
|
|
5
6
|
} from "drizzle-orm/sqlite-core"
|
|
6
7
|
import { jsonText, textArray } from "./custom-types"
|
|
7
8
|
|
|
@@ -114,11 +115,28 @@ export const dailyPlan = sqliteTable("daily_plan", {
|
|
|
114
115
|
date: integer("date", { mode: "timestamp_ms" }).notNull(),
|
|
115
116
|
weekNumber: integer("week_number").notNull(),
|
|
116
117
|
topic: text("topic").notNull(),
|
|
117
|
-
problems: jsonText<{ title: string; titleSlug: string; difficulty: string; topicTags: string[]; leetcodeUrl: string; acRate: number; status?: string; completedAt?: string | null }[]>()("problems").notNull(),
|
|
118
|
+
problems: jsonText<{ title: string; titleSlug: string; difficulty: string; topicTags: string[]; leetcodeUrl: string; acRate: number; status?: string; completedAt?: string | null; isReview?: boolean }[]>()("problems").notNull(),
|
|
118
119
|
explanation: text("explanation"),
|
|
119
120
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
|
120
121
|
})
|
|
121
122
|
|
|
123
|
+
export const problemReview = sqliteTable("problem_review", {
|
|
124
|
+
id: text("id").primaryKey(),
|
|
125
|
+
userId: text("user_id").notNull().references(() => user.id),
|
|
126
|
+
problemId: text("problem_id").notNull(),
|
|
127
|
+
problemName: text("problem_name").notNull(),
|
|
128
|
+
difficulty: text("difficulty").notNull(),
|
|
129
|
+
topics: jsonText<string[]>()("topics").notNull(),
|
|
130
|
+
leetcodeUrl: text("leetcode_url").notNull(),
|
|
131
|
+
acRate: real("ac_rate").notNull(),
|
|
132
|
+
interval: integer("interval").notNull().default(1),
|
|
133
|
+
easinessFactor: real("easiness_factor").notNull().default(2.5),
|
|
134
|
+
nextReviewAt: integer("next_review_at", { mode: "timestamp_ms" }).notNull(),
|
|
135
|
+
lastReviewedAt: integer("last_reviewed_at", { mode: "timestamp_ms" }),
|
|
136
|
+
reviewCount: integer("review_count").notNull().default(0),
|
|
137
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
|
138
|
+
})
|
|
139
|
+
|
|
122
140
|
export const roadmapJob = sqliteTable("roadmap_job", {
|
|
123
141
|
id: text("id").primaryKey(),
|
|
124
142
|
userId: text("user_id").notNull().references(() => user.id),
|
package/server/db/setup.ts
CHANGED
|
@@ -138,6 +138,24 @@ export function createTables(db: Database) {
|
|
|
138
138
|
"updated_at" integer NOT NULL
|
|
139
139
|
)
|
|
140
140
|
`)
|
|
141
|
+
db.run(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS "problem_review" (
|
|
143
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
144
|
+
"user_id" text NOT NULL REFERENCES "user"("id"),
|
|
145
|
+
"problem_id" text NOT NULL,
|
|
146
|
+
"problem_name" text NOT NULL,
|
|
147
|
+
"difficulty" text NOT NULL,
|
|
148
|
+
"topics" text NOT NULL,
|
|
149
|
+
"leetcode_url" text NOT NULL,
|
|
150
|
+
"ac_rate" real NOT NULL,
|
|
151
|
+
"interval" integer NOT NULL DEFAULT 1,
|
|
152
|
+
"easiness_factor" real NOT NULL DEFAULT 2.5,
|
|
153
|
+
"next_review_at" integer NOT NULL,
|
|
154
|
+
"last_reviewed_at" integer,
|
|
155
|
+
"review_count" integer NOT NULL DEFAULT 0,
|
|
156
|
+
"created_at" integer NOT NULL
|
|
157
|
+
)
|
|
158
|
+
`)
|
|
141
159
|
|
|
142
160
|
// Add unique indexes for tables that might already exist without them
|
|
143
161
|
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_leetcode_account_user_id ON leetcode_account(user_id)")
|
package/server/routes/plan.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
|
|
2
2
|
import { streamSSE } from 'hono/streaming'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { db } from '../db'
|
|
5
|
-
import { userPreferences, roadmapPlan, dailyPlan, dailyProgress, roadmapJob } from '../db/schema'
|
|
5
|
+
import { userPreferences, roadmapPlan, dailyPlan, dailyProgress, roadmapJob, problemReview } from '../db/schema'
|
|
6
6
|
import { authMiddleware } from '../middleware/auth'
|
|
7
7
|
import { eq, and, desc, sql } from 'drizzle-orm'
|
|
8
8
|
import { selectDailyProblems, startJobProcessing } from '../services/ai'
|
|
@@ -13,13 +13,116 @@ function sleep(ms: number) {
|
|
|
13
13
|
|
|
14
14
|
async function getCurrentWeek(userId: string, weeks: any[]): Promise<number> {
|
|
15
15
|
if (!weeks.length) return 1
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
const allPlans = await db.query.dailyPlan.findMany({
|
|
18
|
+
where: eq(dailyPlan.userId, userId),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < weeks.length; i++) {
|
|
22
|
+
const week = weeks[i] as Record<string, unknown>
|
|
23
|
+
const weekNum = week.week as number
|
|
24
|
+
const targetCount = (week.problemsCount as number) || 1
|
|
25
|
+
const weekPlans = allPlans.filter((p: any) => p.weekNumber === weekNum)
|
|
26
|
+
const solvedCount = weekPlans.reduce((count, plan) => {
|
|
27
|
+
const problems = Array.isArray(plan.problems) ? plan.problems : []
|
|
28
|
+
return count + problems.filter((p: any) => (p as Record<string, unknown>).status === 'SOLVED').length
|
|
29
|
+
}, 0)
|
|
30
|
+
const percent = Math.round((solvedCount / targetCount) * 100)
|
|
31
|
+
if (percent < 100) return weekNum
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return weeks.length
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function scheduleReview(userId: string, problem: any, status: string) {
|
|
38
|
+
const existing = await db.query.problemReview.findFirst({
|
|
39
|
+
where: and(eq(problemReview.userId, userId), eq(problemReview.problemId, problem.titleSlug)),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const dayMs = 86400000
|
|
44
|
+
|
|
45
|
+
if (status === 'SOLVED') {
|
|
46
|
+
const quality = existing ? 4 : 3
|
|
47
|
+
if (existing) {
|
|
48
|
+
let ef = existing.easinessFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
|
|
49
|
+
if (ef < 1.3) ef = 1.3
|
|
50
|
+
const newInterval = existing.reviewCount === 0 ? 1 : existing.reviewCount === 1 ? 6 : Math.round(existing.interval * ef)
|
|
51
|
+
await db.update(problemReview).set({
|
|
52
|
+
interval: newInterval,
|
|
53
|
+
easinessFactor: ef,
|
|
54
|
+
nextReviewAt: new Date(now + newInterval * dayMs),
|
|
55
|
+
lastReviewedAt: new Date(now),
|
|
56
|
+
reviewCount: existing.reviewCount + 1,
|
|
57
|
+
}).where(eq(problemReview.id, existing.id))
|
|
58
|
+
} else {
|
|
59
|
+
await db.insert(problemReview).values({
|
|
60
|
+
id: crypto.randomUUID(),
|
|
61
|
+
userId,
|
|
62
|
+
problemId: problem.titleSlug,
|
|
63
|
+
problemName: problem.title,
|
|
64
|
+
difficulty: problem.difficulty,
|
|
65
|
+
topics: problem.topicTags || [],
|
|
66
|
+
leetcodeUrl: problem.leetcodeUrl,
|
|
67
|
+
acRate: problem.acRate,
|
|
68
|
+
interval: 1,
|
|
69
|
+
easinessFactor: 2.5,
|
|
70
|
+
nextReviewAt: new Date(now + dayMs),
|
|
71
|
+
lastReviewedAt: new Date(now),
|
|
72
|
+
reviewCount: 1,
|
|
73
|
+
createdAt: new Date(),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
} else if (['TRIED', 'SKIPPED'].includes(status)) {
|
|
77
|
+
if (existing) {
|
|
78
|
+
let ef = existing.easinessFactor - 0.2
|
|
79
|
+
if (ef < 1.3) ef = 1.3
|
|
80
|
+
await db.update(problemReview).set({
|
|
81
|
+
interval: 1,
|
|
82
|
+
easinessFactor: ef,
|
|
83
|
+
nextReviewAt: new Date(now + dayMs),
|
|
84
|
+
lastReviewedAt: new Date(now),
|
|
85
|
+
reviewCount: existing.reviewCount + 1,
|
|
86
|
+
}).where(eq(problemReview.id, existing.id))
|
|
87
|
+
} else {
|
|
88
|
+
await db.insert(problemReview).values({
|
|
89
|
+
id: crypto.randomUUID(),
|
|
90
|
+
userId,
|
|
91
|
+
problemId: problem.titleSlug,
|
|
92
|
+
problemName: problem.title,
|
|
93
|
+
difficulty: problem.difficulty,
|
|
94
|
+
topics: problem.topicTags || [],
|
|
95
|
+
leetcodeUrl: problem.leetcodeUrl,
|
|
96
|
+
acRate: problem.acRate,
|
|
97
|
+
interval: 1,
|
|
98
|
+
easinessFactor: 1.3,
|
|
99
|
+
nextReviewAt: new Date(now + Math.round(dayMs / 2)),
|
|
100
|
+
lastReviewedAt: new Date(now),
|
|
101
|
+
reviewCount: 1,
|
|
102
|
+
createdAt: new Date(),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getDueReviews(userId: string): Promise<any[]> {
|
|
109
|
+
const now = new Date()
|
|
110
|
+
const due = await db.query.problemReview.findMany({
|
|
111
|
+
where: and(eq(problemReview.userId, userId), sql`${problemReview.nextReviewAt} <= ${now}`),
|
|
112
|
+
orderBy: [sql`${problemReview.nextReviewAt} ASC`],
|
|
113
|
+
limit: 2,
|
|
18
114
|
})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
115
|
+
return due.map((r) => ({
|
|
116
|
+
title: r.problemName,
|
|
117
|
+
titleSlug: r.problemId,
|
|
118
|
+
difficulty: r.difficulty,
|
|
119
|
+
topicTags: r.topics,
|
|
120
|
+
leetcodeUrl: r.leetcodeUrl,
|
|
121
|
+
acRate: r.acRate,
|
|
122
|
+
status: 'PENDING' as const,
|
|
123
|
+
completedAt: null,
|
|
124
|
+
isReview: true,
|
|
125
|
+
}))
|
|
23
126
|
}
|
|
24
127
|
|
|
25
128
|
const difficultySchema = z.enum(['EASY', 'MEDIUM', 'HARD', 'MIXED'])
|
|
@@ -86,6 +189,41 @@ app.patch('/roadmap/advance', async (c) => {
|
|
|
86
189
|
}
|
|
87
190
|
})
|
|
88
191
|
|
|
192
|
+
app.patch('/roadmap/week/:weekNumber', async (c) => {
|
|
193
|
+
try {
|
|
194
|
+
const userId = c.get('userId')
|
|
195
|
+
const weekNumber = parseInt(c.req.param('weekNumber'), 10)
|
|
196
|
+
const body: any = await c.req.json()
|
|
197
|
+
const { problemsCount } = body
|
|
198
|
+
if (typeof problemsCount !== 'number' || problemsCount < 1 || problemsCount > 30) {
|
|
199
|
+
return c.json({ success: false, error: 'problemsCount must be a number between 1 and 30' }, 400)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const plan = await db.query.roadmapPlan.findFirst({
|
|
203
|
+
where: eq(roadmapPlan.userId, userId),
|
|
204
|
+
})
|
|
205
|
+
if (!plan) return c.json({ success: false, error: 'No roadmap found' }, 404)
|
|
206
|
+
|
|
207
|
+
const weeks = Array.isArray(plan.weeks) ? [...plan.weeks] : []
|
|
208
|
+
const idx = weeks.findIndex((w: any) => (w as Record<string, unknown>).week === weekNumber)
|
|
209
|
+
if (idx === -1) return c.json({ success: false, error: `Week ${weekNumber} not found` }, 404)
|
|
210
|
+
|
|
211
|
+
weeks[idx] = { ...(weeks[idx] as any), problemsCount }
|
|
212
|
+
|
|
213
|
+
await db.update(roadmapPlan).set({
|
|
214
|
+
weeks: weeks as any,
|
|
215
|
+
updatedAt: new Date(),
|
|
216
|
+
}).where(eq(roadmapPlan.userId, userId))
|
|
217
|
+
|
|
218
|
+
const updated = await db.query.roadmapPlan.findFirst({
|
|
219
|
+
where: eq(roadmapPlan.userId, userId),
|
|
220
|
+
})
|
|
221
|
+
return c.json({ success: true, data: updated })
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
return c.json({ success: false, error: err.message }, 500)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
89
227
|
app.post('/roadmap/generate', async (c) => {
|
|
90
228
|
const userId = c.get('userId')
|
|
91
229
|
|
|
@@ -350,13 +488,16 @@ app.post('/today', async (c) => {
|
|
|
350
488
|
completedAt: null,
|
|
351
489
|
}))
|
|
352
490
|
|
|
491
|
+
const dueReviews = await getDueReviews(userId)
|
|
492
|
+
const allProblems = [...problems, ...dueReviews]
|
|
493
|
+
|
|
353
494
|
const entry = {
|
|
354
495
|
id: crypto.randomUUID(),
|
|
355
496
|
userId,
|
|
356
497
|
date: new Date(),
|
|
357
498
|
weekNumber: currentWeek,
|
|
358
499
|
topic: (roadmap[currentWeek - 1] as Record<string, unknown>)?.topic as string || '',
|
|
359
|
-
problems,
|
|
500
|
+
problems: allProblems,
|
|
360
501
|
explanation: task.explanation,
|
|
361
502
|
}
|
|
362
503
|
|
|
@@ -419,6 +560,8 @@ app.patch('/today/:planId/problem/:slug', async (c) => {
|
|
|
419
560
|
})
|
|
420
561
|
}
|
|
421
562
|
|
|
563
|
+
scheduleReview(userId, problem, status).catch(() => {})
|
|
564
|
+
|
|
422
565
|
return c.json({ success: true, data: { ...problem, status, completedAt: problem.completedAt } })
|
|
423
566
|
} catch (err: any) {
|
|
424
567
|
return c.json({ success: false, error: err.message }, 500)
|