diff --git a/src/games/storywriter/components/editor.tsx b/src/games/storywriter/components/editor.tsx index 9f122ba..2a89980 100644 --- a/src/games/storywriter/components/editor.tsx +++ b/src/games/storywriter/components/editor.tsx @@ -20,27 +20,26 @@ const TABS: { id: Tab; label: string }[] = [ export const Editor = () => { const { currentStory, dispatch } = useAppState(); - if (!currentStory) { - return
; - } - const handleInput = useInputCallback((text: string) => { + if (!currentStory) return; dispatch({ type: 'EDIT_STORY', id: currentStory.id, text, }); - }, []); + }, [currentStory?.id]); const handleLoreInput = useInputCallback((lore: string) => { + if (!currentStory) return; dispatch({ type: 'EDIT_LORE', id: currentStory.id, lore, }); - }, []); + }, [currentStory?.id]); const handleTabChange = (tab: Tab) => { + if (!currentStory) return; dispatch({ type: 'SET_CURRENT_TAB', id: currentStory.id, @@ -48,8 +47,12 @@ export const Editor = () => { }); }; - const storyValue = useMemo(() => highlight(currentStory.text), [currentStory.text]); - const loreValue = useMemo(() => highlight(currentStory.lore), [currentStory.lore]); + const storyValue = useMemo(() => currentStory ? highlight(currentStory.text) : '', [currentStory?.text]); + const loreValue = useMemo(() => currentStory ? highlight(currentStory.lore) : '', [currentStory?.lore]); + + if (!currentStory) { + return
; + } return (
diff --git a/src/games/storywriter/components/menu-sidebar.tsx b/src/games/storywriter/components/menu-sidebar.tsx index 1e43545..e1ddd8d 100644 --- a/src/games/storywriter/components/menu-sidebar.tsx +++ b/src/games/storywriter/components/menu-sidebar.tsx @@ -7,7 +7,7 @@ import { useBool } from "@common/hooks/useBool"; import type { Story } from "../contexts/state"; import styles from '../assets/menu-sidebar.module.css'; import { useState } from "preact/hooks"; -import { Pencil, X, Plus, Plug, Settings } from "lucide-preact"; +import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact"; // ─── Story Item ─────────────────────────────────────────────────────────────── @@ -17,9 +17,10 @@ interface StoryItemProps { onSelect: () => void; onRename: (newTitle: string) => void; onDelete: () => void; + onDuplicate: () => void; } -const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemProps) => { +const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }: StoryItemProps) => { const [isEditing, setIsEditing] = useState(false); const [editTitle, setEditTitle] = useState(story.title); @@ -70,6 +71,9 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemPro + @@ -105,6 +109,10 @@ export const MenuSidebar = () => { } }; + const handleDuplicate = (id: string) => { + dispatch({ type: 'DUPLICATE_STORY', id }); + }; + return (
@@ -120,6 +128,7 @@ export const MenuSidebar = () => { onSelect={() => handleSelect(story.id)} onRename={(newTitle) => handleRename(story.id, newTitle)} onDelete={() => handleDelete(story.id)} + onDuplicate={() => handleDuplicate(story.id)} /> ))}
diff --git a/src/games/storywriter/contexts/state.tsx b/src/games/storywriter/contexts/state.tsx index 7252e04..e1b9c2f 100644 --- a/src/games/storywriter/contexts/state.tsx +++ b/src/games/storywriter/contexts/state.tsx @@ -81,6 +81,7 @@ type Action = | { type: 'SET_CURRENT_TAB'; id: string; tab: Tab } | { type: 'DELETE_STORY'; id: string } | { type: 'SELECT_STORY'; id: string } + | { type: 'DUPLICATE_STORY'; id: string } | { type: 'ADD_CHAT_MESSAGE'; storyId: string; message: ChatMessage } | { type: 'CLEAR_CHAT'; storyId: string } | { type: 'SET_CONNECTION'; connection: LLM.Connection | null } @@ -180,6 +181,26 @@ function reducer(state: IState, action: Action): IState { currentStoryId: deletingCurrent ? null : state.currentStoryId, }; } + case 'DUPLICATE_STORY': { + const original = state.stories.find(s => s.id === action.id); + if (!original) return state; + const newStory: Story = { + id: crypto.randomUUID(), + title: `${original.title} (Copy)`, + text: '', + lore: original.lore, + characters: original.characters, + locations: original.locations, + currentTab: 'story', + chatMessages: [], + chapters: [], + }; + return { + ...state, + stories: [...state.stories, newStory], + currentStoryId: newStory.id, + }; + } case 'SELECT_STORY': { return { ...state,