@rhseung/ps-cli 1.3.3 → 1.5.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/dist/chunk-7MQMPJ3X.js +88 -0
- package/dist/{chunk-PNIGP6LX.js → chunk-7SVCS23X.js} +392 -96
- package/dist/chunk-A6STXEAE.js +54 -0
- package/dist/chunk-AG6KWWHS.js +342 -0
- package/dist/chunk-GCOFFYJ3.js +47 -0
- package/dist/{chunk-2E4VSP6O.js → chunk-HDNNR5OY.js} +200 -140
- package/dist/{chunk-EIFFWFLS.js → chunk-OJZLQ6FK.js} +1 -1
- package/dist/chunk-QGMWUOJ3.js +23 -0
- package/dist/commands/config.js +298 -262
- package/dist/commands/fetch.js +128 -359
- package/dist/commands/init.js +138 -101
- package/dist/commands/open.js +56 -100
- package/dist/commands/run.js +99 -98
- package/dist/commands/search.js +246 -0
- package/dist/commands/stats.js +97 -77
- package/dist/commands/submit.js +112 -126
- package/dist/commands/test.js +203 -203
- package/dist/index.js +7 -6
- package/package.json +16 -6
- package/dist/chunk-6ENX5K3C.js +0 -84
- package/dist/chunk-FYS2JH42.js +0 -31
- package/dist/chunk-TQXMB7XV.js +0 -52
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/services/solved-api.ts
|
|
4
|
+
var BASE_URL = "https://solved.ac/api/v3";
|
|
5
|
+
var USER_AGENT = "ps-cli/1.0.0";
|
|
6
|
+
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
7
|
+
let lastError = null;
|
|
8
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(url, {
|
|
11
|
+
...options,
|
|
12
|
+
headers: {
|
|
13
|
+
"User-Agent": USER_AGENT,
|
|
14
|
+
...options.headers
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (response.status === 429) {
|
|
18
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
19
|
+
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
return response;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
if (attempt < maxRetries - 1) {
|
|
30
|
+
await new Promise(
|
|
31
|
+
(resolve) => setTimeout(resolve, (attempt + 1) * 1e3)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw lastError || new Error("Request failed after retries");
|
|
37
|
+
}
|
|
38
|
+
async function getProblem(problemId) {
|
|
39
|
+
const url = `${BASE_URL}/problem/show?problemId=${problemId}`;
|
|
40
|
+
const response = await fetchWithRetry(url);
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
async function getUserStats(handle) {
|
|
45
|
+
const url = `${BASE_URL}/user/show?handle=${handle}`;
|
|
46
|
+
const response = await fetchWithRetry(url);
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
getProblem,
|
|
53
|
+
getUserStats
|
|
54
|
+
};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/services/scraper.ts
|
|
4
|
+
import * as cheerio from "cheerio";
|
|
5
|
+
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
6
|
+
var SOLVED_AC_BASE_URL = "https://solved.ac";
|
|
7
|
+
function htmlToMarkdown($, element) {
|
|
8
|
+
if (element.length === 0) return "";
|
|
9
|
+
let result = "";
|
|
10
|
+
const contents = element.contents();
|
|
11
|
+
if (contents.length === 0) {
|
|
12
|
+
return element.text().trim();
|
|
13
|
+
}
|
|
14
|
+
contents.each((_, node) => {
|
|
15
|
+
if (node.type === "text") {
|
|
16
|
+
const text = node.data || "";
|
|
17
|
+
if (text.trim()) {
|
|
18
|
+
result += text;
|
|
19
|
+
}
|
|
20
|
+
} else if (node.type === "tag") {
|
|
21
|
+
const tagName = node.name.toLowerCase();
|
|
22
|
+
const $node = $(node);
|
|
23
|
+
switch (tagName) {
|
|
24
|
+
case "sup":
|
|
25
|
+
result += `^${htmlToMarkdown($, $node)}`;
|
|
26
|
+
break;
|
|
27
|
+
case "sub":
|
|
28
|
+
result += `<sub>${htmlToMarkdown($, $node)}</sub>`;
|
|
29
|
+
break;
|
|
30
|
+
case "strong":
|
|
31
|
+
case "b":
|
|
32
|
+
result += `**${htmlToMarkdown($, $node)}**`;
|
|
33
|
+
break;
|
|
34
|
+
case "em":
|
|
35
|
+
case "i":
|
|
36
|
+
result += `*${htmlToMarkdown($, $node)}*`;
|
|
37
|
+
break;
|
|
38
|
+
case "br":
|
|
39
|
+
result += "\n";
|
|
40
|
+
break;
|
|
41
|
+
case "p": {
|
|
42
|
+
const pContent = htmlToMarkdown($, $node);
|
|
43
|
+
if (pContent) {
|
|
44
|
+
result += pContent + "\n\n";
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "div": {
|
|
49
|
+
const divContent = htmlToMarkdown($, $node);
|
|
50
|
+
if (divContent) {
|
|
51
|
+
result += divContent + "\n";
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "span":
|
|
56
|
+
result += htmlToMarkdown($, $node);
|
|
57
|
+
break;
|
|
58
|
+
case "code":
|
|
59
|
+
result += `\`${htmlToMarkdown($, $node)}\``;
|
|
60
|
+
break;
|
|
61
|
+
case "pre": {
|
|
62
|
+
const preContent = htmlToMarkdown($, $node);
|
|
63
|
+
if (preContent) {
|
|
64
|
+
result += `
|
|
65
|
+
\`\`\`
|
|
66
|
+
${preContent}
|
|
67
|
+
\`\`\`
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "ul":
|
|
73
|
+
case "ol":
|
|
74
|
+
$node.find("li").each((i, li) => {
|
|
75
|
+
const liContent = htmlToMarkdown($, $(li));
|
|
76
|
+
if (liContent) {
|
|
77
|
+
result += `- ${liContent}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
case "li":
|
|
83
|
+
result += htmlToMarkdown($, $node);
|
|
84
|
+
break;
|
|
85
|
+
case "img": {
|
|
86
|
+
const imgSrc = $node.attr("src") || "";
|
|
87
|
+
const imgAlt = $node.attr("alt") || "";
|
|
88
|
+
if (imgSrc) {
|
|
89
|
+
let imageUrl = imgSrc;
|
|
90
|
+
if (imgSrc.startsWith("/")) {
|
|
91
|
+
imageUrl = `${BOJ_BASE_URL}${imgSrc}`;
|
|
92
|
+
} else if (!imgSrc.startsWith("http") && !imgSrc.startsWith("data:")) {
|
|
93
|
+
imageUrl = `${BOJ_BASE_URL}/${imgSrc}`;
|
|
94
|
+
}
|
|
95
|
+
result += ``;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
result += htmlToMarkdown($, $node);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return result.trim();
|
|
105
|
+
}
|
|
106
|
+
async function scrapeProblem(problemId) {
|
|
107
|
+
const url = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
headers: {
|
|
110
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`Failed to fetch problem page: HTTP ${response.status}`);
|
|
115
|
+
}
|
|
116
|
+
const html = await response.text();
|
|
117
|
+
const $ = cheerio.load(html);
|
|
118
|
+
const title = $("#problem_title").text().trim();
|
|
119
|
+
const descriptionEl = $("#problem_description");
|
|
120
|
+
let description = "";
|
|
121
|
+
if (descriptionEl.length > 0) {
|
|
122
|
+
description = htmlToMarkdown($, descriptionEl).trim();
|
|
123
|
+
if (!description) {
|
|
124
|
+
description = descriptionEl.text().trim();
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
const altDesc = $('[id*="description"]').first();
|
|
128
|
+
if (altDesc.length > 0) {
|
|
129
|
+
description = altDesc.text().trim();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const inputEl = $("#problem_input");
|
|
133
|
+
let inputFormat = "";
|
|
134
|
+
if (inputEl.length > 0) {
|
|
135
|
+
inputFormat = htmlToMarkdown($, inputEl).trim();
|
|
136
|
+
if (!inputFormat) {
|
|
137
|
+
inputFormat = inputEl.text().trim();
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const altInput = $('[id*="input"]').first();
|
|
141
|
+
if (altInput.length > 0) {
|
|
142
|
+
inputFormat = altInput.text().trim();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const outputEl = $("#problem_output");
|
|
146
|
+
let outputFormat = "";
|
|
147
|
+
if (outputEl.length > 0) {
|
|
148
|
+
outputFormat = htmlToMarkdown($, outputEl).trim();
|
|
149
|
+
if (!outputFormat) {
|
|
150
|
+
outputFormat = outputEl.text().trim();
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const altOutput = $('[id*="output"]').first();
|
|
154
|
+
if (altOutput.length > 0) {
|
|
155
|
+
outputFormat = altOutput.text().trim();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const problemInfo = {};
|
|
159
|
+
const problemInfoTable = $("#problem-info");
|
|
160
|
+
const tableInResponsive = $(".table-responsive table");
|
|
161
|
+
const targetTable = problemInfoTable.length > 0 ? problemInfoTable : tableInResponsive;
|
|
162
|
+
if (targetTable.length > 0) {
|
|
163
|
+
const headerRow = targetTable.find("thead tr");
|
|
164
|
+
const dataRow = targetTable.find("tbody tr");
|
|
165
|
+
if (headerRow.length > 0 && dataRow.length > 0) {
|
|
166
|
+
const headers = headerRow.find("th").map((_, th) => $(th).text().trim()).get();
|
|
167
|
+
const values = dataRow.find("td").map((_, td) => $(td).text().trim()).get();
|
|
168
|
+
headers.forEach((header, index) => {
|
|
169
|
+
if (values[index]) {
|
|
170
|
+
problemInfo[header] = values[index];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
targetTable.find("tr").each((_, row) => {
|
|
175
|
+
const tds = $(row).find("td");
|
|
176
|
+
if (tds.length >= 2) {
|
|
177
|
+
const label = $(tds[0]).text().trim();
|
|
178
|
+
const value = $(tds[1]).text().trim();
|
|
179
|
+
problemInfo[label] = value;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const timeLimit = problemInfo["\uC2DC\uAC04 \uC81C\uD55C"] || problemInfo["Time Limit"] || void 0;
|
|
185
|
+
const memoryLimit = problemInfo["\uBA54\uBAA8\uB9AC \uC81C\uD55C"] || problemInfo["Memory Limit"] || void 0;
|
|
186
|
+
const submissions = problemInfo["\uC81C\uCD9C"] || problemInfo["Submit"] || void 0;
|
|
187
|
+
const accepted = problemInfo["\uC815\uB2F5"] || problemInfo["Accepted"] || void 0;
|
|
188
|
+
const acceptedUsers = problemInfo["\uB9DE\uD78C \uC0AC\uB78C"] || problemInfo["Accepted Users"] || void 0;
|
|
189
|
+
const acceptedRate = problemInfo["\uC815\uB2F5 \uBE44\uC728"] || problemInfo["Accepted Rate"] || void 0;
|
|
190
|
+
const testCases = [];
|
|
191
|
+
const sampleInputs = $(".sampledata").filter((_, el) => {
|
|
192
|
+
const id = $(el).attr("id");
|
|
193
|
+
return id?.startsWith("sample-input-") ?? false;
|
|
194
|
+
});
|
|
195
|
+
sampleInputs.each((_, el) => {
|
|
196
|
+
const inputId = $(el).attr("id");
|
|
197
|
+
if (!inputId) return;
|
|
198
|
+
const match = inputId.match(/sample-input-(\d+)/);
|
|
199
|
+
if (!match) return;
|
|
200
|
+
const sampleNumber = match[1];
|
|
201
|
+
const outputId = `sample-output-${sampleNumber}`;
|
|
202
|
+
const outputEl2 = $(`#${outputId}`);
|
|
203
|
+
if (outputEl2.length > 0) {
|
|
204
|
+
testCases.push({
|
|
205
|
+
input: $(el).text(),
|
|
206
|
+
output: outputEl2.text()
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (testCases.length === 0) {
|
|
211
|
+
$("pre").each((_, el) => {
|
|
212
|
+
const text = $(el).text().trim();
|
|
213
|
+
const prevText = $(el).prev().text().toLowerCase();
|
|
214
|
+
if (prevText.includes("\uC785\uB825") || prevText.includes("input")) {
|
|
215
|
+
const nextPre = $(el).next("pre");
|
|
216
|
+
if (nextPre.length > 0) {
|
|
217
|
+
testCases.push({
|
|
218
|
+
input: text,
|
|
219
|
+
output: nextPre.text().trim()
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (!title) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`\uBB38\uC81C ${problemId}\uC758 \uC81C\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 \uBB38\uC81C\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (!description && !inputFormat && !outputFormat) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`\uBB38\uC81C ${problemId}\uC758 \uB0B4\uC6A9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 API \uC81C\uD55C\uC5D0 \uAC78\uB838\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
title,
|
|
237
|
+
description,
|
|
238
|
+
inputFormat,
|
|
239
|
+
outputFormat,
|
|
240
|
+
testCases,
|
|
241
|
+
timeLimit,
|
|
242
|
+
memoryLimit,
|
|
243
|
+
submissions,
|
|
244
|
+
accepted,
|
|
245
|
+
acceptedUsers,
|
|
246
|
+
acceptedRate
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function searchProblems(query, page = 1) {
|
|
250
|
+
const url = `${SOLVED_AC_BASE_URL}/problems?query=${encodeURIComponent(query)}&page=${page}`;
|
|
251
|
+
const response = await fetch(url, {
|
|
252
|
+
headers: {
|
|
253
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`Failed to fetch search results: HTTP ${response.status}`);
|
|
258
|
+
}
|
|
259
|
+
const html = await response.text();
|
|
260
|
+
const $ = cheerio.load(html);
|
|
261
|
+
const problems = [];
|
|
262
|
+
const rows = $("tbody tr");
|
|
263
|
+
rows.each((_, row) => {
|
|
264
|
+
const $row = $(row);
|
|
265
|
+
const cells = $row.find("td");
|
|
266
|
+
if (cells.length >= 2) {
|
|
267
|
+
const problemIdText = $(cells[0]).text().trim();
|
|
268
|
+
const problemId = parseInt(problemIdText, 10);
|
|
269
|
+
const title = $(cells[1]).text().trim();
|
|
270
|
+
if (!isNaN(problemId) && title) {
|
|
271
|
+
let level;
|
|
272
|
+
const firstCell = $(cells[0]);
|
|
273
|
+
const tierImg = firstCell.find("img[src*='tier_small']");
|
|
274
|
+
if (tierImg.length > 0) {
|
|
275
|
+
const imgSrc = tierImg.attr("src");
|
|
276
|
+
if (imgSrc) {
|
|
277
|
+
const match = imgSrc.match(/tier_small\/(\d+)\.svg/);
|
|
278
|
+
if (match && match[1]) {
|
|
279
|
+
const parsedLevel = parseInt(match[1], 10);
|
|
280
|
+
if (!isNaN(parsedLevel) && parsedLevel >= 0 && parsedLevel <= 31) {
|
|
281
|
+
level = parsedLevel;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
let solvedCount;
|
|
287
|
+
if (cells.length >= 3) {
|
|
288
|
+
const solvedCountText = $(cells[2]).text().trim();
|
|
289
|
+
const parsed = parseInt(solvedCountText.replace(/,/g, ""), 10);
|
|
290
|
+
if (!isNaN(parsed)) {
|
|
291
|
+
solvedCount = parsed;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let averageTries;
|
|
295
|
+
if (cells.length >= 4) {
|
|
296
|
+
const averageTriesText = $(cells[3]).text().trim();
|
|
297
|
+
const parsed = parseFloat(averageTriesText);
|
|
298
|
+
if (!isNaN(parsed)) {
|
|
299
|
+
averageTries = parsed;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
problems.push({
|
|
303
|
+
problemId,
|
|
304
|
+
title,
|
|
305
|
+
level,
|
|
306
|
+
solvedCount,
|
|
307
|
+
averageTries
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
let totalPages = 1;
|
|
313
|
+
const paginationLinks = $('a[href*="page="]');
|
|
314
|
+
const pageNumbers = [];
|
|
315
|
+
paginationLinks.each((_, link) => {
|
|
316
|
+
const href = $(link).attr("href");
|
|
317
|
+
if (href) {
|
|
318
|
+
const match = href.match(/page=(\d+)/);
|
|
319
|
+
if (match) {
|
|
320
|
+
const pageNum = parseInt(match[1], 10);
|
|
321
|
+
if (!isNaN(pageNum)) {
|
|
322
|
+
pageNumbers.push(pageNum);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
if (pageNumbers.length > 0) {
|
|
328
|
+
totalPages = Math.max(...pageNumbers);
|
|
329
|
+
} else {
|
|
330
|
+
totalPages = problems.length > 0 ? 1 : 0;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
problems,
|
|
334
|
+
currentPage: page,
|
|
335
|
+
totalPages
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export {
|
|
340
|
+
scrapeProblem,
|
|
341
|
+
searchProblems
|
|
342
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openBrowser
|
|
4
|
+
} from "./chunk-QGMWUOJ3.js";
|
|
5
|
+
|
|
6
|
+
// src/hooks/use-open-browser.ts
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
9
|
+
function useOpenBrowser({
|
|
10
|
+
problemId,
|
|
11
|
+
onComplete
|
|
12
|
+
}) {
|
|
13
|
+
const [status, setStatus] = useState(
|
|
14
|
+
"loading"
|
|
15
|
+
);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [url, setUrl] = useState("");
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
async function handleOpenBrowser() {
|
|
20
|
+
try {
|
|
21
|
+
const problemUrl = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
22
|
+
setUrl(problemUrl);
|
|
23
|
+
await openBrowser(problemUrl);
|
|
24
|
+
setStatus("success");
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
onComplete?.();
|
|
27
|
+
}, 1500);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
setStatus("error");
|
|
30
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
onComplete?.();
|
|
33
|
+
}, 2e3);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
void handleOpenBrowser();
|
|
37
|
+
}, [problemId, onComplete]);
|
|
38
|
+
return {
|
|
39
|
+
status,
|
|
40
|
+
error,
|
|
41
|
+
url
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
useOpenBrowser
|
|
47
|
+
};
|