ace-interview-prep 0.1.2 → 0.1.3
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 -25
- package/dist/commands/feedback.js +57 -22
- package/dist/commands/generate.js +31 -13
- package/dist/commands/reset.js +60 -27
- package/dist/commands/score.js +41 -26
- package/dist/commands/test.js +19 -11
- package/dist/index.js +26 -2
- package/dist/lib/categories.js +12 -4
- package/dist/lib/scaffold.js +1 -11
- package/dist/lib/scorecard.js +32 -1
- package/dist/prompts/question-brainstorm.md +2 -2
- package/dist/prompts/question-generate.md +19 -10
- package/dist/templates/web-components/Component.test.tsx.hbs +16 -0
- package/dist/templates/web-components/Component.tsx.hbs +29 -0
- package/package.json +1 -1
- package/dist/templates/web-components/component.test.ts.hbs +0 -11
- package/dist/templates/web-components/component.ts.hbs +0 -22
- package/dist/templates/web-components/index.html.hbs +0 -12
- package/questions/web-components/star-rating/README.md +0 -45
- package/questions/web-components/star-rating/component.test.ts +0 -64
- package/questions/web-components/star-rating/component.ts +0 -28
- package/questions/web-components/star-rating/index.html +0 -14
- package/questions/web-components/star-rating/scorecard.json +0 -9
package/README.md
CHANGED
|
@@ -50,7 +50,10 @@ npm install vitest happy-dom @testing-library/jest-dom
|
|
|
50
50
|
### 3. Practice
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
# Generate a question
|
|
53
|
+
# Generate a question interactively (prompts for category, difficulty, topic)
|
|
54
|
+
ace generate
|
|
55
|
+
|
|
56
|
+
# Or pass flags to skip prompts
|
|
54
57
|
ace generate --topic "debounce" --category js-ts --difficulty medium
|
|
55
58
|
|
|
56
59
|
# Interactive brainstorm mode
|
|
@@ -61,43 +64,59 @@ ace add
|
|
|
61
64
|
|
|
62
65
|
# List all questions
|
|
63
66
|
ace list
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Test, Review, Track
|
|
64
70
|
|
|
71
|
+
All commands below work in three modes:
|
|
72
|
+
- **Interactive** — run with no arguments to pick from a selectable list
|
|
73
|
+
- **Direct** — pass a slug to target a specific question
|
|
74
|
+
- **All** — pass `--all` to operate on every question
|
|
75
|
+
|
|
76
|
+
```bash
|
|
65
77
|
# Run tests
|
|
66
|
-
ace test
|
|
67
|
-
ace test
|
|
68
|
-
ace test --
|
|
78
|
+
ace test # pick from list
|
|
79
|
+
ace test debounce # specific question
|
|
80
|
+
ace test --all # run all tests
|
|
81
|
+
ace test --watch # watch mode (with --all)
|
|
69
82
|
|
|
70
|
-
# Get LLM feedback
|
|
71
|
-
ace feedback
|
|
83
|
+
# Get LLM feedback on your solution
|
|
84
|
+
ace feedback # pick from list
|
|
85
|
+
ace feedback debounce # specific question
|
|
86
|
+
ace feedback --all # review all questions (confirms each one)
|
|
72
87
|
|
|
73
88
|
# View scorecard
|
|
74
|
-
ace score
|
|
75
|
-
|
|
76
|
-
#
|
|
77
|
-
|
|
89
|
+
ace score # pick from list
|
|
90
|
+
ace score debounce # specific question
|
|
91
|
+
ace score --all # show all scorecards
|
|
92
|
+
|
|
93
|
+
# Reset a question to its stub
|
|
94
|
+
ace reset # pick from list
|
|
95
|
+
ace reset debounce # specific question
|
|
96
|
+
ace reset --all # reset everything (with confirmation)
|
|
78
97
|
```
|
|
79
98
|
|
|
80
99
|
## Question Categories
|
|
81
100
|
|
|
82
|
-
| Category | Slug | Type |
|
|
83
|
-
|
|
84
|
-
| JS/TS Puzzles | `js-ts` | Coding |
|
|
85
|
-
|
|
|
86
|
-
| React Web Apps | `react-apps` | Coding |
|
|
87
|
-
| LeetCode Data Structures | `leetcode-ds` | Coding |
|
|
88
|
-
| LeetCode Algorithms | `leetcode-algo` | Coding |
|
|
89
|
-
| System Design — Frontend | `design-fe` | Design |
|
|
90
|
-
| System Design — Backend | `design-be` | Design |
|
|
91
|
-
| System Design — Full Stack | `design-full` | Design |
|
|
101
|
+
| Category | Slug | Type | Focus |
|
|
102
|
+
|----------|------|------|-------|
|
|
103
|
+
| JS/TS Puzzles | `js-ts` | Coding | Closures, async patterns, type utilities |
|
|
104
|
+
| React Components | `web-components` | Coding | Props, events, composition, reusable UI |
|
|
105
|
+
| React Web Apps | `react-apps` | Coding | Hooks, state, routing, full features |
|
|
106
|
+
| LeetCode Data Structures | `leetcode-ds` | Coding | Trees, graphs, heaps, hash maps |
|
|
107
|
+
| LeetCode Algorithms | `leetcode-algo` | Coding | DP, greedy, two pointers, sorting |
|
|
108
|
+
| System Design — Frontend | `design-fe` | Design | Component architecture, state, rendering |
|
|
109
|
+
| System Design — Backend | `design-be` | Design | APIs, databases, caching, queues |
|
|
110
|
+
| System Design — Full Stack | `design-full` | Design | End-to-end systems, trade-offs |
|
|
92
111
|
|
|
93
112
|
## How It Works
|
|
94
113
|
|
|
95
|
-
1. **
|
|
114
|
+
1. **Generate a question** — run `ace generate` and follow the prompts (category, difficulty, topic), or use `ace generate --brainstorm` for an interactive design session.
|
|
96
115
|
2. **Open the question folder** — read `README.md` for the problem statement.
|
|
97
|
-
3. **Write your solution** in the solution file (`solution.ts`, `App.tsx`, `
|
|
98
|
-
4. **Run tests** with `ace test
|
|
99
|
-
5. **Get feedback** with `ace feedback
|
|
100
|
-
6. **Track progress** with `ace score
|
|
116
|
+
3. **Write your solution** in the solution file (`solution.ts`, `App.tsx`, `Component.tsx`, or `notes.md`).
|
|
117
|
+
4. **Run tests** with `ace test` to pick a question and check your work.
|
|
118
|
+
5. **Get feedback** with `ace feedback` for an LLM-powered code or design review.
|
|
119
|
+
6. **Track progress** with `ace score` and `ace list`.
|
|
101
120
|
|
|
102
121
|
## Configuration
|
|
103
122
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import prompts from "prompts";
|
|
3
4
|
import chalk from "chalk";
|
|
4
|
-
import { findQuestion, readScorecard, writeScorecard } from "../lib/scorecard.js";
|
|
5
|
+
import { findQuestion, readScorecard, writeScorecard, getAllQuestions, promptForSlug } from "../lib/scorecard.js";
|
|
5
6
|
import { CATEGORIES, isDesignCategory } from "../lib/categories.js";
|
|
6
7
|
import { chatStream, requireProvider } from "../lib/llm.js";
|
|
7
8
|
import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
|
|
@@ -12,35 +13,25 @@ function loadPrompt(name) {
|
|
|
12
13
|
function parseArgs(args) {
|
|
13
14
|
let slug;
|
|
14
15
|
let provider;
|
|
16
|
+
let all = false;
|
|
15
17
|
for (let i = 0; i < args.length; i++) {
|
|
16
18
|
const arg = args[i];
|
|
17
19
|
if (arg === "--provider" && args[i + 1]) {
|
|
18
20
|
provider = args[++i];
|
|
21
|
+
} else if (arg === "--all" || arg === "all") {
|
|
22
|
+
all = true;
|
|
19
23
|
} else if (!arg.startsWith("--")) {
|
|
20
24
|
slug = arg;
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
|
-
return { slug, provider };
|
|
27
|
+
return { slug, provider, all };
|
|
24
28
|
}
|
|
25
|
-
async function
|
|
26
|
-
const
|
|
27
|
-
if (!isWorkspaceInitialized(root)) {
|
|
28
|
-
console.error(chalk.red("\nError: Workspace not initialized."));
|
|
29
|
-
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
const parsed = parseArgs(args);
|
|
33
|
-
if (!parsed.slug) {
|
|
34
|
-
console.error(chalk.red("Missing question slug."));
|
|
35
|
-
console.error(chalk.dim("Usage: npm run ace feedback <slug>"));
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const question = findQuestion(parsed.slug);
|
|
29
|
+
async function runFeedbackForSlug(slug, provider) {
|
|
30
|
+
const question = findQuestion(slug);
|
|
39
31
|
if (!question) {
|
|
40
|
-
console.error(chalk.red(`Question not found: ${
|
|
32
|
+
console.error(chalk.red(`Question not found: ${slug}`));
|
|
41
33
|
return;
|
|
42
34
|
}
|
|
43
|
-
const provider = requireProvider(parsed.provider);
|
|
44
35
|
const config = CATEGORIES[question.category];
|
|
45
36
|
const isDesign = isDesignCategory(question.category);
|
|
46
37
|
const readmePath = path.join(question.dir, "README.md");
|
|
@@ -53,7 +44,7 @@ async function run(args) {
|
|
|
53
44
|
const notes = fs.existsSync(notesPath) ? fs.readFileSync(notesPath, "utf-8") : "";
|
|
54
45
|
if (!notes.trim() || notes.includes("<!-- List the core features")) {
|
|
55
46
|
console.error(chalk.yellow("Notes file appears to be empty. Write your design notes first!"));
|
|
56
|
-
console.error(chalk.dim(`Edit: questions/${question.category}/${
|
|
47
|
+
console.error(chalk.dim(`Edit: questions/${question.category}/${slug}/notes.md`));
|
|
57
48
|
return;
|
|
58
49
|
}
|
|
59
50
|
const designSubType = question.category === "design-fe" ? "frontend" : question.category === "design-be" ? "backend" : "full-stack";
|
|
@@ -102,7 +93,7 @@ ${solutionContent}
|
|
|
102
93
|
${testContent}`;
|
|
103
94
|
}
|
|
104
95
|
console.log(chalk.cyan(`
|
|
105
|
-
--- LLM ${isDesign ? "Design" : "Code"} Review: ${
|
|
96
|
+
--- LLM ${isDesign ? "Design" : "Code"} Review: ${slug} ---`));
|
|
106
97
|
console.log(chalk.dim(`Provider: ${provider}
|
|
107
98
|
`));
|
|
108
99
|
const messages = [
|
|
@@ -116,7 +107,7 @@ ${testContent}`;
|
|
|
116
107
|
fullResponse += chunk;
|
|
117
108
|
}
|
|
118
109
|
console.log("\n");
|
|
119
|
-
const scorecard = readScorecard(question.category,
|
|
110
|
+
const scorecard = readScorecard(question.category, slug);
|
|
120
111
|
if (scorecard) {
|
|
121
112
|
scorecard.llmFeedback = fullResponse;
|
|
122
113
|
const scoreMatch = fullResponse.match(/Overall.*?(\d+(?:\.\d+)?)\s*\/\s*5/i);
|
|
@@ -124,10 +115,54 @@ ${testContent}`;
|
|
|
124
115
|
const lastAttempt = scorecard.attempts[scorecard.attempts.length - 1];
|
|
125
116
|
lastAttempt.llmScore = parseFloat(scoreMatch[1]);
|
|
126
117
|
}
|
|
127
|
-
writeScorecard(question.category,
|
|
118
|
+
writeScorecard(question.category, slug, scorecard);
|
|
128
119
|
console.log(chalk.dim("Feedback saved to scorecard."));
|
|
129
120
|
}
|
|
130
121
|
}
|
|
122
|
+
async function run(args) {
|
|
123
|
+
const root = resolveWorkspaceRoot();
|
|
124
|
+
if (!isWorkspaceInitialized(root)) {
|
|
125
|
+
console.error(chalk.red("\nError: Workspace not initialized."));
|
|
126
|
+
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const parsed = parseArgs(args);
|
|
130
|
+
const provider = requireProvider(parsed.provider);
|
|
131
|
+
if (parsed.all) {
|
|
132
|
+
const questions = getAllQuestions();
|
|
133
|
+
if (questions.length === 0) {
|
|
134
|
+
console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.log(chalk.cyan(`
|
|
138
|
+
Running feedback for ${questions.length} question(s)...
|
|
139
|
+
`));
|
|
140
|
+
for (let i = 0; i < questions.length; i++) {
|
|
141
|
+
const q = questions[i];
|
|
142
|
+
console.log(chalk.bold(`
|
|
143
|
+
[${i + 1}/${questions.length}] ${q.slug}`));
|
|
144
|
+
const { confirm } = await prompts({
|
|
145
|
+
type: "confirm",
|
|
146
|
+
name: "confirm",
|
|
147
|
+
message: `Run feedback for "${q.slug}"?`,
|
|
148
|
+
initial: true
|
|
149
|
+
});
|
|
150
|
+
if (!confirm) {
|
|
151
|
+
console.log(chalk.dim("Skipped."));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
await runFeedbackForSlug(q.slug, provider);
|
|
155
|
+
}
|
|
156
|
+
console.log(chalk.green("\nCompleted feedback for all questions."));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let selectedSlug = parsed.slug;
|
|
160
|
+
if (!selectedSlug) {
|
|
161
|
+
selectedSlug = await promptForSlug();
|
|
162
|
+
if (!selectedSlug) return;
|
|
163
|
+
}
|
|
164
|
+
await runFeedbackForSlug(selectedSlug, provider);
|
|
165
|
+
}
|
|
131
166
|
export {
|
|
132
167
|
run
|
|
133
168
|
};
|
|
@@ -56,6 +56,9 @@ Question type: ${categoryConfig.type}`;
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
const slug = parsed.slug || slugify(parsed.title || topic);
|
|
59
|
+
if (parsed.solutionCode) {
|
|
60
|
+
console.log(chalk.dim("Note: Discarded LLM solutionCode; using signature-based stub."));
|
|
61
|
+
}
|
|
59
62
|
const questionDir = scaffoldQuestion({
|
|
60
63
|
title: parsed.title || topic,
|
|
61
64
|
slug,
|
|
@@ -63,8 +66,7 @@ Question type: ${categoryConfig.type}`;
|
|
|
63
66
|
difficulty,
|
|
64
67
|
description: parsed.description || "",
|
|
65
68
|
signature: parsed.signature,
|
|
66
|
-
testCode: parsed.testCode
|
|
67
|
-
solutionCode: parsed.solutionCode
|
|
69
|
+
testCode: parsed.testCode
|
|
68
70
|
});
|
|
69
71
|
console.log(chalk.green(`
|
|
70
72
|
Created: questions/${category}/${slug}/`));
|
|
@@ -116,7 +118,11 @@ async function brainstormMode(provider) {
|
|
|
116
118
|
type: "select",
|
|
117
119
|
name: "category",
|
|
118
120
|
message: "Which category?",
|
|
119
|
-
choices: CATEGORY_SLUGS.map((s) => ({
|
|
121
|
+
choices: CATEGORY_SLUGS.map((s) => ({
|
|
122
|
+
title: CATEGORIES[s].name,
|
|
123
|
+
description: CATEGORIES[s].hint,
|
|
124
|
+
value: s
|
|
125
|
+
}))
|
|
120
126
|
});
|
|
121
127
|
const { difficulty } = await prompts({
|
|
122
128
|
type: "select",
|
|
@@ -158,6 +164,9 @@ ${brainstormSummary}`
|
|
|
158
164
|
return;
|
|
159
165
|
}
|
|
160
166
|
const slug = parsed.slug || slugify(parsed.title || "brainstorm-question");
|
|
167
|
+
if (parsed.solutionCode) {
|
|
168
|
+
console.log(chalk.dim("Note: Discarded LLM solutionCode; using signature-based stub."));
|
|
169
|
+
}
|
|
161
170
|
const questionDir = scaffoldQuestion({
|
|
162
171
|
title: parsed.title,
|
|
163
172
|
slug,
|
|
@@ -165,8 +174,7 @@ ${brainstormSummary}`
|
|
|
165
174
|
difficulty,
|
|
166
175
|
description: parsed.description || "",
|
|
167
176
|
signature: parsed.signature,
|
|
168
|
-
testCode: parsed.testCode
|
|
169
|
-
solutionCode: parsed.solutionCode
|
|
177
|
+
testCode: parsed.testCode
|
|
170
178
|
});
|
|
171
179
|
console.log(chalk.green(`
|
|
172
180
|
Created: questions/${category}/${slug}/`));
|
|
@@ -187,22 +195,23 @@ async function run(args) {
|
|
|
187
195
|
await brainstormMode(provider);
|
|
188
196
|
return;
|
|
189
197
|
}
|
|
190
|
-
if (!parsed.topic) {
|
|
191
|
-
console.error(chalk.red("Missing --topic. Use --brainstorm for interactive mode."));
|
|
192
|
-
console.error(chalk.dim('Example: npm run ace generate -- --topic "debounce" --category js-ts --difficulty medium'));
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
198
|
let category = parsed.category;
|
|
196
199
|
let difficulty = parsed.difficulty;
|
|
200
|
+
let topic = parsed.topic;
|
|
197
201
|
if (!category) {
|
|
198
202
|
const result = await prompts({
|
|
199
203
|
type: "select",
|
|
200
204
|
name: "category",
|
|
201
205
|
message: "Which category?",
|
|
202
|
-
choices: CATEGORY_SLUGS.map((s) => ({
|
|
206
|
+
choices: CATEGORY_SLUGS.map((s) => ({
|
|
207
|
+
title: CATEGORIES[s].name,
|
|
208
|
+
description: CATEGORIES[s].hint,
|
|
209
|
+
value: s
|
|
210
|
+
}))
|
|
203
211
|
});
|
|
204
212
|
category = result.category;
|
|
205
213
|
}
|
|
214
|
+
if (!category) return;
|
|
206
215
|
if (!difficulty) {
|
|
207
216
|
const result = await prompts({
|
|
208
217
|
type: "select",
|
|
@@ -216,8 +225,17 @@ async function run(args) {
|
|
|
216
225
|
});
|
|
217
226
|
difficulty = result.difficulty;
|
|
218
227
|
}
|
|
219
|
-
if (!
|
|
220
|
-
|
|
228
|
+
if (!difficulty) return;
|
|
229
|
+
if (!topic) {
|
|
230
|
+
const result = await prompts({
|
|
231
|
+
type: "text",
|
|
232
|
+
name: "topic",
|
|
233
|
+
message: "What topic do you want to practice?"
|
|
234
|
+
});
|
|
235
|
+
topic = result.topic;
|
|
236
|
+
}
|
|
237
|
+
if (!topic) return;
|
|
238
|
+
await directMode(provider, topic, category, difficulty);
|
|
221
239
|
}
|
|
222
240
|
export {
|
|
223
241
|
run
|
package/dist/commands/reset.js
CHANGED
|
@@ -2,38 +2,16 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import prompts from "prompts";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { findQuestion, readScorecard, resetScorecard, startNewAttempt, writeScorecard } from "../lib/scorecard.js";
|
|
5
|
+
import { findQuestion, readScorecard, resetScorecard, startNewAttempt, writeScorecard, getAllQuestions, promptForSlug } from "../lib/scorecard.js";
|
|
6
6
|
import { CATEGORIES, isDesignCategory } from "../lib/categories.js";
|
|
7
7
|
import { getStubContent } from "../lib/scaffold.js";
|
|
8
8
|
import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
|
|
9
|
-
|
|
10
|
-
const root = resolveWorkspaceRoot();
|
|
11
|
-
if (!isWorkspaceInitialized(root)) {
|
|
12
|
-
console.error(chalk.red("\nError: Workspace not initialized."));
|
|
13
|
-
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
const slug = args.find((a) => !a.startsWith("--"));
|
|
17
|
-
if (!slug) {
|
|
18
|
-
console.error(chalk.red("Missing question slug."));
|
|
19
|
-
console.error(chalk.dim("Usage: npm run ace reset <slug>"));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
9
|
+
function resetQuestion(slug) {
|
|
22
10
|
const question = findQuestion(slug);
|
|
23
11
|
if (!question) {
|
|
24
12
|
console.error(chalk.red(`Question not found: ${slug}`));
|
|
25
13
|
return;
|
|
26
14
|
}
|
|
27
|
-
const { confirm } = await prompts({
|
|
28
|
-
type: "confirm",
|
|
29
|
-
name: "confirm",
|
|
30
|
-
message: `Reset "${slug}" to unanswered? This will clear your solution.`,
|
|
31
|
-
initial: false
|
|
32
|
-
});
|
|
33
|
-
if (!confirm) {
|
|
34
|
-
console.log(chalk.yellow("Cancelled."));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
15
|
const config = CATEGORIES[question.category];
|
|
38
16
|
const isDesign = isDesignCategory(question.category);
|
|
39
17
|
if (isDesign) {
|
|
@@ -44,7 +22,6 @@ async function run(args) {
|
|
|
44
22
|
}
|
|
45
23
|
} else {
|
|
46
24
|
for (const file of config.solutionFiles) {
|
|
47
|
-
if (file === "index.html") continue;
|
|
48
25
|
const filePath = path.join(question.dir, file);
|
|
49
26
|
const stubContent = getStubContent(question.category, file);
|
|
50
27
|
if (stubContent) {
|
|
@@ -59,8 +36,64 @@ async function run(args) {
|
|
|
59
36
|
scorecard.status = "untouched";
|
|
60
37
|
writeScorecard(question.category, slug, scorecard);
|
|
61
38
|
}
|
|
62
|
-
console.log(chalk.green(`
|
|
63
|
-
|
|
39
|
+
console.log(chalk.green(`Reset: questions/${question.category}/${slug}/`));
|
|
40
|
+
}
|
|
41
|
+
async function run(args) {
|
|
42
|
+
const root = resolveWorkspaceRoot();
|
|
43
|
+
if (!isWorkspaceInitialized(root)) {
|
|
44
|
+
console.error(chalk.red("\nError: Workspace not initialized."));
|
|
45
|
+
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const hasAll = args.includes("--all") || args.includes("all");
|
|
49
|
+
const slug = args.find((a) => !a.startsWith("--") && a !== "all");
|
|
50
|
+
if (hasAll) {
|
|
51
|
+
const questions = getAllQuestions();
|
|
52
|
+
if (questions.length === 0) {
|
|
53
|
+
console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { confirm: confirm2 } = await prompts({
|
|
57
|
+
type: "confirm",
|
|
58
|
+
name: "confirm",
|
|
59
|
+
message: `Reset ALL ${questions.length} question(s) to unanswered? This will clear all solutions.`,
|
|
60
|
+
initial: false
|
|
61
|
+
});
|
|
62
|
+
if (!confirm2) {
|
|
63
|
+
console.log(chalk.yellow("Cancelled."));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.cyan(`
|
|
67
|
+
Resetting ${questions.length} question(s)...
|
|
68
|
+
`));
|
|
69
|
+
for (const q of questions) {
|
|
70
|
+
resetQuestion(q.slug);
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.green("\nCompleted reset for all questions."));
|
|
73
|
+
console.log(chalk.dim("Solution files restored to stubs. Scorecards updated."));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let selectedSlug = slug;
|
|
77
|
+
if (!selectedSlug) {
|
|
78
|
+
selectedSlug = await promptForSlug();
|
|
79
|
+
if (!selectedSlug) return;
|
|
80
|
+
}
|
|
81
|
+
const question = findQuestion(selectedSlug);
|
|
82
|
+
if (!question) {
|
|
83
|
+
console.error(chalk.red(`Question not found: ${selectedSlug}`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const { confirm } = await prompts({
|
|
87
|
+
type: "confirm",
|
|
88
|
+
name: "confirm",
|
|
89
|
+
message: `Reset "${selectedSlug}" to unanswered? This will clear your solution.`,
|
|
90
|
+
initial: false
|
|
91
|
+
});
|
|
92
|
+
if (!confirm) {
|
|
93
|
+
console.log(chalk.yellow("Cancelled."));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
resetQuestion(selectedSlug);
|
|
64
97
|
console.log(chalk.dim("Solution files restored to stubs. Scorecard updated."));
|
|
65
98
|
}
|
|
66
99
|
export {
|
package/dist/commands/score.js
CHANGED
|
@@ -1,35 +1,13 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { findQuestion, readScorecard } from "../lib/scorecard.js";
|
|
2
|
+
import { findQuestion, readScorecard, getAllQuestions, promptForSlug } from "../lib/scorecard.js";
|
|
3
3
|
import { CATEGORIES } from "../lib/categories.js";
|
|
4
4
|
import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
if (!isWorkspaceInitialized(root)) {
|
|
8
|
-
console.error(chalk.red("\nError: Workspace not initialized."));
|
|
9
|
-
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
12
|
-
const slug = args.find((a) => !a.startsWith("--"));
|
|
13
|
-
if (!slug) {
|
|
14
|
-
console.error(chalk.red("Missing question slug."));
|
|
15
|
-
console.error(chalk.dim("Usage: npm run ace score <slug>"));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
const question = findQuestion(slug);
|
|
19
|
-
if (!question) {
|
|
20
|
-
console.error(chalk.red(`Question not found: ${slug}`));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const scorecard = readScorecard(question.category, slug);
|
|
24
|
-
if (!scorecard) {
|
|
25
|
-
console.error(chalk.red("No scorecard found for this question."));
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const config = CATEGORIES[question.category];
|
|
5
|
+
function displayScorecard(slug, category, scorecard) {
|
|
6
|
+
const config = CATEGORIES[category];
|
|
29
7
|
console.log(`
|
|
30
8
|
${chalk.bold.cyan("Scorecard:")} ${chalk.bold(scorecard.title || slug)}`);
|
|
31
9
|
console.log(chalk.dim("\u2500".repeat(60)));
|
|
32
|
-
console.log(` ${chalk.bold("Category:")} ${config?.name ||
|
|
10
|
+
console.log(` ${chalk.bold("Category:")} ${config?.name || category}`);
|
|
33
11
|
console.log(` ${chalk.bold("Difficulty:")} ${scorecard.difficulty}`);
|
|
34
12
|
console.log(` ${chalk.bold("Suggested:")} ~${scorecard.suggestedTime} minutes`);
|
|
35
13
|
const statusColors = {
|
|
@@ -65,6 +43,43 @@ ${chalk.bold(" Last LLM Feedback:")}`);
|
|
|
65
43
|
}
|
|
66
44
|
console.log(chalk.dim("\n" + "\u2500".repeat(60)));
|
|
67
45
|
}
|
|
46
|
+
async function run(args) {
|
|
47
|
+
const root = resolveWorkspaceRoot();
|
|
48
|
+
if (!isWorkspaceInitialized(root)) {
|
|
49
|
+
console.error(chalk.red("\nError: Workspace not initialized."));
|
|
50
|
+
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const hasAll = args.includes("--all") || args.includes("all");
|
|
54
|
+
const slug = args.find((a) => !a.startsWith("--") && a !== "all");
|
|
55
|
+
if (hasAll) {
|
|
56
|
+
const questions = getAllQuestions();
|
|
57
|
+
if (questions.length === 0) {
|
|
58
|
+
console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const q of questions) {
|
|
62
|
+
displayScorecard(q.slug, q.category, q.scorecard);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let selectedSlug = slug;
|
|
67
|
+
if (!selectedSlug) {
|
|
68
|
+
selectedSlug = await promptForSlug();
|
|
69
|
+
if (!selectedSlug) return;
|
|
70
|
+
}
|
|
71
|
+
const question = findQuestion(selectedSlug);
|
|
72
|
+
if (!question) {
|
|
73
|
+
console.error(chalk.red(`Question not found: ${selectedSlug}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const scorecard = readScorecard(question.category, selectedSlug);
|
|
77
|
+
if (!scorecard) {
|
|
78
|
+
console.error(chalk.red("No scorecard found for this question."));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
displayScorecard(selectedSlug, question.category, scorecard);
|
|
82
|
+
}
|
|
68
83
|
export {
|
|
69
84
|
run
|
|
70
85
|
};
|
package/dist/commands/test.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { findQuestion, readScorecard, updateTestResults, writeScorecard } from "../lib/scorecard.js";
|
|
3
|
+
import { findQuestion, readScorecard, updateTestResults, writeScorecard, promptForSlug } from "../lib/scorecard.js";
|
|
4
4
|
import { isDesignCategory } from "../lib/categories.js";
|
|
5
5
|
import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
|
|
6
6
|
function parseArgs(args) {
|
|
7
7
|
let slug;
|
|
8
8
|
let watch = false;
|
|
9
|
+
let all = false;
|
|
9
10
|
for (const arg of args) {
|
|
10
11
|
if (arg === "--watch") {
|
|
11
12
|
watch = true;
|
|
13
|
+
} else if (arg === "--all" || arg === "all") {
|
|
14
|
+
all = true;
|
|
12
15
|
} else if (!arg.startsWith("--")) {
|
|
13
16
|
slug = arg;
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
|
-
return { slug, watch };
|
|
19
|
+
return { slug, watch, all };
|
|
17
20
|
}
|
|
18
21
|
function parseTestOutput(output) {
|
|
19
22
|
const passMatch = output.match(/(\d+)\s+passed/);
|
|
@@ -31,8 +34,8 @@ async function run(args) {
|
|
|
31
34
|
console.error(chalk.dim("Run `ace init` in this folder first.\n"));
|
|
32
35
|
process.exit(1);
|
|
33
36
|
}
|
|
34
|
-
const { slug, watch } = parseArgs(args);
|
|
35
|
-
if (
|
|
37
|
+
const { slug, watch, all } = parseArgs(args);
|
|
38
|
+
if (all) {
|
|
36
39
|
console.log(chalk.cyan("\nRunning all tests...\n"));
|
|
37
40
|
try {
|
|
38
41
|
const cmd = watch ? "npx vitest" : "npx vitest run";
|
|
@@ -41,19 +44,24 @@ async function run(args) {
|
|
|
41
44
|
}
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
44
|
-
|
|
47
|
+
let selectedSlug = slug;
|
|
48
|
+
if (!selectedSlug) {
|
|
49
|
+
selectedSlug = await promptForSlug();
|
|
50
|
+
if (!selectedSlug) return;
|
|
51
|
+
}
|
|
52
|
+
const question = findQuestion(selectedSlug);
|
|
45
53
|
if (!question) {
|
|
46
|
-
console.error(chalk.red(`Question not found: ${
|
|
54
|
+
console.error(chalk.red(`Question not found: ${selectedSlug}`));
|
|
47
55
|
console.error(chalk.dim("Run `npm run ace list` to see all questions."));
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
50
58
|
if (isDesignCategory(question.category)) {
|
|
51
|
-
console.log(chalk.yellow(`"${
|
|
52
|
-
console.log(chalk.dim("Use `npm run ace feedback " +
|
|
59
|
+
console.log(chalk.yellow(`"${selectedSlug}" is a system design question \u2014 no tests to run.`));
|
|
60
|
+
console.log(chalk.dim("Use `npm run ace feedback " + selectedSlug + "` for LLM review."));
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
55
63
|
console.log(chalk.cyan(`
|
|
56
|
-
Running tests for: ${
|
|
64
|
+
Running tests for: ${selectedSlug}
|
|
57
65
|
`));
|
|
58
66
|
let output = "";
|
|
59
67
|
try {
|
|
@@ -67,11 +75,11 @@ Running tests for: ${slug}
|
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
if (!watch) {
|
|
70
|
-
const scorecard = readScorecard(question.category,
|
|
78
|
+
const scorecard = readScorecard(question.category, selectedSlug);
|
|
71
79
|
if (scorecard) {
|
|
72
80
|
const { total, passed } = parseTestOutput(output);
|
|
73
81
|
updateTestResults(scorecard, total, passed);
|
|
74
|
-
writeScorecard(question.category,
|
|
82
|
+
writeScorecard(question.category, selectedSlug, scorecard);
|
|
75
83
|
if (total > 0) {
|
|
76
84
|
const color = passed === total ? chalk.green : chalk.red;
|
|
77
85
|
console.log(color(`
|
package/dist/index.js
CHANGED
|
@@ -35,15 +35,39 @@ ${chalk.bold("Examples:")}
|
|
|
35
35
|
|
|
36
36
|
ace setup
|
|
37
37
|
ace init
|
|
38
|
+
|
|
39
|
+
${chalk.dim("# Generate interactively (prompts for category, difficulty, topic)")}
|
|
40
|
+
ace generate
|
|
41
|
+
|
|
42
|
+
${chalk.dim("# Or pass flags to skip prompts")}
|
|
38
43
|
ace generate --topic "debounce" --category js-ts --difficulty medium
|
|
44
|
+
|
|
45
|
+
${chalk.dim("# Brainstorm mode for design help")}
|
|
39
46
|
ace generate --brainstorm
|
|
47
|
+
|
|
48
|
+
${chalk.dim("# List all questions")}
|
|
40
49
|
ace list
|
|
41
50
|
ace list --category js-ts --status solved
|
|
51
|
+
|
|
52
|
+
${chalk.dim("# Test interactively (shows question picker)")}
|
|
53
|
+
ace test
|
|
54
|
+
|
|
55
|
+
${chalk.dim("# Or test a specific question or all questions")}
|
|
42
56
|
ace test debounce
|
|
43
|
-
ace test --
|
|
57
|
+
ace test --all
|
|
58
|
+
|
|
59
|
+
${chalk.dim("# Feedback, score, and reset also support interactive mode and --all")}
|
|
60
|
+
ace feedback
|
|
44
61
|
ace feedback debounce
|
|
45
|
-
ace
|
|
62
|
+
ace feedback --all
|
|
63
|
+
|
|
64
|
+
ace score
|
|
46
65
|
ace score debounce
|
|
66
|
+
ace score --all
|
|
67
|
+
|
|
68
|
+
ace reset
|
|
69
|
+
ace reset debounce
|
|
70
|
+
ace reset --all
|
|
47
71
|
`);
|
|
48
72
|
}
|
|
49
73
|
async function main() {
|
package/dist/lib/categories.js
CHANGED
|
@@ -3,6 +3,7 @@ const CATEGORIES = {
|
|
|
3
3
|
slug: "js-ts",
|
|
4
4
|
name: "JS/TS Puzzles",
|
|
5
5
|
shortName: "JS/TS",
|
|
6
|
+
hint: "Closures, async patterns, type utilities",
|
|
6
7
|
type: "coding",
|
|
7
8
|
suggestedTimes: { easy: 15, medium: 30, hard: 45 },
|
|
8
9
|
solutionFiles: ["solution.ts"],
|
|
@@ -11,18 +12,20 @@ const CATEGORIES = {
|
|
|
11
12
|
},
|
|
12
13
|
"web-components": {
|
|
13
14
|
slug: "web-components",
|
|
14
|
-
name: "
|
|
15
|
-
shortName: "
|
|
15
|
+
name: "React Components",
|
|
16
|
+
shortName: "React",
|
|
17
|
+
hint: "Props, events, composition, reusable UI",
|
|
16
18
|
type: "coding",
|
|
17
19
|
suggestedTimes: { easy: 20, medium: 35, hard: 50 },
|
|
18
|
-
solutionFiles: ["
|
|
19
|
-
testFiles: ["
|
|
20
|
+
solutionFiles: ["Component.tsx"],
|
|
21
|
+
testFiles: ["Component.test.tsx"],
|
|
20
22
|
templateDir: "web-components"
|
|
21
23
|
},
|
|
22
24
|
"react-apps": {
|
|
23
25
|
slug: "react-apps",
|
|
24
26
|
name: "React Web Apps",
|
|
25
27
|
shortName: "React",
|
|
28
|
+
hint: "Hooks, state, routing, full features",
|
|
26
29
|
type: "coding",
|
|
27
30
|
suggestedTimes: { easy: 25, medium: 45, hard: 60 },
|
|
28
31
|
solutionFiles: ["App.tsx"],
|
|
@@ -33,6 +36,7 @@ const CATEGORIES = {
|
|
|
33
36
|
slug: "leetcode-ds",
|
|
34
37
|
name: "LeetCode Data Structures",
|
|
35
38
|
shortName: "LC-DS",
|
|
39
|
+
hint: "Trees, graphs, heaps, hash maps",
|
|
36
40
|
type: "coding",
|
|
37
41
|
suggestedTimes: { easy: 15, medium: 30, hard: 45 },
|
|
38
42
|
solutionFiles: ["solution.ts"],
|
|
@@ -43,6 +47,7 @@ const CATEGORIES = {
|
|
|
43
47
|
slug: "leetcode-algo",
|
|
44
48
|
name: "LeetCode Algorithms",
|
|
45
49
|
shortName: "LC-Algo",
|
|
50
|
+
hint: "DP, greedy, two pointers, sorting",
|
|
46
51
|
type: "coding",
|
|
47
52
|
suggestedTimes: { easy: 15, medium: 30, hard: 45 },
|
|
48
53
|
solutionFiles: ["solution.ts"],
|
|
@@ -53,6 +58,7 @@ const CATEGORIES = {
|
|
|
53
58
|
slug: "design-fe",
|
|
54
59
|
name: "System Design \u2014 Frontend",
|
|
55
60
|
shortName: "Design-FE",
|
|
61
|
+
hint: "Component architecture, state, rendering",
|
|
56
62
|
type: "design",
|
|
57
63
|
suggestedTimes: { easy: 25, medium: 40, hard: 55 },
|
|
58
64
|
solutionFiles: ["notes.md"],
|
|
@@ -63,6 +69,7 @@ const CATEGORIES = {
|
|
|
63
69
|
slug: "design-be",
|
|
64
70
|
name: "System Design \u2014 Backend",
|
|
65
71
|
shortName: "Design-BE",
|
|
72
|
+
hint: "APIs, databases, caching, queues",
|
|
66
73
|
type: "design",
|
|
67
74
|
suggestedTimes: { easy: 25, medium: 40, hard: 55 },
|
|
68
75
|
solutionFiles: ["notes.md"],
|
|
@@ -73,6 +80,7 @@ const CATEGORIES = {
|
|
|
73
80
|
slug: "design-full",
|
|
74
81
|
name: "System Design \u2014 Full Stack",
|
|
75
82
|
shortName: "Design-Full",
|
|
83
|
+
hint: "End-to-end systems, trade-offs",
|
|
76
84
|
type: "design",
|
|
77
85
|
suggestedTimes: { easy: 30, medium: 45, hard: 60 },
|
|
78
86
|
solutionFiles: ["notes.md"],
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -37,8 +37,7 @@ function scaffoldQuestion(opts) {
|
|
|
37
37
|
description: opts.description,
|
|
38
38
|
signature: opts.signature || "",
|
|
39
39
|
testCode: opts.testCode || "",
|
|
40
|
-
solutionCode: opts.solutionCode || ""
|
|
41
|
-
htmlCode: opts.htmlCode || ""
|
|
40
|
+
solutionCode: opts.solutionCode || ""
|
|
42
41
|
};
|
|
43
42
|
const readmeTemplate = loadTemplate(path.join(TEMPLATES_DIR, "readme.md.hbs"));
|
|
44
43
|
fs.writeFileSync(path.join(questionDir, "README.md"), readmeTemplate(templateData));
|
|
@@ -67,15 +66,6 @@ function scaffoldQuestion(opts) {
|
|
|
67
66
|
fs.writeFileSync(path.join(questionDir, testFile), opts.testCode);
|
|
68
67
|
}
|
|
69
68
|
}
|
|
70
|
-
if (config.solutionFiles.includes("index.html") && opts.htmlCode) {
|
|
71
|
-
fs.writeFileSync(path.join(questionDir, "index.html"), opts.htmlCode);
|
|
72
|
-
} else {
|
|
73
|
-
const htmlTemplatePath = path.join(templateDir, "index.html.hbs");
|
|
74
|
-
if (fs.existsSync(htmlTemplatePath) && config.solutionFiles.includes("index.html")) {
|
|
75
|
-
const tmpl = loadTemplate(htmlTemplatePath);
|
|
76
|
-
fs.writeFileSync(path.join(questionDir, "index.html"), tmpl(templateData));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
69
|
}
|
|
80
70
|
const scorecard = createScorecard(opts.title, opts.category, opts.difficulty);
|
|
81
71
|
writeScorecard(opts.category, opts.slug, scorecard);
|
package/dist/lib/scorecard.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { getSuggestedTime, CATEGORIES } from "./categories.js";
|
|
4
6
|
import { resolveWorkspaceRoot, getQuestionsDir } from "./paths.js";
|
|
5
7
|
function getScorecardPath(category, slug) {
|
|
6
8
|
const root = resolveWorkspaceRoot();
|
|
@@ -102,12 +104,41 @@ function findQuestion(slug) {
|
|
|
102
104
|
}
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
107
|
+
async function promptForSlug() {
|
|
108
|
+
const questions = getAllQuestions();
|
|
109
|
+
if (questions.length === 0) {
|
|
110
|
+
console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const choices = questions.map((q) => {
|
|
114
|
+
const categoryName = CATEGORIES[q.category]?.shortName || q.category;
|
|
115
|
+
const statusColors = {
|
|
116
|
+
untouched: chalk.gray,
|
|
117
|
+
"in-progress": chalk.yellow,
|
|
118
|
+
attempted: chalk.red,
|
|
119
|
+
solved: chalk.green
|
|
120
|
+
};
|
|
121
|
+
const statusColor = statusColors[q.scorecard.status] || chalk.white;
|
|
122
|
+
return {
|
|
123
|
+
title: `${chalk.cyan(categoryName)} / ${q.slug} ${statusColor(`(${q.scorecard.status})`)}`,
|
|
124
|
+
value: q.slug
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
const result = await prompts({
|
|
128
|
+
type: "select",
|
|
129
|
+
name: "slug",
|
|
130
|
+
message: "Select a question:",
|
|
131
|
+
choices
|
|
132
|
+
});
|
|
133
|
+
return result.slug || null;
|
|
134
|
+
}
|
|
105
135
|
export {
|
|
106
136
|
createScorecard,
|
|
107
137
|
findQuestion,
|
|
108
138
|
getAllQuestions,
|
|
109
139
|
getCurrentAttempt,
|
|
110
140
|
getScorecardPath,
|
|
141
|
+
promptForSlug,
|
|
111
142
|
readScorecard,
|
|
112
143
|
resetScorecard,
|
|
113
144
|
startNewAttempt,
|
|
@@ -5,8 +5,8 @@ You are a collaborative interview question designer helping the user explore and
|
|
|
5
5
|
## Supported Categories
|
|
6
6
|
|
|
7
7
|
- **js-ts**: JavaScript/TypeScript puzzles (closures, async, types)
|
|
8
|
-
- **web-components**:
|
|
9
|
-
- **react-apps**: React
|
|
8
|
+
- **web-components**: React components (props, events, composition, reusable UI)
|
|
9
|
+
- **react-apps**: React applications (hooks, state, routing, full features)
|
|
10
10
|
- **leetcode-ds**: Data structure problems (trees, graphs, heaps)
|
|
11
11
|
- **leetcode-algo**: Algorithm problems (DP, greedy, two pointers)
|
|
12
12
|
- **design-fe**: Frontend system design (component architecture, state, rendering)
|
|
@@ -22,26 +22,35 @@ Return a JSON object with:
|
|
|
22
22
|
"title": "Human-readable question title",
|
|
23
23
|
"slug": "kebab-case-slug",
|
|
24
24
|
"description": "Markdown description with problem statement, examples, constraints",
|
|
25
|
-
"signature": "
|
|
26
|
-
"testCode": "Full Vitest test file content as a string"
|
|
27
|
-
"solutionCode": "Stub implementation with the signature only (empty body or minimal placeholder)"
|
|
25
|
+
"signature": "The exported function/class signature line ONLY (see rules below)",
|
|
26
|
+
"testCode": "Full Vitest test file content as a string"
|
|
28
27
|
}
|
|
29
28
|
```
|
|
30
29
|
|
|
30
|
+
**CRITICAL — Do NOT include `solutionCode` in your response. Do NOT implement the solution.**
|
|
31
|
+
|
|
32
|
+
The `signature` field must contain ONLY the bare function or class declaration line that the candidate will implement. It must NOT contain any logic, algorithm, data structure manipulation, loops, conditionals, or meaningful code.
|
|
33
|
+
|
|
34
|
+
Good `signature` examples:
|
|
35
|
+
- `export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): T`
|
|
36
|
+
- `export function deepClone<T>(obj: T): T`
|
|
37
|
+
- `export class LRUCache<K, V>`
|
|
38
|
+
|
|
39
|
+
Bad `signature` examples (NEVER do this):
|
|
40
|
+
- A full function body with implementation logic
|
|
41
|
+
- Code that includes `if`, `for`, `while`, `map`, `reduce`, `setTimeout`, or any working logic
|
|
42
|
+
- A complete class with method implementations
|
|
43
|
+
|
|
31
44
|
**Test requirements:**
|
|
32
45
|
- Generate 6–10 test cases covering: happy path, edge cases, and performance-sensitive scenarios
|
|
33
|
-
- For **React** questions: use `@testing-library/react` with `render` and `screen`
|
|
46
|
+
- For **React** questions (`react-apps`, `web-components`): use `@testing-library/react` with `render` and `screen`
|
|
34
47
|
- Imports must reference the solution file correctly:
|
|
35
48
|
- `js-ts`, `leetcode-ds`, `leetcode-algo`: `import { solution } from './solution'`
|
|
36
49
|
- `react-apps`: `import App from './App'`
|
|
37
|
-
- `web-components`: import
|
|
50
|
+
- `web-components`: `import { ComponentName } from './Component'` (named export)
|
|
38
51
|
- Use `describe`, `it`, `expect` from Vitest
|
|
39
52
|
- Tests must be self-contained and runnable
|
|
40
53
|
|
|
41
|
-
**Solution stub:**
|
|
42
|
-
- Include the exact function/component signature the candidate must implement
|
|
43
|
-
- Leave the body empty or with a minimal placeholder (e.g., `throw new Error('Not implemented')` or `return null`)
|
|
44
|
-
|
|
45
54
|
### For Design Categories (design-fe, design-be, design-full)
|
|
46
55
|
|
|
47
56
|
Return a JSON object with:
|
|
@@ -62,4 +71,4 @@ Return a JSON object with:
|
|
|
62
71
|
- Questions should be achievable within the suggested time for the category and difficulty
|
|
63
72
|
- Avoid ambiguous wording; constraints and expected behavior should be explicit
|
|
64
73
|
- For LeetCode-style questions: include time/space complexity expectations in the description
|
|
65
|
-
- For React
|
|
74
|
+
- For React questions (`react-apps`, `web-components`): focus on realistic UI behavior, not toy examples
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{#if testCode}}
|
|
2
|
+
{{{testCode}}}
|
|
3
|
+
{{else}}
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { render, screen } from '@testing-library/react';
|
|
6
|
+
import { Component } from './Component';
|
|
7
|
+
|
|
8
|
+
describe('{{title}}', () => {
|
|
9
|
+
it('renders without crashing', () => {
|
|
10
|
+
render(<Component />);
|
|
11
|
+
expect(screen.getByText('{{title}}')).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it.todo('add more test cases');
|
|
15
|
+
});
|
|
16
|
+
{{/if}}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{{#if solutionCode}}
|
|
2
|
+
{{{solutionCode}}}
|
|
3
|
+
{{else}}
|
|
4
|
+
{{#if signature}}
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
{{{signature}}} {
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<h1>{{title}}</h1>
|
|
11
|
+
{/* TODO: implement */}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
{{else}}
|
|
16
|
+
import React from 'react';
|
|
17
|
+
|
|
18
|
+
// TODO: implement your React component here
|
|
19
|
+
|
|
20
|
+
export function Component() {
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<h1>{{title}}</h1>
|
|
24
|
+
{/* TODO: implement */}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{/if}}
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{{#if solutionCode}}
|
|
2
|
-
{{{solutionCode}}}
|
|
3
|
-
{{else}}
|
|
4
|
-
{{#if signature}}
|
|
5
|
-
{{{signature}}}
|
|
6
|
-
{{else}}
|
|
7
|
-
// TODO: implement your web component here
|
|
8
|
-
|
|
9
|
-
export class MyComponent extends HTMLElement {
|
|
10
|
-
constructor() {
|
|
11
|
-
super();
|
|
12
|
-
this.attachShadow({ mode: 'open' });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
connectedCallback() {
|
|
16
|
-
// TODO: implement
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// customElements.define('my-component', MyComponent);
|
|
21
|
-
{{/if}}
|
|
22
|
-
{{/if}}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>{{title}}</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<!-- TODO: use your web component here -->
|
|
10
|
-
<script type="module" src="./component.ts"></script>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Build a Star Rating Web Component
|
|
2
|
-
|
|
3
|
-
**Category:** Web Components
|
|
4
|
-
**Difficulty:** Medium
|
|
5
|
-
**Suggested Time:** ~35 minutes
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Problem
|
|
10
|
-
|
|
11
|
-
Build a `<star-rating>` custom element that displays 5 stars, allows users to click to rate, has a `value` attribute/property, and dispatches a `change` event when the rating changes.
|
|
12
|
-
|
|
13
|
-
## Requirements
|
|
14
|
-
|
|
15
|
-
- **Display** — Render 5 star elements (you may use Unicode stars ★/☆, SVG, or styled spans).
|
|
16
|
-
- **Click to rate** — Clicking a star sets the rating to that star's index (1–5).
|
|
17
|
-
- **`value` attribute** — The component accepts a `value` attribute (e.g. `<star-rating value="3">`) to show the initial or current rating.
|
|
18
|
-
- **`value` property** — The component exposes a `value` getter/setter that reflects and updates the rating.
|
|
19
|
-
- **`change` event** — When the user clicks a star, dispatch a `change` event with the new value (e.g. `detail: { value: 3 }`).
|
|
20
|
-
|
|
21
|
-
## Example Usage
|
|
22
|
-
|
|
23
|
-
```html
|
|
24
|
-
<star-rating value="3"></star-rating>
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
```js
|
|
28
|
-
const el = document.querySelector('star-rating');
|
|
29
|
-
el.value = 4;
|
|
30
|
-
el.addEventListener('change', (e) => console.log('New rating:', e.detail.value));
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Constraints
|
|
34
|
-
|
|
35
|
-
- Use the Custom Elements API (extend `HTMLElement`).
|
|
36
|
-
- Use Shadow DOM for encapsulation.
|
|
37
|
-
- Observe the `value` attribute and sync it with the internal state.
|
|
38
|
-
- Clamp `value` to 0–5 (0 = no stars selected).
|
|
39
|
-
|
|
40
|
-
## Hints
|
|
41
|
-
|
|
42
|
-
- Use `attachShadow({ mode: 'open' })` in the constructor.
|
|
43
|
-
- Use `static get observedAttributes()` to return `['value']`.
|
|
44
|
-
- In `attributeChangedCallback`, parse the attribute and update the display.
|
|
45
|
-
- Use `CustomEvent` with `detail: { value }` for the change event.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import './component';
|
|
3
|
-
|
|
4
|
-
describe('star-rating', () => {
|
|
5
|
-
let el: HTMLElement & { value: number };
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
el = document.createElement('star-rating') as HTMLElement & { value: number };
|
|
9
|
-
document.body.appendChild(el);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
el.remove();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('renders 5 star elements', () => {
|
|
17
|
-
const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
|
|
18
|
-
expect(stars.length).toBe(5);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('default value is 0', () => {
|
|
22
|
-
expect(el.value).toBe(0);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('setting value attribute updates display', () => {
|
|
26
|
-
el.setAttribute('value', '3');
|
|
27
|
-
expect(el.value).toBe(3);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('clicking a star updates value', () => {
|
|
31
|
-
const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
|
|
32
|
-
(stars[2] as HTMLElement).click();
|
|
33
|
-
expect(el.value).toBe(3);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('dispatches change event on click', () => {
|
|
37
|
-
let receivedValue: number | undefined;
|
|
38
|
-
const handler = (e: Event) => {
|
|
39
|
-
receivedValue = (e as CustomEvent).detail?.value;
|
|
40
|
-
};
|
|
41
|
-
el.addEventListener('change', handler);
|
|
42
|
-
|
|
43
|
-
const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
|
|
44
|
-
(stars[3] as HTMLElement).click();
|
|
45
|
-
|
|
46
|
-
expect(receivedValue).toBe(4);
|
|
47
|
-
el.removeEventListener('change', handler);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('value property reflects attribute', () => {
|
|
51
|
-
el.setAttribute('value', '2');
|
|
52
|
-
expect(el.value).toBe(2);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('setting value property updates attribute', () => {
|
|
56
|
-
el.value = 5;
|
|
57
|
-
expect(el.getAttribute('value')).toBe('5');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('clamps value to 0-5', () => {
|
|
61
|
-
el.setAttribute('value', '10');
|
|
62
|
-
expect(el.value).toBeLessThanOrEqual(5);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export class StarRating extends HTMLElement {
|
|
2
|
-
constructor() {
|
|
3
|
-
super();
|
|
4
|
-
this.attachShadow({ mode: 'open' });
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
connectedCallback() {
|
|
8
|
-
// TODO: implement - render 5 stars, handle click, support value attribute
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
static get observedAttributes() {
|
|
12
|
-
return ['value'];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null) {
|
|
16
|
-
// TODO: implement
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
get value(): number {
|
|
20
|
-
return 0; // TODO: implement
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
set value(_val: number) {
|
|
24
|
-
// TODO: implement
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
customElements.define('star-rating', StarRating);
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Star Rating Component</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<h1>Star Rating</h1>
|
|
10
|
-
<star-rating value="3"></star-rating>
|
|
11
|
-
|
|
12
|
-
<script type="module" src="./component.ts"></script>
|
|
13
|
-
</body>
|
|
14
|
-
</html>
|