diff --git a/src/games/storywriter/assets/editor.module.css b/src/games/storywriter/assets/editor.module.css index 04878ff..ac07066 100644 --- a/src/games/storywriter/assets/editor.module.css +++ b/src/games/storywriter/assets/editor.module.css @@ -108,6 +108,10 @@ margin-left: auto; } +.tabHidden { + display: none; +} + .promptPreview { width: 100%; color: var(--textColor); diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx index 3066d35..79471d5 100644 --- a/src/games/storywriter/components/chat-sidebar.tsx +++ b/src/games/storywriter/components/chat-sidebar.tsx @@ -59,7 +59,7 @@ const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => { ); }; -export const ChatPanel = () => { +export const ChatPanel = ({ visible }: { visible: boolean }) => { const appState = useAppState(); const { currentWorld, currentStory, dispatch, connection, model, enableThinking, chatOpen, continuePrompt } = appState; const { summarizeAll, isSummarizing } = useChapterSummarization(); @@ -415,6 +415,8 @@ export const ChatPanel = () => { setEditingContent(''); }, [setEditingContent]); + if (!visible) return null; + const isDisabled = !currentStory || !connection || !model || isLoading; return ( @@ -647,7 +649,7 @@ export const ChatSidebar = () => { })} class={sidebarStyles.mobileOverlay} > - + ); }; diff --git a/src/games/storywriter/components/editor.tsx b/src/games/storywriter/components/editor.tsx index 148bcad..ff7db70 100644 --- a/src/games/storywriter/components/editor.tsx +++ b/src/games/storywriter/components/editor.tsx @@ -1,6 +1,4 @@ -import { ContentEditable } from "@common/components/ContentEditable"; import { highlight } from "@common/highlight"; -import { useInputCallback } from "@common/hooks/useInputCallback"; import clsx from "clsx"; import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact"; import { useEffect, useMemo, useRef } from "preact/hooks"; @@ -12,6 +10,9 @@ import { ChaptersEditor } from "./editors/chapters"; import { CharacterEditor } from "./editors/character"; import { LocationEditor } from "./editors/location"; import { LoreEditor } from "./editors/lore"; +import { ScratchpadEditor } from "./editors/scratchpad"; +import { StoryEditor } from "./editors/story"; +import { SystemEditor } from "./editors/system"; import { Menu } from "./menu"; // Tabs available when a story is selected (regular world) @@ -53,54 +54,16 @@ export const Editor = () => { const appState = useAppState(); const { currentWorld, currentStory, currentTab, chatOpen, dispatch } = appState; - const handleInput = useInputCallback((text: string) => { - if (!currentStory || !currentWorld) return; - dispatch({ type: 'EDIT_STORY', worldId: currentWorld.id, id: currentStory.id, text }); - }, [currentStory?.id, currentWorld?.id]); - - const handleScratchpadInput = useInputCallback((text: string) => { - if (!currentStory || !currentWorld) return; - dispatch({ type: 'EDIT_SCRATCHPAD', worldId: currentWorld.id, id: currentStory.id, text }); - }, [currentStory?.id, currentWorld?.id]); - - const handleSystemOverrideInput = useInputCallback((text: string) => { - if (!currentWorld) return; - dispatch({ type: 'SET_WORLD_SYSTEM_INSTRUCTION_OVERRIDE', worldId: currentWorld.id, systemInstructionOverride: text || undefined }); - }, [currentWorld?.id]); - const handleTabChange = (tab: Tab) => { dispatch({ type: 'SET_CURRENT_TAB', tab }); }; const contentRef = useRef(null); - const storyValue = useMemo(() => { - if (!currentStory) return ''; - - const { text, lastEditedText } = currentStory; - if (!lastEditedText) return highlight(text); - - const idx = text.lastIndexOf(lastEditedText); - if (idx === -1) return highlight(text); - - const marked = text.slice(0, idx) + '' + lastEditedText + '' + text.slice(idx + lastEditedText.length); - - return highlight(marked); - }, [currentStory?.text, currentStory?.lastEditedText]); - - const scratchpadValue = useMemo(() => { - return highlight(currentStory?.scratchpad || ''); - }, [currentStory?.scratchpad]); - const promptPreview = useMemo(() => { - if (currentTab !== 'prompt') return ''; const text = Prompt.formatSystemPrompt(appState); return highlight(text, false); - }, [currentTab, appState]); - - const overrideValue = useMemo(() => { - return highlight(currentWorld?.systemInstructionOverride || ''); - }, [currentWorld?.systemInstructionOverride]); + }, [appState]); useEffect(() => { if (currentStory?.lastEditedText) { @@ -144,57 +107,31 @@ export const Editor = () => { ?
{currentWorld.title}
: null; - const isChatTab = currentTab === 'chat'; - return (
{titleBar} -
- {currentTab === "menu" && ( - - )} - {currentTab === "story" && currentStory && ( - + + {currentStory && (<> + + + +
- )} - {currentTab === "lore" && (currentStory || currentWorld) && ( - - )} - {currentTab === "characters" && (currentStory || currentWorld) && ( - - )} - {currentTab === "locations" && (currentStory || currentWorld) && ( - - )} - {currentTab === "chapters" && currentStory && ( - - )} - {currentTab === "scratchpad" && currentStory && ( - - )} - {currentTab === "prompt" && currentStory && ( -
- )} - {currentTab === "system" && currentWorld && ( - - )} - {currentTab === "chat" && currentStory && isChatOnly && ( - - )} + {isChatOnly && } + )} + {(currentStory || currentWorld) && (<> + + + + )} + {currentWorld && }
{tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => ( diff --git a/src/games/storywriter/components/editors/chapters.tsx b/src/games/storywriter/components/editors/chapters.tsx index f4795d2..6a564ac 100644 --- a/src/games/storywriter/components/editors/chapters.tsx +++ b/src/games/storywriter/components/editors/chapters.tsx @@ -5,16 +5,18 @@ import styles from "../../assets/chapters-editor.module.css"; import { useAppState } from "../../contexts/state"; import Chapters from "../../utils/chapters"; -export const ChaptersEditor = () => { +export const ChaptersEditor = ({ visible }: { visible: boolean }) => { const { currentWorld, currentStory, dispatch } = useAppState(); - if (!currentStory) return null; - const parsed = useMemo( - () => Chapters.parseText(currentStory.text), - [currentStory.text] + () => Chapters.parseText(currentStory?.text ?? ''), + [currentStory?.text] ); + if (!currentWorld || !currentStory || !visible) { + return null; + } + if (parsed.length === 0) { return (
diff --git a/src/games/storywriter/components/editors/character.tsx b/src/games/storywriter/components/editors/character.tsx index 2494cf5..9d7e014 100644 --- a/src/games/storywriter/components/editors/character.tsx +++ b/src/games/storywriter/components/editors/character.tsx @@ -5,14 +5,14 @@ import styles from '../../assets/character-editor.module.css'; import { CharacterRole, useAppState, type Character } from "../../contexts/state"; import LLM from "../../utils/llm"; -export const CharacterEditor = () => { +export const CharacterEditor = ({ visible }: { visible: boolean }) => { const { currentWorld, currentStory, mergedCharacters, dispatch, connection, model } = useAppState(); const [newNickname, setNewNickname] = useState>({}); const [newRelation, setNewRelation] = useState>({}); const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); const [generatingShortDesc, setGeneratingShortDesc] = useState(null); - if (!currentWorld) { + if (!currentWorld || !visible) { return null; } diff --git a/src/games/storywriter/components/editors/location.tsx b/src/games/storywriter/components/editors/location.tsx index 55c419f..4a3c09e 100644 --- a/src/games/storywriter/components/editors/location.tsx +++ b/src/games/storywriter/components/editors/location.tsx @@ -4,12 +4,12 @@ import styles from '../../assets/location-editor.module.css'; import { LocationScale, useAppState, type Location } from "../../contexts/state"; import LLM from "../../utils/llm"; -export const LocationEditor = () => { +export const LocationEditor = ({ visible }: { visible: boolean }) => { const { currentWorld, currentStory, dispatch, connection, model } = useAppState(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); const [generatingShortDesc, setGeneratingShortDesc] = useState(null); - if (!currentWorld) { + if (!currentWorld || !visible) { return null; } diff --git a/src/games/storywriter/components/editors/lore.tsx b/src/games/storywriter/components/editors/lore.tsx index 73ce075..3abd9d3 100644 --- a/src/games/storywriter/components/editors/lore.tsx +++ b/src/games/storywriter/components/editors/lore.tsx @@ -3,12 +3,12 @@ import { useState } from "preact/hooks"; import styles from '../../assets/lore-editor.module.css'; import { useAppState, type LoreEntry } from "../../contexts/state"; -export const LoreEditor = () => { +export const LoreEditor = ({ visible }: { visible: boolean }) => { const { currentWorld, currentStory, dispatch } = useAppState(); const [editingId, setEditingId] = useState(null); const [newTitle, setNewTitle] = useState(''); - if (!currentWorld) { + if (!currentWorld || !visible) { return null; } diff --git a/src/games/storywriter/components/editors/scratchpad.tsx b/src/games/storywriter/components/editors/scratchpad.tsx new file mode 100644 index 0000000..9e55db2 --- /dev/null +++ b/src/games/storywriter/components/editors/scratchpad.tsx @@ -0,0 +1,35 @@ +import { ContentEditable } from "@common/components/ContentEditable"; +import { highlight } from "@common/highlight"; +import { useInputCallback } from "@common/hooks/useInputCallback"; +import { useMemo } from "preact/hooks"; +import styles from "../../assets/editor.module.css"; +import { useAppState } from "../../contexts/state"; + +export const ScratchpadEditor = ({ visible }: { visible: boolean }) => { + const { currentWorld, currentStory, dispatch } = useAppState(); + + const handleInput = useInputCallback((text: string) => { + if (!currentStory || !currentWorld) return; + dispatch({ + type: 'EDIT_SCRATCHPAD', + worldId: currentWorld.id, + id: currentStory.id, + text, + }); + }, [currentStory?.id, currentWorld?.id]); + + const value = useMemo(() => highlight(currentStory?.scratchpad || ''), [currentStory?.scratchpad]); + + if (!currentWorld || !currentStory || !visible) { + return null; + } + + return ( + + ); +}; diff --git a/src/games/storywriter/components/editors/story.tsx b/src/games/storywriter/components/editors/story.tsx new file mode 100644 index 0000000..fa28947 --- /dev/null +++ b/src/games/storywriter/components/editors/story.tsx @@ -0,0 +1,44 @@ +import { ContentEditable } from "@common/components/ContentEditable"; +import { highlight } from "@common/highlight"; +import { useInputCallback } from "@common/hooks/useInputCallback"; +import { useMemo } from "preact/hooks"; +import styles from "../../assets/editor.module.css"; +import { useAppState } from "../../contexts/state"; + +export const StoryEditor = ({ visible }: { visible: boolean }) => { + const { currentWorld, currentStory, dispatch } = useAppState(); + + const handleInput = useInputCallback((text: string) => { + if (!currentStory || !currentWorld) return; + dispatch({ + type: 'EDIT_STORY', + worldId: currentWorld.id, + id: currentStory.id, + text, + }); + }, [currentStory?.id, currentWorld?.id]); + + const value = useMemo(() => { + if (!currentStory) return ''; + const { text, lastEditedText } = currentStory; + if (!lastEditedText) return highlight(text); + const idx = text.lastIndexOf(lastEditedText); + if (idx === -1) return highlight(text); + const marked = text.slice(0, idx) + '' + lastEditedText + '' + text.slice(idx + lastEditedText.length); + return highlight(marked); + }, [currentStory?.text, currentStory?.lastEditedText]); + + + if (!currentWorld || !currentStory || !visible) { + return null; + } + + return ( + + ); +}; diff --git a/src/games/storywriter/components/editors/system.tsx b/src/games/storywriter/components/editors/system.tsx new file mode 100644 index 0000000..05ce19e --- /dev/null +++ b/src/games/storywriter/components/editors/system.tsx @@ -0,0 +1,34 @@ +import { ContentEditable } from "@common/components/ContentEditable"; +import { highlight } from "@common/highlight"; +import { useInputCallback } from "@common/hooks/useInputCallback"; +import { useMemo } from "preact/hooks"; +import styles from "../../assets/editor.module.css"; +import { useAppState } from "../../contexts/state"; + +export const SystemEditor = ({ visible }: { visible: boolean }) => { + const { currentWorld, dispatch } = useAppState(); + + const handleInput = useInputCallback((text: string) => { + if (!currentWorld) return; + dispatch({ + type: 'SET_WORLD_SYSTEM_INSTRUCTION_OVERRIDE', + worldId: currentWorld.id, + systemInstructionOverride: text || undefined, + }); + }, [currentWorld?.id]); + + const value = useMemo(() => highlight(currentWorld?.systemInstructionOverride || ''), [currentWorld?.systemInstructionOverride]); + + if (!currentWorld || !visible) { + return null; + } + + return ( + + ); +}; diff --git a/src/games/storywriter/components/menu.tsx b/src/games/storywriter/components/menu.tsx index 3c1ee96..d0b2562 100644 --- a/src/games/storywriter/components/menu.tsx +++ b/src/games/storywriter/components/menu.tsx @@ -187,9 +187,10 @@ const WorldItem = ({ // ─── Menu Sidebar ───────────────────────────────────────────────────────────── -export const Menu = () => { +export const Menu = ({ visible }: { visible: boolean }) => { const { worlds, currentWorld, currentStory, dispatch } = useAppState(); const isSettingsOpen = useBool(false); + if (!visible) return null; const handleCreateWorld = () => { dispatch({ type: 'CREATE_WORLD', title: 'New World' });