import { useEffect, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { api } from '../api' import { Field, StoryCardRow, downloadJSON, pickJSONFile } from '../components' export default function ScenarioEditor() { const { id } = useParams() const navigate = useNavigate() const [scenario, setScenario] = useState(null) const [allScripts, setAllScripts] = useState([]) const [status, setStatus] = useState('') // One timer per field/card: a single shared timer would cancel the pending // save of whatever was edited previously within the debounce window. const saveTimers = useRef(new Map()) const debounceSave = (key, fn) => { clearTimeout(saveTimers.current.get(key)) saveTimers.current.set(key, setTimeout(fn, 600)) } useEffect(() => { api.getScenario(id).then(setScenario).catch(() => navigate('/scenarios')) api.listScripts().then(setAllScripts).catch(() => {}) }, [id, navigate]) const setField = (field, value) => { const next = { ...scenario, [field]: value } setScenario(next) debounceSave(field, async () => { await api.updateScenario(id, { [field]: value }) setStatus('Saved') setTimeout(() => setStatus(''), 1500) }) } const addCard = async () => { const card = await api.createStoryCard({ scenario_id: Number(id) }) setScenario({ ...scenario, story_cards: [...scenario.story_cards, card] }) } const updateCard = (card) => { setScenario({ ...scenario, story_cards: scenario.story_cards.map((c) => (c.id === card.id ? card : c)), }) debounceSave(`card-${card.id}`, () => { api.updateStoryCard(card.id, { name: card.name, type: card.type, keys: card.keys, entry: card.entry, notes: card.notes, }) }) } const deleteCard = async (cardId) => { await api.deleteStoryCard(cardId) setScenario({ ...scenario, story_cards: scenario.story_cards.filter((c) => c.id !== cardId) }) } const exportCards = async () => { const cards = await api.exportStoryCards({ scenario_id: id }) downloadJSON(cards, `${scenario.title.replace(/\W+/g, '-')}-cards.json`) } const importCards = async () => { try { const parsed = await pickJSONFile() const cards = Array.isArray(parsed) ? parsed : (parsed.cards || parsed.storyCards) if (!Array.isArray(cards)) return alert('Expected a JSON array of story cards.') const created = await api.importStoryCards({ scenario_id: Number(id), cards }) setScenario({ ...scenario, story_cards: [...scenario.story_cards, ...created] }) } catch (err) { alert(err.message) } } const toggleScript = async (scriptId) => { const current = scenario.scripts.map((s) => s.id) const next = current.includes(scriptId) ? current.filter((sid) => sid !== scriptId) : [...current, scriptId] const updated = await api.updateScenario(id, { script_ids: next }) setScenario(updated) } const exportScenario = async () => { downloadJSON(await api.exportScenario(id), `${scenario.title.replace(/\W+/g, '-')}.json`) } const deleteScenario = async () => { if (!confirm('Delete this scenario permanently?')) return await api.deleteScenario(id) navigate('/scenarios') } const play = async () => { const adv = await api.createAdventure({ scenario_id: Number(id) }) navigate(`/play/${adv.id}`) } if (!scenario) return null // Shared demo scenarios (Phase 8) are visible to everyone but owned by no // one; the backend rejects edits, so present them read-only. const readOnly = !!scenario.is_public return (

{readOnly ? 'Scenario (read-only)' : 'Edit Scenario'}

{status} {!readOnly && }
{readOnly && (
This is a shared demo scenario — it can’t be edited, but you can hit Play to start your own adventure from it, or Export and re-import it as your own copy.
)}
setField('title', v)} /> setField('description', v)} textarea placeholder="Shown in the scenario list; not sent to the AI." /> setField('prompt', v)} textarea rows={6} placeholder="The opening story text. Supports ${placeholders} (Phase 5)." /> setField('memory', v)} textarea placeholder="Key facts the AI should always remember." /> setField('authors_note', v)} textarea rows={2} placeholder="Style/theme guidance, injected near the end of context." /> setField('ai_instructions', v)} textarea rows={2} placeholder="Behavioral guidance for the model (always included)." /> setField('tags', v)} placeholder="fantasy, mystery" />

Story Cards

{!readOnly && }
{scenario.story_cards.length === 0 && (
No story cards. Cards inject their Entry into context when a trigger word appears in the story.
)} {scenario.story_cards.map((card) => ( deleteCard(card.id)} /> ))}

Attached Scripts

{allScripts.length === 0 ? (
No scripts in your library. Create some on the Scripts page.
) : ( allScripts.map((s) => ( )) )}
) }