Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 121 additions & 5 deletions apps/studio/src/components/pipeline/components/StageSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { createPortal } from "react-dom"
import { Link, useMatchRoute, useSearch } from "@tanstack/react-router"
import { Trans } from "@lingui/react/macro"
import {
ChevronDown,
ChevronRight,
Info,
Loader2,
Puzzle,
RotateCcw,
Settings,
} from "lucide-react"
Expand All @@ -20,6 +24,9 @@ import { StepProgressRing } from "./StepProgressRing"
import { usePages, usePageImage } from "@/hooks/use-pages"
import {
STAGES,
CORE_STAGE_SLUGS,
LEARNING_TOOL_SLUGS,
PACKAGING_SLUGS,
hasStagePages,
toCamelLabel,
} from "../stage-config"
Expand Down Expand Up @@ -161,7 +168,27 @@ export function StageSidebar({
const storyboardDone = stageState("storyboard") === "done"
const validationCompleted = Boolean(accessibilityAssessment?.assessment)

const stageItems = STAGES.map((step, index) => {
// Learning Tools collapse state — persisted in localStorage
const [learningToolsOpen, setLearningToolsOpen] = useState(() => {
try { return localStorage.getItem("adt:learning-tools-open") !== "false" } catch { return true }
})
const toggleLearningTools = useCallback(() => {
setLearningToolsOpen((prev) => {
const next = !prev
try { localStorage.setItem("adt:learning-tools-open", String(next)) } catch { /* noop */ }
return next
})
}, [])

// If the active step is a learning tool, force the section open
const effectiveLearningToolsOpen = learningToolsOpen || LEARNING_TOOL_SLUGS.has(activeStep as any)

// Split stages into sections
const coreStages = STAGES.filter((s) => CORE_STAGE_SLUGS.has(s.slug as any))
const learningToolStages = STAGES.filter((s) => LEARNING_TOOL_SLUGS.has(s.slug as any))
const packagingStages = STAGES.filter((s) => PACKAGING_SLUGS.has(s.slug as any))

function renderStageItem(step: (typeof STAGES)[number], showConnector: boolean) {
const isActive = step.slug === activeStep
const Icon = step.icon
const settingsTabs = getSettingsTabs(step.slug, i18n)
Expand All @@ -184,7 +211,7 @@ export function StageSidebar({
return (
<div key={step.slug} className="relative">
{/* Connector line */}
{index < STAGES.length - 1 && (
{showConnector && (
<div className="absolute left-[24px] top-[36px] bottom-[-10px] w-0.5 bg-border z-10" />
)}

Expand Down Expand Up @@ -301,7 +328,7 @@ export function StageSidebar({
)}
</div>
)
})
}

return (
<nav className="flex flex-col flex-1 min-h-0">
Expand All @@ -317,8 +344,55 @@ export function StageSidebar({
? "w-12 group-hover/rail:w-[220px] z-20 transition-[width] duration-150 delay-150 group-hover/rail:delay-100 group-hover/rail:shadow-lg"
: "inset-x-0"
)}>
<div className="flex flex-col pt-1.5 pb-2 gap-0.5 flex-1 overflow-y-auto overflow-x-hidden">
{stageItems}
<div className="flex flex-col pt-1.5 pb-2 flex-1 overflow-y-auto overflow-x-hidden">
{/* Core stages */}
<div className="flex flex-col gap-0.5">
{coreStages.map((step, i) => renderStageItem(step, i < coreStages.length - 1))}
</div>

{/* Learning Tools section */}
<div className="mt-3 pt-2 border-t border-border/50">
<button
type="button"
onClick={toggleLearningTools}
className={cn(
"flex items-center gap-2 w-full px-2.5 py-1.5 text-xs font-semibold uppercase tracking-wider transition-colors",
"text-muted-foreground/70 hover:text-muted-foreground"
)}
>
<Puzzle className="w-3.5 h-3.5 shrink-0" />
<span className={cn("truncate hidden", x.showLabel)}>
<Trans>Learning Tools</Trans>
</span>
<LearningToolsInfo i18n={i18n} showLabel={x.showLabel} />
<span className={cn("ml-auto hidden", x.showLabel)}>
{effectiveLearningToolsOpen
? <ChevronDown className="w-3.5 h-3.5" />
: <ChevronRight className="w-3.5 h-3.5" />
}
</span>
</button>
{effectiveLearningToolsOpen && (
<div className="flex flex-col gap-0.5 mt-0.5">
{learningToolStages.map((step, i) => renderStageItem(step, i < learningToolStages.length - 1))}
</div>
)}
</div>

{/* Packaging section */}
<div className="mt-3 pt-2 border-t border-border/50">
<div className={cn(
"flex items-center gap-2 px-2.5 py-1.5 text-xs font-semibold uppercase tracking-wider",
"text-muted-foreground/70"
)}>
<span className={cn("truncate hidden", x.showLabel)}>
<Trans>Packaging</Trans>
</span>
</div>
<div className="flex flex-col gap-0.5">
{packagingStages.map((step, i) => renderStageItem(step, i < packagingStages.length - 1))}
</div>
</div>
</div>
{/* Task indicator */}
<TaskIndicator bookLabel={bookLabel} />
Expand Down Expand Up @@ -347,6 +421,48 @@ export function StageSidebar({
)
}

/* ---------- LearningToolsInfo ---------- */

function LearningToolsInfo({ i18n, showLabel }: { i18n: ReturnType<typeof useLingui>["i18n"]; showLabel: string }) {
const [show, setShow] = useState(false)
const [pos, setPos] = useState({ top: 0, left: 0 })
const iconRef = useRef<HTMLSpanElement>(null)

const handleEnter = useCallback(() => {
if (!iconRef.current) return
const rect = iconRef.current.getBoundingClientRect()
setPos({ top: rect.bottom + 6, left: rect.left + rect.width / 2 })
setShow(true)
}, [])

const handleLeave = useCallback(() => setShow(false), [])

return (
<>
<span
ref={iconRef}
className={cn("hidden text-muted-foreground/50 hover:text-muted-foreground", showLabel)}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
onClick={(e) => e.stopPropagation()}
>
<Info className="w-3 h-3" />
</span>
{show && createPortal(
<div
className="fixed z-50 -translate-x-1/2 w-48 px-2.5 py-1.5 rounded-md bg-popover text-popover-foreground text-[10px] leading-snug font-normal normal-case tracking-normal shadow-md ring-1 ring-border animate-in fade-in duration-75"
style={{ top: pos.top, left: pos.left }}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
>
{i18n._(msg`Optional learning tools that can be added to the book. Please complete the Storyboard stage before adding learning tools.`)}
</div>,
document.body,
)}
</>
)
}

/* ---------- TaskIndicator ---------- */

function TaskIndicator({ bookLabel }: { bookLabel: string }) {
Expand Down
8 changes: 4 additions & 4 deletions apps/studio/src/components/pipeline/stage-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ describe("stage-config", () => {
expect(pipelineSlugs).toEqual([
"extract",
"storyboard",
"quizzes",
"captions",
"quizzes",
"glossary",
"toc",
"text-and-speech",
"toc",
"preview",
])
})
Expand All @@ -30,12 +30,12 @@ describe("stage-config", () => {
expect(overviewSlugs).toEqual([
"extract",
"storyboard",
"quizzes",
"captions",
"quizzes",
"glossary",
"toc",
"text-and-speech",
"sign-language",
"toc",
"validation",
"preview",
"export",
Expand Down
22 changes: 20 additions & 2 deletions apps/studio/src/components/pipeline/stage-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ import {
} from "lucide-react"

export const STAGES = [
// Core stages
{ slug: "book", label: "Book", runningLabel: "Loading Book", icon: BookMarked, color: "bg-gray-600", hex: "#4b5563", textColor: "text-gray-600", bgLight: "bg-gray-50", borderColor: "border-gray-200", borderDark: "border-gray-600" },
{ slug: "extract", label: "Extract", runningLabel: "Extracting", icon: FileText, color: "bg-blue-600", hex: "#2563eb", textColor: "text-blue-600", bgLight: "bg-blue-50", borderColor: "border-blue-200", borderDark: "border-blue-600" },
{ slug: "storyboard", label: "Storyboard", runningLabel: "Building Storyboard", icon: LayoutGrid, color: "bg-violet-600", hex: "#7c3aed", textColor: "text-violet-600", bgLight: "bg-violet-50", borderColor: "border-violet-200", borderDark: "border-violet-600" },
{ slug: "quizzes", label: "Quizzes", runningLabel: "Generating Quizzes", icon: HelpCircle, color: "bg-orange-600", hex: "#ea580c", textColor: "text-orange-600", bgLight: "bg-orange-50", borderColor: "border-orange-200", borderDark: "border-orange-600" },
{ slug: "captions", label: "Image Captions", runningLabel: "Captioning Images", icon: Image, color: "bg-teal-600", hex: "#0d9488", textColor: "text-teal-600", bgLight: "bg-teal-50", borderColor: "border-teal-200", borderDark: "border-teal-600" },
// Learning Tools (optional, collapsible)
{ slug: "quizzes", label: "Quizzes", runningLabel: "Generating Quizzes", icon: HelpCircle, color: "bg-orange-600", hex: "#ea580c", textColor: "text-orange-600", bgLight: "bg-orange-50", borderColor: "border-orange-200", borderDark: "border-orange-600" },
{ slug: "glossary", label: "Glossary", runningLabel: "Generating Glossary", icon: BookOpen, color: "bg-lime-600", hex: "#65a30d", textColor: "text-lime-600", bgLight: "bg-lime-50", borderColor: "border-lime-200", borderDark: "border-lime-600" },
{ slug: "toc", label: "Table of Contents", runningLabel: "Generating TOC", icon: List, color: "bg-amber-600", hex: "#d97706", textColor: "text-amber-600", bgLight: "bg-amber-50", borderColor: "border-amber-200", borderDark: "border-amber-600" },
{ slug: "text-and-speech", label: "Text & Speech", runningLabel: "Generating Text & Speech", icon: Languages, color: "bg-pink-600", hex: "#db2777", textColor: "text-pink-600", bgLight: "bg-pink-50", borderColor: "border-pink-200", borderDark: "border-pink-600" },
{ slug: "sign-language", label: "Sign Language", runningLabel: "Sign Language", icon: Hand, color: "bg-cyan-600", hex: "#0891b2", textColor: "text-cyan-600", bgLight: "bg-cyan-50", borderColor: "border-cyan-200", borderDark: "border-cyan-600" },
// Packaging
{ slug: "toc", label: "Table of Contents", runningLabel: "Generating TOC", icon: List, color: "bg-amber-600", hex: "#d97706", textColor: "text-amber-600", bgLight: "bg-amber-50", borderColor: "border-amber-200", borderDark: "border-amber-600" },
{ slug: "validation", label: "Validation", runningLabel: "Running Validation", icon: ShieldCheck, color: "bg-emerald-600", hex: "#059669", textColor: "text-emerald-600", bgLight: "bg-emerald-50", borderColor: "border-emerald-200", borderDark: "border-emerald-600" },
{ slug: "preview", label: "Preview", runningLabel: "Building Preview", icon: Eye, color: "bg-gray-600", hex: "#4b5563", textColor: "text-gray-600", bgLight: "bg-gray-50", borderColor: "border-gray-200", borderDark: "border-gray-600" },
{ slug: "export", label: "Export", runningLabel: "Exporting", icon: FileDown, color: "bg-indigo-700", hex: "#4338ca", textColor: "text-indigo-700", bgLight: "bg-indigo-50", borderColor: "border-indigo-200", borderDark: "border-indigo-700" },
Expand Down Expand Up @@ -105,3 +108,18 @@ export function toCamelLabel(label: string): string {
export function isStageCompleted(slug: string, completedStages: Record<string, boolean>): boolean {
return !!completedStages[slug]
}

/* ---------- Sidebar section groupings ---------- */

/** Stage slugs that belong to the core (always-visible) section. */
export const CORE_STAGE_SLUGS = new Set<StageSlug>(["book", "extract", "storyboard", "captions"])

/** Stage slugs that belong to the optional "Learning Tools" section. */
export const LEARNING_TOOL_SLUGS = new Set<StageSlug>(["quizzes", "glossary", "text-and-speech", "sign-language"])

/** Stage slugs that belong to the "Packaging" section. */
export const PACKAGING_SLUGS = new Set<StageSlug>(["toc", "validation", "preview", "export"])

export function isLearningToolStage(slug: string): boolean {
return LEARNING_TOOL_SLUGS.has(slug as StageSlug)
}
6 changes: 6 additions & 0 deletions apps/studio/src/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,9 @@ msgstr "Languages"
msgid "Last"
msgstr "Last"

msgid "Learning Tools"
msgstr "Learning Tools"

msgid "Leave empty if not required"
msgstr "Leave empty if not required"

Expand Down Expand Up @@ -2220,6 +2223,9 @@ msgstr "OpenAI Voice"
msgid "Optional instructions for the LLM..."
msgstr "Optional instructions for the LLM..."

msgid "Optional learning tools that can be added to the book. Please complete the Storyboard stage before adding learning tools."
msgstr "Optional learning tools that can be added to the book. Please complete the Storyboard stage before adding learning tools."

msgid "Optional notes about the review scope or assignments"
msgstr "Optional notes about the review scope or assignments"

Expand Down
6 changes: 6 additions & 0 deletions apps/studio/src/locales/es.po
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Spanish
msgid ""
msgstr ""
Expand Down Expand Up @@ -1716,6 +1716,9 @@
msgid "Last"
msgstr "Última"

msgid "Learning Tools"
msgstr "Herramientas de Aprendizaje"

msgid "Leave empty if not required"
msgstr "Dejar vacío si no es necesario"

Expand Down Expand Up @@ -2220,6 +2223,9 @@
msgid "Optional instructions for the LLM..."
msgstr "Instrucciones opcionales para el LLM..."

msgid "Optional learning tools that can be added to the book. Please complete the Storyboard stage before adding learning tools."
msgstr "Herramientas de aprendizaje opcionales que se pueden agregar al libro. Complete la etapa de Storyboard antes de agregar herramientas de aprendizaje."

msgid "Optional notes about the review scope or assignments"
msgstr "Optional notes about the review scope or assignments"

Expand Down
6 changes: 6 additions & 0 deletions apps/studio/src/locales/pt-BR.po
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Portuguese (Brazil)
msgid ""
msgstr ""
Expand Down Expand Up @@ -1716,6 +1716,9 @@
msgid "Last"
msgstr "Última"

msgid "Learning Tools"
msgstr "Ferramentas de Aprendizagem"

msgid "Leave empty if not required"
msgstr "Deixe vazio se não for necessário"

Expand Down Expand Up @@ -2220,6 +2223,9 @@
msgid "Optional instructions for the LLM..."
msgstr "Instruções opcionais para o LLM..."

msgid "Optional learning tools that can be added to the book. Please complete the Storyboard stage before adding learning tools."
msgstr "Ferramentas de aprendizagem opcionais que podem ser adicionadas ao livro. Conclua a etapa de Storyboard antes de adicionar ferramentas de aprendizagem."

msgid "Optional notes about the review scope or assignments"
msgstr "Optional notes about the review scope or assignments"

Expand Down
Loading