1
0
Fork 0

Keep all required editors mounted

This commit is contained in:
Pabloader 2026-04-08 13:54:01 +00:00
parent e28835846b
commit 67f70f236d
11 changed files with 161 additions and 102 deletions

View File

@ -108,6 +108,10 @@
margin-left: auto;
}
.tabHidden {
display: none;
}
.promptPreview {
width: 100%;
color: var(--textColor);

View File

@ -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}
>
<ChatPanel />
<ChatPanel visible />
</Sidebar>
);
};

View File

@ -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<HTMLDivElement>(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) + '<mark>' + lastEditedText + '</mark>' + 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 = () => {
? <div class={styles.title}><WorldIcon size={24} />{currentWorld.title}</div>
: null;
const isChatTab = currentTab === 'chat';
return (
<div class={styles.editor}>
{titleBar}
<div class={clsx(styles.content, currentTab === 'menu' && styles.menuContent, isChatTab && styles.chatContent)} ref={contentRef}>
{currentTab === "menu" && (
<Menu />
)}
{currentTab === "story" && currentStory && (
<ContentEditable
class={styles.editable}
value={storyValue}
onInput={handleInput}
placeholder="Start writing your story..."
<div class={clsx(
styles.content,
currentTab === 'menu' && styles.menuContent,
currentTab === 'chat' && styles.chatContent,
)} ref={contentRef}>
<Menu visible={currentTab === "menu"} />
{currentStory && (<>
<StoryEditor visible={currentTab === "story"} />
<ChaptersEditor visible={currentTab === "chapters"} />
<ScratchpadEditor visible={currentTab === "scratchpad"} />
<div
class={clsx(styles.promptPreview, currentTab !== "prompt" && styles.tabHidden)}
dangerouslySetInnerHTML={{ __html: Prompt.substituteVars(appState, promptPreview) }}
/>
)}
{currentTab === "lore" && (currentStory || currentWorld) && (
<LoreEditor />
)}
{currentTab === "characters" && (currentStory || currentWorld) && (
<CharacterEditor />
)}
{currentTab === "locations" && (currentStory || currentWorld) && (
<LocationEditor />
)}
{currentTab === "chapters" && currentStory && (
<ChaptersEditor />
)}
{currentTab === "scratchpad" && currentStory && (
<ContentEditable
class={styles.editable}
value={scratchpadValue}
onInput={handleScratchpadInput}
placeholder="Notes, ideas, outlines — anything you don't want in the story..."
/>
)}
{currentTab === "prompt" && currentStory && (
<div class={styles.promptPreview} dangerouslySetInnerHTML={{ __html: Prompt.substituteVars(appState, promptPreview) }} />
)}
{currentTab === "system" && currentWorld && (
<ContentEditable
class={styles.editable}
value={overrideValue}
onInput={handleSystemOverrideInput}
placeholder="Override the global system instruction for this world. Leave empty to use the global setting."
/>
)}
{currentTab === "chat" && currentStory && isChatOnly && (
<ChatPanel />
)}
{isChatOnly && <ChatPanel visible={currentTab === "chat"} />}
</>)}
{(currentStory || currentWorld) && (<>
<LoreEditor visible={currentTab === "lore"} />
<CharacterEditor visible={currentTab === "characters"} />
<LocationEditor visible={currentTab === "locations"} />
</>)}
{currentWorld && <SystemEditor visible={currentTab === "system"} />}
</div>
<div class={styles.tabs}>
{tabs.filter(tab => hasSelection || tab.id === 'menu').map((tab) => (

View File

@ -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 (
<div class={styles.chaptersEditor}>

View File

@ -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<Record<string, string>>({});
const [newRelation, setNewRelation] = useState<Record<string, { name: string; relation: string }>>({});
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
if (!currentWorld) {
if (!currentWorld || !visible) {
return null;
}

View File

@ -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<string | null>(null);
const [generatingShortDesc, setGeneratingShortDesc] = useState<string | null>(null);
if (!currentWorld) {
if (!currentWorld || !visible) {
return null;
}

View File

@ -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<string | null>(null);
const [newTitle, setNewTitle] = useState('');
if (!currentWorld) {
if (!currentWorld || !visible) {
return null;
}

View File

@ -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 (
<ContentEditable
class={styles.editable}
value={value}
onInput={handleInput}
placeholder="Notes, ideas, outlines — anything you don't want in the story..."
/>
);
};

View File

@ -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) + '<mark>' + lastEditedText + '</mark>' + text.slice(idx + lastEditedText.length);
return highlight(marked);
}, [currentStory?.text, currentStory?.lastEditedText]);
if (!currentWorld || !currentStory || !visible) {
return null;
}
return (
<ContentEditable
class={styles.editable}
value={value}
onInput={handleInput}
placeholder="Start writing your story..."
/>
);
};

View File

@ -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 (
<ContentEditable
class={styles.editable}
value={value}
onInput={handleInput}
placeholder="Override the global system instruction for this world. Leave empty to use the global setting."
/>
);
};

View File

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