Skip to content
This repository was archived by the owner on Jul 7, 2026. It is now read-only.

Commit 30df8fe

Browse files
ChanMeng666claude
andcommitted
feat(story): auto-generate chapter titles for continuations
Phase 5 PR-E. Now that stories can grow chapter-by-chapter (PR-D Lite), each new chapter benefits from its own title for navigation. Changes: - /api/stories/[id]/continue now calls gpt-4o-mini after writing the body to produce a 4-10 character poetic chapter title; saves it alongside the chapter and surfaces it in the SSE complete event - The continuation dialog already shows the message, so users see "第 2 章「夜半潮声」已添加" in the success toast - /api/stories no longer copies the story title onto the first chapter — that was creating a duplicate h2 in the reader - StoryReader skips the chapter h2 entirely for single-chapter stories (the page h1 already serves as the title) and defensively hides chapter titles that equal the story title for legacy data The chapter title prompt is intentionally conservative (no spoilers, poetic but short, no "第 X 章" prefix) and falls back to null on any failure; the reader handles untitled chapters as "第 N 章". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4055be0 commit 30df8fe

3 files changed

Lines changed: 57 additions & 7 deletions

File tree

src/app/api/stories/[id]/continue/route.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,41 @@ const CONTINUE_PROMPT = `你是一位顶尖的同人文作者,正在为一篇
2020
2121
输出要求:直接输出本章正文,不要加章节标题、不要加作者注、不要加任何元信息。`;
2222

23+
const CHAPTER_TITLE_PROMPT = `请为以下同人文章节拟一个简洁的中文章节标题。
24+
25+
要求:
26+
1. 4~10 字
27+
2. 有诗意,能引发读者兴趣
28+
3. 不要剧透关键转折
29+
4. 不要带「第 X 章」字样,不要引号
30+
5. 直接输出标题,不要任何前缀或额外文字
31+
32+
只输出标题本身。`;
33+
34+
async function generateChapterTitle(content: string): Promise<string | null> {
35+
try {
36+
const model = new ChatOpenAI({
37+
temperature: 0.6,
38+
model: "gpt-4o-mini",
39+
maxTokens: 30,
40+
});
41+
const res = await model.invoke([
42+
new SystemMessage(CHAPTER_TITLE_PROMPT),
43+
new HumanMessage(content.slice(0, 3000)),
44+
]);
45+
const raw = (typeof res.content === "string" ? res.content : "").trim();
46+
const cleaned = raw
47+
.replace(/^["]|["]$/g, "")
48+
.replace(/^.{1,4}[:]?\s*/, "")
49+
.trim();
50+
if (cleaned.length < 2 || cleaned.length > 20) return null;
51+
return cleaned;
52+
} catch (e) {
53+
console.warn("[continue] chapter title generation failed:", e);
54+
return null;
55+
}
56+
}
57+
2358
interface ContinueBody {
2459
direction: string;
2560
}
@@ -158,14 +193,17 @@ ${direction}
158193
return;
159194
}
160195

196+
send({ stage: "titling", message: "正在为本章拟标题…" });
197+
const chapterTitle = await generateChapterTitle(content);
198+
161199
send({ stage: "saving", message: "正在保存章节…" });
162200

163201
const wordCount = countWords(content);
164202

165203
const newChapter = await prisma.chapter.create({
166204
data: {
167205
storyId,
168-
title: null,
206+
title: chapterTitle,
169207
content,
170208
chapterNumber: nextChapterNumber,
171209
wordCount,
@@ -179,9 +217,12 @@ ${direction}
179217

180218
send({
181219
stage: "complete",
182-
message: `第 ${nextChapterNumber} 章已添加(${wordCount.toLocaleString()} 字)`,
220+
message: chapterTitle
221+
? `第 ${nextChapterNumber} 章「${chapterTitle}」已添加(${wordCount.toLocaleString()} 字)`
222+
: `第 ${nextChapterNumber} 章已添加(${wordCount.toLocaleString()} 字)`,
183223
chapterId: newChapter.id,
184224
chapterNumber: nextChapterNumber,
225+
chapterTitle,
185226
wordCount,
186227
});
187228
} catch (err) {

src/app/api/stories/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ export async function POST(req: NextRequest) {
5454
authorId: dbUser.id,
5555
chapters: {
5656
create: {
57-
title: result.title,
57+
// Don't duplicate the story title onto its only chapter — when
58+
// a second chapter is added later via /api/stories/[id]/continue
59+
// it gets an auto-generated chapter title, and the reader UI
60+
// relies on chapter.title being null/distinct to avoid showing
61+
// a redundant h2.
62+
title: null,
5863
content: result.body,
5964
chapterNumber: 1,
6065
wordCount: result.wordCount,

src/components/story/StoryReader.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,15 @@ export function StoryReader({
165165
<div className="space-y-12">
166166
{story.chapters.map((chapter, idx) => (
167167
<section key={chapter.id} className="space-y-4">
168-
{(chapter.title || chapterCount > 1) && (
168+
{/* Skip chapter h2 entirely for single-chapter stories — the
169+
page h1 (story title) already serves as the heading and a
170+
second one looks duplicated. */}
171+
{chapterCount > 1 && (
169172
<h2 className="font-display text-xl md:text-2xl font-semibold text-foreground">
170-
{chapterCount > 1 ? `第 ${chapter.chapterNumber} 章` : null}
171-
{chapterCount > 1 && chapter.title ? ":" : ""}
172-
{chapter.title}
173+
{chapter.chapterNumber}
174+
{chapter.title && chapter.title !== story.title
175+
? `:${chapter.title}`
176+
: ""}
173177
</h2>
174178
)}
175179
<div className="font-prose text-foreground/90 leading-8 text-base md:text-lg whitespace-pre-wrap">

0 commit comments

Comments
 (0)