Skip to content

Commit 1a02379

Browse files
authored
Redirect book creation to v2 with extract+storyboard steps (#57)
* Redirect book creation to v2 page with extract+storyboard pipeline After creating a book, navigate to the v2 book page and kick off extract→storyboard steps. The step runner now supports running step ranges (not just single steps). BookView uses StepRunCards with sub-step detail, run buttons, completion state, and click-to-navigate. Also fixes SSE auto-reconnect race condition and normalizes sub-step labels to imperative form. * Improve step-run UX and persist add-book config
1 parent d76a35f commit 1a02379

20 files changed

Lines changed: 476 additions & 194 deletions

apps/api/src/services/pipeline-runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ export function createPipelineRunner(): PipelineRunner {
108108
await extractPDF(
109109
{
110110
pdfPath,
111-
startPage: options.startPage,
112-
endPage: options.endPage,
111+
startPage: options.startPage ?? config.start_page,
112+
endPage: options.endPage ?? config.end_page,
113113
spreadMode: config.spread_mode,
114114
},
115115
storage,

apps/api/src/services/step-runner.ts

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,31 @@ async function processWithConcurrency<T>(
9898
await Promise.all(executing)
9999
}
100100

101+
const STEP_ORDER = [
102+
"extract",
103+
"storyboard",
104+
"quizzes",
105+
"captions",
106+
"glossary",
107+
"translations",
108+
"text-to-speech",
109+
] as const
110+
111+
type RunFn = (label: string, options: StepRunOptions, progress: StepRunProgress) => Promise<void>
112+
113+
const STEP_RUNNERS: Record<string, RunFn> = {
114+
"extract": runExtractStep,
115+
"storyboard": runStoryboardStep,
116+
"quizzes": runQuizzesStep,
117+
"captions": runCaptionsStep,
118+
"glossary": runGlossaryStep,
119+
"translations": runTranslationsStep,
120+
"text-to-speech": runTextToSpeechStep,
121+
}
122+
101123
/**
102-
* Creates a step runner that executes individual pipeline steps.
103-
* Currently supports: "extract" (PDF extraction + metadata + classification).
124+
* Creates a step runner that executes pipeline steps.
125+
* Supports single steps (fromStep === toStep) and ranges (e.g. extract → storyboard).
104126
*/
105127
export function createStepRunner(): StepRunner {
106128
return {
@@ -112,22 +134,16 @@ export function createStepRunner(): StepRunner {
112134
const { fromStep, toStep } = options
113135
console.log(`[step-run] ${label}: starting ${fromStep}${toStep}`)
114136

115-
if (fromStep === "extract" && toStep === "extract") {
116-
await runExtractStep(label, options, progress)
117-
} else if (fromStep === "storyboard" && toStep === "storyboard") {
118-
await runStoryboardStep(label, options, progress)
119-
} else if (fromStep === "quizzes" && toStep === "quizzes") {
120-
await runQuizzesStep(label, options, progress)
121-
} else if (fromStep === "captions" && toStep === "captions") {
122-
await runCaptionsStep(label, options, progress)
123-
} else if (fromStep === "glossary" && toStep === "glossary") {
124-
await runGlossaryStep(label, options, progress)
125-
} else if (fromStep === "translations" && toStep === "translations") {
126-
await runTranslationsStep(label, options, progress)
127-
} else if (fromStep === "text-to-speech" && toStep === "text-to-speech") {
128-
await runTextToSpeechStep(label, options, progress)
129-
} else {
130-
throw new Error(`Step range "${fromStep}" to "${toStep}" is not yet implemented`)
137+
const fromIndex = STEP_ORDER.indexOf(fromStep as typeof STEP_ORDER[number])
138+
const toIndex = STEP_ORDER.indexOf(toStep as typeof STEP_ORDER[number])
139+
140+
if (fromIndex === -1 || toIndex === -1 || fromIndex > toIndex) {
141+
throw new Error(`Invalid step range "${fromStep}" to "${toStep}"`)
142+
}
143+
144+
for (let i = fromIndex; i <= toIndex; i++) {
145+
const stepSlug = STEP_ORDER[i]
146+
await STEP_RUNNERS[stepSlug](label, options, progress)
131147
}
132148

133149
console.log(`[step-run] ${label}: completed ${fromStep}${toStep}`)
@@ -154,7 +170,12 @@ async function runExtractStep(
154170
// Step 1: Extract PDF
155171
console.log(`[step-run] ${label}: extracting PDF from ${pdfPath}`)
156172
await extractPDF(
157-
{ pdfPath, spreadMode: config.spread_mode },
173+
{
174+
pdfPath,
175+
startPage: config.start_page,
176+
endPage: config.end_page,
177+
spreadMode: config.spread_mode,
178+
},
158179
storage,
159180
progress
160181
)

apps/studio/src/components/v2/StepRunCard.tsx

Lines changed: 112 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,100 +13,151 @@ interface StepRunCardProps {
1313
subSteps: StepRunCardSubStep[]
1414
description?: string
1515
isRunning: boolean
16+
completed?: boolean
17+
showRunButton?: boolean
1618
onRun: () => void
1719
disabled: boolean
1820
}
1921

20-
export function StepRunCard({ stepSlug, subSteps, description, isRunning, onRun, disabled }: StepRunCardProps) {
22+
const HOVER_BG_BY_COLOR: Record<string, string> = {
23+
"bg-gray-500": "hover:bg-gray-500",
24+
"bg-blue-500": "hover:bg-blue-500",
25+
"bg-violet-500": "hover:bg-violet-500",
26+
"bg-orange-500": "hover:bg-orange-500",
27+
"bg-teal-500": "hover:bg-teal-500",
28+
"bg-lime-500": "hover:bg-lime-500",
29+
"bg-pink-500": "hover:bg-pink-500",
30+
"bg-amber-500": "hover:bg-amber-500",
31+
}
32+
33+
export function StepRunCard({
34+
stepSlug,
35+
subSteps,
36+
description,
37+
isRunning,
38+
completed,
39+
showRunButton = true,
40+
onRun,
41+
disabled,
42+
}: StepRunCardProps) {
2143
const stepConfig = STEPS.find((s) => s.slug === stepSlug)
2244
const { progress } = useStepRun()
2345
const { subSteps: subStepProgress, error, targetSteps } = progress
2446

2547
const Icon = stepConfig?.icon ?? Play
2648
const bgDark = stepConfig?.bgDark ?? "bg-gray-700"
2749
const color = stepConfig?.color ?? "bg-gray-500"
50+
const borderColor = stepConfig?.borderColor ?? "border-gray-200"
2851
const hasError = !!error && targetSteps.has(stepSlug)
52+
const isCompleted = completed || progress.steps.get(stepSlug)?.state === "done"
53+
const hasSubSteps = subSteps.length > 0
54+
const hoverColorClass = HOVER_BG_BY_COLOR[color] ?? "hover:bg-gray-500"
55+
const buttonToneClass = isCompleted
56+
? cn(color, "text-white")
57+
: cn("bg-gray-200 text-gray-700", hoverColorClass, "hover:text-white")
2958

3059
return (
31-
<div className="rounded-lg border bg-card overflow-hidden max-w-xl">
60+
<div className={cn("rounded-lg border bg-card overflow-hidden max-w-xl", borderColor)}>
3261
{/* Colored header */}
33-
<div className={cn("px-4 py-3 flex items-center gap-2.5 text-white", bgDark)}>
62+
<div className={cn("px-4 py-2 flex items-center gap-2.5 text-white", bgDark)}>
3463
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-white/20">
3564
<Icon className="w-3 h-3" />
3665
</div>
3766
<span className="text-sm font-semibold">
3867
{isRunning
39-
? `Running ${stepConfig?.label?.toLowerCase() ?? stepSlug}...`
68+
? `${stepConfig?.runningLabel ?? stepSlug}...`
4069
: stepConfig?.label ?? stepSlug}
4170
</span>
4271
</div>
4372

4473
{/* Main row: sub-steps | button | description */}
45-
<div className="flex items-center gap-5 p-5">
74+
<div
75+
className={cn(
76+
"flex items-center px-5 py-3",
77+
showRunButton || hasSubSteps ? "gap-5" : "justify-center"
78+
)}
79+
>
4680
{/* Sub-steps */}
47-
<div className="space-y-2.5 shrink-0">
48-
{subSteps.map(({ key, label }) => {
49-
const sub = subStepProgress.get(key)
50-
const isDone = sub?.state === "done"
51-
const isSubRunning = sub?.state === "running"
52-
const isError = sub?.state === "error"
53-
const hasPages = sub?.page != null && sub?.totalPages != null && sub.totalPages > 0
81+
{hasSubSteps && (
82+
<div className="space-y-1.5 w-48 shrink-0">
83+
{subSteps.map(({ key, label }) => {
84+
const sub = subStepProgress.get(key)
85+
const isDone = sub?.state === "done" || (completed && !sub)
86+
const isSubRunning = sub?.state === "running"
87+
const isError = sub?.state === "error"
88+
const hasPages = sub?.page != null && sub?.totalPages != null && sub.totalPages > 0
5489

55-
return (
56-
<div
57-
key={key}
58-
className={cn(
59-
"flex items-center gap-2.5 text-xs whitespace-nowrap",
60-
isDone ? "text-muted-foreground" : isError ? "text-red-500" : isSubRunning ? "text-foreground" : "text-muted-foreground/50",
61-
)}
62-
>
63-
{isDone ? (
64-
<Check className="w-4 h-4 text-green-500 shrink-0" />
65-
) : isError ? (
66-
<XCircle className="w-4 h-4 text-red-500 shrink-0" />
67-
) : isSubRunning ? (
68-
<Loader2 className="w-4 h-4 animate-spin text-blue-500 shrink-0" />
69-
) : (
70-
<div className="w-4 h-4 rounded-full border border-current opacity-30 shrink-0" />
71-
)}
72-
<span>{label}</span>
73-
{isSubRunning && hasPages && (
74-
<span className="text-muted-foreground tabular-nums">{sub.page}/{sub.totalPages}</span>
75-
)}
76-
</div>
77-
)
78-
})}
79-
</div>
90+
return (
91+
<div
92+
key={key}
93+
className={cn(
94+
"flex items-center gap-2.5 text-xs whitespace-nowrap",
95+
isDone ? "text-muted-foreground" : isError ? "text-red-500" : isSubRunning ? "text-foreground" : "text-muted-foreground/50",
96+
)}
97+
>
98+
{isDone ? (
99+
<Check className="w-4 h-4 text-green-500 shrink-0" />
100+
) : isError ? (
101+
<XCircle className="w-4 h-4 text-red-500 shrink-0" />
102+
) : isSubRunning ? (
103+
<Loader2 className="w-4 h-4 animate-spin text-blue-500 shrink-0" />
104+
) : (
105+
<div className="w-4 h-4 rounded-full border border-current opacity-30 shrink-0" />
106+
)}
107+
<span>{label}</span>
108+
{isSubRunning && hasPages && (
109+
<span className="text-muted-foreground tabular-nums">{sub.page}/{sub.totalPages}</span>
110+
)}
111+
</div>
112+
)
113+
})}
114+
</div>
115+
)}
80116

81117
{/* Play / Retry / Spinner button */}
82-
<div className="shrink-0">
83-
{isRunning ? (
84-
<div className={cn(
85-
"flex items-center justify-center w-12 h-12 rounded-full opacity-60",
86-
color, "text-white",
87-
)}>
88-
<Loader2 className="w-5 h-5 animate-spin" />
89-
</div>
90-
) : (
91-
<button
92-
type="button"
93-
className={cn(
94-
"flex items-center justify-center w-12 h-12 rounded-full transition-all cursor-pointer",
95-
"hover:scale-105 active:scale-95 disabled:opacity-30 disabled:cursor-default disabled:hover:scale-100",
118+
{showRunButton && (
119+
<div className="shrink-0">
120+
{isRunning ? (
121+
<div className={cn(
122+
"flex items-center justify-center w-12 h-12 rounded-full opacity-60",
96123
color, "text-white",
97-
)}
98-
disabled={disabled}
99-
onClick={onRun}
100-
title={hasError ? "Retry" : `Run ${stepConfig?.label?.toLowerCase() ?? stepSlug}`}
101-
>
102-
{hasError ? <RotateCcw className="w-5 h-5" /> : <Play className="w-5 h-5 ml-0.5" />}
103-
</button>
104-
)}
105-
</div>
124+
)}>
125+
<Loader2 className="w-5 h-5 animate-spin" />
126+
</div>
127+
) : (
128+
<button
129+
type="button"
130+
className={cn(
131+
"flex items-center justify-center w-12 h-12 rounded-full transition-all cursor-pointer",
132+
"hover:scale-105 active:scale-95 disabled:opacity-30 disabled:cursor-default disabled:hover:scale-100",
133+
buttonToneClass,
134+
)}
135+
disabled={disabled}
136+
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onRun() }}
137+
title={
138+
hasError
139+
? "Retry"
140+
: isCompleted
141+
? `Re-run ${stepConfig?.label?.toLowerCase() ?? stepSlug}`
142+
: `Run ${stepConfig?.label?.toLowerCase() ?? stepSlug}`
143+
}
144+
>
145+
{hasError || isCompleted ? <RotateCcw className="w-5 h-5" /> : <Play className="w-5 h-5 ml-0.5" />}
146+
</button>
147+
)}
148+
</div>
149+
)}
106150

107151
{/* Description */}
108152
{description && (
109-
<p className="flex-1 min-w-0 text-xs text-muted-foreground leading-relaxed">{description}</p>
153+
<p
154+
className={cn(
155+
"min-w-0 text-xs text-muted-foreground leading-relaxed",
156+
showRunButton || hasSubSteps ? "flex-1" : "max-w-md text-center"
157+
)}
158+
>
159+
{description}
160+
</p>
110161
)}
111162
</div>
112163

apps/studio/src/components/v2/StepSidebar.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ import { useStepRun } from "@/hooks/use-step-run"
1818
import { StepProgressRing } from "./StepProgressRing"
1919

2020
export const STEPS = [
21-
{ slug: "book", label: "Book", icon: BookMarked, color: "bg-gray-500", textColor: "text-gray-600", bgLight: "bg-gray-50", bgDark: "bg-gray-700", borderColor: "border-gray-200" },
22-
{ slug: "extract", label: "Extract", icon: FileText, color: "bg-blue-500", textColor: "text-blue-600", bgLight: "bg-blue-50", bgDark: "bg-blue-700", borderColor: "border-blue-200" },
23-
{ slug: "storyboard", label: "Storyboard", icon: LayoutGrid, color: "bg-violet-500", textColor: "text-violet-600", bgLight: "bg-violet-50", bgDark: "bg-violet-700", borderColor: "border-violet-200" },
24-
{ slug: "quizzes", label: "Quizzes", icon: HelpCircle, color: "bg-orange-500", textColor: "text-orange-600", bgLight: "bg-orange-50", bgDark: "bg-orange-700", borderColor: "border-orange-200" },
25-
{ slug: "captions", label: "Captions", icon: Image, color: "bg-teal-500", textColor: "text-teal-600", bgLight: "bg-teal-50", bgDark: "bg-teal-700", borderColor: "border-teal-200" },
26-
{ slug: "glossary", label: "Glossary", icon: BookOpen, color: "bg-lime-500", textColor: "text-lime-600", bgLight: "bg-lime-50", bgDark: "bg-lime-700", borderColor: "border-lime-200" },
27-
{ slug: "translations", label: "Translations", icon: Languages, color: "bg-pink-500", textColor: "text-pink-600", bgLight: "bg-pink-50", bgDark: "bg-pink-700", borderColor: "border-pink-200" },
28-
{ slug: "text-to-speech", label: "Text to Speech", icon: Volume2, color: "bg-amber-500", textColor: "text-amber-600", bgLight: "bg-amber-50", bgDark: "bg-amber-700", borderColor: "border-amber-200" },
29-
{ slug: "preview", label: "Preview", icon: Eye, color: "bg-gray-500", textColor: "text-gray-600", bgLight: "bg-gray-50", bgDark: "bg-gray-700", borderColor: "border-gray-200" },
21+
{ slug: "book", label: "Book", runningLabel: "Loading Book", icon: BookMarked, color: "bg-gray-500", textColor: "text-gray-600", bgLight: "bg-gray-50", bgDark: "bg-gray-700", borderColor: "border-gray-200" },
22+
{ slug: "extract", label: "Extract", runningLabel: "Extracting", icon: FileText, color: "bg-blue-500", textColor: "text-blue-600", bgLight: "bg-blue-50", bgDark: "bg-blue-700", borderColor: "border-blue-200" },
23+
{ slug: "storyboard", label: "Storyboard", runningLabel: "Building Storyboard", icon: LayoutGrid, color: "bg-violet-500", textColor: "text-violet-600", bgLight: "bg-violet-50", bgDark: "bg-violet-700", borderColor: "border-violet-200" },
24+
{ slug: "quizzes", label: "Quizzes", runningLabel: "Generating Quizzes", icon: HelpCircle, color: "bg-orange-500", textColor: "text-orange-600", bgLight: "bg-orange-50", bgDark: "bg-orange-700", borderColor: "border-orange-200" },
25+
{ slug: "captions", label: "Captions", runningLabel: "Captioning Images", icon: Image, color: "bg-teal-500", textColor: "text-teal-600", bgLight: "bg-teal-50", bgDark: "bg-teal-700", borderColor: "border-teal-200" },
26+
{ slug: "glossary", label: "Glossary", runningLabel: "Generating Glossary", icon: BookOpen, color: "bg-lime-500", textColor: "text-lime-600", bgLight: "bg-lime-50", bgDark: "bg-lime-700", borderColor: "border-lime-200" },
27+
{ slug: "translations", label: "Translations", runningLabel: "Translating", icon: Languages, color: "bg-pink-500", textColor: "text-pink-600", bgLight: "bg-pink-50", bgDark: "bg-pink-700", borderColor: "border-pink-200" },
28+
{ slug: "text-to-speech", label: "Text to Speech", runningLabel: "Generating Audio", icon: Volume2, color: "bg-amber-500", textColor: "text-amber-600", bgLight: "bg-amber-50", bgDark: "bg-amber-700", borderColor: "border-amber-200" },
29+
{ slug: "preview", label: "Preview", runningLabel: "Building Preview", icon: Eye, color: "bg-gray-500", textColor: "text-gray-600", bgLight: "bg-gray-50", bgDark: "bg-gray-700", borderColor: "border-gray-200" },
3030
] as const
3131

3232
export type StepSlug = (typeof STEPS)[number]["slug"]
@@ -147,7 +147,7 @@ export function StepSidebar({ bookLabel, activeStep }: { bookLabel: string; acti
147147
<div
148148
className={cn(
149149
"flex items-center justify-center w-7 h-7 rounded-full transition-colors",
150-
isActive || step.slug === "book" || completedSteps[step.slug]
150+
isActive || step.slug === "book" || completedSteps[step.slug] || ringState === "done"
151151
? cn(step.color, "text-white")
152152
: "bg-muted text-muted-foreground"
153153
)}

0 commit comments

Comments
 (0)