- {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' });