algocoach 0.1.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 +101 -0
- package/cli/index.ts +97 -0
- package/dist/assets/index-C4ELaZwf.css +1 -0
- package/dist/assets/index-D5iezweF.js +55 -0
- package/dist/favicon.svg +11 -0
- package/dist/icons.svg +24 -0
- package/dist/index.html +17 -0
- package/package.json +59 -0
- package/server/auth/index.ts +14 -0
- package/server/db/custom-types.ts +13 -0
- package/server/db/index.ts +32 -0
- package/server/db/schema.ts +130 -0
- package/server/db/setup.ts +144 -0
- package/server/index.ts +57 -0
- package/server/lib/validation.test.ts +91 -0
- package/server/lib/validation.ts +25 -0
- package/server/local-dev/index.ts +5 -0
- package/server/local-dev/test-ai.ts +88 -0
- package/server/middleware/auth.ts +30 -0
- package/server/middleware/rate-limit.test.ts +77 -0
- package/server/middleware/rate-limit.ts +30 -0
- package/server/routes/leetcode.ts +189 -0
- package/server/routes/onboard.ts +75 -0
- package/server/routes/plan.ts +595 -0
- package/server/routes/survey.ts +44 -0
- package/server/services/ai-provider.ts +171 -0
- package/server/services/ai.test.ts +61 -0
- package/server/services/ai.ts +368 -0
- package/server/services/leetcode-search.test.ts +85 -0
- package/server/services/leetcode-search.ts +84 -0
- package/server/services/leetcode.ts +84 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const LEETCODE_API = "https://leetcode.com/graphql";
|
|
2
|
+
|
|
3
|
+
interface LeetCodeProblem {
|
|
4
|
+
title: string
|
|
5
|
+
titleSlug: string
|
|
6
|
+
difficulty: "Easy" | "Medium" | "Hard"
|
|
7
|
+
frontendQuestionId: string
|
|
8
|
+
topicTags: { name: string; slug: string }[]
|
|
9
|
+
acRate: number
|
|
10
|
+
paidOnly: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface LeetCodeSearchResult {
|
|
14
|
+
problemsetQuestionList: {
|
|
15
|
+
total: number
|
|
16
|
+
questions: LeetCodeProblem[]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const searchQuery = `
|
|
21
|
+
query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {
|
|
22
|
+
problemsetQuestionList: questionList(
|
|
23
|
+
categorySlug: $categorySlug
|
|
24
|
+
limit: $limit
|
|
25
|
+
skip: $skip
|
|
26
|
+
filters: $filters
|
|
27
|
+
) {
|
|
28
|
+
total: totalNum
|
|
29
|
+
questions: data {
|
|
30
|
+
acRate
|
|
31
|
+
difficulty
|
|
32
|
+
frontendQuestionId: questionFrontendId
|
|
33
|
+
paidOnly: isPaidOnly
|
|
34
|
+
title
|
|
35
|
+
titleSlug
|
|
36
|
+
topicTags {
|
|
37
|
+
name
|
|
38
|
+
slug
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
export async function searchLeetCodeProblems(params: {
|
|
46
|
+
topics?: string[]
|
|
47
|
+
difficulty?: "EASY" | "MEDIUM" | "HARD"
|
|
48
|
+
limit?: number
|
|
49
|
+
excludeSlugs?: string[]
|
|
50
|
+
}): Promise<LeetCodeProblem[]> {
|
|
51
|
+
const { topics, difficulty, limit = 15, excludeSlugs = [] } = params
|
|
52
|
+
|
|
53
|
+
const filters: Record<string, unknown> = {}
|
|
54
|
+
if (topics?.length) filters.tags = topics
|
|
55
|
+
if (difficulty) filters.difficulty = difficulty
|
|
56
|
+
|
|
57
|
+
const res = await fetch(LEETCODE_API, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
query: searchQuery,
|
|
62
|
+
variables: {
|
|
63
|
+
categorySlug: "",
|
|
64
|
+
limit: limit + excludeSlugs.length,
|
|
65
|
+
skip: 0,
|
|
66
|
+
filters,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const json = await res.json() as { data?: LeetCodeSearchResult; errors?: { message: string }[] }
|
|
72
|
+
if (json.errors?.length) {
|
|
73
|
+
throw new Error(json.errors[0].message || "LeetCode search API error")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let problems = json.data?.problemsetQuestionList?.questions || []
|
|
77
|
+
problems = problems.filter((p) => !p.paidOnly)
|
|
78
|
+
|
|
79
|
+
if (excludeSlugs.length) {
|
|
80
|
+
problems = problems.filter((p) => !excludeSlugs.includes(p.titleSlug))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return problems.slice(0, limit)
|
|
84
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const LEETCODE_API = "https://leetcode.com/graphql";
|
|
2
|
+
|
|
3
|
+
const getLeetcodeDataQuery = `
|
|
4
|
+
query GetLeetCodeProfile($username: String!) {
|
|
5
|
+
matchedUser(username: $username) {
|
|
6
|
+
username
|
|
7
|
+
submitStats {
|
|
8
|
+
acSubmissionNum {
|
|
9
|
+
difficulty
|
|
10
|
+
count
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
userContestRanking(username: $username) {
|
|
15
|
+
rating
|
|
16
|
+
globalRanking
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export interface LeetCodeStats {
|
|
22
|
+
username: string
|
|
23
|
+
totalSolved: number
|
|
24
|
+
easySolved: number
|
|
25
|
+
mediumSolved: number
|
|
26
|
+
hardSolved: number
|
|
27
|
+
rating: number | null
|
|
28
|
+
ranking: number | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function graphqlRequest<T>(query: string, variables: Record<string, unknown>): Promise<T> {
|
|
32
|
+
const res = await fetch(LEETCODE_API, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({ query, variables }),
|
|
36
|
+
})
|
|
37
|
+
const json = await res.json() as { data: T; errors?: { message: string }[] }
|
|
38
|
+
if (json.errors?.length) {
|
|
39
|
+
throw new Error(json.errors[0].message || "LeetCode API error")
|
|
40
|
+
}
|
|
41
|
+
return json.data
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function validateUsername(username: string): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
const data = await graphqlRequest<{ matchedUser: { username: string } | null }>(
|
|
47
|
+
`query ($username: String!) { matchedUser(username: $username) { username } }`,
|
|
48
|
+
{ username },
|
|
49
|
+
)
|
|
50
|
+
return data.matchedUser !== null
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function fetchLeetcodeStats(username: string): Promise<LeetCodeStats> {
|
|
57
|
+
const valid = await validateUsername(username)
|
|
58
|
+
if (!valid) throw new Error("LeetCode username not found")
|
|
59
|
+
|
|
60
|
+
const data = await graphqlRequest<{
|
|
61
|
+
matchedUser: {
|
|
62
|
+
username: string
|
|
63
|
+
submitStats: {
|
|
64
|
+
acSubmissionNum: { difficulty: string; count: number }[]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
userContestRanking: { rating: number; globalRanking: number } | null
|
|
68
|
+
}>(getLeetcodeDataQuery, { username })
|
|
69
|
+
|
|
70
|
+
const counts: Record<string, number> = {}
|
|
71
|
+
for (const entry of data.matchedUser.submitStats.acSubmissionNum) {
|
|
72
|
+
counts[entry.difficulty.toLowerCase()] = entry.count
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
username: data.matchedUser.username,
|
|
77
|
+
totalSolved: counts.all || 0,
|
|
78
|
+
easySolved: counts.easy || 0,
|
|
79
|
+
mediumSolved: counts.medium || 0,
|
|
80
|
+
hardSolved: counts.hard || 0,
|
|
81
|
+
rating: data.userContestRanking?.rating ?? null,
|
|
82
|
+
ranking: data.userContestRanking?.globalRanking ?? null,
|
|
83
|
+
}
|
|
84
|
+
}
|