From 8f96ae0ce4cd93216a1cf845e88810ebfb1ba749 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Fri, 24 Apr 2026 16:50:24 +0900 Subject: [PATCH 1/3] [#169] Add genre selector dropdown for publish, auto-detect from structure.md Replace unreliable regex-only genre detection with a dropdown that lets the user pick from the allowed genres list. Auto-detects genre from structure.md as a default but user can override before publishing. Genre selector shown only for genesis files. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/web/components/PreviewPanel.tsx | 36 +++++++++++++++++++++++++++-- app/web/components/StoriesPage.tsx | 13 +---------- package.json | 2 +- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/app/web/components/PreviewPanel.tsx b/app/web/components/PreviewPanel.tsx index 83ae353..9bddd82 100644 --- a/app/web/components/PreviewPanel.tsx +++ b/app/web/components/PreviewPanel.tsx @@ -3,12 +3,13 @@ import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; import rehypeSanitize from "rehype-sanitize"; +import { GENRES } from "../../../lib/genres"; interface PreviewPanelProps { storyName: string | null; fileName: string | null; authFetch: (url: string, opts?: RequestInit) => Promise; - onPublish?: (storyName: string, fileName: string) => void; + onPublish?: (storyName: string, fileName: string, genre: string) => void; publishingFile?: string | null; } @@ -34,6 +35,7 @@ export function PreviewPanel({ storyName, fileName, authFetch, onPublish, publis const [dirty, setDirty] = useState(false); const [retrying, setRetrying] = useState(false); const [indexTimeLeft, setIndexTimeLeft] = useState(null); + const [selectedGenre, setSelectedGenre] = useState("Fiction"); const textareaRef = useRef(null); const dirtyRef = useRef(false); @@ -75,6 +77,25 @@ export function PreviewPanel({ storyName, fileName, authFetch, onPublish, publis return () => clearInterval(interval); }, [storyName, fileName, loadFile, activeTab, dirty]); + // Auto-detect genre from structure.md when story changes + useEffect(() => { + if (!storyName) return; + let cancelled = false; + authFetch(`/api/stories/${storyName}/structure.md`) + .then((res) => res.ok ? res.json() : null) + .then((data) => { + if (cancelled || !data?.content) return; + const match = data.content.match(/\*{0,2}genre\*{0,2}[:\s]+(.+)/i); + if (match) { + const detected = match[1].replace(/\*+/g, "").trim(); + const found = GENRES.find((g) => g.toLowerCase() === detected.toLowerCase()); + if (found) setSelectedGenre(found); + } + }) + .catch(() => {}); + return () => { cancelled = true; }; + }, [storyName, authFetch]); + const handleSave = useCallback(async () => { if (!storyName || !fileName) return; setSaving(true); @@ -370,8 +391,19 @@ export function PreviewPanel({ storyName, fileName, authFetch, onPublish, publis ) : (
+ {(isGenesis) && ( + + )}