import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { api } from '../api' import { extractPlaceholders, pickJSONFile, PlaceholderModal } from '../components' function splitTags(tags) { return (tags || '').split(',').map((t) => t.trim()).filter(Boolean) } export default function Scenarios() { const [scenarios, setScenarios] = useState(null) const [search, setSearch] = useState('') const [tagFilter, setTagFilter] = useState(null) const [pending, setPending] = useState(null) // { scenario, names } awaiting placeholder values const navigate = useNavigate() useEffect(() => { api.listScenarios().then(setScenarios).catch(() => setScenarios([])) }, []) const allTags = useMemo(() => { const tags = new Set() for (const sc of scenarios || []) splitTags(sc.tags).forEach((t) => tags.add(t)) return [...tags].sort() }, [scenarios]) const visible = useMemo(() => { if (!scenarios) return null const q = search.trim().toLowerCase() return scenarios.filter((sc) => { if (tagFilter && !splitTags(sc.tags).includes(tagFilter)) return false if (q && !`${sc.title} ${sc.description} ${sc.tags}`.toLowerCase().includes(q)) return false return true }) }, [scenarios, search, tagFilter]) const createScenario = async () => { const scenario = await api.createScenario({ title: 'New Scenario' }) navigate(`/scenarios/${scenario.id}`) } const begin = async (scenarioId, placeholders = {}) => { const adv = await api.createAdventure({ scenario_id: scenarioId, placeholders }) navigate(`/play/${adv.id}`) } const startAdventure = async (e, scenarioId) => { e.stopPropagation() const scenario = await api.getScenario(scenarioId) const names = extractPlaceholders( scenario.prompt, scenario.memory, scenario.authors_note, scenario.ai_instructions, // Cards can carry ${placeholders} in trigger keys too, not just entries. ...scenario.story_cards.flatMap((c) => [c.keys, c.entry]), ) if (names.length === 0) return begin(scenarioId) setPending({ scenario, names }) } const startBlank = async () => { const adv = await api.createAdventure({ title: 'Blank Adventure' }) navigate(`/play/${adv.id}`) } return (

Scenarios

setSearch(e.target.value)} /> {allTags.length > 0 && (
{allTags.map((tag) => ( ))}
)}
{visible === null ? null : visible.length === 0 ? (
{scenarios.length === 0 ? 'No scenarios yet. Create one to define a reusable story template.' : 'No scenarios match your search.'}
) : (
{visible.map((sc) => (
navigate(`/scenarios/${sc.id}`)}>

{sc.title}

{sc.description || 'No description'}

{sc.is_public && demo ✦} {splitTags(sc.tags).map((tag) => ( {tag} ))}
))}
)} {pending && ( setPending(null)} onSubmit={(values) => { setPending(null); begin(pending.scenario.id, values) }} /> )}
) }