Skip to content

Commit 2c18cc4

Browse files
fix(paper2novel): add stop & download button
1 parent 2ea67da commit 2c18cc4

File tree

3 files changed

+80
-9
lines changed

3 files changed

+80
-9
lines changed

apps/arxiv-to-novel/app.js

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ function chunkText(text, maxChars = 6000) {
306306

307307
// ── OpenAI Streaming (direct from browser) ───────────
308308

309-
async function* streamChatCompletion(messages) {
309+
async function* streamChatCompletion(messages, signal) {
310310
const settings = getSettings();
311311

312312
if (!settings.apiKey) {
@@ -329,6 +329,7 @@ async function* streamChatCompletion(messages) {
329329
temperature: 0.85,
330330
max_tokens: 4096,
331331
}),
332+
signal: signal,
332333
});
333334

334335
if (!response.ok) {
@@ -466,10 +467,14 @@ function doSaveSettings() {
466467
}
467468

468469

470+
let controller = null;
471+
469472
// ── Event Binding ────────────────────────────────────
470473
function bindEvents() {
471474
document.getElementById("btnConvert").addEventListener("click", startConvert);
475+
document.getElementById("btnStop").addEventListener("click", stopConvert);
472476
document.getElementById("btnCopy").addEventListener("click", copyNovel);
477+
document.getElementById("btnDownload").addEventListener("click", downloadNovel);
473478
document.getElementById("btnSettings").addEventListener("click", showSettingsModal);
474479
document.getElementById("btnCloseSettings").addEventListener("click", hideSettingsModal);
475480
document.getElementById("btnSaveSettings").addEventListener("click", doSaveSettings);
@@ -504,11 +509,13 @@ async function startConvert() {
504509

505510
// UI state
506511
isConverting = true;
512+
controller = new AbortController();
507513
const btn = document.getElementById("btnConvert");
508-
btn.querySelector(".btn-text").style.display = "none";
509-
btn.querySelector(".btn-loading").style.display = "inline-flex";
510-
btn.disabled = true;
511-
514+
const btnStop = document.getElementById("btnStop");
515+
516+
btn.style.display = "none";
517+
btnStop.style.display = "inline-flex";
518+
512519
const progressArea = document.getElementById("progressArea");
513520
const novelOutput = document.getElementById("novelOutput");
514521
progressArea.style.display = "block";
@@ -517,11 +524,13 @@ async function startConvert() {
517524

518525
try {
519526
// Step 1: Fetch paper
527+
if (controller.signal.aborted) throw new Error("已停止");
520528
document.getElementById("paperTitle").textContent = "正在获取论文...";
521529
document.getElementById("paperAuthors").textContent = "从 ar5iv 提取全文中...";
522530

523531
const paper = await fetchPaper(url);
524532

533+
if (controller.signal.aborted) throw new Error("已停止");
525534
document.getElementById("paperTitle").textContent = paper.title;
526535
document.getElementById("paperAuthors").textContent = paper.authors.join(", ") || "Unknown";
527536

@@ -540,6 +549,8 @@ async function startConvert() {
540549

541550
// Step 3: Stream each chunk through LLM
542551
for (let idx = 0; idx < totalChunks; idx++) {
552+
if (controller.signal.aborted) throw new Error("已停止");
553+
543554
setChunkStatus(idx, "active");
544555
updateProgress(idx, totalChunks);
545556
ensureChapterEl(idx, totalChunks);
@@ -550,7 +561,7 @@ async function startConvert() {
550561
];
551562

552563
let chapterText = "";
553-
for await (const token of streamChatCompletion(messages)) {
564+
for await (const token of streamChatCompletion(messages, controller.signal)) {
554565
chapterText += token;
555566
renderChapterContent(idx, chapterText);
556567
}
@@ -561,16 +572,58 @@ async function startConvert() {
561572
updateProgress(totalChunks, totalChunks);
562573

563574
} catch (err) {
564-
alert("转换出错:" + err.message);
565-
console.error(err);
575+
if (err.name === 'AbortError' || err.message === "已停止") {
576+
console.log("Conversion stopped by user");
577+
} else {
578+
alert("转换出错:" + err.message);
579+
console.error(err);
580+
}
566581
} finally {
567582
isConverting = false;
583+
controller = null;
584+
btn.style.display = "inline-flex";
568585
btn.querySelector(".btn-text").style.display = "inline-flex";
569586
btn.querySelector(".btn-loading").style.display = "none";
570587
btn.disabled = false;
588+
btnStop.style.display = "none";
571589
}
572590
}
573591

592+
function stopConvert() {
593+
if (controller) {
594+
controller.abort();
595+
}
596+
}
597+
598+
function downloadNovel() {
599+
const title = document.getElementById("novelTitle").textContent.trim();
600+
if (!title || title === "—") {
601+
alert("暂无内容可下载");
602+
return;
603+
}
604+
605+
let content = `# ${title}\n\n`;
606+
const meta = document.getElementById("novelGenre").textContent;
607+
content += `> 风格:${meta}\n\n---\n\n`;
608+
609+
const chapters = document.querySelectorAll("#novelContent .chapter");
610+
chapters.forEach(ch => {
611+
const chTitle = ch.querySelector(".chapter-title").innerText;
612+
const chBody = ch.querySelector(".chapter-body").innerText; // innerText preserves newlines better than textContent usually
613+
content += `## ${chTitle}\n\n${chBody}\n\n`;
614+
});
615+
616+
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
617+
const url = URL.createObjectURL(blob);
618+
const a = document.createElement("a");
619+
a.href = url;
620+
a.download = `${title}.md`;
621+
document.body.appendChild(a);
622+
a.click();
623+
document.body.removeChild(a);
624+
URL.revokeObjectURL(url);
625+
}
626+
574627

575628
// ── UI Helpers ───────────────────────────────────────
576629
function renderChunkIndicators(total) {

apps/arxiv-to-novel/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ <h2>将论文变成你的专属小说</h2>
249249
<span class="spinner"></span> 创作中...
250250
</span>
251251
</button>
252+
<button class="btn btn-danger btn-lg btn-full" id="btnStop" style="display:none; margin-top: 10px;">
253+
⏹ 停止生成
254+
</button>
252255
</div>
253256

254257
<!-- Progress -->
@@ -275,7 +278,10 @@ <h2 id="novelTitle">—</h2>
275278
<span id="novelGenre"></span>
276279
<span id="novelChapters"></span>
277280
</div>
278-
<button class="btn btn-outline btn-sm" id="btnCopy">📋 复制全文</button>
281+
<div style="display: flex; gap: 0.5rem;">
282+
<button class="btn btn-outline btn-sm" id="btnDownload">📥 下载全文</button>
283+
<button class="btn btn-outline btn-sm" id="btnCopy">📋 复制全文</button>
284+
</div>
279285
</div>
280286
<div class="novel-content" id="novelContent">
281287
<!-- Chapters rendered here -->

apps/arxiv-to-novel/style.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ h2 {
132132
box-shadow: 0 8px 20px rgba(108, 92, 231, 0.4);
133133
}
134134

135+
.btn-danger {
136+
background: #ff7675;
137+
color: white;
138+
box-shadow: 0 4px 15px rgba(255, 118, 117, 0.3);
139+
}
140+
141+
.btn-danger:hover {
142+
background: #d63031;
143+
transform: translateY(-2px);
144+
box-shadow: 0 8px 20px rgba(255, 118, 117, 0.4);
145+
}
146+
135147
.btn-outline {
136148
background: white;
137149
color: var(--text);

0 commit comments

Comments
 (0)